@adonis-agora/authkit-server 0.55.2 → 0.57.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 +1 -1
- package/build/index.js +1 -1
- package/build/src/host/durable/account_deletion_workflow.d.ts +9 -7
- package/build/src/host/durable/account_deletion_workflow.js +13 -13
- package/build/src/host/durable/account_export_workflow.d.ts +1 -1
- package/build/src/host/durable/account_export_workflow.js +5 -5
- package/build/src/host/impersonation_session.d.ts +22 -2
- package/build/src/host/impersonation_session.js +70 -3
- package/build/src/host/ui-dist/assets/index-pAbBjdHC.js +155 -0
- package/build/src/host/ui-dist/index.html +1 -1
- package/package.json +33 -33
- package/build/src/host/ui-dist/assets/index-BQoTDiLV.js +0 -137
package/build/index.d.ts
CHANGED
|
@@ -129,7 +129,7 @@ export { resolveRegistration } from './src/define_config.js';
|
|
|
129
129
|
export type { RegistrationConfigInput, ResolvedRegistrationConfig, } from './src/define_config.js';
|
|
130
130
|
export { getAccountId, realAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/console_session.js';
|
|
131
131
|
export { ACCOUNT_SESSION_KEY } from './src/host/middleware/account_auth.js';
|
|
132
|
-
export { rememberAccessToken, startImpersonation, impersonationState, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
132
|
+
export { rememberAccessToken, rememberRefreshToken, refreshAccessToken, startImpersonation, impersonationState, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
133
133
|
export type { StartImpersonationParams, ImpersonationState, } from './src/host/impersonation_session.js';
|
|
134
134
|
export { SUDO_SESSION_KEY, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, requireSudo, isSudoActive, markSudo, resolveEffectiveSudoMode, } from './src/host/sudo_mode.js';
|
|
135
135
|
export type { SudoModeSetting, ResolvedSudoModeSetting, } from './src/host/sudo_mode.js';
|
package/build/index.js
CHANGED
|
@@ -91,7 +91,7 @@ export { ACCOUNT_SESSION_KEY } from './src/host/middleware/account_auth.js';
|
|
|
91
91
|
// Session impersonation — RP-side glue that routes through the IdP's RFC 8693
|
|
92
92
|
// token-exchange (the IdP validates the admin role + audits). See
|
|
93
93
|
// src/host/impersonation_session.ts.
|
|
94
|
-
export { rememberAccessToken, startImpersonation, impersonationState, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
94
|
+
export { rememberAccessToken, rememberRefreshToken, refreshAccessToken, startImpersonation, impersonationState, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
95
95
|
// Sudo mode — helpers for host controllers that require step-up authentication.
|
|
96
96
|
export { SUDO_SESSION_KEY, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, requireSudo, isSudoActive, markSudo, resolveEffectiveSudoMode, } from './src/host/sudo_mode.js';
|
|
97
97
|
// SPI de métodos de confirmação de identidade (sudo mode).
|
|
@@ -8,13 +8,15 @@ export interface AccountDeleteWorkflowInput {
|
|
|
8
8
|
actor: DeletionActor;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Forma mínima do `ctx` durável que o corpo usa (um `ctx.
|
|
12
|
-
* idempotente
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Forma mínima do `ctx` durável que o corpo usa (um `ctx.localStep` checkpointado
|
|
12
|
+
* e idempotente — executado IN-PROCESS, nunca dispatchado a um worker remoto;
|
|
13
|
+
* distinto do `ctx.step` do engine, que é SEMPRE dispatchado a um handler
|
|
14
|
+
* registrado por nome). Tipada estruturalmente para NÃO acoplar o build do
|
|
15
|
+
* authkit ao pacote `@adonis-agora/durable` (peer OPCIONAL): o app passa o
|
|
16
|
+
* `engine.register` real e o ctx satisfaz esta interface em runtime.
|
|
15
17
|
*/
|
|
16
18
|
export interface DurableStepCtx {
|
|
17
|
-
|
|
19
|
+
localStep<T>(name: string, fn: (...args: any[]) => Promise<T>, options?: unknown): Promise<T>;
|
|
18
20
|
}
|
|
19
21
|
/** A assinatura do corpo do workflow (compatível com `engine.register`). */
|
|
20
22
|
export type WorkflowBody = (ctx: DurableStepCtx, input: AccountDeleteWorkflowInput) => Promise<unknown>;
|
|
@@ -35,9 +37,9 @@ export interface AccountDeletionWorkflowDeps {
|
|
|
35
37
|
* ```
|
|
36
38
|
*
|
|
37
39
|
* O corpo é FORWARD-ONLY (sem compensação — nunca des-deleta): cada etapa do
|
|
38
|
-
* cascade é um `ctx.
|
|
40
|
+
* cascade é um `ctx.localStep` checkpointado, com retry por-etapa e resumabilidade. A
|
|
39
41
|
* linha da conta é a ÚLTIMA etapa. Todos os efeitos colaterais ficam DENTRO de
|
|
40
|
-
* `ctx.
|
|
42
|
+
* `ctx.localStep` (corpo determinístico: nada de Date.now()/random no corpo). A
|
|
41
43
|
* idempotência por `accountId` é feita pelo run-id no enqueue (ver
|
|
42
44
|
* `enqueueAccountDeletion`).
|
|
43
45
|
*/
|
|
@@ -30,9 +30,9 @@ function emptyResult() {
|
|
|
30
30
|
* ```
|
|
31
31
|
*
|
|
32
32
|
* O corpo é FORWARD-ONLY (sem compensação — nunca des-deleta): cada etapa do
|
|
33
|
-
* cascade é um `ctx.
|
|
33
|
+
* cascade é um `ctx.localStep` checkpointado, com retry por-etapa e resumabilidade. A
|
|
34
34
|
* linha da conta é a ÚLTIMA etapa. Todos os efeitos colaterais ficam DENTRO de
|
|
35
|
-
* `ctx.
|
|
35
|
+
* `ctx.localStep` (corpo determinístico: nada de Date.now()/random no corpo). A
|
|
36
36
|
* idempotência por `accountId` é feita pelo run-id no enqueue (ver
|
|
37
37
|
* `enqueueAccountDeletion`).
|
|
38
38
|
*/
|
|
@@ -42,7 +42,7 @@ export function defineAccountDeletionWorkflow(deps) {
|
|
|
42
42
|
// Snapshot da conta ANTES de destruir (e-mail + avatar) — capturado num step
|
|
43
43
|
// para ser determinístico no replay. Se a conta não existe (ou já foi
|
|
44
44
|
// deletada num run anterior), encerra como no-op.
|
|
45
|
-
const snapshot = await ctx.
|
|
45
|
+
const snapshot = await ctx.localStep('snapshot', async () => {
|
|
46
46
|
const cfg = (await deps.oidc()).config;
|
|
47
47
|
return snapshotAccount(cfg, accountId);
|
|
48
48
|
});
|
|
@@ -50,36 +50,36 @@ export function defineAccountDeletionWorkflow(deps) {
|
|
|
50
50
|
return emptyResult();
|
|
51
51
|
const result = emptyResult();
|
|
52
52
|
// 1) Audit `account.deleted` ANTES de qualquer destruição.
|
|
53
|
-
await ctx.
|
|
53
|
+
await ctx.localStep('audit.deleted', async () => {
|
|
54
54
|
const cfg = (await deps.oidc()).config;
|
|
55
55
|
await auditDeleted(cfg, snapshot, actor);
|
|
56
56
|
});
|
|
57
57
|
// 2) Sessões + grants (cascateia os tokens do oidc-provider).
|
|
58
|
-
const revoke = await ctx.
|
|
58
|
+
const revoke = await ctx.localStep('revoke.sessions', async () => revokeSessions(await deps.oidc(), accountId));
|
|
59
59
|
result.sessions = revoke.sessions;
|
|
60
60
|
result.grants = revoke.grants;
|
|
61
61
|
result.accessTokens = revoke.accessTokens;
|
|
62
62
|
result.refreshTokens = revoke.refreshTokens;
|
|
63
63
|
// 3) Personal Access Tokens.
|
|
64
|
-
result.pats = (await ctx.
|
|
64
|
+
result.pats = (await ctx.localStep('revoke.pats', async () => revokePats((await deps.oidc()).config, accountId))).pats;
|
|
65
65
|
// 4) Passkeys / credenciais WebAuthn.
|
|
66
|
-
result.passkeys = (await ctx.
|
|
66
|
+
result.passkeys = (await ctx.localStep('remove.passkeys', async () => removePasskeys((await deps.oidc()).config, accountId))).passkeys;
|
|
67
67
|
// 5) MFA / TOTP.
|
|
68
|
-
await ctx.
|
|
68
|
+
await ctx.localStep('disable.mfa', async () => {
|
|
69
69
|
await disableMfa((await deps.oidc()).config, accountId);
|
|
70
70
|
});
|
|
71
71
|
// 6) Identidades de provider linkadas.
|
|
72
|
-
result.providerIdentities = (await ctx.
|
|
72
|
+
result.providerIdentities = (await ctx.localStep('unlink.providers', async () => unlinkProviders((await deps.oidc()).config, accountId))).providerIdentities;
|
|
73
73
|
// 6b) Organizations.
|
|
74
|
-
const orgResult = await ctx.
|
|
74
|
+
const orgResult = await ctx.localStep('remove.orgs', async () => removeFromOrgs((await deps.oidc()).config, accountId));
|
|
75
75
|
result.orgMemberships = orgResult.orgMemberships;
|
|
76
76
|
result.orgInvitations = orgResult.orgInvitations;
|
|
77
77
|
// 7) Avatar no drive.
|
|
78
|
-
result.avatarDeleted = (await ctx.
|
|
78
|
+
result.avatarDeleted = (await ctx.localStep('delete.avatar', async () => deleteAccountAvatar((await deps.oidc()).config, accountId, snapshot.avatarUrl))).avatarDeleted;
|
|
79
79
|
// 8) Anonimiza o histórico de audit.
|
|
80
|
-
result.auditAnonymized = (await ctx.
|
|
80
|
+
result.auditAnonymized = (await ctx.localStep('anonymize.audit', async () => anonymizeAudit((await deps.oidc()).config, accountId))).auditAnonymized;
|
|
81
81
|
// 9) Deleta a linha da conta (ÚLTIMA etapa, forward-only).
|
|
82
|
-
result.ok = (await ctx.
|
|
82
|
+
result.ok = (await ctx.localStep('delete.account', async () => deleteAccountRow((await deps.oidc()).config, accountId))).ok;
|
|
83
83
|
return result;
|
|
84
84
|
};
|
|
85
85
|
return { name: ACCOUNT_DELETE_WORKFLOW, version: '1', body };
|
|
@@ -50,7 +50,7 @@ export interface AccountExportWorkflowDeps {
|
|
|
50
50
|
/**
|
|
51
51
|
* Define a REGISTRAÇÃO do workflow durável `authkit.account.export`.
|
|
52
52
|
*
|
|
53
|
-
* Etapas (todas efeitos colaterais dentro de `ctx.
|
|
53
|
+
* Etapas (todas efeitos colaterais dentro de `ctx.localStep`, corpo determinístico):
|
|
54
54
|
* 1. `collect` — reúne o payload (reusa {@link AccountExportService.collect});
|
|
55
55
|
* 2. `audit` — registra `account.exported`;
|
|
56
56
|
* 3. `persist` — serializa + grava o artefato no drive;
|
|
@@ -45,7 +45,7 @@ const noopDeliver = async () => { };
|
|
|
45
45
|
/**
|
|
46
46
|
* Define a REGISTRAÇÃO do workflow durável `authkit.account.export`.
|
|
47
47
|
*
|
|
48
|
-
* Etapas (todas efeitos colaterais dentro de `ctx.
|
|
48
|
+
* Etapas (todas efeitos colaterais dentro de `ctx.localStep`, corpo determinístico):
|
|
49
49
|
* 1. `collect` — reúne o payload (reusa {@link AccountExportService.collect});
|
|
50
50
|
* 2. `audit` — registra `account.exported`;
|
|
51
51
|
* 3. `persist` — serializa + grava o artefato no drive;
|
|
@@ -61,14 +61,14 @@ export function defineAccountExportWorkflow(deps) {
|
|
|
61
61
|
const { accountId } = input;
|
|
62
62
|
const runId = ctx.runId ?? accountId;
|
|
63
63
|
// 1) Coleta o payload (reusa a coleta inline do AccountExportService).
|
|
64
|
-
const payload = await ctx.
|
|
64
|
+
const payload = await ctx.localStep('collect', async () => {
|
|
65
65
|
const oidc = await deps.oidc();
|
|
66
66
|
return new AccountExportService(oidc).collect(accountId);
|
|
67
67
|
});
|
|
68
68
|
if (!payload)
|
|
69
69
|
return { ok: false, artifactKey: null, bytes: 0 };
|
|
70
70
|
// 2) Audita o export (account.exported).
|
|
71
|
-
await ctx.
|
|
71
|
+
await ctx.localStep('audit', async () => {
|
|
72
72
|
const cfg = (await deps.oidc()).config;
|
|
73
73
|
await cfg.audit?.record({
|
|
74
74
|
type: 'account.exported',
|
|
@@ -78,12 +78,12 @@ export function defineAccountExportWorkflow(deps) {
|
|
|
78
78
|
});
|
|
79
79
|
// 3) Serializa + persiste o artefato.
|
|
80
80
|
const json = JSON.stringify(payload, null, 2);
|
|
81
|
-
const artifactKey = await ctx.
|
|
81
|
+
const artifactKey = await ctx.localStep('persist', async () => {
|
|
82
82
|
const oidc = await deps.oidc();
|
|
83
83
|
return persist({ accountId, runId, json, oidc });
|
|
84
84
|
});
|
|
85
85
|
// 4) Entrega ao titular (signal + e-mail, pluggable).
|
|
86
|
-
await ctx.
|
|
86
|
+
await ctx.localStep('deliver', async () => {
|
|
87
87
|
const oidc = await deps.oidc();
|
|
88
88
|
await deliver({ accountId, runId, artifactKey, oidc });
|
|
89
89
|
});
|
|
@@ -4,10 +4,16 @@ import type { HttpContext } from '@adonisjs/core/http';
|
|
|
4
4
|
* após o login). Necessário porque o token-exchange exige o access token do admin
|
|
5
5
|
* como `subject_token`, e o RP normalmente descarta os tokens após o login.
|
|
6
6
|
*
|
|
7
|
-
* Access tokens são curtos: se expirar, `startImpersonation`
|
|
8
|
-
*
|
|
7
|
+
* Access tokens são curtos: se expirar, `startImpersonation` renova via refresh
|
|
8
|
+
* token (`rememberRefreshToken`) e tenta de novo — o admin não precisa relogar.
|
|
9
9
|
*/
|
|
10
10
|
export declare function rememberAccessToken(ctx: HttpContext, accessToken: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Guarda o refresh token do admin (do scope `offline_access`), usado para
|
|
13
|
+
* renovar o access token expirado antes do token-exchange. Chame no callback
|
|
14
|
+
* OIDC do RP junto com `rememberAccessToken`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function rememberRefreshToken(ctx: HttpContext, refreshToken: string): void;
|
|
11
17
|
export interface StartImpersonationParams {
|
|
12
18
|
/** Id do usuário-alvo a personificar. */
|
|
13
19
|
targetId: string;
|
|
@@ -27,6 +33,20 @@ export interface ImpersonationState {
|
|
|
27
33
|
/** O admin real (impersonator), quando `active`. */
|
|
28
34
|
impersonatorId?: string;
|
|
29
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Renova o access token do admin via refresh grant (RFC 6749 §6), usando o
|
|
38
|
+
* refresh token guardado por `rememberRefreshToken`. Retorna o novo access
|
|
39
|
+
* token (e o refresh token rotacionado, se o IdP emitir outro), ou null se não
|
|
40
|
+
* houver refresh token / o refresh falhar.
|
|
41
|
+
*
|
|
42
|
+
* O IdP deste pacote roda `refresh_token` grant por default
|
|
43
|
+
* (`build_provider.ts`: `grant_types: ['authorization_code', 'refresh_token']`),
|
|
44
|
+
* então o refresh é válido na configuração padrão.
|
|
45
|
+
*/
|
|
46
|
+
export declare function refreshAccessToken(ctx: HttpContext, params: Pick<StartImpersonationParams, 'issuer' | 'clientId' | 'clientSecret' | 'tokenEndpoint' | 'fetchImpl'>): Promise<{
|
|
47
|
+
accessToken: string;
|
|
48
|
+
refreshToken?: string;
|
|
49
|
+
} | null>;
|
|
30
50
|
/**
|
|
31
51
|
* Inicia a impersonation: lê o access token do admin da sessão, chama o
|
|
32
52
|
* token-exchange (o IdP valida a role admin e audita) e SÓ em sucesso guarda o
|
|
@@ -27,17 +27,32 @@ const IMPERSONATOR_SESSION_KEY = 'impersonator_user_id';
|
|
|
27
27
|
* `subject_token` do token-exchange. Interna: NÃO exporte o literal.
|
|
28
28
|
*/
|
|
29
29
|
const ADMIN_ACCESS_TOKEN_SESSION_KEY = 'admin_access_token';
|
|
30
|
+
/**
|
|
31
|
+
* Key de sessão que guarda o refresh token do admin (do `offline_access`),
|
|
32
|
+
* usado para renovar o access token expirado antes do token-exchange. Interna:
|
|
33
|
+
* NÃO exporte o literal.
|
|
34
|
+
*/
|
|
35
|
+
const ADMIN_REFRESH_TOKEN_SESSION_KEY = 'admin_refresh_token';
|
|
36
|
+
const REFRESH_TOKEN_GRANT = 'refresh_token';
|
|
30
37
|
/**
|
|
31
38
|
* Guarda o access token do admin na sessão (chame no callback OIDC do RP, logo
|
|
32
39
|
* após o login). Necessário porque o token-exchange exige o access token do admin
|
|
33
40
|
* como `subject_token`, e o RP normalmente descarta os tokens após o login.
|
|
34
41
|
*
|
|
35
|
-
* Access tokens são curtos: se expirar, `startImpersonation`
|
|
36
|
-
*
|
|
42
|
+
* Access tokens são curtos: se expirar, `startImpersonation` renova via refresh
|
|
43
|
+
* token (`rememberRefreshToken`) e tenta de novo — o admin não precisa relogar.
|
|
37
44
|
*/
|
|
38
45
|
export function rememberAccessToken(ctx, accessToken) {
|
|
39
46
|
ctx.session.put(ADMIN_ACCESS_TOKEN_SESSION_KEY, accessToken);
|
|
40
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Guarda o refresh token do admin (do scope `offline_access`), usado para
|
|
50
|
+
* renovar o access token expirado antes do token-exchange. Chame no callback
|
|
51
|
+
* OIDC do RP junto com `rememberAccessToken`.
|
|
52
|
+
*/
|
|
53
|
+
export function rememberRefreshToken(ctx, refreshToken) {
|
|
54
|
+
ctx.session.put(ADMIN_REFRESH_TOKEN_SESSION_KEY, refreshToken);
|
|
55
|
+
}
|
|
41
56
|
/**
|
|
42
57
|
* POST inline do RFC 8693 token-exchange. Inline (em vez de depender de
|
|
43
58
|
* `@adonis-agora/authkit-client`) porque o client NÃO é dependência do server e
|
|
@@ -68,6 +83,43 @@ async function requestTokenExchange(params, subjectToken) {
|
|
|
68
83
|
throw new Error(`Token exchange failed: ${res.status}`);
|
|
69
84
|
}
|
|
70
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Renova o access token do admin via refresh grant (RFC 6749 §6), usando o
|
|
88
|
+
* refresh token guardado por `rememberRefreshToken`. Retorna o novo access
|
|
89
|
+
* token (e o refresh token rotacionado, se o IdP emitir outro), ou null se não
|
|
90
|
+
* houver refresh token / o refresh falhar.
|
|
91
|
+
*
|
|
92
|
+
* O IdP deste pacote roda `refresh_token` grant por default
|
|
93
|
+
* (`build_provider.ts`: `grant_types: ['authorization_code', 'refresh_token']`),
|
|
94
|
+
* então o refresh é válido na configuração padrão.
|
|
95
|
+
*/
|
|
96
|
+
export async function refreshAccessToken(ctx, params) {
|
|
97
|
+
const refreshToken = ctx.session.get(ADMIN_REFRESH_TOKEN_SESSION_KEY);
|
|
98
|
+
if (!refreshToken)
|
|
99
|
+
return null;
|
|
100
|
+
const body = new URLSearchParams({
|
|
101
|
+
grant_type: REFRESH_TOKEN_GRANT,
|
|
102
|
+
refresh_token: refreshToken,
|
|
103
|
+
client_id: params.clientId,
|
|
104
|
+
});
|
|
105
|
+
if (params.clientSecret)
|
|
106
|
+
body.set('client_secret', params.clientSecret);
|
|
107
|
+
const fetchImpl = params.fetchImpl ?? fetch;
|
|
108
|
+
const res = await fetchImpl(params.tokenEndpoint ?? `${params.issuer}/token`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
111
|
+
body: body.toString(),
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok)
|
|
114
|
+
return null;
|
|
115
|
+
const data = (await res.json());
|
|
116
|
+
if (!data.access_token)
|
|
117
|
+
return null;
|
|
118
|
+
const result = { accessToken: data.access_token };
|
|
119
|
+
if (data.refresh_token)
|
|
120
|
+
result.refreshToken = data.refresh_token;
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
71
123
|
/**
|
|
72
124
|
* Inicia a impersonation: lê o access token do admin da sessão, chama o
|
|
73
125
|
* token-exchange (o IdP valida a role admin e audita) e SÓ em sucesso guarda o
|
|
@@ -94,7 +146,22 @@ export async function startImpersonation(ctx, params) {
|
|
|
94
146
|
}
|
|
95
147
|
// O IdP é o gatekeeper: lança se o admin não puder personificar. Chamado ANTES
|
|
96
148
|
// de qualquer mutação de sessão — em caso de erro nada é trocado.
|
|
97
|
-
|
|
149
|
+
try {
|
|
150
|
+
await requestTokenExchange(params, adminAccessToken);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
// Access token expirado (4xx do exchange): renova via refresh token e tenta
|
|
154
|
+
// de novo. Sem refresh token / refresh falho → propaga o erro original.
|
|
155
|
+
if (!(err instanceof Error) || !/exchange failed: 4\d\d/.test(err.message))
|
|
156
|
+
throw err;
|
|
157
|
+
const refreshed = await refreshAccessToken(ctx, params);
|
|
158
|
+
if (!refreshed)
|
|
159
|
+
throw err;
|
|
160
|
+
rememberAccessToken(ctx, refreshed.accessToken);
|
|
161
|
+
if (refreshed.refreshToken)
|
|
162
|
+
rememberRefreshToken(ctx, refreshed.refreshToken);
|
|
163
|
+
await requestTokenExchange(params, refreshed.accessToken);
|
|
164
|
+
}
|
|
98
165
|
// Anti-fixation: rotaciona o id da sessão (mantém os dados) antes de gravar a
|
|
99
166
|
// nova identidade. Mesmo padrão do consumidor real no RP.
|
|
100
167
|
await ctx.session.regenerate();
|