@adonis-agora/authkit-server 0.68.4 → 0.70.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 (54) hide show
  1. package/build/commands/import_users.js +6 -1
  2. package/build/index.d.ts +5 -1
  3. package/build/index.js +7 -1
  4. package/build/src/accounts/account_store.d.ts +40 -0
  5. package/build/src/accounts/account_store.js +20 -0
  6. package/build/src/accounts/lucid_store/mfa.js +22 -0
  7. package/build/src/audit/audit_sink.d.ts +1 -1
  8. package/build/src/audit/audit_sink.js +4 -0
  9. package/build/src/commands/import_users.d.ts +7 -0
  10. package/build/src/commands/import_users.js +15 -3
  11. package/build/src/define_config.d.ts +81 -0
  12. package/build/src/define_config.js +18 -3
  13. package/build/src/host/account_api/account_api_controller.d.ts +2 -0
  14. package/build/src/host/account_api/account_api_controller.js +22 -3
  15. package/build/src/host/account_api/account_mfa_api_controller.d.ts +101 -0
  16. package/build/src/host/account_api/account_mfa_api_controller.js +286 -0
  17. package/build/src/host/account_api/account_orgs_api_controller.d.ts +126 -0
  18. package/build/src/host/account_api/account_orgs_api_controller.js +468 -0
  19. package/build/src/host/account_lockout.js +7 -2
  20. package/build/src/host/admin_api/admin_orgs_service.js +3 -2
  21. package/build/src/host/admin_api/admin_users_service.js +13 -3
  22. package/build/src/host/admin_api/dto.d.ts +1 -1
  23. package/build/src/host/admin_validators.d.ts +2 -2
  24. package/build/src/host/admin_validators.js +3 -2
  25. package/build/src/host/controllers/account_mfa_controller.js +1 -2
  26. package/build/src/host/controllers/account_orgs_controller.js +19 -11
  27. package/build/src/host/controllers/account_security_controller.js +3 -1
  28. package/build/src/host/controllers/account_session_controller.js +17 -6
  29. package/build/src/host/controllers/interaction_controller.d.ts +10 -0
  30. package/build/src/host/controllers/interaction_controller.js +93 -34
  31. package/build/src/host/controllers/registration_controller.js +35 -9
  32. package/build/src/host/controllers/social_controller.js +11 -2
  33. package/build/src/host/email_identifier.d.ts +92 -0
  34. package/build/src/host/email_identifier.js +234 -0
  35. package/build/src/host/idp_session_bridge.d.ts +55 -0
  36. package/build/src/host/idp_session_bridge.js +108 -0
  37. package/build/src/host/middleware/account_auth.js +3 -2
  38. package/build/src/host/org_policy.d.ts +26 -0
  39. package/build/src/host/org_policy.js +35 -0
  40. package/build/src/host/passkey_registration_challenge.d.ts +12 -0
  41. package/build/src/host/passkey_registration_challenge.js +12 -0
  42. package/build/src/host/register_auth_host.js +56 -1
  43. package/build/src/host/runtime_toggles.d.ts +2 -2
  44. package/build/src/host/runtime_toggles.js +3 -1
  45. package/build/src/host/sudo_mode.d.ts +17 -0
  46. package/build/src/host/sudo_mode.js +31 -12
  47. package/build/src/host/ui-dist/assets/{index-D9CYQnZR.js → index-Dct63ai-.js} +2 -2
  48. package/build/src/host/ui-dist/index.html +1 -1
  49. package/build/src/host/validators.d.ts +5 -5
  50. package/build/src/host/validators.js +17 -5
  51. package/build/src/provider/build_provider.js +46 -14
  52. package/build/src/provider/registration_policy.d.ts +99 -0
  53. package/build/src/provider/registration_policy.js +229 -0
  54. package/package.json +2 -2
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Account Self-Service JSON API — SEGUNDO FATOR (TOTP + passkeys)
3
+ *
4
+ * Espelho JSON do `AccountMfaController`, para o host que desenha a própria
5
+ * tela de "segundo fator" e não pode navegar o browser no meio do fluxo.
6
+ *
7
+ * Mapa de rotas (sob o `accountGuard`; mutantes sob o CSRF do shield do host):
8
+ * POST /account/api/mfa/totp/enroll → inicia o enrolamento (sudo)
9
+ * POST /account/api/mfa/totp/confirm → confirma com o código (throttled)
10
+ * POST /account/api/mfa/totp/disable → desliga o MFA (sudo)
11
+ * POST /account/api/mfa/recovery-codes → regenera os códigos (sudo)
12
+ * POST /account/api/mfa/passkeys/options → options da cerimônia de registro
13
+ * POST /account/api/mfa/passkeys/verify → verifica o attestation (sudo)
14
+ *
15
+ * Paridade de gates com o console HTML, endpoint a endpoint:
16
+ *
17
+ * | ação | console HTML | aqui |
18
+ * | ---------------- | ------------ | ---------------- |
19
+ * | enroll | sudo | sudo → 403 JSON |
20
+ * | confirm | sem sudo | sem sudo |
21
+ * | disable | sudo | sudo → 403 JSON |
22
+ * | passkey options | sem sudo | sem sudo |
23
+ * | passkey verify | sudo | sudo → 403 JSON |
24
+ * | recovery codes | (não existe) | sudo → 403 JSON |
25
+ *
26
+ * A única diferença é a FORMA da recusa: `requireSudo` devolve um redirect para
27
+ * `/account/confirm`, que numa SPA chega como uma página HTML onde ela esperava
28
+ * JSON. Aqui a recusa vira `403 { error: { code: 'sudo_required' } }` e a tela
29
+ * do host manda o usuário confirmar identidade por conta própria. Mesma
30
+ * política, resposta legível por máquina.
31
+ *
32
+ * `confirm` NÃO exige sudo — porque o `enroll` que criou o segredo pendente já
33
+ * exigiu, e pedir de novo no passo seguinte quebraria o enrolamento de quem
34
+ * demorou a digitar o código. É exatamente o que o console faz. O que o console
35
+ * não tem, e aqui existe, é o THROTTLE: o código de 6 dígitos é adivinhável, e
36
+ * a rota carrega o bucket de sudo (por IP) — mais apertado que o form, nunca
37
+ * mais frouxo.
38
+ *
39
+ * Os dois caminhos (clássico e JSON) COMPARTILHAM o slot do desafio WebAuthn
40
+ * (`PASSKEY_REG_CHALLENGE_KEY`), então um `options` pedido num deles pode ser
41
+ * finalizado pelo outro e nunca existem dois desafios vivos na mesma sessão.
42
+ */
43
+ import '../augmentations.js';
44
+ import QRCode from 'qrcode';
45
+ import { supportsMfa, supportsPasskeys, supportsRecoveryCodeRegeneration, } from '../../accounts/account_store.js';
46
+ import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
47
+ import { translate } from '../i18n.js';
48
+ import { PASSKEY_REG_CHALLENGE_KEY } from '../passkey_registration_challenge.js';
49
+ import { resolveRuntimeSettings } from '../runtime_settings.js';
50
+ import { dispatchSecurityNotice } from '../security_notice_service.js';
51
+ import { isSudoSatisfied } from '../sudo_mode.js';
52
+ /** Erro JSON padrão — mesmo envelope do `account_api_controller`. */
53
+ function apiErr(code, message) {
54
+ return { error: { code, message } };
55
+ }
56
+ export default class AccountMfaApiController {
57
+ // ─── POST /account/api/mfa/totp/enroll ──────────────────────────────────
58
+ /**
59
+ * Inicia o enrolamento TOTP: segredo pendente + `otpauth://` URI + o QR já
60
+ * renderizado como data-URL (o console renderiza server-side; aqui a tela do
61
+ * host recebe pronto e decide se mostra o QR, o segredo, ou os dois).
62
+ */
63
+ async enrollTotp(ctx) {
64
+ const c = await this.#mfaContext(ctx);
65
+ if (!c)
66
+ return;
67
+ if (!(await this.#requireSudoJson(ctx)))
68
+ return;
69
+ const started = await c.store.startTotpEnrollment(c.userId);
70
+ if (!started) {
71
+ return ctx.response
72
+ .status(422)
73
+ .send(apiErr('enroll_failed', 'Could not start TOTP enrollment.'));
74
+ }
75
+ const qrDataUrl = await QRCode.toDataURL(started.otpauthUri);
76
+ return { secret: started.secret, otpauthUri: started.otpauthUri, qrDataUrl };
77
+ }
78
+ // ─── POST /account/api/mfa/totp/confirm ─────────────────────────────────
79
+ /**
80
+ * Confirma o enrolamento com o código do app autenticador. Sucesso ativa o
81
+ * MFA e devolve os recovery codes — UMA vez, como no console (lá eles vão num
82
+ * flash; aqui, no corpo da resposta, que é o equivalente headless).
83
+ */
84
+ async confirmTotp(ctx) {
85
+ const c = await this.#mfaContext(ctx);
86
+ if (!c)
87
+ return;
88
+ const code = String(ctx.request.input('code', '') ?? '').trim();
89
+ const result = await c.store.confirmTotpEnrollment(c.userId, code);
90
+ if (!result.ok) {
91
+ // NÃO regenera o segredo pendente: o usuário já escaneou o QR, e um
92
+ // segredo novo invalidaria o app autenticador dele. Mesma decisão do
93
+ // console — a tela pede outro código sobre o MESMO segredo.
94
+ return ctx.response
95
+ .status(422)
96
+ .send(apiErr('invalid_code', translate(c.cfg.messages, 'errors.invalid_code')));
97
+ }
98
+ await c.cfg.audit?.record({
99
+ type: 'mfa.enabled',
100
+ accountId: c.userId,
101
+ ip: ctx.request.ip?.() ?? null,
102
+ metadata: { method: 'totp' },
103
+ });
104
+ await this.#notice(ctx, c, 'mfa_enabled');
105
+ return { ok: true, enabled: true, recoveryCodes: result.recoveryCodes ?? [] };
106
+ }
107
+ // ─── POST /account/api/mfa/totp/disable ─────────────────────────────────
108
+ /** Desliga o MFA (TOTP + recovery codes). Exige sudo, como o console. */
109
+ async disableTotp(ctx) {
110
+ const c = await this.#mfaContext(ctx);
111
+ if (!c)
112
+ return;
113
+ if (!(await this.#requireSudoJson(ctx)))
114
+ return;
115
+ await c.store.disableMfa(c.userId);
116
+ await c.cfg.audit?.record({
117
+ type: 'mfa.disabled',
118
+ accountId: c.userId,
119
+ ip: ctx.request.ip?.() ?? null,
120
+ });
121
+ await this.#notice(ctx, c, 'mfa_disabled');
122
+ return { ok: true, enabled: false };
123
+ }
124
+ // ─── POST /account/api/mfa/recovery-codes ───────────────────────────────
125
+ /**
126
+ * Regenera os recovery codes de uma conta com MFA ATIVO e devolve os novos —
127
+ * uma única vez. Exige sudo: o resultado é um conjunto de credenciais que
128
+ * contorna o segundo fator, então vale o mesmo gate do `enroll`/`disable`.
129
+ *
130
+ * Capability-probed: stores que não implementam
131
+ * `regenerateRecoveryCodes` respondem 422 em vez de 500.
132
+ */
133
+ async regenerateRecoveryCodes(ctx) {
134
+ const c = await this.#mfaContext(ctx);
135
+ if (!c)
136
+ return;
137
+ if (!supportsRecoveryCodeRegeneration(c.store)) {
138
+ return ctx.response
139
+ .status(422)
140
+ .send(apiErr('capability_unsupported', 'Recovery code regeneration not supported.'));
141
+ }
142
+ if (!(await this.#requireSudoJson(ctx)))
143
+ return;
144
+ const codes = await c.store.regenerateRecoveryCodes(c.userId);
145
+ if (!codes) {
146
+ // Sem MFA ativo não há conjunto a regenerar — enrolar é o caminho.
147
+ return ctx.response.status(422).send(apiErr('mfa_not_enabled', 'MFA is not enabled.'));
148
+ }
149
+ await c.cfg.audit?.record({
150
+ type: 'mfa.recovery_codes_regenerated',
151
+ accountId: c.userId,
152
+ ip: ctx.request.ip?.() ?? null,
153
+ });
154
+ return { ok: true, recoveryCodes: codes };
155
+ }
156
+ // ─── POST /account/api/mfa/passkeys/options ─────────────────────────────
157
+ /**
158
+ * Options da cerimônia de REGISTRO de passkey, guardando o desafio na sessão.
159
+ * Sem sudo, igual ao endpoint clássico: quem paga o gate é o `verify`, que é
160
+ * onde a credencial passa a existir.
161
+ */
162
+ async passkeyRegisterOptions(ctx) {
163
+ const service = await ctx.containerResolver.make('authkit.server');
164
+ const cfg = service.config;
165
+ const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
166
+ if (!supportsPasskeys(cfg.accountStore)) {
167
+ return ctx.response
168
+ .status(422)
169
+ .send(apiErr('capability_unsupported', translate(cfg.messages, 'errors.passkeys_unavailable')));
170
+ }
171
+ const generated = await cfg.accountStore.generatePasskeyRegistrationOptions?.(userId);
172
+ if (!generated) {
173
+ return ctx.response
174
+ .status(422)
175
+ .send(apiErr('capability_unsupported', translate(cfg.messages, 'errors.passkeys_unavailable')));
176
+ }
177
+ ctx.session.put(PASSKEY_REG_CHALLENGE_KEY, generated.challenge);
178
+ return generated.options;
179
+ }
180
+ // ─── POST /account/api/mfa/passkeys/verify ──────────────────────────────
181
+ /**
182
+ * Verifica o attestation contra o desafio da sessão e persiste a credencial.
183
+ *
184
+ * O endpoint clássico responde 302 numa navegação e `{ok:true}` num fetch;
185
+ * este responde SEMPRE JSON — inclusive na recusa de sudo, que lá é um
186
+ * redirect. É a diferença que faz a cerimônia caber numa SPA: o browser não
187
+ * pode navegar entre `startRegistration()` e a verificação.
188
+ */
189
+ async passkeyRegisterVerify(ctx) {
190
+ const service = await ctx.containerResolver.make('authkit.server');
191
+ const cfg = service.config;
192
+ const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
193
+ if (!supportsPasskeys(cfg.accountStore)) {
194
+ return ctx.response
195
+ .status(422)
196
+ .send(apiErr('capability_unsupported', translate(cfg.messages, 'errors.passkeys_unavailable')));
197
+ }
198
+ if (!(await this.#requireSudoJson(ctx)))
199
+ return;
200
+ const challenge = ctx.session.get(PASSKEY_REG_CHALLENGE_KEY);
201
+ if (!challenge) {
202
+ return ctx.response
203
+ .status(400)
204
+ .send(apiErr('challenge_expired', translate(cfg.messages, 'errors.challenge_expired')));
205
+ }
206
+ const body = ctx.request.input('response', ctx.request.body());
207
+ const ok = (await cfg.accountStore.verifyPasskeyRegistration?.(userId, body, challenge)) ?? false;
208
+ // Queima o desafio em QUALQUER desfecho: um attestation recusado não pode
209
+ // ser retentado contra o mesmo challenge.
210
+ ctx.session.forget(PASSKEY_REG_CHALLENGE_KEY);
211
+ if (!ok) {
212
+ return ctx.response
213
+ .status(400)
214
+ .send(apiErr('invalid_response', translate(cfg.messages, 'errors.invalid_code')));
215
+ }
216
+ const ip = ctx.request.ip?.() ?? null;
217
+ await cfg.audit?.record({
218
+ type: 'mfa.enabled',
219
+ accountId: userId,
220
+ ip,
221
+ metadata: { method: 'webauthn' },
222
+ });
223
+ await cfg.audit?.record({ type: 'passkey.registered', accountId: userId, ip });
224
+ const account = await cfg.accountStore.findById(userId);
225
+ if (account) {
226
+ const ts = new Date().toISOString();
227
+ for (const kind of ['passkey_added', 'mfa_enabled']) {
228
+ await dispatchSecurityNotice(ctx, { account: { id: userId, email: account.email }, kind, ip, timestamp: ts }, cfg.mail, cfg.audit, cfg);
229
+ }
230
+ }
231
+ return { ok: true };
232
+ }
233
+ // ─── Internos ───────────────────────────────────────────────────────────
234
+ /**
235
+ * Resolve config + store com MFA + a conta da sessão. Quando o pré-requisito
236
+ * falha, JÁ RESPONDE e devolve `null`.
237
+ */
238
+ async #mfaContext(ctx) {
239
+ const service = await ctx.containerResolver.make('authkit.server');
240
+ const cfg = service.config;
241
+ const store = cfg.accountStore;
242
+ if (!supportsMfa(store)) {
243
+ ctx.response
244
+ .status(422)
245
+ .send(apiErr('capability_unsupported', 'MFA not supported by the account store.'));
246
+ return null;
247
+ }
248
+ const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
249
+ if (!userId) {
250
+ ctx.response.unauthorized(apiErr('unauthorized', 'Not authenticated.'));
251
+ return null;
252
+ }
253
+ return { cfg, store, userId };
254
+ }
255
+ /**
256
+ * Gate de sudo em versão JSON. A DECISÃO é a mesmíssima do console —
257
+ * `isSudoSatisfied`, de onde o `requireSudo` do form também tira a dele
258
+ * (mesma setting, mesma janela de graça, mesma vinculação à conta, mesmo
259
+ * fail-safe) —, só a RECUSA muda de forma: `403 sudo_required` em vez do
260
+ * redirect para `/account/confirm`.
261
+ *
262
+ * Chama a decisão, e não o `requireSudo`, porque aquele ESCREVE na resposta
263
+ * ao recusar (`response.redirect`): sobrepor um 403 depois deixaria um
264
+ * `Location` pendurado numa resposta que não é redirect. Devolve `false`
265
+ * quando já respondeu.
266
+ */
267
+ async #requireSudoJson(ctx) {
268
+ const settings = await resolveRuntimeSettings(ctx);
269
+ if (await isSudoSatisfied(ctx, settings))
270
+ return true;
271
+ ctx.response.status(403).send(apiErr('sudo_required', 'Identity confirmation required.'));
272
+ return false;
273
+ }
274
+ /** Notificação de segurança best-effort (nunca derruba a operação). */
275
+ async #notice(ctx, c, kind) {
276
+ const account = await c.cfg.accountStore.findById(c.userId);
277
+ if (!account)
278
+ return;
279
+ await dispatchSecurityNotice(ctx, {
280
+ account: { id: c.userId, email: account.email },
281
+ kind: kind,
282
+ ip: ctx.request.ip?.() ?? null,
283
+ timestamp: new Date().toISOString(),
284
+ }, c.cfg.mail, c.cfg.audit, c.cfg);
285
+ }
286
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Account Self-Service JSON API — ESCRITA de organizações
3
+ *
4
+ * Espelho JSON dos POSTs de formulário do `AccountOrgsController`, para hosts
5
+ * que desenham as próprias telas de organização dentro do shell do produto e
6
+ * não querem mandar o usuário ao console `/account/orgs`.
7
+ *
8
+ * Mapa de rotas (todas sob o `accountGuard`; as mutantes sob o CSRF do shield
9
+ * do host — nenhuma delas entra em `authkitCsrfExceptions`):
10
+ * POST /account/api/orgs → criar org (allowSelfCreate)
11
+ * POST /account/api/orgs/deactivate → limpar a org ativa
12
+ * POST /account/api/orgs/invitations/:token/accept → aceitar convite
13
+ * POST /account/api/orgs/:id/activate → definir org ativa
14
+ * POST /account/api/orgs/:id/leave → sair da org
15
+ * POST /account/api/orgs/:id/invitations → convidar por e-mail
16
+ * DELETE /account/api/orgs/:id/invitations/:invId → revogar convite
17
+ * PATCH /account/api/orgs/:id/members/:accountId → trocar papel
18
+ * DELETE /account/api/orgs/:id/members/:accountId → remover membro
19
+ *
20
+ * O que este controller NÃO afrouxa em relação ao formulário:
21
+ *
22
+ * - **Escopo por conta.** O ator é sempre `session[ACCOUNT_SESSION_KEY]`;
23
+ * nenhum handler aceita um id de ator vindo do corpo.
24
+ * - **Papel na org.** Convidar, revogar convite, remover membro e trocar
25
+ * papel exigem membership `owner`/`admin` NA ORG DO PATH — a mesma checagem
26
+ * do form, que é o que impede o IDOR cross-org.
27
+ * - **Escalonamento.** Só um `owner` concede o papel `owner`; um admin
28
+ * tentando isso leva 403, igual ao form.
29
+ * - **Catálogo de papéis.** O papel passa pelo `isRoleInCatalog` (runtime →
30
+ * config → defaults), o MESMO helper puro do form e do caminho admin.
31
+ *
32
+ * O que muda de propósito: a resposta. Onde o form redireciona para
33
+ * `/account/orgs` (com ou sem flash), aqui sai JSON — `{ error: { code,
34
+ * message } }` com o status certo, para a tela do host poder reagir.
35
+ *
36
+ * Sudo: o console HTML NÃO exige sudo em nenhuma operação de org, e este
37
+ * espelho segue igual. Exigir aqui o que o form não exige seria divergência na
38
+ * outra direção — e a decisão de qual superfície é sensível pertence a uma
39
+ * mudança de política, não a um espelho de formato.
40
+ *
41
+ * Política EFETIVA, não o config estático. `allowSelfCreate`, o catálogo de
42
+ * papéis e o TTL do convite saem do MESMO módulo que o console HTML usa
43
+ * (`host/org_policy.ts`: setting da org → setting global → config → default da
44
+ * lib). Ler só o config estático daria uma superfície que diverge da outra na
45
+ * primeira vez que um admin mexesse na setting — e divergência entre o form e
46
+ * o espelho é exatamente o bug que este controller não pode ter.
47
+ */
48
+ import '../augmentations.js';
49
+ import type { HttpContext } from '@adonisjs/core/http';
50
+ export default class AccountOrgsApiController {
51
+ #private;
52
+ /** Criar uma org. Exige `allowSelfCreate` na política efetiva. */
53
+ createOrg(ctx: HttpContext): Promise<void | {
54
+ id: string;
55
+ name: string;
56
+ slug: string;
57
+ logoUrl: string | null;
58
+ role: string;
59
+ }>;
60
+ /** Define a org ativa (cookie `authkit_active_org`). Valida membership. */
61
+ activateOrg(ctx: HttpContext): Promise<void | {
62
+ ok: boolean;
63
+ activeOrgId: string;
64
+ slug: any;
65
+ role: any;
66
+ }>;
67
+ /** Limpa a org ativa. Não depende de membership (só apaga o cookie). */
68
+ deactivateOrg(ctx: HttpContext): Promise<{
69
+ ok: boolean;
70
+ activeOrgId: null;
71
+ }>;
72
+ /** Sai da org. O store recusa o último owner (`reason: 'last_owner'`). */
73
+ leaveOrg(ctx: HttpContext): Promise<void | {
74
+ ok: boolean;
75
+ orgId: string;
76
+ }>;
77
+ /** Convida alguém por e-mail. Exige owner/admin; papel validado no catálogo. */
78
+ inviteMember(ctx: HttpContext): Promise<void | {
79
+ id: any;
80
+ organizationId: string;
81
+ email: any;
82
+ role: string;
83
+ expiresAt: any;
84
+ createdAt: any;
85
+ }>;
86
+ /** Revoga um convite pendente. Escopado por org (anti-IDOR), como o form. */
87
+ revokeInvitation(ctx: HttpContext): Promise<void | {
88
+ ok: boolean;
89
+ revoked: string;
90
+ }>;
91
+ /** Remove um membro. Exige owner/admin na org. */
92
+ removeMember(ctx: HttpContext): Promise<void | {
93
+ ok: boolean;
94
+ orgId: string;
95
+ accountId: string;
96
+ }>;
97
+ /**
98
+ * Troca o papel de um membro.
99
+ *
100
+ * Não existe equivalente member-facing em formulário (o console HTML só
101
+ * troca papel pelo caminho ADMIN). As regras vieram, então, da interseção
102
+ * das duas superfícies que já existem: o guard de owner/admin + catálogo de
103
+ * papéis do `invite` member-facing, e a checagem de `last_owner` do
104
+ * `AdminOrgsService.updateMemberRole`. Conceder `owner` continua privativo
105
+ * de um owner.
106
+ */
107
+ updateMemberRole(ctx: HttpContext): Promise<void | {
108
+ ok: boolean;
109
+ orgId: string;
110
+ accountId: string;
111
+ role: string;
112
+ }>;
113
+ /**
114
+ * Aceita um convite pelo token do e-mail.
115
+ *
116
+ * Diferente do form (montado FORA do guard para tratar o não-autenticado com
117
+ * um redirect para o login), esta rota vive DENTRO do `accountGuard`: uma
118
+ * tela SPA já está logada, e a resposta a um visitante anônimo aqui teria de
119
+ * ser 401 em JSON, não uma navegação.
120
+ */
121
+ acceptInvitation(ctx: HttpContext): Promise<void | {
122
+ ok: boolean;
123
+ organizationId: any;
124
+ role: any;
125
+ }>;
126
+ }