@adonis-agora/authkit-server 0.82.1 → 0.82.3
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/src/accounts/account_store.d.ts +12 -0
- package/build/src/accounts/lucid_account_store.js +32 -6
- package/build/src/accounts/lucid_store/organizations.d.ts +6 -0
- package/build/src/accounts/lucid_store/organizations.js +65 -1
- package/build/src/accounts/lucid_store/shared.d.ts +7 -1
- package/build/src/accounts/lucid_store/shared.js +24 -0
- package/build/src/host/admin_api/admin_orgs_service.js +5 -0
- package/package.json +1 -1
|
@@ -608,6 +608,18 @@ export interface OrganizationsCapability {
|
|
|
608
608
|
listOrgsForAccount(accountId: string): Promise<Array<OrgSummary & {
|
|
609
609
|
role: string;
|
|
610
610
|
}>>;
|
|
611
|
+
/**
|
|
612
|
+
* OPTIONAL: every org that has at least one member whose account exists, with
|
|
613
|
+
* its member count, in a bounded number of queries. Must return exactly what
|
|
614
|
+
* the admin fallback builds by walking `listAccounts` (admin order) and calling
|
|
615
|
+
* `listOrgsForAccount` per account: orgs in first-seen order, `role` being the
|
|
616
|
+
* role of the first member seen in that order, `memberCount` being
|
|
617
|
+
* `listOrgMembers(orgId).length`. Absent → the admin service uses that walk.
|
|
618
|
+
*/
|
|
619
|
+
listAllOrgsWithMemberCount?(): Promise<Array<OrgSummary & {
|
|
620
|
+
role: string;
|
|
621
|
+
memberCount: number;
|
|
622
|
+
}>>;
|
|
611
623
|
updateOrg(orgId: string, patch: {
|
|
612
624
|
name?: string;
|
|
613
625
|
logoUrl?: string | null;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from '@simplewebauthn/server';
|
|
2
1
|
import { PasswordManager } from '../password/password_manager.js';
|
|
3
2
|
import { buildCore } from './lucid_store/core.js';
|
|
4
3
|
import { buildLoginMethods, supportsLoginMethodsColumn } from './lucid_store/login_methods.js';
|
|
@@ -7,7 +6,7 @@ import { defaultOrganizationModels } from './lucid_store/organization_models.js'
|
|
|
7
6
|
import { buildOrganizations } from './lucid_store/organizations.js';
|
|
8
7
|
import { buildPasswordExpiration, buildPasswordHistory } from './lucid_store/password_hygiene.js';
|
|
9
8
|
import { buildProviderIdentity } from './lucid_store/provider_identity.js';
|
|
10
|
-
import { hasTable, } from './lucid_store/shared.js';
|
|
9
|
+
import { defaultWebauthnCeremonies, hasTable, } from './lucid_store/shared.js';
|
|
11
10
|
import { buildDeletion, buildEmailVerificationStatus, buildProfile, buildStatus, hasColumn, } from './lucid_store/status_profile.js';
|
|
12
11
|
import { buildWebauthn } from './lucid_store/webauthn.js';
|
|
13
12
|
export function resolveEmailTokens(input) {
|
|
@@ -122,6 +121,35 @@ function resolveAccountTable(Model) {
|
|
|
122
121
|
}
|
|
123
122
|
return undefined;
|
|
124
123
|
}
|
|
124
|
+
/** Up to this many ids, filter with `WHERE IN`; above it, read every id instead. */
|
|
125
|
+
const ADMIN_ORDER_IN_LIMIT = 500;
|
|
126
|
+
/**
|
|
127
|
+
* Of `accountIds`, the ones that exist, in the order `listAccounts` pages them
|
|
128
|
+
* (`ORDER BY email ASC`, sorted by the database so collation matches). One query,
|
|
129
|
+
* plain rows (no model hydration), primary key only.
|
|
130
|
+
*/
|
|
131
|
+
async function listAccountIdsInAdminOrder(Model, accountIds) {
|
|
132
|
+
if (accountIds.length === 0)
|
|
133
|
+
return [];
|
|
134
|
+
Model.boot?.();
|
|
135
|
+
const pk = Model.$columnsDefinitions?.get?.('id')?.columnName ?? 'id';
|
|
136
|
+
const readAll = async () => (await Model.query().select(pk).orderBy('email', 'asc').pojo());
|
|
137
|
+
let rows;
|
|
138
|
+
if (accountIds.length <= ADMIN_ORDER_IN_LIMIT) {
|
|
139
|
+
try {
|
|
140
|
+
rows = await Model.query().select(pk).whereIn(pk, accountIds).orderBy('email', 'asc').pojo();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// e.g. a member row whose account id doesn't fit the key type: the full read
|
|
144
|
+
// never binds member ids, exactly like the `listAccounts` walk.
|
|
145
|
+
rows = await readAll();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
rows = await readAll();
|
|
150
|
+
}
|
|
151
|
+
return rows.map((row) => String(row[pk]));
|
|
152
|
+
}
|
|
125
153
|
export function lucidAccountStore(Model, options = {}) {
|
|
126
154
|
const mfaIssuer = options.mfaIssuer ?? 'AuthKit';
|
|
127
155
|
const recoveryCodeCount = options.recoveryCodeCount ?? 8;
|
|
@@ -147,10 +175,7 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
147
175
|
};
|
|
148
176
|
// Cerimônias WebAuthn: reais por default, injetáveis para testes.
|
|
149
177
|
const ceremonies = {
|
|
150
|
-
|
|
151
|
-
verifyRegistrationResponse,
|
|
152
|
-
generateAuthenticationOptions,
|
|
153
|
-
verifyAuthenticationResponse,
|
|
178
|
+
...defaultWebauthnCeremonies,
|
|
154
179
|
...options.webauthnCeremonies,
|
|
155
180
|
};
|
|
156
181
|
const passwords = new PasswordManager(options.password, {
|
|
@@ -246,6 +271,7 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
246
271
|
const row = await Model.find(accountId);
|
|
247
272
|
return row?.email ?? null;
|
|
248
273
|
},
|
|
274
|
+
listAccountIdsInAdminOrder: (accountIds) => listAccountIdsInAdminOrder(Model, accountIds),
|
|
249
275
|
}),
|
|
250
276
|
}
|
|
251
277
|
: {}),
|
|
@@ -9,5 +9,11 @@ export interface OrgStoreContext {
|
|
|
9
9
|
InvitationModel: any;
|
|
10
10
|
/** Mapa accountId → email, para validar aceitação de convite. */
|
|
11
11
|
findAccountEmail: (accountId: string) => Promise<string | null>;
|
|
12
|
+
/**
|
|
13
|
+
* Of the given account ids, the ones whose account exists, in the admin
|
|
14
|
+
* listing order (`listAccounts`: email asc). When present, the store exposes
|
|
15
|
+
* `listAllOrgsWithMemberCount` (bounded queries for the admin org list).
|
|
16
|
+
*/
|
|
17
|
+
listAccountIdsInAdminOrder?: (accountIds: string[]) => Promise<string[]>;
|
|
12
18
|
}
|
|
13
19
|
export declare function buildOrganizations(ctx: OrgStoreContext): OrganizationsCapability;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import { DateTime } from 'luxon';
|
|
3
|
+
/** Max ids per `WHERE IN` (stays well below every dialect's bind-parameter limit). */
|
|
4
|
+
const IN_CHUNK = 500;
|
|
5
|
+
/** `Model.findMany` in chunks of {@link IN_CHUNK} ids. */
|
|
6
|
+
async function findManyChunked(Model, ids) {
|
|
7
|
+
const rows = [];
|
|
8
|
+
for (let i = 0; i < ids.length; i += IN_CHUNK) {
|
|
9
|
+
rows.push(...(await Model.findMany(ids.slice(i, i + IN_CHUNK))));
|
|
10
|
+
}
|
|
11
|
+
return rows;
|
|
12
|
+
}
|
|
3
13
|
function toOrgSummary(row) {
|
|
4
14
|
return {
|
|
5
15
|
id: row.id,
|
|
@@ -49,8 +59,62 @@ async function countOwners(MemberModel, orgId) {
|
|
|
49
59
|
return Number(result[0]?.$extras?.total ?? 0);
|
|
50
60
|
}
|
|
51
61
|
export function buildOrganizations(ctx) {
|
|
52
|
-
const { OrgModel, MemberModel, InvitationModel, findAccountEmail } = ctx;
|
|
62
|
+
const { OrgModel, MemberModel, InvitationModel, findAccountEmail, listAccountIdsInAdminOrder } = ctx;
|
|
53
63
|
return {
|
|
64
|
+
...(listAccountIdsInAdminOrder
|
|
65
|
+
? {
|
|
66
|
+
/**
|
|
67
|
+
* Admin org list in 3 queries (all memberships, the member accounts in
|
|
68
|
+
* admin order, the orgs) instead of walking every account. Mirrors the
|
|
69
|
+
* fallback in `AdminOrgsService.listOrgs` exactly: accounts in
|
|
70
|
+
* `listAccounts` order (email asc), each account's memberships in the
|
|
71
|
+
* same (natural) order `listOrgsForAccount` reads them, first sighting of
|
|
72
|
+
* an org wins (its `role`), and `memberCount` counts every member row
|
|
73
|
+
* (like `listOrgMembers(orgId).length`).
|
|
74
|
+
*/
|
|
75
|
+
async listAllOrgsWithMemberCount() {
|
|
76
|
+
const memberships = await MemberModel.query();
|
|
77
|
+
if (memberships.length === 0)
|
|
78
|
+
return [];
|
|
79
|
+
const byAccount = new Map();
|
|
80
|
+
const memberCount = new Map();
|
|
81
|
+
for (const m of memberships) {
|
|
82
|
+
const accountId = String(m.accountId);
|
|
83
|
+
const orgId = String(m.organizationId);
|
|
84
|
+
memberCount.set(orgId, (memberCount.get(orgId) ?? 0) + 1);
|
|
85
|
+
const list = byAccount.get(accountId);
|
|
86
|
+
if (list)
|
|
87
|
+
list.push(m);
|
|
88
|
+
else
|
|
89
|
+
byAccount.set(accountId, [m]);
|
|
90
|
+
}
|
|
91
|
+
const accountIds = await listAccountIdsInAdminOrder([...byAccount.keys()]);
|
|
92
|
+
const orgs = new Map();
|
|
93
|
+
for (const org of await findManyChunked(OrgModel, [...memberCount.keys()])) {
|
|
94
|
+
orgs.set(String(org.id), org);
|
|
95
|
+
}
|
|
96
|
+
const result = [];
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
for (const accountId of accountIds) {
|
|
99
|
+
for (const m of byAccount.get(String(accountId)) ?? []) {
|
|
100
|
+
const orgId = String(m.organizationId);
|
|
101
|
+
if (seen.has(orgId))
|
|
102
|
+
continue;
|
|
103
|
+
const org = orgs.get(orgId);
|
|
104
|
+
if (!org)
|
|
105
|
+
continue;
|
|
106
|
+
seen.add(orgId);
|
|
107
|
+
result.push({
|
|
108
|
+
...toOrgSummary(org),
|
|
109
|
+
role: m.role,
|
|
110
|
+
memberCount: memberCount.get(orgId) ?? 0,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
: {}),
|
|
54
118
|
async createOrg(input) {
|
|
55
119
|
const { randomUUID } = await import('node:crypto');
|
|
56
120
|
const org = await OrgModel.create({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from '@simplewebauthn/server';
|
|
1
|
+
import type { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from '@simplewebauthn/server';
|
|
2
2
|
import type { AuditSink } from '../../audit/audit_sink.js';
|
|
3
3
|
import type { PasswordManager } from '../../password/password_manager.js';
|
|
4
4
|
import type { AuthAccount } from '../account_store.js';
|
|
@@ -23,6 +23,12 @@ export interface WebauthnCeremonies {
|
|
|
23
23
|
generateAuthenticationOptions: typeof generateAuthenticationOptions;
|
|
24
24
|
verifyAuthenticationResponse: typeof verifyAuthenticationResponse;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* As cerimônias reais (default de {@link WebauthnCeremonies}). Todas já são
|
|
28
|
+
* assíncronas no `@simplewebauthn/server`; cada uma só espera o import lazy antes
|
|
29
|
+
* de delegar.
|
|
30
|
+
*/
|
|
31
|
+
export declare const defaultWebauthnCeremonies: WebauthnCeremonies;
|
|
26
32
|
/** RP (Relying Party) do WebAuthn usado nas cerimônias. */
|
|
27
33
|
export interface ResolvedRp {
|
|
28
34
|
rpName: string;
|
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* `@simplewebauthn/server` carregado na primeira cerimônia, não no boot: a maioria
|
|
4
|
+
* dos processos (workers, ace, apps sem passkeys) nunca faz uma. Um import por
|
|
5
|
+
* processo; uma falha não fica em cache (a próxima cerimônia tenta de novo).
|
|
6
|
+
*/
|
|
7
|
+
let simpleWebauthn;
|
|
8
|
+
function loadSimpleWebauthn() {
|
|
9
|
+
simpleWebauthn ??= import('@simplewebauthn/server').catch((error) => {
|
|
10
|
+
simpleWebauthn = undefined;
|
|
11
|
+
throw error;
|
|
12
|
+
});
|
|
13
|
+
return simpleWebauthn;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* As cerimônias reais (default de {@link WebauthnCeremonies}). Todas já são
|
|
17
|
+
* assíncronas no `@simplewebauthn/server`; cada uma só espera o import lazy antes
|
|
18
|
+
* de delegar.
|
|
19
|
+
*/
|
|
20
|
+
export const defaultWebauthnCeremonies = {
|
|
21
|
+
generateRegistrationOptions: async (options) => (await loadSimpleWebauthn()).generateRegistrationOptions(options),
|
|
22
|
+
verifyRegistrationResponse: async (options) => (await loadSimpleWebauthn()).verifyRegistrationResponse(options),
|
|
23
|
+
generateAuthenticationOptions: async (options) => (await loadSimpleWebauthn()).generateAuthenticationOptions(options),
|
|
24
|
+
verifyAuthenticationResponse: async (options) => (await loadSimpleWebauthn()).verifyAuthenticationResponse(options),
|
|
25
|
+
};
|
|
2
26
|
export const sha256 = (value) => createHash('sha256').update(value).digest('hex');
|
|
3
27
|
/** Recovery code legível: 10 chars hex em duas metades (ex.: a1b2c-3d4e5). */
|
|
4
28
|
export function generateRecoveryCode() {
|
|
@@ -55,6 +55,11 @@ export class AdminOrgsService {
|
|
|
55
55
|
const store = this.cfg.accountStore;
|
|
56
56
|
if (!supportsOrganizations(store))
|
|
57
57
|
return { ok: false, reason: 'not_supported' };
|
|
58
|
+
// Fast path: stores that can answer in a bounded number of queries (the Lucid
|
|
59
|
+
// store does). Same list, same order as the walk below.
|
|
60
|
+
if (typeof store.listAllOrgsWithMemberCount === 'function') {
|
|
61
|
+
return store.listAllOrgsWithMemberCount();
|
|
62
|
+
}
|
|
58
63
|
// O store não tem listAllOrgs — acumulamos via memberships de todas as contas.
|
|
59
64
|
// Para admin, fazemos um listAccounts full e coletamos todas as orgs únicas.
|
|
60
65
|
// Mais eficiente: usa a tabela de organizations diretamente pelo listOrgMembers
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.82.
|
|
3
|
+
"version": "0.82.3",
|
|
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",
|