@shipfox/api-auth 9.3.0 → 10.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +35 -0
- package/README.md +10 -3
- package/dist/core/administration.d.ts +18 -2
- package/dist/core/administration.d.ts.map +1 -1
- package/dist/core/administration.js +34 -4
- package/dist/core/administration.js.map +1 -1
- package/dist/core/entities/administrator-read-model.d.ts +21 -0
- package/dist/core/entities/administrator-read-model.d.ts.map +1 -0
- package/dist/core/entities/administrator-read-model.js +3 -0
- package/dist/core/entities/administrator-read-model.js.map +1 -0
- package/dist/db/admin-grants.d.ts +16 -1
- package/dist/db/admin-grants.d.ts.map +1 -1
- package/dist/db/admin-grants.js +32 -4
- package/dist/db/admin-grants.js.map +1 -1
- package/dist/db/admin-users.d.ts +21 -0
- package/dist/db/admin-users.d.ts.map +1 -0
- package/dist/db/admin-users.js +32 -0
- package/dist/db/admin-users.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +1 -1
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/routes/administration.d.ts +1 -0
- package/dist/presentation/routes/administration.d.ts.map +1 -1
- package/dist/presentation/routes/administration.js +91 -10
- package/dist/presentation/routes/administration.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +6 -0
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/core/administration.ts +45 -4
- package/src/core/entities/administrator-read-model.ts +23 -0
- package/src/db/admin-grants.test.ts +1 -7
- package/src/db/admin-grants.ts +52 -4
- package/src/db/admin-users.ts +58 -0
- package/src/index.test.ts +1 -1
- package/src/index.ts +5 -2
- package/src/metrics/instance.ts +1 -1
- package/src/presentation/routes/administration.test.ts +304 -20
- package/src/presentation/routes/administration.ts +91 -10
- package/src/presentation/routes/rate-limit.ts +3 -0
- package/test/routes.ts +5 -2
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-auth",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "10.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -27,19 +27,19 @@
|
|
|
27
27
|
"drizzle-orm": "^0.45.2",
|
|
28
28
|
"zod": "^4.4.3",
|
|
29
29
|
"@shipfox/api-common-dto": "9.2.0",
|
|
30
|
-
"@shipfox/api-auth-context": "
|
|
31
|
-
"@shipfox/api-auth-dto": "
|
|
32
|
-
"@shipfox/api-email-challenges": "1.1.
|
|
33
|
-
"@shipfox/api-workspaces-dto": "
|
|
30
|
+
"@shipfox/api-auth-context": "10.0.0",
|
|
31
|
+
"@shipfox/api-auth-dto": "10.0.0",
|
|
32
|
+
"@shipfox/api-email-challenges": "1.1.6",
|
|
33
|
+
"@shipfox/api-workspaces-dto": "10.0.0",
|
|
34
34
|
"@shipfox/inter-module": "0.2.2",
|
|
35
35
|
"@shipfox/config": "1.2.4",
|
|
36
36
|
"@shipfox/node-drizzle": "0.3.4",
|
|
37
37
|
"@shipfox/node-auth-root-key": "0.2.3",
|
|
38
|
-
"@shipfox/node-fastify": "0.
|
|
38
|
+
"@shipfox/node-fastify": "0.4.0",
|
|
39
39
|
"@shipfox/node-jwt": "0.3.2",
|
|
40
40
|
"@shipfox/node-email": "0.3.4",
|
|
41
41
|
"@shipfox/node-mailer": "0.2.4",
|
|
42
|
-
"@shipfox/node-module": "1.0.
|
|
42
|
+
"@shipfox/node-module": "1.0.4",
|
|
43
43
|
"@shipfox/node-opentelemetry": "0.6.3",
|
|
44
44
|
"@shipfox/node-outbox": "0.2.6",
|
|
45
45
|
"@shipfox/node-postgres": "0.4.4",
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import {createHash, timingSafeEqual} from 'node:crypto';
|
|
2
2
|
import type {AdminRole} from '@shipfox/api-auth-dto';
|
|
3
3
|
import {createAdministrationActionEvent} from '@shipfox/api-common-dto';
|
|
4
|
+
import type {TimestampIdCursor} from '@shipfox/node-drizzle';
|
|
4
5
|
import {config} from '#config.js';
|
|
5
6
|
import {
|
|
6
7
|
bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb,
|
|
7
8
|
grantAdminRoleWithAudit,
|
|
8
|
-
|
|
9
|
+
listAdminGrantSummaries,
|
|
9
10
|
revokeAdminGrantWithAudit,
|
|
10
11
|
} from '#db/admin-grants.js';
|
|
12
|
+
import {findAdministratorUser as findAdministratorUserInDb} from '#db/admin-users.js';
|
|
11
13
|
import {requireAdminRole} from './admin-role.js';
|
|
12
14
|
import type {AdminGrant} from './entities/admin-grant.js';
|
|
15
|
+
import type {
|
|
16
|
+
AdministratorGrantSummary,
|
|
17
|
+
AdministratorUserSummary,
|
|
18
|
+
} from './entities/administrator-read-model.js';
|
|
13
19
|
import {
|
|
14
20
|
AdminBootstrapClosedError,
|
|
15
21
|
AdminGrantAlreadyExistsError,
|
|
@@ -21,6 +27,7 @@ import {
|
|
|
21
27
|
} from './errors.js';
|
|
22
28
|
|
|
23
29
|
const ADMIN_OWNER_ROLE: AdminRole = 'admin-owner';
|
|
30
|
+
const ADMIN_OBSERVER_ROLE: AdminRole = 'admin-observer';
|
|
24
31
|
const BOOTSTRAP_COMMAND = 'auth.admin_grant.bootstrap';
|
|
25
32
|
const GRANT_COMMAND = 'auth.admin_grant.grant';
|
|
26
33
|
const REVOKE_COMMAND = 'auth.admin_grant.revoke';
|
|
@@ -98,9 +105,43 @@ export async function bootstrapFirstAdminOwner(
|
|
|
98
105
|
});
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
export async function
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
export async function findAdministratorUserSummary(
|
|
109
|
+
params: {actorId: string} & ({id: string; email?: never} | {email: string; id?: never}),
|
|
110
|
+
): Promise<AdministratorUserSummary | undefined> {
|
|
111
|
+
await requireAdminRole({userId: params.actorId, minimumRole: ADMIN_OBSERVER_ROLE});
|
|
112
|
+
|
|
113
|
+
const user = await findAdministratorUserInDb(
|
|
114
|
+
'id' in params ? {id: params.id} : {email: params.email},
|
|
115
|
+
);
|
|
116
|
+
if (!user) return undefined;
|
|
117
|
+
|
|
118
|
+
return user;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function listAdministratorGrantSummaries(params: {
|
|
122
|
+
actorId: string;
|
|
123
|
+
limit: number;
|
|
124
|
+
cursor?: TimestampIdCursor;
|
|
125
|
+
}): Promise<{
|
|
126
|
+
grants: AdministratorGrantSummary[];
|
|
127
|
+
nextCursor: TimestampIdCursor | null;
|
|
128
|
+
}> {
|
|
129
|
+
await requireAdminRole({userId: params.actorId, minimumRole: ADMIN_OBSERVER_ROLE});
|
|
130
|
+
|
|
131
|
+
const result = await listAdminGrantSummaries({
|
|
132
|
+
limit: params.limit,
|
|
133
|
+
...(params.cursor ? {cursor: params.cursor} : {}),
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
grants: result.rows.map((row) => ({
|
|
137
|
+
grantId: row.id,
|
|
138
|
+
role: row.role,
|
|
139
|
+
createdAt: row.createdAt,
|
|
140
|
+
revokedAt: row.revokedAt,
|
|
141
|
+
user: row.user,
|
|
142
|
+
})),
|
|
143
|
+
nextCursor: result.nextCursor,
|
|
144
|
+
};
|
|
104
145
|
}
|
|
105
146
|
|
|
106
147
|
export async function grantAdministratorRole(
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {AdminRole} from '@shipfox/api-auth-dto';
|
|
2
|
+
import type {UserStatus} from './user.js';
|
|
3
|
+
|
|
4
|
+
export interface AdministratorUserIdentity {
|
|
5
|
+
id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
name: string | null;
|
|
8
|
+
status: UserStatus;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AdministratorUserSummary extends AdministratorUserIdentity {
|
|
12
|
+
emailVerifiedAt: Date | null;
|
|
13
|
+
createdAt: Date;
|
|
14
|
+
adminRole: AdminRole | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AdministratorGrantSummary {
|
|
18
|
+
grantId: string;
|
|
19
|
+
role: AdminRole;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
revokedAt: Date | null;
|
|
22
|
+
user: AdministratorUserIdentity;
|
|
23
|
+
}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import {eq} from 'drizzle-orm';
|
|
2
2
|
import {LastAdminOwnerError} from '#core/errors.js';
|
|
3
3
|
import {userFactory} from '#test/index.js';
|
|
4
|
-
import {
|
|
5
|
-
createAdminGrant,
|
|
6
|
-
findCurrentAdminRole,
|
|
7
|
-
listAdminGrants,
|
|
8
|
-
revokeAdminGrant,
|
|
9
|
-
} from './admin-grants.js';
|
|
4
|
+
import {createAdminGrant, findCurrentAdminRole, revokeAdminGrant} from './admin-grants.js';
|
|
10
5
|
import {db} from './db.js';
|
|
11
6
|
import {users} from './schema/users.js';
|
|
12
7
|
|
|
@@ -18,7 +13,6 @@ describe('admin grants db', () => {
|
|
|
18
13
|
|
|
19
14
|
expect(observer.userId).toBe(user.id);
|
|
20
15
|
expect(await findCurrentAdminRole({userId: user.id})).toBe('admin-operator');
|
|
21
|
-
expect((await listAdminGrants()).filter((grant) => grant.userId === user.id)).toHaveLength(2);
|
|
22
16
|
});
|
|
23
17
|
|
|
24
18
|
test('does not evaluate revoked grants or grants for suspended users', async () => {
|
package/src/db/admin-grants.ts
CHANGED
|
@@ -3,10 +3,16 @@ import type {
|
|
|
3
3
|
AdministrationActionEvent,
|
|
4
4
|
AdministrationActionEventMap,
|
|
5
5
|
} from '@shipfox/api-common-dto';
|
|
6
|
+
import {
|
|
7
|
+
paginateTimestampIdRows,
|
|
8
|
+
type TimestampIdCursor,
|
|
9
|
+
timestampIdCursorWhere,
|
|
10
|
+
} from '@shipfox/node-drizzle';
|
|
6
11
|
import {writeOutboxEvent} from '@shipfox/node-outbox';
|
|
7
|
-
import {and,
|
|
12
|
+
import {and, desc, eq, isNull, sql} from 'drizzle-orm';
|
|
8
13
|
import {highestAdminRole} from '#core/admin-role-model.js';
|
|
9
14
|
import type {AdminGrant} from '#core/entities/admin-grant.js';
|
|
15
|
+
import type {AdministratorGrantSummary} from '#core/entities/administrator-read-model.js';
|
|
10
16
|
import {
|
|
11
17
|
AdminBootstrapClosedError,
|
|
12
18
|
AdminGrantAlreadyExistsError,
|
|
@@ -42,9 +48,51 @@ export async function createAdminGrant(params: CreateAdminGrantParams): Promise<
|
|
|
42
48
|
return toAdminGrant(row);
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
export
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
export interface AdministratorGrantSummaryRecord {
|
|
52
|
+
id: string;
|
|
53
|
+
role: AdminRole;
|
|
54
|
+
createdAt: Date;
|
|
55
|
+
revokedAt: Date | null;
|
|
56
|
+
user: AdministratorGrantSummary['user'];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function listAdminGrantSummaries(params: {
|
|
60
|
+
limit: number;
|
|
61
|
+
cursor?: TimestampIdCursor;
|
|
62
|
+
}): Promise<{
|
|
63
|
+
rows: AdministratorGrantSummaryRecord[];
|
|
64
|
+
nextCursor: TimestampIdCursor | null;
|
|
65
|
+
}> {
|
|
66
|
+
const cursorCondition = timestampIdCursorWhere({
|
|
67
|
+
timestampColumn: adminGrants.createdAt,
|
|
68
|
+
idColumn: adminGrants.id,
|
|
69
|
+
cursor: params.cursor,
|
|
70
|
+
});
|
|
71
|
+
const conditions = cursorCondition ? [cursorCondition] : [];
|
|
72
|
+
const rows = await db()
|
|
73
|
+
.select({
|
|
74
|
+
id: adminGrants.id,
|
|
75
|
+
role: adminGrants.role,
|
|
76
|
+
createdAt: adminGrants.createdAt,
|
|
77
|
+
revokedAt: adminGrants.revokedAt,
|
|
78
|
+
user: {
|
|
79
|
+
id: users.id,
|
|
80
|
+
email: users.email,
|
|
81
|
+
name: users.name,
|
|
82
|
+
status: users.status,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
.from(adminGrants)
|
|
86
|
+
.innerJoin(users, eq(adminGrants.userId, users.id))
|
|
87
|
+
.where(and(...conditions))
|
|
88
|
+
.orderBy(desc(adminGrants.createdAt), desc(adminGrants.id))
|
|
89
|
+
.limit(params.limit + 1);
|
|
90
|
+
|
|
91
|
+
const page = paginateTimestampIdRows({rows, limit: params.limit, timestampKey: 'createdAt'});
|
|
92
|
+
return {
|
|
93
|
+
rows: page.pageRows,
|
|
94
|
+
nextCursor: page.nextCursor,
|
|
95
|
+
};
|
|
48
96
|
}
|
|
49
97
|
|
|
50
98
|
export async function findCurrentAdminRole(params: {userId: string}): Promise<AdminRole | null> {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type {AdminRole} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {and, eq, isNull} from 'drizzle-orm';
|
|
3
|
+
import {highestAdminRole} from '#core/admin-role-model.js';
|
|
4
|
+
import type {UserStatus} from '#core/entities/user.js';
|
|
5
|
+
import {db} from './db.js';
|
|
6
|
+
import {adminGrants} from './schema/admin-grants.js';
|
|
7
|
+
import {users} from './schema/users.js';
|
|
8
|
+
|
|
9
|
+
export interface AdministratorUserRecord {
|
|
10
|
+
id: string;
|
|
11
|
+
email: string;
|
|
12
|
+
name: string | null;
|
|
13
|
+
emailVerifiedAt: Date | null;
|
|
14
|
+
status: UserStatus;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
adminRole: AdminRole | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type AdministratorUserLookup = {id: string; email?: never} | {email: string; id?: never};
|
|
20
|
+
|
|
21
|
+
export async function findAdministratorUser(
|
|
22
|
+
params: AdministratorUserLookup,
|
|
23
|
+
): Promise<AdministratorUserRecord | undefined> {
|
|
24
|
+
const identifier = 'id' in params ? eq(users.id, params.id) : eq(users.email, params.email);
|
|
25
|
+
const rows = await db()
|
|
26
|
+
.select({
|
|
27
|
+
id: users.id,
|
|
28
|
+
email: users.email,
|
|
29
|
+
name: users.name,
|
|
30
|
+
emailVerifiedAt: users.emailVerifiedAt,
|
|
31
|
+
status: users.status,
|
|
32
|
+
createdAt: users.createdAt,
|
|
33
|
+
adminRole: adminGrants.role,
|
|
34
|
+
})
|
|
35
|
+
.from(users)
|
|
36
|
+
.leftJoin(
|
|
37
|
+
adminGrants,
|
|
38
|
+
and(
|
|
39
|
+
eq(adminGrants.userId, users.id),
|
|
40
|
+
isNull(adminGrants.revokedAt),
|
|
41
|
+
eq(users.status, 'active'),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
.where(identifier);
|
|
45
|
+
|
|
46
|
+
const first = rows[0];
|
|
47
|
+
if (!first) return undefined;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
id: first.id,
|
|
51
|
+
email: first.email,
|
|
52
|
+
name: first.name,
|
|
53
|
+
emailVerifiedAt: first.emailVerifiedAt,
|
|
54
|
+
status: first.status,
|
|
55
|
+
createdAt: first.createdAt,
|
|
56
|
+
adminRole: highestAdminRole(rows.flatMap(({adminRole}) => (adminRole ? [adminRole] : []))),
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/index.test.ts
CHANGED
|
@@ -78,7 +78,7 @@ describe('authModule', () => {
|
|
|
78
78
|
getWorkspaceOperatingState: vi.fn(),
|
|
79
79
|
},
|
|
80
80
|
});
|
|
81
|
-
expect(module.routes).toHaveLength(
|
|
81
|
+
expect(module.routes).toHaveLength(3);
|
|
82
82
|
const signupPolicy = buildAuthRoutes.mock.calls[0]?.[2];
|
|
83
83
|
|
|
84
84
|
expect(signupPolicy).toEqual(expect.objectContaining({isSignupAllowed: expect.any(Function)}));
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,10 @@ import {createLeaseTokenAuthMethod} from '#presentation/auth/lease-token-auth.js
|
|
|
17
17
|
import {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-auth.js';
|
|
18
18
|
import {createAuthE2eRoutes} from '#presentation/e2eRoutes/index.js';
|
|
19
19
|
import {createAuthInterModulePresentation} from '#presentation/inter-module.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
administrationRoutes,
|
|
22
|
+
administrationUserRoutes,
|
|
23
|
+
} from '#presentation/routes/administration.js';
|
|
21
24
|
import {buildAuthRoutes} from '#presentation/routes/index.js';
|
|
22
25
|
import {onPasswordResetSendRequested} from '#presentation/subscribers/index.js';
|
|
23
26
|
import {passwordLoginMethods} from './login-methods.js';
|
|
@@ -36,7 +39,6 @@ export {
|
|
|
36
39
|
export {
|
|
37
40
|
bootstrapFirstAdminOwner,
|
|
38
41
|
grantAdministratorRole,
|
|
39
|
-
listAdministratorGrants,
|
|
40
42
|
revokeAdministratorGrant,
|
|
41
43
|
} from '#core/administration.js';
|
|
42
44
|
export type {
|
|
@@ -113,6 +115,7 @@ export function createAuthModule({
|
|
|
113
115
|
routes: [
|
|
114
116
|
buildAuthRoutes(config.AUTH_PASSWORD_ENABLED, workspaces, signupPolicy),
|
|
115
117
|
administrationRoutes,
|
|
118
|
+
administrationUserRoutes,
|
|
116
119
|
],
|
|
117
120
|
e2eRoutes: [createAuthE2eRoutes(workspaces)],
|
|
118
121
|
publishers: [{name: 'auth', table: authOutbox, db, eventSchemas: authPublisherEventSchemas}],
|
package/src/metrics/instance.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {instanceMetrics} from '@shipfox/node-opentelemetry';
|
|
|
3
3
|
export type AuthTokenType = 'session' | 'job_lease' | 'runner_session';
|
|
4
4
|
export type AuthTokenVerificationOutcome = 'ok' | 'rejected';
|
|
5
5
|
export type AuthTokenRefreshOutcome = 'rotated' | 'grace' | 'rejected';
|
|
6
|
-
export type AuthRateLimitAction = 'login' | 'email-send' | 'bootstrap';
|
|
6
|
+
export type AuthRateLimitAction = 'login' | 'email-send' | 'bootstrap' | 'lookup';
|
|
7
7
|
export type AuthRateLimitScope = 'ip' | 'email';
|
|
8
8
|
export type AuthRateLimitOutcome = 'allowed' | 'blocked' | 'unavailable';
|
|
9
9
|
|