@shipfox/api-auth 10.0.0 → 10.2.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.
Files changed (82) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/README.md +20 -1
  4. package/dist/core/administration.d.ts +18 -1
  5. package/dist/core/administration.d.ts.map +1 -1
  6. package/dist/core/administration.js +111 -15
  7. package/dist/core/administration.js.map +1 -1
  8. package/dist/core/auth.d.ts.map +1 -1
  9. package/dist/core/auth.js +11 -2
  10. package/dist/core/auth.js.map +1 -1
  11. package/dist/db/admin-command.d.ts +18 -0
  12. package/dist/db/admin-command.d.ts.map +1 -0
  13. package/dist/db/admin-command.js +37 -0
  14. package/dist/db/admin-command.js.map +1 -0
  15. package/dist/db/admin-grants.d.ts +9 -0
  16. package/dist/db/admin-grants.d.ts.map +1 -1
  17. package/dist/db/admin-grants.js +31 -40
  18. package/dist/db/admin-grants.js.map +1 -1
  19. package/dist/db/admin-user-moderation.d.ts +29 -0
  20. package/dist/db/admin-user-moderation.d.ts.map +1 -0
  21. package/dist/db/admin-user-moderation.js +156 -0
  22. package/dist/db/admin-user-moderation.js.map +1 -0
  23. package/dist/db/admin-user-summary.d.ts +14 -0
  24. package/dist/db/admin-user-summary.d.ts.map +1 -0
  25. package/dist/db/admin-user-summary.js +31 -0
  26. package/dist/db/admin-user-summary.js.map +1 -0
  27. package/dist/db/admin-users.d.ts +2 -11
  28. package/dist/db/admin-users.d.ts.map +1 -1
  29. package/dist/db/admin-users.js +2 -27
  30. package/dist/db/admin-users.js.map +1 -1
  31. package/dist/db/refresh-tokens.d.ts +6 -0
  32. package/dist/db/refresh-tokens.d.ts.map +1 -1
  33. package/dist/db/refresh-tokens.js +31 -0
  34. package/dist/db/refresh-tokens.js.map +1 -1
  35. package/dist/db/schema/admin-command-results.d.ts +19 -2
  36. package/dist/db/schema/admin-command-results.d.ts.map +1 -1
  37. package/dist/db/schema/admin-command-results.js.map +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/metrics/instance.d.ts +1 -1
  43. package/dist/metrics/instance.d.ts.map +1 -1
  44. package/dist/metrics/instance.js.map +1 -1
  45. package/dist/presentation/auth/bearer-token-auth.d.ts +1 -0
  46. package/dist/presentation/auth/bearer-token-auth.d.ts.map +1 -1
  47. package/dist/presentation/auth/bearer-token-auth.js +4 -3
  48. package/dist/presentation/auth/bearer-token-auth.js.map +1 -1
  49. package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
  50. package/dist/presentation/auth/jwt-auth.js +32 -4
  51. package/dist/presentation/auth/jwt-auth.js.map +1 -1
  52. package/dist/presentation/routes/administration.d.ts +1 -0
  53. package/dist/presentation/routes/administration.d.ts.map +1 -1
  54. package/dist/presentation/routes/administration.js +116 -3
  55. package/dist/presentation/routes/administration.js.map +1 -1
  56. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  57. package/dist/presentation/routes/rate-limit.js +6 -0
  58. package/dist/presentation/routes/rate-limit.js.map +1 -1
  59. package/dist/tsconfig.test.tsbuildinfo +1 -1
  60. package/package.json +4 -4
  61. package/src/core/administration.ts +142 -15
  62. package/src/core/auth.ts +9 -2
  63. package/src/db/admin-command.ts +85 -0
  64. package/src/db/admin-grants.test.ts +17 -1
  65. package/src/db/admin-grants.ts +52 -92
  66. package/src/db/admin-user-moderation.ts +230 -0
  67. package/src/db/admin-user-summary.ts +51 -0
  68. package/src/db/admin-users.ts +4 -49
  69. package/src/db/refresh-tokens.ts +54 -0
  70. package/src/db/schema/admin-command-results.ts +23 -2
  71. package/src/index.test.ts +4 -1
  72. package/src/index.ts +5 -0
  73. package/src/metrics/instance.ts +6 -1
  74. package/src/presentation/auth/bearer-token-auth.test.ts +17 -0
  75. package/src/presentation/auth/bearer-token-auth.ts +9 -2
  76. package/src/presentation/auth/jwt-auth.test.ts +1 -1
  77. package/src/presentation/auth/jwt-auth.ts +30 -1
  78. package/src/presentation/routes/administration.test.ts +373 -2
  79. package/src/presentation/routes/administration.ts +106 -1
  80. package/src/presentation/routes/rate-limit.ts +3 -0
  81. package/test/routes.ts +7 -1
  82. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 64 files with swc (349.3ms)
2
+ Successfully compiled: 67 files with swc (347.36ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @shipfox/api-auth
2
2
 
3
+ ## 10.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 95d1456: Add administrator user suspension, reactivation, and all-session revocation commands.
8
+
9
+ ### Patch Changes
10
+
11
+ - 07e7371: Add idempotent administrator workspace suspension and reactivation commands with atomic redacted administration events.
12
+ - Updated dependencies [95d1456]
13
+ - Updated dependencies [0773b85]
14
+ - Updated dependencies [07e7371]
15
+ - @shipfox/api-auth-dto@10.2.0
16
+ - @shipfox/api-workspaces-dto@10.2.0
17
+ - @shipfox/api-auth-context@10.2.0
18
+
19
+ ## 10.1.0
20
+
21
+ ### Minor Changes
22
+
23
+ - fb34b6a: Expose authenticated administrator bootstrap availability through the Auth API.
24
+
25
+ ### Patch Changes
26
+
27
+ - 3db7189: Treats suspended users as inactive admin owners during bootstrap recovery.
28
+ - Updated dependencies [fb34b6a]
29
+ - @shipfox/api-auth-dto@10.1.0
30
+ - @shipfox/api-auth-context@10.1.0
31
+
3
32
  ## 10.0.0
4
33
 
5
34
  ### Major Changes
package/README.md CHANGED
@@ -234,7 +234,9 @@ When password login is disabled, the password and email-verification rows in thi
234
234
 
235
235
  ### Administrator grants
236
236
 
237
- All routes are mounted under `/admin/auth/admin-grants` and require an authenticated bearer token. Administrator role checks are enforced in the core layer, not by route-level middleware.
237
+ `GET /admin/auth/bootstrap-state` requires an authenticated bearer token and returns exactly `{state: 'available' | 'closed'}`. It reports `available` only while no active `admin-owner` exists and never reports deployment-token configuration.
238
+
239
+ Bootstrap and grant-management routes are mounted under `/admin/auth/admin-grants` and require an authenticated bearer token. Administrator role checks are enforced in the core layer, not by route-level middleware.
238
240
 
239
241
  | Method | Path | Required role | Result |
240
242
  | --- | --- | --- | --- |
@@ -245,6 +247,8 @@ All routes are mounted under `/admin/auth/admin-grants` and require an authentic
245
247
 
246
248
  `POST` and `DELETE` routes require an `Idempotency-Key` header; a repeated key replays the same result, and reusing a key with a different command returns `409 idempotency-key-reused`. Every mutation writes a redacted `administration.action.performed` outbox event.
247
249
 
250
+ Bootstrap remains closed only while an active `admin-owner` exists. An owner is active when its grant is not revoked and its user account has `active` status. A suspended owner does not count as active, so suspending the sole owner intentionally reopens deployment-token bootstrap for recovery. The suspended grant remains stored; reactivating that user closes bootstrap again.
251
+
248
252
  ### Rate limiting
249
253
 
250
254
  The public auth endpoints include an application-layer abuse baseline for open source installs:
@@ -269,6 +273,20 @@ This app-layer limiter protects semantic auth work such as Argon2 verification a
269
273
 
270
274
  `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.
271
275
 
276
+ ### Administrator user moderation
277
+
278
+ All routes are mounted under `/admin/auth/users` and require an authenticated bearer token.
279
+
280
+ | Method | Path | Required role | Result |
281
+ | --- | --- | --- | --- |
282
+ | `POST` | `/:userId/suspend` | `admin-operator` | Suspends the user, revokes all active sessions, and returns the final safe user summary with a correlation ID. |
283
+ | `POST` | `/:userId/reactivate` | `admin-operator` | Restores sign-in eligibility without restoring revoked sessions. |
284
+ | `POST` | `/:userId/revoke-sessions` | `admin-operator` | Revokes all active sessions without changing account status and returns the number of affected sessions. |
285
+
286
+ 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`.
287
+
288
+ 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.
289
+
272
290
  ## API
273
291
 
274
292
  The package exports a module factory. Pass it the Workspaces inter-module client from the
@@ -423,6 +441,7 @@ Passwords use Argon2id. Password reset tokens and refresh tokens are opaque toke
423
441
  - A denied password signup returns `403` with error code `signup-not-allowed`. The policy message is capped at 500 characters.
424
442
  - Invitation signup validates the invitation and bypasses the signup policy. Invitations remain a separate authorization path.
425
443
  - Login only succeeds for active users with verified email addresses and a password hash.
444
+ - Authenticated requests reject suspended or deleted users. Session-bound access tokens also reject a revoked refresh session.
426
445
  - Refresh tokens rotate on each refresh.
427
446
  - Password reset tokens and email challenge proofs are consumed once.
428
447
  - Password change revokes other refresh sessions. It keeps the current session when the current refresh cookie is valid.
@@ -3,16 +3,21 @@ 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>;
20
+ export declare function getAdminBootstrapState(): Promise<'available' | 'closed'>;
16
21
  export declare function findAdministratorUserSummary(params: {
17
22
  actorId: string;
18
23
  } & ({
@@ -39,5 +44,17 @@ export declare function revokeAdministratorGrant(params: AdministrationMutationC
39
44
  grantId: string;
40
45
  reason: string;
41
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>;
42
59
  export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError, };
43
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;AAU7D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EACV,yBAAyB,EACzB,wBAAwB,EACzB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EACL,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC7B,+BAA+B,EAC/B,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAQrB,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAExF;AAkCD,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,6BAA6B,GAAG;IAAC,cAAc,EAAE,MAAM,CAAA;CAAC,GAC/D,OAAO,CAAC,UAAU,CAAC,CAuBrB;AAED,wBAAsB,4BAA4B,CAChD,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAAG,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAC,GAAG;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,KAAK,CAAA;CAAC,CAAC,GACtF,OAAO,CAAC,wBAAwB,GAAG,SAAS,CAAC,CAS/C;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B,GAAG,OAAO,CAAC;IACV,MAAM,EAAE,yBAAyB,EAAE,CAAC;IACpC,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACtC,CAAC,CAiBD;AAED,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,6BAA6B,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACxF,OAAO,CAAC,UAAU,CAAC,CA0BrB;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,6BAA6B,GAAG;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACxE,OAAO,CAAC,UAAU,CAAC,CAwBrB;AAED,OAAO,EACL,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC7B,+BAA+B,EAC/B,mBAAmB,EACnB,iBAAiB,GAClB,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 { createHash, timingSafeEqual } from 'node:crypto';
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
- import { bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb, grantAdminRoleWithAudit, listAdminGrantSummaries, revokeAdminGrantWithAudit } from '#db/admin-grants.js';
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
- export function hashAdministrationValue(value) {
14
- return createHash('sha256').update(value).digest('hex');
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 hashAdministrationValue(`${command}:${JSON.stringify(input)}`);
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(hashAdministrationValue(candidate), 'hex');
22
- const expectedHash = Buffer.from(hashAdministrationValue(expected), 'hex');
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: hashAdministrationValue(params.idempotencyKey),
57
+ idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey),
48
58
  requestFingerprint: administrationCommandFingerprint(BOOTSTRAP_COMMAND, {
49
59
  actorId: params.actorId
50
60
  }),
@@ -56,10 +66,13 @@ export async function bootstrapFirstAdminOwner(params) {
56
66
  targetId: params.actorId,
57
67
  reason: 'Initial administrator owner bootstrap',
58
68
  correlationId: params.correlationId,
59
- idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey)
69
+ idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey)
60
70
  })
61
71
  });
62
72
  }
73
+ export async function getAdminBootstrapState() {
74
+ return await hasActiveAdminOwner() ? 'closed' : 'available';
75
+ }
63
76
  export async function findAdministratorUserSummary(params) {
64
77
  await requireAdminRole({
65
78
  userId: params.actorId,
@@ -104,7 +117,7 @@ export async function grantAdministratorRole(params) {
104
117
  userId: params.userId,
105
118
  role: params.role,
106
119
  actorId: params.actorId,
107
- idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),
120
+ idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey),
108
121
  requestFingerprint: administrationCommandFingerprint(GRANT_COMMAND, {
109
122
  userId: params.userId,
110
123
  role: params.role,
@@ -118,7 +131,7 @@ export async function grantAdministratorRole(params) {
118
131
  targetId: params.userId,
119
132
  reason: params.reason,
120
133
  correlationId: params.correlationId,
121
- idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey)
134
+ idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey)
122
135
  })
123
136
  });
124
137
  }
@@ -130,7 +143,7 @@ export async function revokeAdministratorGrant(params) {
130
143
  return await revokeAdminGrantWithAudit({
131
144
  grantId: params.grantId,
132
145
  actorId: params.actorId,
133
- idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),
146
+ idempotencyKeyFingerprint: hashOpaqueToken(params.idempotencyKey),
134
147
  requestFingerprint: administrationCommandFingerprint(REVOKE_COMMAND, {
135
148
  grantId: params.grantId,
136
149
  reason: params.reason
@@ -143,9 +156,92 @@ export async function revokeAdministratorGrant(params) {
143
156
  targetId: params.grantId,
144
157
  reason: params.reason,
145
158
  correlationId: params.correlationId,
146
- idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey)
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
187
+ })
188
+ }));
189
+ }
190
+ export async function reactivateAdministratorUser(params) {
191
+ const actorRole = await requireAdminRole({
192
+ userId: params.actorId,
193
+ minimumRole: ADMIN_OPERATOR_ROLE
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
147
215
  })
216
+ }));
217
+ }
218
+ export async function revokeAdministratorUserSessions(params) {
219
+ const actorRole = await requireAdminRole({
220
+ userId: params.actorId,
221
+ minimumRole: ADMIN_OPERATOR_ROLE
148
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
+ }));
149
245
  }
150
246
  export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError };
151
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 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 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","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","findAdministratorUserSummary","minimumRole","user","id","email","undefined","listAdministratorGrantSummaries","limit","cursor","grants","rows","map","row","grantId","role","createdAt","revokedAt","nextCursor","grantAdministratorRole","revokeAdministratorGrant"],"mappings":"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"}
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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"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"}
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 { createRefreshToken, findActiveRefreshTokenByHash, findRefreshTokenByHash, revokeRefreshSession, revokeRefreshTokensForUser, rotateRefreshToken } from '#db/refresh-tokens.js';
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';
@@ -69,12 +69,13 @@ function isWithinRotationGrace(refreshToken) {
69
69
  }
70
70
  async function createRefreshSession(user, refreshSessionId) {
71
71
  const refreshToken = generateOpaqueToken('refreshToken');
72
- const session = await createRefreshToken({
72
+ const session = await createRefreshTokenForActiveUser({
73
73
  sessionId: refreshSessionId,
74
74
  userId: user.id,
75
75
  hashedToken: hashOpaqueToken(refreshToken),
76
76
  expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS)
77
77
  });
78
+ if (!session) throw new InvalidCredentialsError();
78
79
  return {
79
80
  refreshToken,
80
81
  refreshSessionId: session.sessionId
@@ -385,12 +386,20 @@ export async function refreshAccessToken(params) {
385
386
  // revoked, or expired the token before this refresh could claim it.
386
387
  const nextRefreshToken = generateOpaqueToken('refreshToken');
387
388
  const rotated = await rotateRefreshToken({
389
+ userId: current.userId,
388
390
  id: current.id,
389
391
  currentHashedToken,
390
392
  nextHashedToken: hashOpaqueToken(nextRefreshToken),
391
393
  expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS)
392
394
  });
393
395
  if (!rotated) {
396
+ const latestUser = await findUserById({
397
+ id: current.userId
398
+ });
399
+ if (latestUser?.status !== 'active') {
400
+ recordRefreshOutcome('rejected');
401
+ throw new TokenInvalidError('Refresh token is invalid or expired');
402
+ }
394
403
  const latest = await findRefreshTokenByHash({
395
404
  hashedToken: currentHashedToken
396
405
  });