@adonis-agora/authkit-server 0.88.1 → 0.90.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 CHANGED
@@ -10,6 +10,7 @@ export { appKeyEncrypter, lucidAccountStore } from './src/accounts/lucid_account
10
10
  export { AuthOrganization, AuthOrganizationInvitation, AuthOrganizationMember, defaultOrganizationModels, } from './src/accounts/lucid_store/organization_models.js';
11
11
  export type { LucidStoresModels, LucidStoresOptions, LucidStoresResult, } from './src/accounts/lucid_stores.js';
12
12
  export { lucidStores } from './src/accounts/lucid_stores.js';
13
+ export { type LinkProviderIdentityResult, ProviderIdentities, ProviderIdentitiesUnsupportedError, } from './src/accounts/provider_identities.js';
13
14
  export type { PersonalAgentIdentity } from './src/agents/agent_identity.js';
14
15
  export { PersonalAgentVerifier } from './src/agents/agent_identity.js';
15
16
  export type { PersonalAgentDelegationConfigInput, PersonalAgentRegistration, PersonalAgentResolver, PersonalAgentsConfigInput, ResolvedPersonalAgentsConfig, } from './src/agents/config.js';
package/build/index.js CHANGED
@@ -7,6 +7,7 @@ export { supportsAccountDeletion, supportsAccountEmailRewrite, supportsAccountIm
7
7
  export { appKeyEncrypter, lucidAccountStore } from './src/accounts/lucid_account_store.js';
8
8
  export { AuthOrganization, AuthOrganizationInvitation, AuthOrganizationMember, defaultOrganizationModels, } from './src/accounts/lucid_store/organization_models.js';
9
9
  export { lucidStores } from './src/accounts/lucid_stores.js';
10
+ export { ProviderIdentities, ProviderIdentitiesUnsupportedError, } from './src/accounts/provider_identities.js';
10
11
  export { PersonalAgentVerifier } from './src/agents/agent_identity.js';
11
12
  export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgentSecurity, personalAgentStepUp, } from './src/agents/middleware.js';
12
13
  export { accessTokenHash, hmacNonceSource, verifyDpopProof, } from './src/agents/poppy/dpop.js';
@@ -0,0 +1,43 @@
1
+ import { type AccountStore, type AuthAccount, type LinkProviderIdentityInput, type ProviderIdentitySummary } from './account_store.js';
2
+ /** O account store não tem account linking (o host não passou `providerIdentityModel`). */
3
+ export declare class ProviderIdentitiesUnsupportedError extends Error {
4
+ constructor();
5
+ }
6
+ /** Resultado de {@link ProviderIdentities.link}. */
7
+ export interface LinkProviderIdentityResult {
8
+ /**
9
+ * Conta a que a identidade estava ligada antes; `null` se era nova. Diferente do
10
+ * `accountId` pedido = a identidade MUDOU de conta (o "Entrar com <provider>" passa
11
+ * a abrir a conta nova).
12
+ */
13
+ previousAccountId: string | null;
14
+ }
15
+ /**
16
+ * Identidades de provider (Google, GitHub, …) das contas: a MESMA ligação que o
17
+ * "Entrar com <provider>" consulta antes de cair no e-mail. Exposto como
18
+ * `authkit.identities` (`@adonis-agora/authkit-server/services/main`) para o host
19
+ * ligar uma conta a um provider fora do login social — ex.: quando a pessoa conecta
20
+ * o Google numa tela de integrações — sem tocar na tabela nem no account store cru.
21
+ *
22
+ * Store sem a capacidade: `supported` é `false` e os demais métodos lançam
23
+ * {@link ProviderIdentitiesUnsupportedError}. Nada falha em silêncio.
24
+ */
25
+ export declare class ProviderIdentities {
26
+ #private;
27
+ constructor(getStore: () => AccountStore);
28
+ /** O account store tem account linking por provider. */
29
+ get supported(): boolean;
30
+ /** A conta ligada a `(provider, providerUserId)`; `null` se ninguém. */
31
+ find(provider: string, providerUserId: string): Promise<AuthAccount | null>;
32
+ /**
33
+ * Liga a identidade à conta. Idempotente na chave `(provider, providerUserId)`: se
34
+ * ela já estava em OUTRA conta, MUDA para esta (e `previousAccountId` diz de onde).
35
+ * Só chame depois de provar que a pessoa é dona da identidade (OAuth concluído)
36
+ * E da conta (sessão nela).
37
+ */
38
+ link(input: LinkProviderIdentityInput): Promise<LinkProviderIdentityResult>;
39
+ /** As identidades da conta, sem tokens. */
40
+ list(accountId: string): Promise<ProviderIdentitySummary[]>;
41
+ /** Remove todas as identidades da conta; devolve quantas. */
42
+ unlinkAll(accountId: string): Promise<number>;
43
+ }
@@ -0,0 +1,58 @@
1
+ import { supportsProviderIdentity, } from './account_store.js';
2
+ /** O account store não tem account linking (o host não passou `providerIdentityModel`). */
3
+ export class ProviderIdentitiesUnsupportedError extends Error {
4
+ constructor() {
5
+ super('[authkit] o accountStore não suporta identidades de provider — passe `providerIdentityModel` ao lucidAccountStore');
6
+ this.name = 'ProviderIdentitiesUnsupportedError';
7
+ }
8
+ }
9
+ /**
10
+ * Identidades de provider (Google, GitHub, …) das contas: a MESMA ligação que o
11
+ * "Entrar com <provider>" consulta antes de cair no e-mail. Exposto como
12
+ * `authkit.identities` (`@adonis-agora/authkit-server/services/main`) para o host
13
+ * ligar uma conta a um provider fora do login social — ex.: quando a pessoa conecta
14
+ * o Google numa tela de integrações — sem tocar na tabela nem no account store cru.
15
+ *
16
+ * Store sem a capacidade: `supported` é `false` e os demais métodos lançam
17
+ * {@link ProviderIdentitiesUnsupportedError}. Nada falha em silêncio.
18
+ */
19
+ export class ProviderIdentities {
20
+ #getStore;
21
+ constructor(getStore) {
22
+ this.#getStore = getStore;
23
+ }
24
+ /** O account store tem account linking por provider. */
25
+ get supported() {
26
+ return supportsProviderIdentity(this.#getStore());
27
+ }
28
+ #store() {
29
+ const store = this.#getStore();
30
+ if (!supportsProviderIdentity(store))
31
+ throw new ProviderIdentitiesUnsupportedError();
32
+ return store;
33
+ }
34
+ /** A conta ligada a `(provider, providerUserId)`; `null` se ninguém. */
35
+ find(provider, providerUserId) {
36
+ return this.#store().findByProviderIdentity(provider, providerUserId);
37
+ }
38
+ /**
39
+ * Liga a identidade à conta. Idempotente na chave `(provider, providerUserId)`: se
40
+ * ela já estava em OUTRA conta, MUDA para esta (e `previousAccountId` diz de onde).
41
+ * Só chame depois de provar que a pessoa é dona da identidade (OAuth concluído)
42
+ * E da conta (sessão nela).
43
+ */
44
+ async link(input) {
45
+ const store = this.#store();
46
+ const previous = await store.findByProviderIdentity(input.provider, input.providerUserId);
47
+ await store.linkProviderIdentity(input);
48
+ return { previousAccountId: previous?.id ?? null };
49
+ }
50
+ /** As identidades da conta, sem tokens. */
51
+ list(accountId) {
52
+ return this.#store().listProviderIdentities(accountId);
53
+ }
54
+ /** Remove todas as identidades da conta; devolve quantas. */
55
+ unlinkAll(accountId) {
56
+ return this.#store().unlinkAllProviderIdentities(accountId);
57
+ }
58
+ }
@@ -122,6 +122,22 @@ export interface MailHooks {
122
122
  agentName: string;
123
123
  expiresInMinutes: number;
124
124
  }) => Promise<void>;
125
+ /**
126
+ * Disparado num login bem-sucedido a partir de um IP NOVO para a conta (nenhum
127
+ * `login.success` anterior desse IP no audit sink), quando `notifications.newLoginEmail`
128
+ * está ligado. Best-effort, fire-and-forget: uma falha aqui NUNCA quebra o login.
129
+ * Quando ausente mas o mail estiver configurado, o host-kit envia o e-mail default de
130
+ * "novo login". O `login.new_ip_notified` é auditado nos dois casos.
131
+ */
132
+ onNewLogin?: (data: {
133
+ account: {
134
+ id: string;
135
+ email: string;
136
+ };
137
+ ip: string;
138
+ userAgent?: string | null;
139
+ timestamp: string;
140
+ }) => Promise<void>;
125
141
  /**
126
142
  * Disparado num login bem-sucedido a partir de um dispositivo NOVO (sem cookie
127
143
  * de dispositivo confiável válido para a conta). Best-effort, fire-and-forget:
@@ -51,7 +51,12 @@ export async function notifyLoginSuccess(ctx, cfg, input) {
51
51
  resolvedEmail = (await cfg.accountStore.findById(accountId))?.email ?? null;
52
52
  }
53
53
  if (cfg.notifications.newLoginEmail) {
54
- await maybeNotifyNewLogin(ctx, cfg, { accountId, email: resolvedEmail, ip: ip ?? null });
54
+ await maybeNotifyNewLogin(ctx, cfg, {
55
+ accountId,
56
+ email: resolvedEmail,
57
+ ip: ip ?? null,
58
+ userAgent,
59
+ });
55
60
  }
56
61
  if (onNewDevice && cfg.notifications.newDeviceEmail && resolvedEmail) {
57
62
  await notifyNewDevice(ctx, cfg, {
@@ -108,12 +113,12 @@ async function notifyNewDevice(ctx, cfg, data) {
108
113
  }
109
114
  /**
110
115
  * Verifica se o IP é novo para a conta (consultando o audit sink por
111
- * `login.success` do subject) e, se for, envia o e-mail de alerta + audita
112
- * `login.new_ip_notified`. Degrada para no-op quando: sem IP, sem e-mail, sem
116
+ * `login.success` do subject) e, se for, envia o alerta (hook `mail.onNewLogin`,
117
+ * override do host, ou o mailer default) + audita `login.new_ip_notified`. Degrada para no-op quando: sem IP, sem e-mail, sem
113
118
  * sink consultável (`list`), ou já houve um login.success deste IP antes.
114
119
  */
115
120
  async function maybeNotifyNewLogin(ctx, cfg, data) {
116
- const { accountId, email, ip } = data;
121
+ const { accountId, email, ip, userAgent } = data;
117
122
  if (!ip || !email)
118
123
  return;
119
124
  // Sem consulta do histórico não dá pra decidir se o IP é novo → no-op.
@@ -133,7 +138,25 @@ async function maybeNotifyNewLogin(ctx, cfg, data) {
133
138
  if (sameIpCount > 1)
134
139
  return;
135
140
  const when = new Date().toISOString();
136
- await sendNewLoginEmail(ctx, cfg, { email, ip, when });
141
+ if (cfg.mail?.onNewLogin) {
142
+ // Isolado: o mailer default engole os próprios erros, o hook do host não. Sem
143
+ // este try, um hook que lança cortaria o audit abaixo e o alerta de novo
144
+ // dispositivo, que roda depois deste na mesma tarefa.
145
+ try {
146
+ await cfg.mail.onNewLogin({
147
+ account: { id: accountId, email },
148
+ ip,
149
+ userAgent,
150
+ timestamp: when,
151
+ });
152
+ }
153
+ catch (error) {
154
+ ctx.logger?.error?.({ err: error, accountId }, 'authkit: falha no hook mail.onNewLogin');
155
+ }
156
+ }
157
+ else {
158
+ await sendNewLoginEmail(ctx, cfg, { email, ip, when });
159
+ }
137
160
  await cfg.audit?.record({
138
161
  type: 'login.new_ip_notified',
139
162
  accountId,
@@ -1,4 +1,5 @@
1
1
  import { type MetricsRecorder } from '@adonis-agora/authkit-core';
2
+ import { ProviderIdentities } from '../accounts/provider_identities.js';
2
3
  import type { ResolvedServerConfig } from '../define_config.js';
3
4
  import { type ManagedKeyInfo } from '../keys/keystore.js';
4
5
  import type { KeystoreManager } from '../keys/keystore_manager.js';
@@ -38,6 +39,12 @@ export declare class OidcService {
38
39
  /** Holder mutável dos TTLs de access/id/refresh tokens (lido sincronamente pelo oidc-provider). */
39
40
  readonly tokenTtlHolder: TokenTtlHolder;
40
41
  get config(): ResolvedServerConfig;
42
+ /**
43
+ * Identidades de provider das contas — a ligação que o "Entrar com <provider>" usa.
44
+ * Para o host ligar uma conta a um provider fora do login social sem tocar no
45
+ * account store cru. Ver {@link ProviderIdentities}.
46
+ */
47
+ get identities(): ProviderIdentities;
41
48
  /** @internal Loader de JWKS do cofre (usado por reloadKeys). */
42
49
  get jwksLoader(): (() => Promise<{
43
50
  keys: Record<string, any>[];
@@ -2,6 +2,7 @@ import { createHmac, timingSafeEqual } from 'node:crypto';
2
2
  import { NoopRecorder } from '@adonis-agora/authkit-core';
3
3
  import Koa from 'koa';
4
4
  import mount from 'koa-mount';
5
+ import { ProviderIdentities } from '../accounts/provider_identities.js';
5
6
  import { normalizeActiveOrg, readActiveOrgFromKoaCtx, verifyActiveOrgMembership, } from '../host/active_org_cookie.js';
6
7
  import { isFirstPartyClient } from '../host/branding.js';
7
8
  import { listKeyInfos, signingKeyAgeDays, toPublicJwks, } from '../keys/keystore.js';
@@ -66,6 +67,16 @@ export class OidcService {
66
67
  get config() {
67
68
  return this.#config;
68
69
  }
70
+ #identities;
71
+ /**
72
+ * Identidades de provider das contas — a ligação que o "Entrar com <provider>" usa.
73
+ * Para o host ligar uma conta a um provider fora do login social sem tocar no
74
+ * account store cru. Ver {@link ProviderIdentities}.
75
+ */
76
+ get identities() {
77
+ this.#identities ??= new ProviderIdentities(() => this.#config.accountStore);
78
+ return this.#identities;
79
+ }
69
80
  /** @internal Loader de JWKS do cofre (usado por reloadKeys). */
70
81
  get jwksLoader() {
71
82
  return this.#deps.jwksLoader;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis-agora/authkit-server",
3
- "version": "0.88.1",
3
+ "version": "0.90.0",
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",
@@ -117,8 +117,8 @@
117
117
  },
118
118
  "devDependencies": {
119
119
  "@adonis-agora/authkit-react": "0.26.0",
120
- "@adonis-agora/durable": "0.56.7",
121
- "@adonis-agora/telescope": "0.23.0",
120
+ "@adonis-agora/durable": "0.58.2",
121
+ "@adonis-agora/telescope": "0.27.0",
122
122
  "@adonisjs/ally": "6.3.2",
123
123
  "@adonisjs/auth": "10.1.0",
124
124
  "@adonisjs/core": "7.6.0",