@adonis-agora/authkit-server 0.79.0 → 0.80.1

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/build/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgent
19
19
  export type { PoppyAgentProtocolEntry, PoppyApiEntry, PoppyBrowserLogin, PoppyClientsConfigInput, PoppyConfigInput, PoppyLimiter, PoppyMediatedConfigInput, PoppyMediatedField, PoppyMediatedResult, PoppyMediatedVerifyInput, PoppyPrincipalBase, PoppyReplayStore, ResolvedPoppyConfig, } from './src/agents/poppy/config.js';
20
20
  export { POPPY_READ, POPPY_WRITE } from './src/agents/poppy/config.js';
21
21
  export { accessTokenHash, type DpopNonceSource, type DpopResult, hmacNonceSource, type VerifyDpopInput, verifyDpopProof, } from './src/agents/poppy/dpop.js';
22
+ export { maskEmail, type PoppyEmailCodeSignInOptions, poppyEmailCodeSignIn, } from './src/agents/poppy/email_code_sign_in.js';
22
23
  export { PoppyError, type PoppyErrorCode } from './src/agents/poppy/errors.js';
23
24
  export { databaseReplayStore, memoryReplayStore, redisReplayStore, } from './src/agents/poppy/replay.js';
24
25
  export { installPoppySlots, POPPY_AUTHENTICATE_SLOT, type PoppyAuthenticate, type PoppyAuthResult, type PoppyPrincipal, type PoppySlotRequest, poppyAuth, poppyOf, verifyPoppyRequest, writePoppyAuthError, } from './src/agents/poppy/resource.js';
package/build/index.js CHANGED
@@ -11,6 +11,7 @@ export { PersonalAgentVerifier } from './src/agents/agent_identity.js';
11
11
  export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgentSecurity, personalAgentStepUp, } from './src/agents/middleware.js';
12
12
  export { POPPY_READ, POPPY_WRITE } from './src/agents/poppy/config.js';
13
13
  export { accessTokenHash, hmacNonceSource, verifyDpopProof, } from './src/agents/poppy/dpop.js';
14
+ export { maskEmail, poppyEmailCodeSignIn, } from './src/agents/poppy/email_code_sign_in.js';
14
15
  export { PoppyError } from './src/agents/poppy/errors.js';
15
16
  export { databaseReplayStore, memoryReplayStore, redisReplayStore, } from './src/agents/poppy/replay.js';
16
17
  export { installPoppySlots, POPPY_AUTHENTICATE_SLOT, poppyAuth, poppyOf, verifyPoppyRequest, writePoppyAuthError, } from './src/agents/poppy/resource.js';
@@ -0,0 +1,27 @@
1
+ import { type AuthAccount } from '../../accounts/account_store.js';
2
+ import type { PoppyMediatedConfigInput } from './config.js';
3
+ export interface PoppyEmailCodeSignInOptions {
4
+ /** Scopes que este sign-in pode conceder. Default: `['poppy:read']` (o default do Mediated). */
5
+ scopes?: string[];
6
+ /** Validade do código, em segundos. Default: 600. */
7
+ codeTtl?: number;
8
+ /** Tentativas de código antes de `failed`. Default: 5. */
9
+ maxCodeAttempts?: number;
10
+ /**
11
+ * Quem pode entrar por aqui (ex.: só pacientes, não profissionais). `false` = recebe a
12
+ * resposta de conta inexistente. Default: toda conta ativa.
13
+ */
14
+ eligible?: (account: AuthAccount) => boolean | Promise<boolean>;
15
+ /**
16
+ * Códigos por e-mail por hora, nesta instância — impede um agente de lotar a caixa de
17
+ * alguém. Acima disso a resposta é a mesma, sem e-mail. Default: 5.
18
+ */
19
+ maxPerHour?: number;
20
+ /** Rótulo do campo que o agente mostra ao usuário. Default: `'Email address'`. */
21
+ label?: string;
22
+ /** Texto de `sentTo` a partir do e-mail mascarado. Default: `` `Email to ${masked}` ``. */
23
+ sentTo?: (masked: string) => string;
24
+ }
25
+ /** `ana.souza@gmail.com` → `a***@gmail.com`: o bastante para a pessoa reconhecer. */
26
+ export declare function maskEmail(email: string): string;
27
+ export declare function poppyEmailCodeSignIn(options?: PoppyEmailCodeSignInOptions): PoppyMediatedConfigInput;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @experimental Poppy (Personal Agent Protocol) Draft 0.1 — pode mudar de forma incompatível enquanto a spec for draft.
3
+ *
4
+ * Mediated Sign-In pronto (§4.7): o agente pede o e-mail da conta, o authkit manda
5
+ * um código de 6 dígitos para ele e o agente devolve o código. Usa as contas
6
+ * (`accountStore`) e o e-mail (hook `mail.onPoppySignInCode` ou o mailer default)
7
+ * do próprio authkit — o app só liga:
8
+ *
9
+ * ```ts
10
+ * personalAgents: {
11
+ * poppy: { signIn: { mediated: poppyEmailCodeSignIn() } },
12
+ * }
13
+ * ```
14
+ *
15
+ * Sem enumeração de contas: e-mail sem conta, conta desativada, conta recusada
16
+ * por `eligible` ou e-mail acima do limite recebem a MESMA resposta, sem código
17
+ * (`accountId: null`) — a lib recusa qualquer código como errado. O envio não é
18
+ * esperado, então o tempo de resposta também não denuncia.
19
+ */
20
+ import { randomInt } from 'node:crypto';
21
+ import { supportsAccountStatus } from '../../accounts/account_store.js';
22
+ import { sendPoppySignInCodeEmail } from '../../host/default_mailer.js';
23
+ import { personalAgentsFor } from '../runtime.js';
24
+ /** `ana.souza@gmail.com` → `a***@gmail.com`: o bastante para a pessoa reconhecer. */
25
+ export function maskEmail(email) {
26
+ const at = email.lastIndexOf('@');
27
+ const user = at > 0 ? email.slice(0, at) : '';
28
+ const domain = at > 0 ? email.slice(at + 1) : '';
29
+ if (!user || !domain)
30
+ return '***';
31
+ return `${user[0]}***@${domain}`;
32
+ }
33
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
34
+ const HOUR_MS = 3600 * 1000;
35
+ const MAX_TRACKED = 10_000;
36
+ export function poppyEmailCodeSignIn(options = {}) {
37
+ const positive = (value, fallback, what) => {
38
+ if (value === undefined)
39
+ return fallback;
40
+ if (!Number.isFinite(value) || value < 1) {
41
+ throw new Error(`authkit: poppyEmailCodeSignIn ${what} precisa ser um número >= 1.`);
42
+ }
43
+ return Math.floor(value);
44
+ };
45
+ const codeTtl = positive(options.codeTtl, 600, 'codeTtl');
46
+ const maxPerHour = positive(options.maxPerHour, 5, 'maxPerHour');
47
+ const maxCodeAttempts = positive(options.maxCodeAttempts, 5, 'maxCodeAttempts');
48
+ const sentToText = options.sentTo ?? ((masked) => `Email to ${masked}`);
49
+ const sends = new Map();
50
+ /** `true` = ainda cabe um envio para este e-mail na última hora (e conta este). */
51
+ const claim = (email) => {
52
+ const now = Date.now();
53
+ const recent = (sends.get(email) ?? []).filter((t) => now - t < HOUR_MS);
54
+ if (recent.length >= maxPerHour) {
55
+ sends.set(email, recent);
56
+ return false;
57
+ }
58
+ recent.push(now);
59
+ sends.delete(email);
60
+ sends.set(email, recent);
61
+ if (sends.size > MAX_TRACKED)
62
+ sends.delete(sends.keys().next().value);
63
+ return true;
64
+ };
65
+ const eligibleAccount = async (ctx, email) => {
66
+ const service = await ctx.containerResolver.make('authkit.server');
67
+ const store = service.config.accountStore;
68
+ const account = await store.findByEmail(email);
69
+ if (!account)
70
+ return null;
71
+ if (supportsAccountStatus(store) && (await store.isDisabled(account.id)))
72
+ return null;
73
+ if (options.eligible) {
74
+ // Um `eligible` que lança vira "não elegível": um 500 só para algumas contas as denunciaria.
75
+ try {
76
+ if (!(await options.eligible(account)))
77
+ return null;
78
+ }
79
+ catch (error) {
80
+ ctx.logger.error({ err: error }, 'authkit: poppyEmailCodeSignIn eligible lançou');
81
+ return null;
82
+ }
83
+ }
84
+ return account;
85
+ };
86
+ return {
87
+ fields: [{ name: 'email', label: options.label ?? 'Email address', secret: false }],
88
+ ...(options.scopes ? { scopes: options.scopes } : {}),
89
+ codeTtl,
90
+ maxCodeAttempts,
91
+ async verify({ credentials, clientId, ctx }) {
92
+ const email = (credentials.email ?? '').trim().toLowerCase();
93
+ if (!EMAIL.test(email))
94
+ return { status: 'failed' };
95
+ const sentTo = sentToText(maskEmail(email));
96
+ const decoy = { status: 'code_required', accountId: null, sentTo };
97
+ // O limite conta para qualquer e-mail — com ou sem conta, a resposta não muda.
98
+ if (!claim(email))
99
+ return decoy;
100
+ const account = await eligibleAccount(ctx, email);
101
+ if (!account)
102
+ return decoy;
103
+ const code = String(randomInt(0, 1_000_000)).padStart(6, '0');
104
+ // Sem `await`: o caminho com conta não pode demorar mais que o sem conta.
105
+ void deliver(ctx, { email, code, clientId, expiresInMinutes: Math.ceil(codeTtl / 60) });
106
+ return { status: 'code_required', accountId: account.id, sentTo, code };
107
+ },
108
+ };
109
+ }
110
+ function hostOf(clientId) {
111
+ try {
112
+ return new URL(clientId).host;
113
+ }
114
+ catch {
115
+ return clientId;
116
+ }
117
+ }
118
+ async function deliver(ctx, data) {
119
+ try {
120
+ const runtime = await personalAgentsFor(ctx);
121
+ const agentName = runtime?.poppy
122
+ ? await runtime.poppy.registry.displayName(data.clientId)
123
+ : hostOf(data.clientId);
124
+ const service = await ctx.containerResolver.make('authkit.server');
125
+ const hook = service.config.mail?.onPoppySignInCode;
126
+ const input = {
127
+ email: data.email,
128
+ code: data.code,
129
+ agentName,
130
+ expiresInMinutes: data.expiresInMinutes,
131
+ };
132
+ if (hook)
133
+ await hook(input);
134
+ else
135
+ await sendPoppySignInCodeEmail(ctx, input);
136
+ }
137
+ catch (error) {
138
+ ctx.logger.error({ err: error }, 'authkit: o código do Poppy Mediated Sign-In não saiu');
139
+ }
140
+ }
@@ -78,6 +78,18 @@ export interface MailHooks {
78
78
  email: string;
79
79
  sudoUrl: string;
80
80
  }) => Promise<void>;
81
+ /**
82
+ * Código do Mediated Sign-In pronto do Poppy (`poppyEmailCodeSignIn`): um personal
83
+ * agent quer entrar na conta e a pessoa precisa dar o código a ele. Sem o hook, o
84
+ * host-kit envia pelo mailer default, com branding e i18n.
85
+ */
86
+ onPoppySignInCode?: (data: {
87
+ email: string;
88
+ code: string;
89
+ /** Nome do agente (metadata dele, ou o host do `client_id`). */
90
+ agentName: string;
91
+ expiresInMinutes: number;
92
+ }) => Promise<void>;
81
93
  /**
82
94
  * Disparado num login bem-sucedido a partir de um dispositivo NOVO (sem cookie
83
95
  * de dispositivo confiável válido para a conta). Best-effort, fire-and-forget:
@@ -66,6 +66,17 @@ export declare function sendMagicLinkEmail(ctx: HttpContext, data: {
66
66
  code?: string;
67
67
  channel?: 'code' | 'link';
68
68
  }): Promise<void>;
69
+ /**
70
+ * Envia o código do Mediated Sign-In pronto do Poppy (`poppyEmailCodeSignIn`) pelo
71
+ * mailer default do host. Só código, sem link: quem digita é a pessoa, no agente.
72
+ * Best-effort: no fallback (sem mail) loga o código, como o magic link loga o link; nunca lança.
73
+ */
74
+ export declare function sendPoppySignInCodeEmail(ctx: HttpContext, data: {
75
+ email: string;
76
+ code: string;
77
+ agentName: string;
78
+ expiresInMinutes: number;
79
+ }): Promise<void>;
69
80
  /**
70
81
  * Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo) pelo mailer default do host.
71
82
  *
@@ -294,6 +294,35 @@ export async function sendMagicLinkEmail(ctx, data) {
294
294
  ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar magic link de login');
295
295
  }
296
296
  }
297
+ /**
298
+ * Envia o código do Mediated Sign-In pronto do Poppy (`poppyEmailCodeSignIn`) pelo
299
+ * mailer default do host. Só código, sem link: quem digita é a pessoa, no agente.
300
+ * Best-effort: no fallback (sem mail) loga o código, como o magic link loga o link; nunca lança.
301
+ */
302
+ export async function sendPoppySignInCodeEmail(ctx, data) {
303
+ try {
304
+ const brand = resolveBrand(ctx);
305
+ const { messages: t, locale } = resolveMailMessages(ctx);
306
+ const params = { agent: data.agentName, minutes: data.expiresInMinutes };
307
+ const content = renderTransactionalEmail({
308
+ brand,
309
+ locale,
310
+ subject: translate(t, 'mail.poppy_sign_in.subject', params),
311
+ heading: translate(t, 'mail.poppy_sign_in.heading', params),
312
+ intro: translate(t, 'mail.poppy_sign_in.intro', params),
313
+ footnote: translate(t, 'mail.poppy_sign_in.fallback'),
314
+ code: data.code,
315
+ codeLabel: translate(t, 'mail.poppy_sign_in.code_label'),
316
+ });
317
+ const sent = await sendEmail(ctx, data.email, content);
318
+ if (!sent) {
319
+ ctx.logger.info({ email: data.email, agent: data.agentName, code: data.code }, 'authkit: código do Poppy Mediated Sign-In (dev — @adonisjs/mail ausente)');
320
+ }
321
+ }
322
+ catch (error) {
323
+ ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar o código do Poppy Mediated Sign-In');
324
+ }
325
+ }
297
326
  /**
298
327
  * Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo) pelo mailer default do host.
299
328
  *
@@ -561,6 +561,11 @@ export declare const DEFAULT_MESSAGES: {
561
561
  'mail.magic_link.code_subject': string;
562
562
  'mail.magic_link.code_intro': string;
563
563
  'mail.magic_link.code_only_label': string;
564
+ 'mail.poppy_sign_in.subject': string;
565
+ 'mail.poppy_sign_in.heading': string;
566
+ 'mail.poppy_sign_in.intro': string;
567
+ 'mail.poppy_sign_in.code_label': string;
568
+ 'mail.poppy_sign_in.fallback': string;
564
569
  'mail.sudo_link.subject': string;
565
570
  'mail.sudo_link.heading': string;
566
571
  'mail.sudo_link.intro': string;
@@ -1315,6 +1320,11 @@ export declare const PT_BR_MESSAGES: {
1315
1320
  'mail.magic_link.code_subject': string;
1316
1321
  'mail.magic_link.code_intro': string;
1317
1322
  'mail.magic_link.code_only_label': string;
1323
+ 'mail.poppy_sign_in.subject': string;
1324
+ 'mail.poppy_sign_in.heading': string;
1325
+ 'mail.poppy_sign_in.intro': string;
1326
+ 'mail.poppy_sign_in.code_label': string;
1327
+ 'mail.poppy_sign_in.fallback': string;
1318
1328
  'mail.sudo_link.subject': string;
1319
1329
  'mail.sudo_link.heading': string;
1320
1330
  'mail.sudo_link.intro': string;
@@ -604,6 +604,12 @@ export const DEFAULT_MESSAGES = {
604
604
  'mail.magic_link.code_subject': 'Your login code',
605
605
  'mail.magic_link.code_intro': 'Use the code below to sign in. It expires shortly and can be used once.',
606
606
  'mail.magic_link.code_only_label': 'Enter this code to sign in:',
607
+ // Código do Mediated Sign-In do Poppy: um personal agent pede para entrar na conta.
608
+ 'mail.poppy_sign_in.subject': 'Your code to connect {agent}',
609
+ 'mail.poppy_sign_in.heading': 'Connect {agent} to your account',
610
+ 'mail.poppy_sign_in.intro': '{agent} asked to sign in to your account. If that was you, give it the code below. It expires in {minutes} minutes and works once.',
611
+ 'mail.poppy_sign_in.code_label': 'Your code:',
612
+ 'mail.poppy_sign_in.fallback': 'If you did not ask for this, ignore this email — nobody gets in without the code. Never share it with anyone else.',
607
613
  // Link de CONFIRMAÇÃO DE IDENTIDADE (sudo). Copy DISTINTA da do magic link de
608
614
  // login de propósito: este link não autentica ninguém — quem o abre já está
609
615
  // logado e só reprova que é quem diz ser. Prometer "entre na sua conta" num
@@ -1444,6 +1450,12 @@ export const PT_BR_MESSAGES = {
1444
1450
  'mail.magic_link.code_subject': 'Seu código de login',
1445
1451
  'mail.magic_link.code_intro': 'Use o código abaixo para entrar. Ele expira em instantes e serve para um único acesso.',
1446
1452
  'mail.magic_link.code_only_label': 'Digite este código para entrar:',
1453
+ // Código do Mediated Sign-In do Poppy — ver o comentário no catálogo `en`.
1454
+ 'mail.poppy_sign_in.subject': 'Seu código para conectar {agent}',
1455
+ 'mail.poppy_sign_in.heading': 'Conecte {agent} à sua conta',
1456
+ 'mail.poppy_sign_in.intro': '{agent} pediu para entrar na sua conta. Se foi você, informe o código abaixo. Ele expira em {minutes} minutos e serve uma única vez.',
1457
+ 'mail.poppy_sign_in.code_label': 'Seu código:',
1458
+ 'mail.poppy_sign_in.fallback': 'Se não foi você, ignore este e-mail — ninguém entra sem o código. Nunca o compartilhe com outra pessoa.',
1447
1459
  // Link de CONFIRMAÇÃO DE IDENTIDADE (sudo) — ver o comentário no catálogo `en`.
1448
1460
  'mail.sudo_link.subject': 'Confirme que é você',
1449
1461
  'mail.sudo_link.heading': 'Confirme sua identidade',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.79.0",
3
+ "version": "0.80.1",
4
4
  "description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
5
5
  "license": "MIT",
6
6
  "author": "dudousxd",
@@ -60,17 +60,17 @@
60
60
  "@adonis-agora/durable": ">=0.7.0 <1.0.0",
61
61
  "@adonis-agora/media": ">=0.5.0 <1.0.0",
62
62
  "@adonis-agora/telescope": ">=0.1.0 <1.0.0",
63
- "@adonisjs/ally": "6.3.0",
64
- "@adonisjs/auth": "10.1.0",
65
- "@adonisjs/core": "7.5.0",
66
- "@adonisjs/drive": "4.0.0",
67
- "@adonisjs/lock": "2.1.0",
68
- "@adonisjs/lucid": "22.4.2",
69
- "@adonisjs/redis": "11.0.0",
70
- "@adonisjs/session": "8.1.0",
71
- "@adonisjs/shield": "9.0.0",
72
- "@vinejs/vine": "4.4.0",
73
- "edge.js": "6.5.1"
63
+ "@adonisjs/ally": "^6.3.0",
64
+ "@adonisjs/auth": "^10.1.0",
65
+ "@adonisjs/core": "^7.5.0",
66
+ "@adonisjs/drive": "^4.0.0",
67
+ "@adonisjs/lock": "^2.1.0",
68
+ "@adonisjs/lucid": "^22.4.2",
69
+ "@adonisjs/redis": "^9.2.0 || ^10.0.0 || ^11.0.0",
70
+ "@adonisjs/session": "^8.1.0",
71
+ "@adonisjs/shield": "^9.0.0",
72
+ "@vinejs/vine": "^4.4.0",
73
+ "edge.js": "^6.5.1"
74
74
  },
75
75
  "peerDependenciesMeta": {
76
76
  "@adonis-agora/durable": {
@@ -113,7 +113,7 @@
113
113
  "qrcode": "1.5.4"
114
114
  },
115
115
  "devDependencies": {
116
- "@adonis-agora/authkit-react": "0.24.0",
116
+ "@adonis-agora/authkit-react": "0.24.1",
117
117
  "@adonis-agora/durable": "0.40.0",
118
118
  "@adonis-agora/telescope": "0.21.1",
119
119
  "@adonisjs/ally": "6.3.0",
@@ -145,7 +145,7 @@ JWKS, and fakes the authenticator for controller tests:
145
145
 
146
146
  ```ts
147
147
  import { mintTestIdToken, serveJwks, fakeAuthenticator } from '@adonis-agora/authkit-testing'
148
- import { resolvers } from '@adonis-agora/authkit-client'
148
+ import { conventionEndpoints, resolvers } from '@adonis-agora/authkit-client'
149
149
 
150
150
  const { token, jwks } = await mintTestIdToken({
151
151
  issuer: 'https://idp.test',
@@ -160,6 +160,8 @@ const resolver = await factory.resolver({
160
160
  clientId: 'my-app',
161
161
  sessionKey: 'authkit',
162
162
  globalRolesClaim: 'roles',
163
+ endpoints: async () => conventionEndpoints('https://idp.test'),
164
+ logger: { warn: () => {} },
163
165
  })
164
166
  const ctx = { auth: fakeAuthenticator({ identity: null }) } // anonymous request fake
165
167
  await served.close()