@shipfox/api-auth 10.1.0 → 11.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 +30 -0
- package/README.md +42 -11
- package/dist/core/administration.d.ts +17 -1
- package/dist/core/administration.d.ts.map +1 -1
- package/dist/core/administration.js +107 -14
- package/dist/core/administration.js.map +1 -1
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +27 -10
- package/dist/core/auth.js.map +1 -1
- package/dist/core/jwt.d.ts +10 -0
- package/dist/core/jwt.d.ts.map +1 -1
- package/dist/core/jwt.js +3 -2
- package/dist/core/jwt.js.map +1 -1
- package/dist/db/admin-command.d.ts +18 -0
- package/dist/db/admin-command.d.ts.map +1 -0
- package/dist/db/admin-command.js +37 -0
- package/dist/db/admin-command.js.map +1 -0
- package/dist/db/admin-grants.d.ts +1 -1
- package/dist/db/admin-grants.d.ts.map +1 -1
- package/dist/db/admin-grants.js +12 -30
- package/dist/db/admin-grants.js.map +1 -1
- package/dist/db/admin-user-moderation.d.ts +29 -0
- package/dist/db/admin-user-moderation.d.ts.map +1 -0
- package/dist/db/admin-user-moderation.js +156 -0
- package/dist/db/admin-user-moderation.js.map +1 -0
- package/dist/db/admin-user-summary.d.ts +14 -0
- package/dist/db/admin-user-summary.d.ts.map +1 -0
- package/dist/db/admin-user-summary.js +31 -0
- package/dist/db/admin-user-summary.js.map +1 -0
- package/dist/db/admin-users.d.ts +2 -11
- package/dist/db/admin-users.d.ts.map +1 -1
- package/dist/db/admin-users.js +2 -27
- package/dist/db/admin-users.js.map +1 -1
- package/dist/db/refresh-tokens.d.ts +6 -4
- package/dist/db/refresh-tokens.d.ts.map +1 -1
- package/dist/db/refresh-tokens.js +31 -6
- package/dist/db/refresh-tokens.js.map +1 -1
- package/dist/db/schema/admin-command-results.d.ts +19 -2
- package/dist/db/schema/admin-command-results.d.ts.map +1 -1
- package/dist/db/schema/admin-command-results.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/auth/bearer-token-auth.d.ts +1 -0
- package/dist/presentation/auth/bearer-token-auth.d.ts.map +1 -1
- package/dist/presentation/auth/bearer-token-auth.js +4 -3
- package/dist/presentation/auth/bearer-token-auth.js.map +1 -1
- package/dist/presentation/auth/jwt-auth.d.ts +1 -4
- package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
- package/dist/presentation/auth/jwt-auth.js +23 -19
- package/dist/presentation/auth/jwt-auth.js.map +1 -1
- package/dist/presentation/routes/administration.d.ts.map +1 -1
- package/dist/presentation/routes/administration.js +94 -3
- package/dist/presentation/routes/administration.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/core/administration.ts +137 -15
- package/src/core/auth.test.ts +29 -10
- package/src/core/auth.ts +36 -14
- package/src/core/jwt.test.ts +20 -4
- package/src/core/jwt.ts +2 -1
- package/src/db/admin-command.ts +85 -0
- package/src/db/admin-grants.ts +19 -57
- package/src/db/admin-user-moderation.ts +230 -0
- package/src/db/admin-user-summary.ts +51 -0
- package/src/db/admin-users.ts +4 -49
- package/src/db/refresh-tokens.test.ts +0 -19
- package/src/db/refresh-tokens.ts +54 -23
- package/src/db/schema/admin-command-results.ts +23 -2
- package/src/index.ts +3 -0
- package/src/presentation/auth/bearer-token-auth.test.ts +17 -0
- package/src/presentation/auth/bearer-token-auth.ts +9 -2
- package/src/presentation/auth/jwt-auth.test.ts +34 -9
- package/src/presentation/auth/jwt-auth.ts +19 -13
- package/src/presentation/routes/administration.test.ts +253 -2
- package/src/presentation/routes/administration.ts +88 -1
- package/src/presentation/routes/password/change-password.test.ts +6 -0
- package/src/presentation/routes/password/password-reset-confirm.test.ts +14 -0
- package/src/presentation/routes/registration/signup.test.ts +2 -2
- package/src/presentation/routes/session/logout.test.ts +6 -0
- package/src/presentation/routes/session/refresh.test.ts +11 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
$ shipfox-swc
|
|
2
|
-
Successfully compiled:
|
|
2
|
+
Successfully compiled: 67 files with swc (446.61ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @shipfox/api-auth
|
|
2
2
|
|
|
3
|
+
## 11.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 25158c8: Carry workspace lifecycle status in JWT membership claims and enforce suspended or inactive access at the stateless workspace gate while keeping access-token verification stateless.
|
|
8
|
+
|
|
9
|
+
`getAuthenticatedSessionContext()` now reads refresh-session metadata from verified access-token claims without checking active refresh-session state; revoking a refresh session does not invalidate an already-issued access token.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [25158c8]
|
|
14
|
+
- @shipfox/api-auth-context@11.0.0
|
|
15
|
+
- @shipfox/api-workspaces-dto@11.0.0
|
|
16
|
+
|
|
17
|
+
## 10.2.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 95d1456: Add administrator user suspension, reactivation, and all-session revocation commands.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 07e7371: Add idempotent administrator workspace suspension and reactivation commands with atomic redacted administration events.
|
|
26
|
+
- Updated dependencies [95d1456]
|
|
27
|
+
- Updated dependencies [0773b85]
|
|
28
|
+
- Updated dependencies [07e7371]
|
|
29
|
+
- @shipfox/api-auth-dto@10.2.0
|
|
30
|
+
- @shipfox/api-workspaces-dto@10.2.0
|
|
31
|
+
- @shipfox/api-auth-context@10.2.0
|
|
32
|
+
|
|
3
33
|
## 10.1.0
|
|
4
34
|
|
|
5
35
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -70,6 +70,17 @@ Required environment:
|
|
|
70
70
|
| `CLIENT_BASE_URL` | `http://localhost:5173` | Base URL used in email verification and password reset links. |
|
|
71
71
|
| `ADMIN_BOOTSTRAP_TOKEN` | none | Deployment secret accepted once to create the first administrator owner. Set it before bootstrap and remove or rotate it after successful bootstrap. |
|
|
72
72
|
|
|
73
|
+
The recommended access-token lifetime is 15 minutes. Because user JWTs are
|
|
74
|
+
verified from their claims, a token issued before logout, password changes,
|
|
75
|
+
session revocation, or workspace suspension remains usable until its configured
|
|
76
|
+
`exp` time. Logout and session revocation block refresh-session renewal, while
|
|
77
|
+
password changes revoke other refresh sessions and retain the current session
|
|
78
|
+
when its refresh cookie is valid. Workspace suspension is carried into the next
|
|
79
|
+
status-aware snapshot, where the stateless workspace gate rejects member access;
|
|
80
|
+
none of these actions retroactively invalidate an issued access token. Newly
|
|
81
|
+
issued and refreshed tokens carry workspace lifecycle status so the gate can
|
|
82
|
+
return the stable suspended or inactive access result.
|
|
83
|
+
|
|
73
84
|
Email delivery uses the shared `@shipfox/node-mailer` configuration.
|
|
74
85
|
|
|
75
86
|
The signup gate is off by default. When it is on, provide at least one non-empty
|
|
@@ -99,8 +110,9 @@ itself.
|
|
|
99
110
|
### User session token
|
|
100
111
|
|
|
101
112
|
- **Design:** a stateless session token. It carries the user's identity and
|
|
102
|
-
|
|
103
|
-
a per-request user or
|
|
113
|
+
workspace membership list, including each workspace's lifecycle status, in
|
|
114
|
+
its claims so protected routes can authorize without a per-request user or
|
|
115
|
+
membership lookup.
|
|
104
116
|
- **Audience:** signed-in users on first-party clients (web app, CLI).
|
|
105
117
|
- **Grants:** the user's own account routes and, downstream, any route that
|
|
106
118
|
authorizes against the membership claims. Its scope is "this user, with these
|
|
@@ -111,12 +123,12 @@ itself.
|
|
|
111
123
|
fresh token when it expires.
|
|
112
124
|
- *Store* — held in client memory only, never persisted to disk or local
|
|
113
125
|
storage. Refresh sessions are stored server-side as hashes, never in raw form.
|
|
114
|
-
- *Discard* — expires
|
|
115
|
-
|
|
126
|
+
- *Discard* — expires at the configured `exp` time. Revoking the refresh
|
|
127
|
+
session blocks renewal but does not invalidate the issued access token.
|
|
116
128
|
- **Tradeoff — stale memberships:** because memberships ride in the claims, a
|
|
117
|
-
membership change only takes effect on the next
|
|
118
|
-
|
|
119
|
-
membership read is avoided.
|
|
129
|
+
membership or workspace-status change only takes effect on the next token
|
|
130
|
+
issuance or refresh. Accepted because the recommended lifetime is 15 minutes,
|
|
131
|
+
so the staleness window is bounded and a per-request membership read is avoided.
|
|
120
132
|
|
|
121
133
|
### Runner session token
|
|
122
134
|
|
|
@@ -273,6 +285,20 @@ This app-layer limiter protects semantic auth work such as Argon2 verification a
|
|
|
273
285
|
|
|
274
286
|
`GET /admin/auth/admin-grants` also requires `admin-observer`. It returns at most 100 newest-first `AdministratorGrantSummary` rows per page, with an opaque cursor and a safe embedded user identity. It does not return credentials, sessions, provider payloads, OAuth tokens, refresh tokens, or raw authentication metadata.
|
|
275
287
|
|
|
288
|
+
### Administrator user moderation
|
|
289
|
+
|
|
290
|
+
All routes are mounted under `/admin/auth/users` and require an authenticated bearer token.
|
|
291
|
+
|
|
292
|
+
| Method | Path | Required role | Result |
|
|
293
|
+
| --- | --- | --- | --- |
|
|
294
|
+
| `POST` | `/:userId/suspend` | `admin-operator` | Suspends the user, revokes all refresh sessions, and returns the final safe user summary with a correlation ID. Issued access tokens remain valid until `exp`. |
|
|
295
|
+
| `POST` | `/:userId/reactivate` | `admin-operator` | Restores sign-in eligibility without restoring revoked sessions. |
|
|
296
|
+
| `POST` | `/:userId/revoke-sessions` | `admin-operator` | Revokes all refresh sessions without changing account status and returns the number of affected sessions. Issued access tokens remain valid until `exp`. |
|
|
297
|
+
|
|
298
|
+
Each command requires an `Idempotency-Key`. Suspension requires a bounded reason; reactivation and session revocation accept an optional bounded reason. Repeating a successful command returns its committed result. Reusing a key for a different command or request returns `409 idempotency-key-reused`.
|
|
299
|
+
|
|
300
|
+
Suspension preserves the user record and rejects the final active administrator owner. State, the command result, and one redacted `administration.action.performed` outbox event commit atomically. The event stores only the idempotency-key fingerprint, never the raw key or session material.
|
|
301
|
+
|
|
276
302
|
## API
|
|
277
303
|
|
|
278
304
|
The package exports a module factory. Pass it the Workspaces inter-module client from the
|
|
@@ -297,7 +323,7 @@ It also exports lower-level pieces for tests and advanced integration:
|
|
|
297
323
|
- `issueJobLeaseToken(claims)` / `verifyJobLeaseToken(token)`: mint and verify job lease tokens.
|
|
298
324
|
- `createEnvironmentSignupPolicy()`: creates the environment-backed signup policy used by default. Pass `signupPolicy` to `createAuthModule` to replace it.
|
|
299
325
|
- `getClientContext(request)`: reads the authenticated user context from a Fastify request.
|
|
300
|
-
- `getAuthenticatedSessionContext(request)`:
|
|
326
|
+
- `getAuthenticatedSessionContext(request)`: reads the user ID and required refresh-session ID from verified access-token claims. It does not check whether the refresh session is still active.
|
|
301
327
|
- `findUserByEmail({email})`: read-only lookup of the current owner of a normalized email; see below.
|
|
302
328
|
- Entity types: `User`, `UserStatus`, `RefreshToken`, `PasswordReset`, and `EmailOwner`.
|
|
303
329
|
|
|
@@ -402,9 +428,9 @@ const session = await getAuthenticatedSessionContext(request);
|
|
|
402
428
|
```
|
|
403
429
|
|
|
404
430
|
The refresh-session ID remains the same when its refresh token rotates. The
|
|
405
|
-
helper
|
|
406
|
-
|
|
407
|
-
|
|
431
|
+
helper does not query the refresh-session table. It returns the standard `401
|
|
432
|
+
unauthorized` error when the verified request context is missing, inconsistent,
|
|
433
|
+
or has no refresh-session ID. It exposes neither refresh-token material nor
|
|
408
434
|
storage details.
|
|
409
435
|
|
|
410
436
|
## Data Model
|
|
@@ -427,6 +453,11 @@ Passwords use Argon2id. Password reset tokens and refresh tokens are opaque toke
|
|
|
427
453
|
- A denied password signup returns `403` with error code `signup-not-allowed`. The policy message is capped at 500 characters.
|
|
428
454
|
- Invitation signup validates the invitation and bypasses the signup policy. Invitations remain a separate authorization path.
|
|
429
455
|
- Login only succeeds for active users with verified email addresses and a password hash.
|
|
456
|
+
- Login and refresh reject suspended or deleted users. Current-state reads such
|
|
457
|
+
as `/auth/me` still return the persisted suspended-user status. Ordinary JWT
|
|
458
|
+
verification does not query user or refresh-session rows.
|
|
459
|
+
- Revoking a refresh session blocks renewal. An already-issued access token
|
|
460
|
+
remains valid until its configured `exp` time.
|
|
430
461
|
- Refresh tokens rotate on each refresh.
|
|
431
462
|
- Password reset tokens and email challenge proofs are consumed once.
|
|
432
463
|
- Password change revokes other refresh sessions. It keeps the current session when the current refresh cookie is valid.
|
|
@@ -3,13 +3,17 @@ import type { TimestampIdCursor } from '@shipfox/node-drizzle';
|
|
|
3
3
|
import type { AdminGrant } from './entities/admin-grant.js';
|
|
4
4
|
import type { AdministratorGrantSummary, AdministratorUserSummary } from './entities/administrator-read-model.js';
|
|
5
5
|
import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from './errors.js';
|
|
6
|
-
export declare function hashAdministrationValue(value: string): string;
|
|
7
6
|
export declare function administrationCommandFingerprint(command: string, input: unknown): string;
|
|
8
7
|
export interface AdministrationMutationContext {
|
|
9
8
|
actorId: string;
|
|
10
9
|
idempotencyKey: string;
|
|
11
10
|
correlationId: string;
|
|
12
11
|
}
|
|
12
|
+
export interface AdministratorUserMutationResult {
|
|
13
|
+
user: AdministratorUserSummary;
|
|
14
|
+
correlationId: string;
|
|
15
|
+
sessionsRevoked: number;
|
|
16
|
+
}
|
|
13
17
|
export declare function bootstrapFirstAdminOwner(params: AdministrationMutationContext & {
|
|
14
18
|
bootstrapToken: string;
|
|
15
19
|
}): Promise<AdminGrant>;
|
|
@@ -40,5 +44,17 @@ export declare function revokeAdministratorGrant(params: AdministrationMutationC
|
|
|
40
44
|
grantId: string;
|
|
41
45
|
reason: string;
|
|
42
46
|
}): Promise<AdminGrant>;
|
|
47
|
+
export declare function suspendAdministratorUser(params: AdministrationMutationContext & {
|
|
48
|
+
userId: string;
|
|
49
|
+
reason: string;
|
|
50
|
+
}): Promise<AdministratorUserMutationResult>;
|
|
51
|
+
export declare function reactivateAdministratorUser(params: AdministrationMutationContext & {
|
|
52
|
+
userId: string;
|
|
53
|
+
reason?: string;
|
|
54
|
+
}): Promise<AdministratorUserMutationResult>;
|
|
55
|
+
export declare function revokeAdministratorUserSessions(params: AdministrationMutationContext & {
|
|
56
|
+
userId: string;
|
|
57
|
+
reason?: string;
|
|
58
|
+
}): Promise<AdministratorUserMutationResult>;
|
|
43
59
|
export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError, };
|
|
44
60
|
//# sourceMappingURL=administration.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"administration.d.ts","sourceRoot":"","sources":["../../src/core/administration.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAErD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"administration.d.ts","sourceRoot":"","sources":["../../src/core/administration.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,32 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { createAdministrationActionEvent } from '@shipfox/api-common-dto';
|
|
3
|
+
import { hashOpaqueToken } from '@shipfox/node-tokens';
|
|
3
4
|
import { config } from '#config.js';
|
|
4
5
|
import { bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb, grantAdminRoleWithAudit, hasActiveAdminOwner, listAdminGrantSummaries, revokeAdminGrantWithAudit } from '#db/admin-grants.js';
|
|
6
|
+
import { reactivateUserWithAudit, revokeUserSessionsWithAudit, suspendUserWithAudit } from '#db/admin-user-moderation.js';
|
|
5
7
|
import { findAdministratorUser as findAdministratorUserInDb } from '#db/admin-users.js';
|
|
6
8
|
import { requireAdminRole } from './admin-role.js';
|
|
7
9
|
import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from './errors.js';
|
|
8
10
|
const ADMIN_OWNER_ROLE = 'admin-owner';
|
|
9
11
|
const ADMIN_OBSERVER_ROLE = 'admin-observer';
|
|
12
|
+
const ADMIN_OPERATOR_ROLE = 'admin-operator';
|
|
10
13
|
const BOOTSTRAP_COMMAND = 'auth.admin_grant.bootstrap';
|
|
11
14
|
const GRANT_COMMAND = 'auth.admin_grant.grant';
|
|
12
15
|
const REVOKE_COMMAND = 'auth.admin_grant.revoke';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
const SUSPEND_USER_COMMAND = 'auth.user.suspend';
|
|
17
|
+
const REACTIVATE_USER_COMMAND = 'auth.user.reactivate';
|
|
18
|
+
const REVOKE_USER_SESSIONS_COMMAND = 'auth.user.revoke-sessions';
|
|
16
19
|
export function administrationCommandFingerprint(command, input) {
|
|
17
|
-
return
|
|
20
|
+
return hashOpaqueToken(`${command}:${JSON.stringify(input)}`);
|
|
18
21
|
}
|
|
19
22
|
function bootstrapTokenMatches(candidate, expected) {
|
|
20
23
|
if (!expected) return false;
|
|
21
|
-
const candidateHash = Buffer.from(
|
|
22
|
-
const expectedHash = Buffer.from(
|
|
24
|
+
const candidateHash = Buffer.from(hashOpaqueToken(candidate), 'hex');
|
|
25
|
+
const expectedHash = Buffer.from(hashOpaqueToken(expected), 'hex');
|
|
23
26
|
return timingSafeEqual(candidateHash, expectedHash);
|
|
24
27
|
}
|
|
25
28
|
function administrationEvent(params) {
|
|
26
29
|
return createAdministrationActionEvent({
|
|
27
30
|
actorId: params.actorId,
|
|
28
31
|
actorRole: params.actorRole,
|
|
29
|
-
requiredRole: ADMIN_OWNER_ROLE,
|
|
32
|
+
requiredRole: params.requiredRole ?? ADMIN_OWNER_ROLE,
|
|
30
33
|
command: params.command,
|
|
31
34
|
targetType: params.targetType,
|
|
32
35
|
targetId: params.targetId,
|
|
@@ -37,6 +40,13 @@ function administrationEvent(params) {
|
|
|
37
40
|
occurredAt: new Date().toISOString()
|
|
38
41
|
});
|
|
39
42
|
}
|
|
43
|
+
function toAdministratorUserMutationResult(result) {
|
|
44
|
+
return {
|
|
45
|
+
user: result.user,
|
|
46
|
+
correlationId: result.correlationId,
|
|
47
|
+
sessionsRevoked: result.sessionsRevoked
|
|
48
|
+
};
|
|
49
|
+
}
|
|
40
50
|
export async function bootstrapFirstAdminOwner(params) {
|
|
41
51
|
if (!bootstrapTokenMatches(params.bootstrapToken, config.ADMIN_BOOTSTRAP_TOKEN)) {
|
|
42
52
|
throw new InvalidAdminBootstrapTokenError();
|
|
@@ -44,7 +54,7 @@ export async function bootstrapFirstAdminOwner(params) {
|
|
|
44
54
|
return await bootstrapFirstAdminOwnerInDb({
|
|
45
55
|
userId: params.actorId,
|
|
46
56
|
actorId: params.actorId,
|
|
47
|
-
idempotencyKeyFingerprint:
|
|
57
|
+
idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey),
|
|
48
58
|
requestFingerprint: administrationCommandFingerprint(BOOTSTRAP_COMMAND, {
|
|
49
59
|
actorId: params.actorId
|
|
50
60
|
}),
|
|
@@ -56,7 +66,7 @@ export async function bootstrapFirstAdminOwner(params) {
|
|
|
56
66
|
targetId: params.actorId,
|
|
57
67
|
reason: 'Initial administrator owner bootstrap',
|
|
58
68
|
correlationId: params.correlationId,
|
|
59
|
-
idempotencyKeyFingerprint:
|
|
69
|
+
idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey)
|
|
60
70
|
})
|
|
61
71
|
});
|
|
62
72
|
}
|
|
@@ -107,7 +117,7 @@ export async function grantAdministratorRole(params) {
|
|
|
107
117
|
userId: params.userId,
|
|
108
118
|
role: params.role,
|
|
109
119
|
actorId: params.actorId,
|
|
110
|
-
idempotencyKeyFingerprint:
|
|
120
|
+
idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey),
|
|
111
121
|
requestFingerprint: administrationCommandFingerprint(GRANT_COMMAND, {
|
|
112
122
|
userId: params.userId,
|
|
113
123
|
role: params.role,
|
|
@@ -121,7 +131,7 @@ export async function grantAdministratorRole(params) {
|
|
|
121
131
|
targetId: params.userId,
|
|
122
132
|
reason: params.reason,
|
|
123
133
|
correlationId: params.correlationId,
|
|
124
|
-
idempotencyKeyFingerprint:
|
|
134
|
+
idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey)
|
|
125
135
|
})
|
|
126
136
|
});
|
|
127
137
|
}
|
|
@@ -133,7 +143,7 @@ export async function revokeAdministratorGrant(params) {
|
|
|
133
143
|
return await revokeAdminGrantWithAudit({
|
|
134
144
|
grantId: params.grantId,
|
|
135
145
|
actorId: params.actorId,
|
|
136
|
-
idempotencyKeyFingerprint:
|
|
146
|
+
idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey),
|
|
137
147
|
requestFingerprint: administrationCommandFingerprint(REVOKE_COMMAND, {
|
|
138
148
|
grantId: params.grantId,
|
|
139
149
|
reason: params.reason
|
|
@@ -146,9 +156,92 @@ export async function revokeAdministratorGrant(params) {
|
|
|
146
156
|
targetId: params.grantId,
|
|
147
157
|
reason: params.reason,
|
|
148
158
|
correlationId: params.correlationId,
|
|
149
|
-
idempotencyKeyFingerprint:
|
|
159
|
+
idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey)
|
|
160
|
+
})
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
export async function suspendAdministratorUser(params) {
|
|
164
|
+
const actorRole = await requireAdminRole({
|
|
165
|
+
userId: params.actorId,
|
|
166
|
+
minimumRole: ADMIN_OPERATOR_ROLE
|
|
167
|
+
});
|
|
168
|
+
const idempotencyKeyFingerprint = hashOpaqueToken(params.idempotencyKey);
|
|
169
|
+
return toAdministratorUserMutationResult(await suspendUserWithAudit({
|
|
170
|
+
actorId: params.actorId,
|
|
171
|
+
userId: params.userId,
|
|
172
|
+
idempotencyKeyFingerprint,
|
|
173
|
+
requestFingerprint: administrationCommandFingerprint(SUSPEND_USER_COMMAND, {
|
|
174
|
+
userId: params.userId,
|
|
175
|
+
reason: params.reason
|
|
176
|
+
}),
|
|
177
|
+
event: administrationEvent({
|
|
178
|
+
actorId: params.actorId,
|
|
179
|
+
actorRole,
|
|
180
|
+
requiredRole: ADMIN_OPERATOR_ROLE,
|
|
181
|
+
command: SUSPEND_USER_COMMAND,
|
|
182
|
+
targetType: 'user',
|
|
183
|
+
targetId: params.userId,
|
|
184
|
+
reason: params.reason,
|
|
185
|
+
correlationId: params.correlationId,
|
|
186
|
+
idempotencyKeyFingerprint
|
|
150
187
|
})
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
export async function reactivateAdministratorUser(params) {
|
|
191
|
+
const actorRole = await requireAdminRole({
|
|
192
|
+
userId: params.actorId,
|
|
193
|
+
minimumRole: ADMIN_OPERATOR_ROLE
|
|
151
194
|
});
|
|
195
|
+
const reason = params.reason ?? 'User reactivation requested by an administrator';
|
|
196
|
+
const idempotencyKeyFingerprint = hashOpaqueToken(params.idempotencyKey);
|
|
197
|
+
return toAdministratorUserMutationResult(await reactivateUserWithAudit({
|
|
198
|
+
actorId: params.actorId,
|
|
199
|
+
userId: params.userId,
|
|
200
|
+
idempotencyKeyFingerprint,
|
|
201
|
+
requestFingerprint: administrationCommandFingerprint(REACTIVATE_USER_COMMAND, {
|
|
202
|
+
userId: params.userId,
|
|
203
|
+
reason
|
|
204
|
+
}),
|
|
205
|
+
event: administrationEvent({
|
|
206
|
+
actorId: params.actorId,
|
|
207
|
+
actorRole,
|
|
208
|
+
requiredRole: ADMIN_OPERATOR_ROLE,
|
|
209
|
+
command: REACTIVATE_USER_COMMAND,
|
|
210
|
+
targetType: 'user',
|
|
211
|
+
targetId: params.userId,
|
|
212
|
+
reason,
|
|
213
|
+
correlationId: params.correlationId,
|
|
214
|
+
idempotencyKeyFingerprint
|
|
215
|
+
})
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
export async function revokeAdministratorUserSessions(params) {
|
|
219
|
+
const actorRole = await requireAdminRole({
|
|
220
|
+
userId: params.actorId,
|
|
221
|
+
minimumRole: ADMIN_OPERATOR_ROLE
|
|
222
|
+
});
|
|
223
|
+
const reason = params.reason ?? 'All active user sessions revoked by an administrator';
|
|
224
|
+
const idempotencyKeyFingerprint = hashOpaqueToken(params.idempotencyKey);
|
|
225
|
+
return toAdministratorUserMutationResult(await revokeUserSessionsWithAudit({
|
|
226
|
+
actorId: params.actorId,
|
|
227
|
+
userId: params.userId,
|
|
228
|
+
idempotencyKeyFingerprint,
|
|
229
|
+
requestFingerprint: administrationCommandFingerprint(REVOKE_USER_SESSIONS_COMMAND, {
|
|
230
|
+
userId: params.userId,
|
|
231
|
+
reason
|
|
232
|
+
}),
|
|
233
|
+
event: administrationEvent({
|
|
234
|
+
actorId: params.actorId,
|
|
235
|
+
actorRole,
|
|
236
|
+
requiredRole: ADMIN_OPERATOR_ROLE,
|
|
237
|
+
command: REVOKE_USER_SESSIONS_COMMAND,
|
|
238
|
+
targetType: 'user',
|
|
239
|
+
targetId: params.userId,
|
|
240
|
+
reason,
|
|
241
|
+
correlationId: params.correlationId,
|
|
242
|
+
idempotencyKeyFingerprint
|
|
243
|
+
})
|
|
244
|
+
}));
|
|
152
245
|
}
|
|
153
246
|
export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError };
|
|
154
247
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/administration.ts"],"sourcesContent":["import {createHash, timingSafeEqual} from 'node:crypto';\nimport type {AdminRole} from '@shipfox/api-auth-dto';\nimport {createAdministrationActionEvent} from '@shipfox/api-common-dto';\nimport type {TimestampIdCursor} from '@shipfox/node-drizzle';\nimport {config} from '#config.js';\nimport {\n bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb,\n grantAdminRoleWithAudit,\n hasActiveAdminOwner,\n listAdminGrantSummaries,\n revokeAdminGrantWithAudit,\n} from '#db/admin-grants.js';\nimport {findAdministratorUser as findAdministratorUserInDb} from '#db/admin-users.js';\nimport {requireAdminRole} from './admin-role.js';\nimport type {AdminGrant} from './entities/admin-grant.js';\nimport type {\n AdministratorGrantSummary,\n AdministratorUserSummary,\n} from './entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n InvalidAdminBootstrapTokenError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from './errors.js';\n\nconst ADMIN_OWNER_ROLE: AdminRole = 'admin-owner';\nconst ADMIN_OBSERVER_ROLE: AdminRole = 'admin-observer';\nconst BOOTSTRAP_COMMAND = 'auth.admin_grant.bootstrap';\nconst GRANT_COMMAND = 'auth.admin_grant.grant';\nconst REVOKE_COMMAND = 'auth.admin_grant.revoke';\n\nexport function hashAdministrationValue(value: string): string {\n return createHash('sha256').update(value).digest('hex');\n}\n\nexport function administrationCommandFingerprint(command: string, input: unknown): string {\n return hashAdministrationValue(`${command}:${JSON.stringify(input)}`);\n}\n\nfunction bootstrapTokenMatches(candidate: string, expected: string | undefined): boolean {\n if (!expected) return false;\n const candidateHash = Buffer.from(hashAdministrationValue(candidate), 'hex');\n const expectedHash = Buffer.from(hashAdministrationValue(expected), 'hex');\n return timingSafeEqual(candidateHash, expectedHash);\n}\n\nfunction administrationEvent(params: {\n actorId: string;\n actorRole: AdminRole;\n command: string;\n targetType: string;\n targetId: string;\n reason: string;\n correlationId: string;\n idempotencyKeyFingerprint: string;\n}) {\n return createAdministrationActionEvent({\n actorId: params.actorId,\n actorRole: params.actorRole,\n requiredRole: ADMIN_OWNER_ROLE,\n command: params.command,\n targetType: params.targetType,\n targetId: params.targetId,\n reason: params.reason,\n result: 'succeeded',\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n occurredAt: new Date().toISOString(),\n });\n}\n\nexport interface AdministrationMutationContext {\n actorId: string;\n idempotencyKey: string;\n correlationId: string;\n}\n\nexport async function bootstrapFirstAdminOwner(\n params: AdministrationMutationContext & {bootstrapToken: string},\n): Promise<AdminGrant> {\n if (!bootstrapTokenMatches(params.bootstrapToken, config.ADMIN_BOOTSTRAP_TOKEN)) {\n throw new InvalidAdminBootstrapTokenError();\n }\n\n return await bootstrapFirstAdminOwnerInDb({\n userId: params.actorId,\n actorId: params.actorId,\n idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),\n requestFingerprint: administrationCommandFingerprint(BOOTSTRAP_COMMAND, {\n actorId: params.actorId,\n }),\n event: administrationEvent({\n actorId: params.actorId,\n actorRole: ADMIN_OWNER_ROLE,\n command: BOOTSTRAP_COMMAND,\n targetType: 'user',\n targetId: params.actorId,\n reason: 'Initial administrator owner bootstrap',\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),\n }),\n });\n}\n\nexport async function getAdminBootstrapState(): Promise<'available' | 'closed'> {\n return (await hasActiveAdminOwner()) ? 'closed' : 'available';\n}\n\nexport async function findAdministratorUserSummary(\n params: {actorId: string} & ({id: string; email?: never} | {email: string; id?: never}),\n): Promise<AdministratorUserSummary | undefined> {\n await requireAdminRole({userId: params.actorId, minimumRole: ADMIN_OBSERVER_ROLE});\n\n const user = await findAdministratorUserInDb(\n 'id' in params ? {id: params.id} : {email: params.email},\n );\n if (!user) return undefined;\n\n return user;\n}\n\nexport async function listAdministratorGrantSummaries(params: {\n actorId: string;\n limit: number;\n cursor?: TimestampIdCursor;\n}): Promise<{\n grants: AdministratorGrantSummary[];\n nextCursor: TimestampIdCursor | null;\n}> {\n await requireAdminRole({userId: params.actorId, minimumRole: ADMIN_OBSERVER_ROLE});\n\n const result = await listAdminGrantSummaries({\n limit: params.limit,\n ...(params.cursor ? {cursor: params.cursor} : {}),\n });\n return {\n grants: result.rows.map((row) => ({\n grantId: row.id,\n role: row.role,\n createdAt: row.createdAt,\n revokedAt: row.revokedAt,\n user: row.user,\n })),\n nextCursor: result.nextCursor,\n };\n}\n\nexport async function grantAdministratorRole(\n params: AdministrationMutationContext & {userId: string; role: AdminRole; reason: string},\n): Promise<AdminGrant> {\n const actorRole = await requireAdminRole({\n userId: params.actorId,\n minimumRole: ADMIN_OWNER_ROLE,\n });\n return await grantAdminRoleWithAudit({\n userId: params.userId,\n role: params.role,\n actorId: params.actorId,\n idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),\n requestFingerprint: administrationCommandFingerprint(GRANT_COMMAND, {\n userId: params.userId,\n role: params.role,\n reason: params.reason,\n }),\n event: administrationEvent({\n actorId: params.actorId,\n actorRole,\n command: GRANT_COMMAND,\n targetType: 'user',\n targetId: params.userId,\n reason: params.reason,\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),\n }),\n });\n}\n\nexport async function revokeAdministratorGrant(\n params: AdministrationMutationContext & {grantId: string; reason: string},\n): Promise<AdminGrant> {\n const actorRole = await requireAdminRole({\n userId: params.actorId,\n minimumRole: ADMIN_OWNER_ROLE,\n });\n return await revokeAdminGrantWithAudit({\n grantId: params.grantId,\n actorId: params.actorId,\n idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),\n requestFingerprint: administrationCommandFingerprint(REVOKE_COMMAND, {\n grantId: params.grantId,\n reason: params.reason,\n }),\n event: administrationEvent({\n actorId: params.actorId,\n actorRole,\n command: REVOKE_COMMAND,\n targetType: 'admin-grant',\n targetId: params.grantId,\n reason: params.reason,\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),\n }),\n });\n}\n\nexport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n InvalidAdminBootstrapTokenError,\n LastAdminOwnerError,\n UserNotFoundError,\n};\n"],"names":["createHash","timingSafeEqual","createAdministrationActionEvent","config","bootstrapFirstAdminOwner","bootstrapFirstAdminOwnerInDb","grantAdminRoleWithAudit","hasActiveAdminOwner","listAdminGrantSummaries","revokeAdminGrantWithAudit","findAdministratorUser","findAdministratorUserInDb","requireAdminRole","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","InvalidAdminBootstrapTokenError","LastAdminOwnerError","UserNotFoundError","ADMIN_OWNER_ROLE","ADMIN_OBSERVER_ROLE","BOOTSTRAP_COMMAND","GRANT_COMMAND","REVOKE_COMMAND","hashAdministrationValue","value","update","digest","administrationCommandFingerprint","command","input","JSON","stringify","bootstrapTokenMatches","candidate","expected","candidateHash","Buffer","from","expectedHash","administrationEvent","params","actorId","actorRole","requiredRole","targetType","targetId","reason","result","correlationId","idempotencyKeyFingerprint","occurredAt","Date","toISOString","bootstrapToken","ADMIN_BOOTSTRAP_TOKEN","userId","idempotencyKey","requestFingerprint","event","getAdminBootstrapState","findAdministratorUserSummary","minimumRole","user","id","email","undefined","listAdministratorGrantSummaries","limit","cursor","grants","rows","map","row","grantId","role","createdAt","revokedAt","nextCursor","grantAdministratorRole","revokeAdministratorGrant"],"mappings":"AAAA,SAAQA,UAAU,EAAEC,eAAe,QAAO,cAAc;AAExD,SAAQC,+BAA+B,QAAO,0BAA0B;AAExE,SAAQC,MAAM,QAAO,aAAa;AAClC,SACEC,4BAA4BC,4BAA4B,EACxDC,uBAAuB,EACvBC,mBAAmB,EACnBC,uBAAuB,EACvBC,yBAAyB,QACpB,sBAAsB;AAC7B,SAAQC,yBAAyBC,yBAAyB,QAAO,qBAAqB;AACtF,SAAQC,gBAAgB,QAAO,kBAAkB;AAMjD,SACEC,yBAAyB,EACzBC,4BAA4B,EAC5BC,uBAAuB,EACvBC,6BAA6B,EAC7BC,+BAA+B,EAC/BC,mBAAmB,EACnBC,iBAAiB,QACZ,cAAc;AAErB,MAAMC,mBAA8B;AACpC,MAAMC,sBAAiC;AACvC,MAAMC,oBAAoB;AAC1B,MAAMC,gBAAgB;AACtB,MAAMC,iBAAiB;AAEvB,OAAO,SAASC,wBAAwBC,KAAa;IACnD,OAAO1B,WAAW,UAAU2B,MAAM,CAACD,OAAOE,MAAM,CAAC;AACnD;AAEA,OAAO,SAASC,iCAAiCC,OAAe,EAAEC,KAAc;IAC9E,OAAON,wBAAwB,GAAGK,QAAQ,CAAC,EAAEE,KAAKC,SAAS,CAACF,QAAQ;AACtE;AAEA,SAASG,sBAAsBC,SAAiB,EAAEC,QAA4B;IAC5E,IAAI,CAACA,UAAU,OAAO;IACtB,MAAMC,gBAAgBC,OAAOC,IAAI,CAACd,wBAAwBU,YAAY;IACtE,MAAMK,eAAeF,OAAOC,IAAI,CAACd,wBAAwBW,WAAW;IACpE,OAAOnC,gBAAgBoC,eAAeG;AACxC;AAEA,SAASC,oBAAoBC,MAS5B;IACC,OAAOxC,gCAAgC;QACrCyC,SAASD,OAAOC,OAAO;QACvBC,WAAWF,OAAOE,SAAS;QAC3BC,cAAczB;QACdU,SAASY,OAAOZ,OAAO;QACvBgB,YAAYJ,OAAOI,UAAU;QAC7BC,UAAUL,OAAOK,QAAQ;QACzBC,QAAQN,OAAOM,MAAM;QACrBC,QAAQ;QACRC,eAAeR,OAAOQ,aAAa;QACnCC,2BAA2BT,OAAOS,yBAAyB;QAC3DC,YAAY,IAAIC,OAAOC,WAAW;IACpC;AACF;AAQA,OAAO,eAAelD,yBACpBsC,MAAgE;IAEhE,IAAI,CAACR,sBAAsBQ,OAAOa,cAAc,EAAEpD,OAAOqD,qBAAqB,GAAG;QAC/E,MAAM,IAAIvC;IACZ;IAEA,OAAO,MAAMZ,6BAA6B;QACxCoD,QAAQf,OAAOC,OAAO;QACtBA,SAASD,OAAOC,OAAO;QACvBQ,2BAA2B1B,wBAAwBiB,OAAOgB,cAAc;QACxEC,oBAAoB9B,iCAAiCP,mBAAmB;YACtEqB,SAASD,OAAOC,OAAO;QACzB;QACAiB,OAAOnB,oBAAoB;YACzBE,SAASD,OAAOC,OAAO;YACvBC,WAAWxB;YACXU,SAASR;YACTwB,YAAY;YACZC,UAAUL,OAAOC,OAAO;YACxBK,QAAQ;YACRE,eAAeR,OAAOQ,aAAa;YACnCC,2BAA2B1B,wBAAwBiB,OAAOgB,cAAc;QAC1E;IACF;AACF;AAEA,OAAO,eAAeG;IACpB,OAAO,AAAC,MAAMtD,wBAAyB,WAAW;AACpD;AAEA,OAAO,eAAeuD,6BACpBpB,MAAuF;IAEvF,MAAM9B,iBAAiB;QAAC6C,QAAQf,OAAOC,OAAO;QAAEoB,aAAa1C;IAAmB;IAEhF,MAAM2C,OAAO,MAAMrD,0BACjB,QAAQ+B,SAAS;QAACuB,IAAIvB,OAAOuB,EAAE;IAAA,IAAI;QAACC,OAAOxB,OAAOwB,KAAK;IAAA;IAEzD,IAAI,CAACF,MAAM,OAAOG;IAElB,OAAOH;AACT;AAEA,OAAO,eAAeI,gCAAgC1B,MAIrD;IAIC,MAAM9B,iBAAiB;QAAC6C,QAAQf,OAAOC,OAAO;QAAEoB,aAAa1C;IAAmB;IAEhF,MAAM4B,SAAS,MAAMzC,wBAAwB;QAC3C6D,OAAO3B,OAAO2B,KAAK;QACnB,GAAI3B,OAAO4B,MAAM,GAAG;YAACA,QAAQ5B,OAAO4B,MAAM;QAAA,IAAI,CAAC,CAAC;IAClD;IACA,OAAO;QACLC,QAAQtB,OAAOuB,IAAI,CAACC,GAAG,CAAC,CAACC,MAAS,CAAA;gBAChCC,SAASD,IAAIT,EAAE;gBACfW,MAAMF,IAAIE,IAAI;gBACdC,WAAWH,IAAIG,SAAS;gBACxBC,WAAWJ,IAAII,SAAS;gBACxBd,MAAMU,IAAIV,IAAI;YAChB,CAAA;QACAe,YAAY9B,OAAO8B,UAAU;IAC/B;AACF;AAEA,OAAO,eAAeC,uBACpBtC,MAAyF;IAEzF,MAAME,YAAY,MAAMhC,iBAAiB;QACvC6C,QAAQf,OAAOC,OAAO;QACtBoB,aAAa3C;IACf;IACA,OAAO,MAAMd,wBAAwB;QACnCmD,QAAQf,OAAOe,MAAM;QACrBmB,MAAMlC,OAAOkC,IAAI;QACjBjC,SAASD,OAAOC,OAAO;QACvBQ,2BAA2B1B,wBAAwBiB,OAAOgB,cAAc;QACxEC,oBAAoB9B,iCAAiCN,eAAe;YAClEkC,QAAQf,OAAOe,MAAM;YACrBmB,MAAMlC,OAAOkC,IAAI;YACjB5B,QAAQN,OAAOM,MAAM;QACvB;QACAY,OAAOnB,oBAAoB;YACzBE,SAASD,OAAOC,OAAO;YACvBC;YACAd,SAASP;YACTuB,YAAY;YACZC,UAAUL,OAAOe,MAAM;YACvBT,QAAQN,OAAOM,MAAM;YACrBE,eAAeR,OAAOQ,aAAa;YACnCC,2BAA2B1B,wBAAwBiB,OAAOgB,cAAc;QAC1E;IACF;AACF;AAEA,OAAO,eAAeuB,yBACpBvC,MAAyE;IAEzE,MAAME,YAAY,MAAMhC,iBAAiB;QACvC6C,QAAQf,OAAOC,OAAO;QACtBoB,aAAa3C;IACf;IACA,OAAO,MAAMX,0BAA0B;QACrCkE,SAASjC,OAAOiC,OAAO;QACvBhC,SAASD,OAAOC,OAAO;QACvBQ,2BAA2B1B,wBAAwBiB,OAAOgB,cAAc;QACxEC,oBAAoB9B,iCAAiCL,gBAAgB;YACnEmD,SAASjC,OAAOiC,OAAO;YACvB3B,QAAQN,OAAOM,MAAM;QACvB;QACAY,OAAOnB,oBAAoB;YACzBE,SAASD,OAAOC,OAAO;YACvBC;YACAd,SAASN;YACTsB,YAAY;YACZC,UAAUL,OAAOiC,OAAO;YACxB3B,QAAQN,OAAOM,MAAM;YACrBE,eAAeR,OAAOQ,aAAa;YACnCC,2BAA2B1B,wBAAwBiB,OAAOgB,cAAc;QAC1E;IACF;AACF;AAEA,SACE7C,yBAAyB,EACzBC,4BAA4B,EAC5BC,uBAAuB,EACvBC,6BAA6B,EAC7BC,+BAA+B,EAC/BC,mBAAmB,EACnBC,iBAAiB,GACjB"}
|
|
1
|
+
{"version":3,"sources":["../../src/core/administration.ts"],"sourcesContent":["import {timingSafeEqual} from 'node:crypto';\nimport type {AdminRole} from '@shipfox/api-auth-dto';\nimport {createAdministrationActionEvent} from '@shipfox/api-common-dto';\nimport type {TimestampIdCursor} from '@shipfox/node-drizzle';\nimport {hashOpaqueToken} from '@shipfox/node-tokens';\nimport {config} from '#config.js';\nimport {\n bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb,\n grantAdminRoleWithAudit,\n hasActiveAdminOwner,\n listAdminGrantSummaries,\n revokeAdminGrantWithAudit,\n} from '#db/admin-grants.js';\nimport {\n reactivateUserWithAudit,\n revokeUserSessionsWithAudit,\n suspendUserWithAudit,\n type UserModerationResult,\n} from '#db/admin-user-moderation.js';\nimport {findAdministratorUser as findAdministratorUserInDb} from '#db/admin-users.js';\nimport {requireAdminRole} from './admin-role.js';\nimport type {AdminGrant} from './entities/admin-grant.js';\nimport type {\n AdministratorGrantSummary,\n AdministratorUserSummary,\n} from './entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n InvalidAdminBootstrapTokenError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from './errors.js';\n\nconst ADMIN_OWNER_ROLE: AdminRole = 'admin-owner';\nconst ADMIN_OBSERVER_ROLE: AdminRole = 'admin-observer';\nconst ADMIN_OPERATOR_ROLE: AdminRole = 'admin-operator';\nconst BOOTSTRAP_COMMAND = 'auth.admin_grant.bootstrap';\nconst GRANT_COMMAND = 'auth.admin_grant.grant';\nconst REVOKE_COMMAND = 'auth.admin_grant.revoke';\nconst SUSPEND_USER_COMMAND = 'auth.user.suspend';\nconst REACTIVATE_USER_COMMAND = 'auth.user.reactivate';\nconst REVOKE_USER_SESSIONS_COMMAND = 'auth.user.revoke-sessions';\n\nexport function administrationCommandFingerprint(command: string, input: unknown): string {\n return hashOpaqueToken(`${command}:${JSON.stringify(input)}`);\n}\n\nfunction bootstrapTokenMatches(candidate: string, expected: string | undefined): boolean {\n if (!expected) return false;\n const candidateHash = Buffer.from(hashOpaqueToken(candidate), 'hex');\n const expectedHash = Buffer.from(hashOpaqueToken(expected), 'hex');\n return timingSafeEqual(candidateHash, expectedHash);\n}\n\nfunction administrationEvent(params: {\n actorId: string;\n actorRole: AdminRole;\n command: string;\n targetType: string;\n targetId: string;\n reason: string;\n requiredRole?: AdminRole;\n correlationId: string;\n idempotencyKeyFingerprint: string;\n}) {\n return createAdministrationActionEvent({\n actorId: params.actorId,\n actorRole: params.actorRole,\n requiredRole: params.requiredRole ?? ADMIN_OWNER_ROLE,\n command: params.command,\n targetType: params.targetType,\n targetId: params.targetId,\n reason: params.reason,\n result: 'succeeded',\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n occurredAt: new Date().toISOString(),\n });\n}\n\nexport interface AdministrationMutationContext {\n actorId: string;\n idempotencyKey: string;\n correlationId: string;\n}\n\nexport interface AdministratorUserMutationResult {\n user: AdministratorUserSummary;\n correlationId: string;\n sessionsRevoked: number;\n}\n\nfunction toAdministratorUserMutationResult(\n result: UserModerationResult,\n): AdministratorUserMutationResult {\n return {\n user: result.user,\n correlationId: result.correlationId,\n sessionsRevoked: result.sessionsRevoked,\n };\n}\n\nexport async function bootstrapFirstAdminOwner(\n params: AdministrationMutationContext & {bootstrapToken: string},\n): Promise<AdminGrant> {\n if (!bootstrapTokenMatches(params.bootstrapToken, config.ADMIN_BOOTSTRAP_TOKEN)) {\n throw new InvalidAdminBootstrapTokenError();\n }\n\n return await bootstrapFirstAdminOwnerInDb({\n userId: params.actorId,\n actorId: params.actorId,\n idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey),\n requestFingerprint: administrationCommandFingerprint(BOOTSTRAP_COMMAND, {\n actorId: params.actorId,\n }),\n event: administrationEvent({\n actorId: params.actorId,\n actorRole: ADMIN_OWNER_ROLE,\n command: BOOTSTRAP_COMMAND,\n targetType: 'user',\n targetId: params.actorId,\n reason: 'Initial administrator owner bootstrap',\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey),\n }),\n });\n}\n\nexport async function getAdminBootstrapState(): Promise<'available' | 'closed'> {\n return (await hasActiveAdminOwner()) ? 'closed' : 'available';\n}\n\nexport async function findAdministratorUserSummary(\n params: {actorId: string} & ({id: string; email?: never} | {email: string; id?: never}),\n): Promise<AdministratorUserSummary | undefined> {\n await requireAdminRole({userId: params.actorId, minimumRole: ADMIN_OBSERVER_ROLE});\n\n const user = await findAdministratorUserInDb(\n 'id' in params ? {id: params.id} : {email: params.email},\n );\n if (!user) return undefined;\n\n return user;\n}\n\nexport async function listAdministratorGrantSummaries(params: {\n actorId: string;\n limit: number;\n cursor?: TimestampIdCursor;\n}): Promise<{\n grants: AdministratorGrantSummary[];\n nextCursor: TimestampIdCursor | null;\n}> {\n await requireAdminRole({userId: params.actorId, minimumRole: ADMIN_OBSERVER_ROLE});\n\n const result = await listAdminGrantSummaries({\n limit: params.limit,\n ...(params.cursor ? {cursor: params.cursor} : {}),\n });\n return {\n grants: result.rows.map((row) => ({\n grantId: row.id,\n role: row.role,\n createdAt: row.createdAt,\n revokedAt: row.revokedAt,\n user: row.user,\n })),\n nextCursor: result.nextCursor,\n };\n}\n\nexport async function grantAdministratorRole(\n params: AdministrationMutationContext & {userId: string; role: AdminRole; reason: string},\n): Promise<AdminGrant> {\n const actorRole = await requireAdminRole({\n userId: params.actorId,\n minimumRole: ADMIN_OWNER_ROLE,\n });\n return await grantAdminRoleWithAudit({\n userId: params.userId,\n role: params.role,\n actorId: params.actorId,\n idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey),\n requestFingerprint: administrationCommandFingerprint(GRANT_COMMAND, {\n userId: params.userId,\n role: params.role,\n reason: params.reason,\n }),\n event: administrationEvent({\n actorId: params.actorId,\n actorRole,\n command: GRANT_COMMAND,\n targetType: 'user',\n targetId: params.userId,\n reason: params.reason,\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey),\n }),\n });\n}\n\nexport async function revokeAdministratorGrant(\n params: AdministrationMutationContext & {grantId: string; reason: string},\n): Promise<AdminGrant> {\n const actorRole = await requireAdminRole({\n userId: params.actorId,\n minimumRole: ADMIN_OWNER_ROLE,\n });\n return await revokeAdminGrantWithAudit({\n grantId: params.grantId,\n actorId: params.actorId,\n idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey),\n requestFingerprint: administrationCommandFingerprint(REVOKE_COMMAND, {\n grantId: params.grantId,\n reason: params.reason,\n }),\n event: administrationEvent({\n actorId: params.actorId,\n actorRole,\n command: REVOKE_COMMAND,\n targetType: 'admin-grant',\n targetId: params.grantId,\n reason: params.reason,\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey),\n }),\n });\n}\n\nexport async function suspendAdministratorUser(\n params: AdministrationMutationContext & {userId: string; reason: string},\n): Promise<AdministratorUserMutationResult> {\n const actorRole = await requireAdminRole({\n userId: params.actorId,\n minimumRole: ADMIN_OPERATOR_ROLE,\n });\n const idempotencyKeyFingerprint = hashOpaqueToken(params.idempotencyKey);\n return toAdministratorUserMutationResult(\n await suspendUserWithAudit({\n actorId: params.actorId,\n userId: params.userId,\n idempotencyKeyFingerprint,\n requestFingerprint: administrationCommandFingerprint(SUSPEND_USER_COMMAND, {\n userId: params.userId,\n reason: params.reason,\n }),\n event: administrationEvent({\n actorId: params.actorId,\n actorRole,\n requiredRole: ADMIN_OPERATOR_ROLE,\n command: SUSPEND_USER_COMMAND,\n targetType: 'user',\n targetId: params.userId,\n reason: params.reason,\n correlationId: params.correlationId,\n idempotencyKeyFingerprint,\n }),\n }),\n );\n}\n\nexport async function reactivateAdministratorUser(\n params: AdministrationMutationContext & {userId: string; reason?: string},\n): Promise<AdministratorUserMutationResult> {\n const actorRole = await requireAdminRole({\n userId: params.actorId,\n minimumRole: ADMIN_OPERATOR_ROLE,\n });\n const reason = params.reason ?? 'User reactivation requested by an administrator';\n const idempotencyKeyFingerprint = hashOpaqueToken(params.idempotencyKey);\n return toAdministratorUserMutationResult(\n await reactivateUserWithAudit({\n actorId: params.actorId,\n userId: params.userId,\n idempotencyKeyFingerprint,\n requestFingerprint: administrationCommandFingerprint(REACTIVATE_USER_COMMAND, {\n userId: params.userId,\n reason,\n }),\n event: administrationEvent({\n actorId: params.actorId,\n actorRole,\n requiredRole: ADMIN_OPERATOR_ROLE,\n command: REACTIVATE_USER_COMMAND,\n targetType: 'user',\n targetId: params.userId,\n reason,\n correlationId: params.correlationId,\n idempotencyKeyFingerprint,\n }),\n }),\n );\n}\n\nexport async function revokeAdministratorUserSessions(\n params: AdministrationMutationContext & {userId: string; reason?: string},\n): Promise<AdministratorUserMutationResult> {\n const actorRole = await requireAdminRole({\n userId: params.actorId,\n minimumRole: ADMIN_OPERATOR_ROLE,\n });\n const reason = params.reason ?? 'All active user sessions revoked by an administrator';\n const idempotencyKeyFingerprint = hashOpaqueToken(params.idempotencyKey);\n return toAdministratorUserMutationResult(\n await revokeUserSessionsWithAudit({\n actorId: params.actorId,\n userId: params.userId,\n idempotencyKeyFingerprint,\n requestFingerprint: administrationCommandFingerprint(REVOKE_USER_SESSIONS_COMMAND, {\n userId: params.userId,\n reason,\n }),\n event: administrationEvent({\n actorId: params.actorId,\n actorRole,\n requiredRole: ADMIN_OPERATOR_ROLE,\n command: REVOKE_USER_SESSIONS_COMMAND,\n targetType: 'user',\n targetId: params.userId,\n reason,\n correlationId: params.correlationId,\n idempotencyKeyFingerprint,\n }),\n }),\n );\n}\n\nexport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n InvalidAdminBootstrapTokenError,\n LastAdminOwnerError,\n UserNotFoundError,\n};\n"],"names":["timingSafeEqual","createAdministrationActionEvent","hashOpaqueToken","config","bootstrapFirstAdminOwner","bootstrapFirstAdminOwnerInDb","grantAdminRoleWithAudit","hasActiveAdminOwner","listAdminGrantSummaries","revokeAdminGrantWithAudit","reactivateUserWithAudit","revokeUserSessionsWithAudit","suspendUserWithAudit","findAdministratorUser","findAdministratorUserInDb","requireAdminRole","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","InvalidAdminBootstrapTokenError","LastAdminOwnerError","UserNotFoundError","ADMIN_OWNER_ROLE","ADMIN_OBSERVER_ROLE","ADMIN_OPERATOR_ROLE","BOOTSTRAP_COMMAND","GRANT_COMMAND","REVOKE_COMMAND","SUSPEND_USER_COMMAND","REACTIVATE_USER_COMMAND","REVOKE_USER_SESSIONS_COMMAND","administrationCommandFingerprint","command","input","JSON","stringify","bootstrapTokenMatches","candidate","expected","candidateHash","Buffer","from","expectedHash","administrationEvent","params","actorId","actorRole","requiredRole","targetType","targetId","reason","result","correlationId","idempotencyKeyFingerprint","occurredAt","Date","toISOString","toAdministratorUserMutationResult","user","sessionsRevoked","bootstrapToken","ADMIN_BOOTSTRAP_TOKEN","userId","idempotencyKey","requestFingerprint","event","getAdminBootstrapState","findAdministratorUserSummary","minimumRole","id","email","undefined","listAdministratorGrantSummaries","limit","cursor","grants","rows","map","row","grantId","role","createdAt","revokedAt","nextCursor","grantAdministratorRole","revokeAdministratorGrant","suspendAdministratorUser","reactivateAdministratorUser","revokeAdministratorUserSessions"],"mappings":"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"}
|
package/dist/core/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,SAAS,EAAc,MAAM,uBAAuB,CAAC;AAOlE,OAAO,EACL,KAAK,2BAA2B,EAEjC,MAAM,0CAA0C,CAAC;AAyBlD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EACL,8BAA8B,EAC9B,qBAAqB,EAErB,uBAAuB,EAMvB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAGrB,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/core/auth.js
CHANGED
|
@@ -6,7 +6,7 @@ import { userAccessTokenKey } from '@shipfox/node-auth-root-key';
|
|
|
6
6
|
import { generateOpaqueToken, hashOpaqueToken } from '@shipfox/node-tokens';
|
|
7
7
|
import { config } from '#config.js';
|
|
8
8
|
import { consumePasswordReset, createPasswordReset } from '#db/password-resets.js';
|
|
9
|
-
import {
|
|
9
|
+
import { createRefreshTokenForActiveUser, findActiveRefreshTokenByHash, findRefreshTokenByHash, revokeRefreshSession, revokeRefreshTokensForUser, rotateRefreshToken } from '#db/refresh-tokens.js';
|
|
10
10
|
import { createUser as createDbUser, findUserByEmail, findUserById, markEmailVerified, provisionUser as provisionDbUser, updateUserPassword } from '#db/users.js';
|
|
11
11
|
import { recordTokenRefreshed } from '#metrics/index.js';
|
|
12
12
|
import { getCurrentAdminRole } from './admin-role.js';
|
|
@@ -46,17 +46,20 @@ function daysFromNow(days) {
|
|
|
46
46
|
function recordRefreshOutcome(outcome) {
|
|
47
47
|
recordTokenRefreshed(outcome);
|
|
48
48
|
}
|
|
49
|
-
async function
|
|
49
|
+
async function loadTokenMemberships(userId, workspaces) {
|
|
50
50
|
const memberships = await workspaces.listMembershipsForTokenClaims({
|
|
51
|
-
userId
|
|
51
|
+
userId
|
|
52
52
|
}).catch((error)=>{
|
|
53
53
|
throw new AuthDependencyUnavailableError('workspaces', error);
|
|
54
54
|
});
|
|
55
|
+
return memberships.memberships;
|
|
56
|
+
}
|
|
57
|
+
async function signAccessToken(user, memberships, refreshSessionId) {
|
|
55
58
|
return await signUserToken({
|
|
56
59
|
userId: user.id,
|
|
57
60
|
email: user.email,
|
|
58
61
|
name: user.name,
|
|
59
|
-
memberships
|
|
62
|
+
memberships,
|
|
60
63
|
refreshSessionId,
|
|
61
64
|
secret: userAccessTokenKey(),
|
|
62
65
|
expiresIn: config.AUTH_JWT_EXPIRES_IN
|
|
@@ -69,12 +72,13 @@ function isWithinRotationGrace(refreshToken) {
|
|
|
69
72
|
}
|
|
70
73
|
async function createRefreshSession(user, refreshSessionId) {
|
|
71
74
|
const refreshToken = generateOpaqueToken('refreshToken');
|
|
72
|
-
const session = await
|
|
75
|
+
const session = await createRefreshTokenForActiveUser({
|
|
73
76
|
sessionId: refreshSessionId,
|
|
74
77
|
userId: user.id,
|
|
75
78
|
hashedToken: hashOpaqueToken(refreshToken),
|
|
76
79
|
expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS)
|
|
77
80
|
});
|
|
81
|
+
if (!session) throw new InvalidCredentialsError();
|
|
78
82
|
return {
|
|
79
83
|
refreshToken,
|
|
80
84
|
refreshSessionId: session.sessionId
|
|
@@ -82,7 +86,8 @@ async function createRefreshSession(user, refreshSessionId) {
|
|
|
82
86
|
}
|
|
83
87
|
async function createSessionTokens(user, workspaces) {
|
|
84
88
|
const refreshSessionId = crypto.randomUUID();
|
|
85
|
-
const
|
|
89
|
+
const memberships = await loadTokenMemberships(user.id, workspaces);
|
|
90
|
+
const token = await signAccessToken(user, memberships, refreshSessionId);
|
|
86
91
|
const adminRole = await getCurrentAdminRole({
|
|
87
92
|
userId: user.id
|
|
88
93
|
});
|
|
@@ -247,7 +252,7 @@ export async function signupWithInvitation(params) {
|
|
|
247
252
|
};
|
|
248
253
|
}
|
|
249
254
|
}
|
|
250
|
-
// Step 4: Issue session.
|
|
255
|
+
// Step 4: Issue session. createSessionTokens reads memberships through the
|
|
251
256
|
// workspaces module API, so a successful accept is reflected in the JWT.
|
|
252
257
|
const { token, refreshToken, adminRole } = await createSessionTokens(user, params.workspaces);
|
|
253
258
|
if (acceptError) {
|
|
@@ -366,7 +371,8 @@ export async function refreshAccessToken(params) {
|
|
|
366
371
|
// second tab); past it, reuse of a retired token means a compromised session.
|
|
367
372
|
if (current.rotatedAt) {
|
|
368
373
|
if (isWithinRotationGrace(current)) {
|
|
369
|
-
const
|
|
374
|
+
const memberships = await loadTokenMemberships(user.id, params.workspaces);
|
|
375
|
+
const token = await signAccessToken(user, memberships, current.sessionId);
|
|
370
376
|
recordRefreshOutcome('grace');
|
|
371
377
|
return {
|
|
372
378
|
token,
|
|
@@ -381,16 +387,27 @@ export async function refreshAccessToken(params) {
|
|
|
381
387
|
recordRefreshOutcome('rejected');
|
|
382
388
|
throw new TokenInvalidError('Refresh token reused after rotation');
|
|
383
389
|
}
|
|
390
|
+
// Keep the external membership snapshot ahead of rotation so an outage
|
|
391
|
+
// cannot retire a refresh token that the caller can still retry.
|
|
392
|
+
const memberships = await loadTokenMemberships(user.id, params.workspaces);
|
|
384
393
|
// Losing the CAS requires a state check: another request may have rotated,
|
|
385
394
|
// revoked, or expired the token before this refresh could claim it.
|
|
386
395
|
const nextRefreshToken = generateOpaqueToken('refreshToken');
|
|
387
396
|
const rotated = await rotateRefreshToken({
|
|
397
|
+
userId: current.userId,
|
|
388
398
|
id: current.id,
|
|
389
399
|
currentHashedToken,
|
|
390
400
|
nextHashedToken: hashOpaqueToken(nextRefreshToken),
|
|
391
401
|
expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS)
|
|
392
402
|
});
|
|
393
403
|
if (!rotated) {
|
|
404
|
+
const latestUser = await findUserById({
|
|
405
|
+
id: current.userId
|
|
406
|
+
});
|
|
407
|
+
if (latestUser?.status !== 'active') {
|
|
408
|
+
recordRefreshOutcome('rejected');
|
|
409
|
+
throw new TokenInvalidError('Refresh token is invalid or expired');
|
|
410
|
+
}
|
|
394
411
|
const latest = await findRefreshTokenByHash({
|
|
395
412
|
hashedToken: currentHashedToken
|
|
396
413
|
});
|
|
@@ -398,7 +415,7 @@ export async function refreshAccessToken(params) {
|
|
|
398
415
|
recordRefreshOutcome('rejected');
|
|
399
416
|
throw new TokenInvalidError('Refresh token is invalid or expired');
|
|
400
417
|
}
|
|
401
|
-
const token = await signAccessToken(user,
|
|
418
|
+
const token = await signAccessToken(user, memberships, latest.sessionId);
|
|
402
419
|
recordRefreshOutcome('grace');
|
|
403
420
|
return {
|
|
404
421
|
token,
|
|
@@ -407,7 +424,7 @@ export async function refreshAccessToken(params) {
|
|
|
407
424
|
adminRole
|
|
408
425
|
};
|
|
409
426
|
}
|
|
410
|
-
const token = await signAccessToken(user,
|
|
427
|
+
const token = await signAccessToken(user, memberships, current.sessionId);
|
|
411
428
|
recordRefreshOutcome('rotated');
|
|
412
429
|
return {
|
|
413
430
|
token,
|