@adonis-agora/authkit-server 0.57.1 → 0.58.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/build/index.d.ts +2 -2
- package/build/index.js +1 -1
- package/build/providers/authkit_server_provider.d.ts +1 -12
- package/build/providers/authkit_server_provider.js +13 -0
- package/build/src/audit/audit_sink.d.ts +11 -2
- package/build/src/audit/audit_sink.js +104 -1
- package/build/src/define_config.d.ts +36 -1
- package/build/src/define_config.js +3 -0
- package/build/src/doctor/checks.d.ts +10 -0
- package/build/src/doctor/checks.js +38 -0
- package/build/src/host/admin_api/admin_orgs_service.d.ts +9 -2
- package/build/src/host/admin_api/admin_orgs_service.js +29 -16
- package/build/src/host/admin_api/api_orgs_controller.js +1 -1
- package/build/src/host/admin_api/dto.d.ts +1 -1
- package/build/src/host/admin_console/console_impersonation_controller.js +37 -3
- package/build/src/host/admin_console/console_orgs_controller.js +1 -1
- package/build/src/host/branding.d.ts +19 -0
- package/build/src/host/branding.js +25 -0
- package/build/src/host/controllers/account_orgs_controller.js +21 -14
- package/build/src/host/default_mailer.d.ts +21 -0
- package/build/src/host/default_mailer.js +41 -0
- package/build/src/host/i18n.d.ts +10 -0
- package/build/src/host/i18n.js +15 -2
- package/build/src/host/impersonation.d.ts +23 -2
- package/build/src/host/impersonation.js +21 -5
- package/build/src/host/ui-dist/assets/{index-pAbBjdHC.js → index-6cE5JMyP.js} +1 -1
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/observability/telescope/data_providers.js +4 -1
- package/build/src/provider/oidc_service.js +18 -6
- package/build/types.d.ts +21 -1
- package/package.json +3 -3
|
@@ -3,6 +3,7 @@ import { supportsOrganizations } from '../../accounts/account_store.js';
|
|
|
3
3
|
import { getAccountLoginUrl } from '../account_login_url.js';
|
|
4
4
|
import { accountPath } from '../account_paths.js';
|
|
5
5
|
import { ACTIVE_ORG_COOKIE, ACTIVE_ORG_COOKIE_TTL, encodeActiveOrgCookie, } from '../active_org_cookie.js';
|
|
6
|
+
import { sendOrgInvitationEmail } from '../default_mailer.js';
|
|
6
7
|
import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
|
|
7
8
|
import { authkitOrigin } from '../origin.js';
|
|
8
9
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
@@ -188,25 +189,31 @@ export default class AccountOrgsController {
|
|
|
188
189
|
invitedBy: accountId,
|
|
189
190
|
ttlHours: cfg.organizations.invitationTtlHours,
|
|
190
191
|
});
|
|
191
|
-
//
|
|
192
|
-
|
|
192
|
+
// Sends the invitation email (best-effort). The host hook wins when present;
|
|
193
|
+
// otherwise the host-kit sends its own branded/translated email, like every
|
|
194
|
+
// other email of the library. Delivery NEVER breaks invitation creation.
|
|
195
|
+
try {
|
|
193
196
|
const org = await store.findOrgById(params.id);
|
|
194
197
|
const acceptUrl = `${authkitOrigin(cfg)}${accountPath('orgs')}/invitations/${token}/accept`;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
const payload = {
|
|
199
|
+
email,
|
|
200
|
+
invitationId: invitation.id,
|
|
201
|
+
orgName: org?.name ?? params.id,
|
|
202
|
+
orgSlug: org?.slug ?? params.id,
|
|
203
|
+
role,
|
|
204
|
+
acceptUrl,
|
|
205
|
+
token,
|
|
206
|
+
};
|
|
207
|
+
if (cfg.mail?.onOrgInvitation) {
|
|
208
|
+
await cfg.mail.onOrgInvitation(payload);
|
|
205
209
|
}
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
else {
|
|
211
|
+
await sendOrgInvitationEmail(ctx, payload);
|
|
208
212
|
}
|
|
209
213
|
}
|
|
214
|
+
catch {
|
|
215
|
+
// best-effort
|
|
216
|
+
}
|
|
210
217
|
await cfg.audit?.record({
|
|
211
218
|
type: 'organization.invitation_sent',
|
|
212
219
|
accountId,
|
|
@@ -134,6 +134,27 @@ export declare function sendOtpUnlockEmail(ctx: HttpContext, data: {
|
|
|
134
134
|
email: string;
|
|
135
135
|
unlockUrl: string;
|
|
136
136
|
}): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* Sends the organization invitation email through the host default mailer.
|
|
139
|
+
*
|
|
140
|
+
* WHY IT EXISTS. `mail.onOrgInvitation` used to be the ONLY hook without a
|
|
141
|
+
* default-mailer fallback: without it the invitation row was created and NO
|
|
142
|
+
* email was ever sent — a silent failure where the invited person never learned
|
|
143
|
+
* about the invitation. This restores the same posture as every other email of
|
|
144
|
+
* the library: the host hook wins when present, otherwise the host-kit itself
|
|
145
|
+
* sends a branded/translated email; with no mailer at all, the link is logged (dev).
|
|
146
|
+
*
|
|
147
|
+
* Best-effort: never throws — invitation creation must not depend on delivery.
|
|
148
|
+
*/
|
|
149
|
+
export declare function sendOrgInvitationEmail(ctx: HttpContext, data: {
|
|
150
|
+
email: string;
|
|
151
|
+
invitationId: string;
|
|
152
|
+
orgName: string;
|
|
153
|
+
orgSlug: string;
|
|
154
|
+
role: string;
|
|
155
|
+
acceptUrl: string;
|
|
156
|
+
token: string;
|
|
157
|
+
}): Promise<void>;
|
|
137
158
|
/**
|
|
138
159
|
* Envia o e-mail de verificação pelo mailer default do host.
|
|
139
160
|
* Best-effort: no fallback (sem mail) loga o link; nunca lança.
|
|
@@ -475,6 +475,47 @@ export async function sendOtpUnlockEmail(ctx, data) {
|
|
|
475
475
|
ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar e-mail de desbloqueio OTP');
|
|
476
476
|
}
|
|
477
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* Sends the organization invitation email through the host default mailer.
|
|
480
|
+
*
|
|
481
|
+
* WHY IT EXISTS. `mail.onOrgInvitation` used to be the ONLY hook without a
|
|
482
|
+
* default-mailer fallback: without it the invitation row was created and NO
|
|
483
|
+
* email was ever sent — a silent failure where the invited person never learned
|
|
484
|
+
* about the invitation. This restores the same posture as every other email of
|
|
485
|
+
* the library: the host hook wins when present, otherwise the host-kit itself
|
|
486
|
+
* sends a branded/translated email; with no mailer at all, the link is logged (dev).
|
|
487
|
+
*
|
|
488
|
+
* Best-effort: never throws — invitation creation must not depend on delivery.
|
|
489
|
+
*/
|
|
490
|
+
export async function sendOrgInvitationEmail(ctx, data) {
|
|
491
|
+
try {
|
|
492
|
+
const brand = resolveBrand(ctx);
|
|
493
|
+
const { messages: t, locale } = resolveMailMessages(ctx);
|
|
494
|
+
const content = renderTransactionalEmail({
|
|
495
|
+
brand,
|
|
496
|
+
locale,
|
|
497
|
+
linkFallback: translate(t, 'mail.common.link_fallback'),
|
|
498
|
+
subject: translate(t, 'mail.org_invitation.subject', { org: data.orgName }),
|
|
499
|
+
heading: translate(t, 'mail.org_invitation.heading', { org: data.orgName }),
|
|
500
|
+
intro: translate(t, 'mail.org_invitation.intro', { org: data.orgName, role: data.role }),
|
|
501
|
+
ctaLabel: translate(t, 'mail.org_invitation.cta'),
|
|
502
|
+
ctaUrl: data.acceptUrl,
|
|
503
|
+
footnote: translate(t, 'mail.org_invitation.fallback'),
|
|
504
|
+
});
|
|
505
|
+
const sent = await sendEmail(ctx, data.email, content);
|
|
506
|
+
if (!sent) {
|
|
507
|
+
ctx.logger?.info({
|
|
508
|
+
acceptUrl: data.acceptUrl,
|
|
509
|
+
email: data.email,
|
|
510
|
+
orgSlug: data.orgSlug,
|
|
511
|
+
invitationId: data.invitationId,
|
|
512
|
+
}, 'authkit: convite de organização (dev — @adonisjs/mail ausente)');
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
ctx.logger?.error({ err: error, email: data.email, orgSlug: data.orgSlug }, 'authkit: falha ao enviar convite de organização');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
478
519
|
/**
|
|
479
520
|
* Envia o e-mail de verificação pelo mailer default do host.
|
|
480
521
|
* Best-effort: no fallback (sem mail) loga o link; nunca lança.
|
package/build/src/host/i18n.d.ts
CHANGED
|
@@ -570,6 +570,11 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
570
570
|
'mail.security_notice.kind_passkey_added': string;
|
|
571
571
|
'mail.security_notice.kind_passkey_removed': string;
|
|
572
572
|
'mail.security_notice.kind_email_changed': string;
|
|
573
|
+
'mail.org_invitation.subject': string;
|
|
574
|
+
'mail.org_invitation.heading': string;
|
|
575
|
+
'mail.org_invitation.intro': string;
|
|
576
|
+
'mail.org_invitation.cta': string;
|
|
577
|
+
'mail.org_invitation.fallback': string;
|
|
573
578
|
'mail.otp_unlock.subject': string;
|
|
574
579
|
'mail.otp_unlock.heading': string;
|
|
575
580
|
'mail.otp_unlock.intro': string;
|
|
@@ -1309,6 +1314,11 @@ export declare const PT_BR_MESSAGES: {
|
|
|
1309
1314
|
'admin.settings.sudo_mode_from_config': string;
|
|
1310
1315
|
'admin.settings.sudo_mode_from_setting': string;
|
|
1311
1316
|
'admin.settings.sudo_mode_grace_label': string;
|
|
1317
|
+
'mail.org_invitation.subject': string;
|
|
1318
|
+
'mail.org_invitation.heading': string;
|
|
1319
|
+
'mail.org_invitation.intro': string;
|
|
1320
|
+
'mail.org_invitation.cta': string;
|
|
1321
|
+
'mail.org_invitation.fallback': string;
|
|
1312
1322
|
'mail.otp_unlock.subject': string;
|
|
1313
1323
|
'mail.otp_unlock.heading': string;
|
|
1314
1324
|
'mail.otp_unlock.intro': string;
|
package/build/src/host/i18n.js
CHANGED
|
@@ -323,7 +323,7 @@ export const DEFAULT_MESSAGES = {
|
|
|
323
323
|
'admin.impersonation.title': 'Impersonate this user',
|
|
324
324
|
'admin.impersonation.help': 'Token Exchange (RFC 8693) lets an admin act as this user. There is no auth bypass: you exchange YOUR OWN admin access token for one scoped to the target.',
|
|
325
325
|
'admin.impersonation.curl_label': 'Ready-to-run request',
|
|
326
|
-
'admin.impersonation.note': 'Replace <ADMIN_ACCESS_TOKEN> with a current admin access token. The resulting id_token carries act={sub: admin}; the
|
|
326
|
+
'admin.impersonation.note': 'Replace <ADMIN_ACCESS_TOKEN> with a current admin access token. The resulting id_token carries act={sub: admin}; the exchange is audited as impersonation.',
|
|
327
327
|
'admin.impersonation.no_client': 'No client has the token-exchange grant enabled. Add "urn:ietf:params:oauth:grant-type:token-exchange" to a client to enable impersonation.',
|
|
328
328
|
// Console admin — clients.
|
|
329
329
|
'admin.clients.page_title': 'OAuth clients',
|
|
@@ -621,6 +621,13 @@ export const DEFAULT_MESSAGES = {
|
|
|
621
621
|
'mail.security_notice.kind_passkey_added': 'passkey added',
|
|
622
622
|
'mail.security_notice.kind_passkey_removed': 'passkey removed',
|
|
623
623
|
'mail.security_notice.kind_email_changed': 'email address changed',
|
|
624
|
+
// E-mail de convite para organização (fallback quando o host não define o
|
|
625
|
+
// hook `mail.onOrgInvitation`).
|
|
626
|
+
'mail.org_invitation.subject': 'You have been invited to join {org}',
|
|
627
|
+
'mail.org_invitation.heading': 'You have been invited to join {org}',
|
|
628
|
+
'mail.org_invitation.intro': 'You have been invited to join the organization {org} as {role}. Accept the invitation to get access.',
|
|
629
|
+
'mail.org_invitation.cta': 'Accept invitation',
|
|
630
|
+
'mail.org_invitation.fallback': 'If you were not expecting this invitation, you can ignore this email.',
|
|
624
631
|
// E-mail de desbloqueio do fator OTP.
|
|
625
632
|
'mail.otp_unlock.subject': 'Two-factor authentication unlock',
|
|
626
633
|
'mail.otp_unlock.heading': 'Unlock your two-factor authentication',
|
|
@@ -1161,7 +1168,7 @@ export const PT_BR_MESSAGES = {
|
|
|
1161
1168
|
'admin.impersonation.title': 'Personificar este usuário',
|
|
1162
1169
|
'admin.impersonation.help': 'O Token Exchange (RFC 8693) permite que um admin aja como este usuário. Não há bypass de auth: você troca o SEU PRÓPRIO access token de admin por um escopado ao alvo.',
|
|
1163
1170
|
'admin.impersonation.curl_label': 'Requisição pronta para rodar',
|
|
1164
|
-
'admin.impersonation.note': 'Troque <ADMIN_ACCESS_TOKEN> por um access token de admin válido. O id_token resultante carrega act={sub: admin}; o
|
|
1171
|
+
'admin.impersonation.note': 'Troque <ADMIN_ACCESS_TOKEN> por um access token de admin válido. O id_token resultante carrega act={sub: admin}; o exchange é auditado como impersonation.',
|
|
1165
1172
|
'admin.impersonation.no_client': 'Nenhum client tem o grant token-exchange habilitado. Adicione "urn:ietf:params:oauth:grant-type:token-exchange" a um client para habilitar a personificação.',
|
|
1166
1173
|
// Console admin — clients.
|
|
1167
1174
|
'admin.clients.page_title': 'Clients OAuth',
|
|
@@ -1443,6 +1450,12 @@ export const PT_BR_MESSAGES = {
|
|
|
1443
1450
|
'admin.settings.sudo_mode_from_config': 'Fonte: padrão',
|
|
1444
1451
|
'admin.settings.sudo_mode_from_setting': 'Fonte: setting em runtime',
|
|
1445
1452
|
'admin.settings.sudo_mode_grace_label': 'Período de graça (minutos)',
|
|
1453
|
+
// E-mail de convite para organização (pt-BR).
|
|
1454
|
+
'mail.org_invitation.subject': 'Você foi convidado para {org}',
|
|
1455
|
+
'mail.org_invitation.heading': 'Você foi convidado para {org}',
|
|
1456
|
+
'mail.org_invitation.intro': 'Você foi convidado para participar da organização {org} como {role}. Aceite o convite para ter acesso.',
|
|
1457
|
+
'mail.org_invitation.cta': 'Aceitar convite',
|
|
1458
|
+
'mail.org_invitation.fallback': 'Se você não esperava este convite, pode ignorar este e-mail.',
|
|
1446
1459
|
// E-mail de desbloqueio do fator OTP (pt-BR).
|
|
1447
1460
|
'mail.otp_unlock.subject': 'Desbloqueio da verificação em duas etapas',
|
|
1448
1461
|
'mail.otp_unlock.heading': 'Desbloqueie sua verificação em duas etapas',
|
|
@@ -17,9 +17,30 @@ export interface ImpersonationPanel {
|
|
|
17
17
|
/** Comando curl pronto (subject_token como placeholder a preencher). */
|
|
18
18
|
curl: string;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Um client candidato a hospedar o token-exchange. Modelado estruturalmente para
|
|
22
|
+
* aceitar tanto os clients ESTÁTICOS do config (`ResolvedServerConfig['clients']`,
|
|
23
|
+
* que trazem o secret) quanto os clients de RUNTIME lidos do adapter pelo
|
|
24
|
+
* `AdminClientsService` (que NÃO trazem o secret — ele não é recuperável).
|
|
25
|
+
*/
|
|
26
|
+
export interface ImpersonationClientLike {
|
|
27
|
+
clientId: string;
|
|
28
|
+
clientSecret?: string;
|
|
29
|
+
grants?: string[];
|
|
30
|
+
/** Runtime clients reportam confidencialidade sem expor o secret. */
|
|
31
|
+
confidential?: boolean;
|
|
32
|
+
}
|
|
20
33
|
/**
|
|
21
34
|
* Monta o painel de impersonation para o `targetId`. Retorna `null` quando NENHUM
|
|
22
|
-
* client
|
|
35
|
+
* client candidato tem o grant token-exchange habilitado (sem ele o fluxo não
|
|
23
36
|
* funciona) — a UI então mostra a instrução de habilitar o grant.
|
|
37
|
+
*
|
|
38
|
+
* ⚠️ `clients` é OBRIGATÓRIO na prática. `cfg.clients` é sempre `[]` num host
|
|
39
|
+
* real: clients são 100% de runtime (console admin / Admin API / `authkit:clients:create`),
|
|
40
|
+
* e o config resolvido nunca os carrega — é o que o `checkClients` do doctor
|
|
41
|
+
* afirma. Passar só o config faria o painel devolver `null` SEMPRE, que foi
|
|
42
|
+
* exatamente o bug: o botão do console nunca teve um client para oferecer.
|
|
43
|
+
* O fallback para `cfg.clients` existe só para hosts que ainda declaram clients
|
|
44
|
+
* estáticos e para os testes que os declaram.
|
|
24
45
|
*/
|
|
25
|
-
export declare function buildImpersonationPanel(cfg: Pick<ResolvedServerConfig, 'issuer' | 'clients'>, targetId: string): ImpersonationPanel | null;
|
|
46
|
+
export declare function buildImpersonationPanel(cfg: Pick<ResolvedServerConfig, 'issuer' | 'clients'>, targetId: string, clients?: readonly ImpersonationClientLike[]): ImpersonationPanel | null;
|
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
const TOKEN_EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange';
|
|
2
2
|
const ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
|
|
3
|
+
/** Placeholder do secret quando o client é confidencial mas o secret não é legível. */
|
|
4
|
+
const SECRET_PLACEHOLDER = '<CLIENT_SECRET>';
|
|
3
5
|
/**
|
|
4
6
|
* Monta o painel de impersonation para o `targetId`. Retorna `null` quando NENHUM
|
|
5
|
-
* client
|
|
7
|
+
* client candidato tem o grant token-exchange habilitado (sem ele o fluxo não
|
|
6
8
|
* funciona) — a UI então mostra a instrução de habilitar o grant.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ `clients` é OBRIGATÓRIO na prática. `cfg.clients` é sempre `[]` num host
|
|
11
|
+
* real: clients são 100% de runtime (console admin / Admin API / `authkit:clients:create`),
|
|
12
|
+
* e o config resolvido nunca os carrega — é o que o `checkClients` do doctor
|
|
13
|
+
* afirma. Passar só o config faria o painel devolver `null` SEMPRE, que foi
|
|
14
|
+
* exatamente o bug: o botão do console nunca teve um client para oferecer.
|
|
15
|
+
* O fallback para `cfg.clients` existe só para hosts que ainda declaram clients
|
|
16
|
+
* estáticos e para os testes que os declaram.
|
|
7
17
|
*/
|
|
8
|
-
export function buildImpersonationPanel(cfg, targetId) {
|
|
9
|
-
const
|
|
18
|
+
export function buildImpersonationPanel(cfg, targetId, clients) {
|
|
19
|
+
const candidates = clients && clients.length > 0 ? clients : (cfg.clients ?? []);
|
|
20
|
+
const client = candidates.find((c) => (c.grants ?? []).includes(TOKEN_EXCHANGE));
|
|
10
21
|
if (!client)
|
|
11
22
|
return null;
|
|
12
23
|
const tokenEndpoint = `${cfg.issuer.replace(/\/+$/, '')}/token`;
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
// Confidencial = tem secret conhecido, ou o adapter marcou como confidencial.
|
|
25
|
+
// No segundo caso o secret NÃO é recuperável (o adapter guarda o hash/valor
|
|
26
|
+
// opaco e a lib mostra o secret uma única vez na criação), então o curl sai
|
|
27
|
+
// com um placeholder em vez de mentir com uma string vazia.
|
|
28
|
+
const confidential = client.confidential ?? client.clientSecret !== undefined;
|
|
29
|
+
const auth = confidential
|
|
30
|
+
? ` -u '${client.clientId}:${client.clientSecret ?? SECRET_PLACEHOLDER}' \\\n`
|
|
15
31
|
: ` -d 'client_id=${client.clientId}' \\\n`;
|
|
16
32
|
const curl = `curl -X POST '${tokenEndpoint}' \\\n${auth} -d 'grant_type=${TOKEN_EXCHANGE}' \\\n -d 'subject_token=<ADMIN_ACCESS_TOKEN>' \\\n -d 'subject_token_type=${ACCESS_TOKEN_TYPE}' \\\n -d 'requested_subject=${targetId}' \\\n -d 'scope=openid profile email'`;
|
|
17
33
|
return {
|