@shipfox/api-auth 9.3.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +35 -0
  3. package/README.md +10 -3
  4. package/dist/core/administration.d.ts +18 -2
  5. package/dist/core/administration.d.ts.map +1 -1
  6. package/dist/core/administration.js +34 -4
  7. package/dist/core/administration.js.map +1 -1
  8. package/dist/core/entities/administrator-read-model.d.ts +21 -0
  9. package/dist/core/entities/administrator-read-model.d.ts.map +1 -0
  10. package/dist/core/entities/administrator-read-model.js +3 -0
  11. package/dist/core/entities/administrator-read-model.js.map +1 -0
  12. package/dist/db/admin-grants.d.ts +16 -1
  13. package/dist/db/admin-grants.d.ts.map +1 -1
  14. package/dist/db/admin-grants.js +32 -4
  15. package/dist/db/admin-grants.js.map +1 -1
  16. package/dist/db/admin-users.d.ts +21 -0
  17. package/dist/db/admin-users.d.ts.map +1 -0
  18. package/dist/db/admin-users.js +32 -0
  19. package/dist/db/admin-users.js.map +1 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +4 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/metrics/instance.d.ts +1 -1
  25. package/dist/metrics/instance.d.ts.map +1 -1
  26. package/dist/metrics/instance.js.map +1 -1
  27. package/dist/presentation/routes/administration.d.ts +1 -0
  28. package/dist/presentation/routes/administration.d.ts.map +1 -1
  29. package/dist/presentation/routes/administration.js +91 -10
  30. package/dist/presentation/routes/administration.js.map +1 -1
  31. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  32. package/dist/presentation/routes/rate-limit.js +6 -0
  33. package/dist/presentation/routes/rate-limit.js.map +1 -1
  34. package/dist/tsconfig.test.tsbuildinfo +1 -1
  35. package/package.json +7 -7
  36. package/src/core/administration.ts +45 -4
  37. package/src/core/entities/administrator-read-model.ts +23 -0
  38. package/src/db/admin-grants.test.ts +1 -7
  39. package/src/db/admin-grants.ts +52 -4
  40. package/src/db/admin-users.ts +58 -0
  41. package/src/index.test.ts +1 -1
  42. package/src/index.ts +5 -2
  43. package/src/metrics/instance.ts +1 -1
  44. package/src/presentation/routes/administration.test.ts +304 -20
  45. package/src/presentation/routes/administration.ts +91 -10
  46. package/src/presentation/routes/rate-limit.ts +3 -0
  47. package/test/routes.ts +5 -2
  48. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 62 files with swc (608.87ms)
2
+ Successfully compiled: 64 files with swc (349.3ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @shipfox/api-auth
2
2
 
3
+ ## 10.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 6054364: Add bounded administrator user lookup and safe administrator-grant summaries while removing the unbounded grant reader.
8
+
9
+ ### Minor Changes
10
+
11
+ - dd43d63: Moves Auth administration routes to the unversioned `/admin/...` namespace.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [6054364]
16
+ - Updated dependencies [74f9e31]
17
+ - Updated dependencies [e9280fc]
18
+ - @shipfox/api-auth-dto@10.0.0
19
+ - @shipfox/node-fastify@0.4.0
20
+ - @shipfox/api-workspaces-dto@10.0.0
21
+ - @shipfox/api-auth-context@10.0.0
22
+ - @shipfox/node-module@1.0.4
23
+ - @shipfox/api-common-dto@9.2.0
24
+ - @shipfox/api-email-challenges@1.1.6
25
+ - @shipfox/config@1.2.4
26
+ - @shipfox/inter-module@0.2.2
27
+ - @shipfox/node-auth-root-key@0.2.3
28
+ - @shipfox/node-drizzle@0.3.4
29
+ - @shipfox/node-email@0.3.4
30
+ - @shipfox/node-jwt@0.3.2
31
+ - @shipfox/node-mailer@0.2.4
32
+ - @shipfox/node-opentelemetry@0.6.3
33
+ - @shipfox/node-outbox@0.2.6
34
+ - @shipfox/node-postgres@0.4.4
35
+ - @shipfox/node-rate-limit@0.3.2
36
+ - @shipfox/node-tokens@0.3.2
37
+
3
38
  ## 9.3.0
4
39
 
5
40
  ### Minor Changes
package/README.md CHANGED
@@ -234,12 +234,12 @@ 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/v1/auth/admin-grants` and require an authenticated bearer token. Ownership is enforced in the core layer, not by route-level middleware.
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.
238
238
 
239
239
  | Method | Path | Required role | Result |
240
240
  | --- | --- | --- | --- |
241
241
  | `POST` | `/bootstrap` | none (deployment token) | Claims the `admin-owner` role for the calling user using `ADMIN_BOOTSTRAP_TOKEN`. Fails with `409 bootstrap-closed` once an active owner exists. |
242
- | `GET` | `/` | `admin-owner` | Lists local administrator grants. |
242
+ | `GET` | `/` | `admin-observer` | Lists bounded local administrator grant summaries with embedded safe user identities. |
243
243
  | `POST` | `/` | `admin-owner` | Grants a role to an active user. |
244
244
  | `DELETE` | `/:grantId` | `admin-owner` | Revokes a grant. Fails with `409 last-owner` when it would remove the final active owner. |
245
245
 
@@ -254,7 +254,8 @@ The public auth endpoints include an application-layer abuse baseline for open s
254
254
  | `POST /auth/login` | 60 attempts per 5 minutes | 10 attempts per 15 minutes |
255
255
  | `POST /auth/password-reset` | 30 email-send attempts per hour | 3 email-send attempts per hour |
256
256
  | `POST /auth/verify-email/resend` | Shared with password reset | Shared with password reset |
257
- | `POST /admin/v1/auth/admin-grants/bootstrap` | 5 attempts per 15 minutes | n/a |
257
+ | `POST /admin/auth/admin-grants/bootstrap` | 5 attempts per 15 minutes | n/a |
258
+ | `GET /admin/auth/users` | 60 attempts per 5 minutes | n/a |
258
259
 
259
260
  Counters are stored in PostgreSQL as fixed windows in `auth_rate_limits`. IP addresses and email addresses are HMAC-SHA256 values before storage; raw identifiers are not persisted. The identifier key is derived from `AUTH_ROOT_KEY` separately from every token key.
260
261
 
@@ -262,6 +263,12 @@ The limiter uses `request.ip`, so production deployments behind a reverse proxy
262
263
 
263
264
  This app-layer limiter protects semantic auth work such as Argon2 verification and email sending. It is not a volumetric DDoS control. Public production deployments should still use load balancer, CDN, WAF, or firewall rate limits at the network edge.
264
265
 
266
+ ### Administrator user reads
267
+
268
+ `GET /admin/auth/users` requires `admin-observer` and accepts exactly one checked query filter: `id`, `user_id`, or normalized `email`. The response is a bounded `AdministratorUserSummary` containing only the stable user identity, verified-email timestamp, display name, account status, creation timestamp, and current administrator role.
269
+
270
+ `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
+
265
272
  ## API
266
273
 
267
274
  The package exports a module factory. Pass it the Workspaces inter-module client from the
@@ -1,5 +1,7 @@
1
1
  import type { AdminRole } from '@shipfox/api-auth-dto';
2
+ import type { TimestampIdCursor } from '@shipfox/node-drizzle';
2
3
  import type { AdminGrant } from './entities/admin-grant.js';
4
+ import type { AdministratorGrantSummary, AdministratorUserSummary } from './entities/administrator-read-model.js';
3
5
  import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from './errors.js';
4
6
  export declare function hashAdministrationValue(value: string): string;
5
7
  export declare function administrationCommandFingerprint(command: string, input: unknown): string;
@@ -11,9 +13,23 @@ export interface AdministrationMutationContext {
11
13
  export declare function bootstrapFirstAdminOwner(params: AdministrationMutationContext & {
12
14
  bootstrapToken: string;
13
15
  }): Promise<AdminGrant>;
14
- export declare function listAdministratorGrants(params: {
16
+ export declare function findAdministratorUserSummary(params: {
15
17
  actorId: string;
16
- }): Promise<AdminGrant[]>;
18
+ } & ({
19
+ id: string;
20
+ email?: never;
21
+ } | {
22
+ email: string;
23
+ id?: never;
24
+ })): Promise<AdministratorUserSummary | undefined>;
25
+ export declare function listAdministratorGrantSummaries(params: {
26
+ actorId: string;
27
+ limit: number;
28
+ cursor?: TimestampIdCursor;
29
+ }): Promise<{
30
+ grants: AdministratorGrantSummary[];
31
+ nextCursor: TimestampIdCursor | null;
32
+ }>;
17
33
  export declare function grantAdministratorRole(params: AdministrationMutationContext & {
18
34
  userId: string;
19
35
  role: AdminRole;
@@ -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;AAUrD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EACL,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC7B,+BAA+B,EAC/B,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAOrB,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,uBAAuB,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAG9F;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":"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,10 +1,12 @@
1
1
  import { createHash, timingSafeEqual } from 'node:crypto';
2
2
  import { createAdministrationActionEvent } from '@shipfox/api-common-dto';
3
3
  import { config } from '#config.js';
4
- import { bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb, grantAdminRoleWithAudit, listAdminGrants, revokeAdminGrantWithAudit } from '#db/admin-grants.js';
4
+ import { bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb, grantAdminRoleWithAudit, listAdminGrantSummaries, revokeAdminGrantWithAudit } from '#db/admin-grants.js';
5
+ import { findAdministratorUser as findAdministratorUserInDb } from '#db/admin-users.js';
5
6
  import { requireAdminRole } from './admin-role.js';
6
7
  import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from './errors.js';
7
8
  const ADMIN_OWNER_ROLE = 'admin-owner';
9
+ const ADMIN_OBSERVER_ROLE = 'admin-observer';
8
10
  const BOOTSTRAP_COMMAND = 'auth.admin_grant.bootstrap';
9
11
  const GRANT_COMMAND = 'auth.admin_grant.grant';
10
12
  const REVOKE_COMMAND = 'auth.admin_grant.revoke';
@@ -58,12 +60,40 @@ export async function bootstrapFirstAdminOwner(params) {
58
60
  })
59
61
  });
60
62
  }
61
- export async function listAdministratorGrants(params) {
63
+ export async function findAdministratorUserSummary(params) {
62
64
  await requireAdminRole({
63
65
  userId: params.actorId,
64
- minimumRole: ADMIN_OWNER_ROLE
66
+ minimumRole: ADMIN_OBSERVER_ROLE
67
+ });
68
+ const user = await findAdministratorUserInDb('id' in params ? {
69
+ id: params.id
70
+ } : {
71
+ email: params.email
72
+ });
73
+ if (!user) return undefined;
74
+ return user;
75
+ }
76
+ export async function listAdministratorGrantSummaries(params) {
77
+ await requireAdminRole({
78
+ userId: params.actorId,
79
+ minimumRole: ADMIN_OBSERVER_ROLE
80
+ });
81
+ const result = await listAdminGrantSummaries({
82
+ limit: params.limit,
83
+ ...params.cursor ? {
84
+ cursor: params.cursor
85
+ } : {}
65
86
  });
66
- return await listAdminGrants();
87
+ return {
88
+ grants: result.rows.map((row)=>({
89
+ grantId: row.id,
90
+ role: row.role,
91
+ createdAt: row.createdAt,
92
+ revokedAt: row.revokedAt,
93
+ user: row.user
94
+ })),
95
+ nextCursor: result.nextCursor
96
+ };
67
97
  }
68
98
  export async function grantAdministratorRole(params) {
69
99
  const actorRole = await requireAdminRole({
@@ -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 {config} from '#config.js';\nimport {\n bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb,\n grantAdminRoleWithAudit,\n listAdminGrants,\n revokeAdminGrantWithAudit,\n} from '#db/admin-grants.js';\nimport {requireAdminRole} from './admin-role.js';\nimport type {AdminGrant} from './entities/admin-grant.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 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 listAdministratorGrants(params: {actorId: string}): Promise<AdminGrant[]> {\n await requireAdminRole({userId: params.actorId, minimumRole: ADMIN_OWNER_ROLE});\n return await listAdminGrants();\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","listAdminGrants","revokeAdminGrantWithAudit","requireAdminRole","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","InvalidAdminBootstrapTokenError","LastAdminOwnerError","UserNotFoundError","ADMIN_OWNER_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","listAdministratorGrants","minimumRole","grantAdministratorRole","role","revokeAdministratorGrant","grantId"],"mappings":"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"}
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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"}
@@ -0,0 +1,21 @@
1
+ import type { AdminRole } from '@shipfox/api-auth-dto';
2
+ import type { UserStatus } from './user.js';
3
+ export interface AdministratorUserIdentity {
4
+ id: string;
5
+ email: string;
6
+ name: string | null;
7
+ status: UserStatus;
8
+ }
9
+ export interface AdministratorUserSummary extends AdministratorUserIdentity {
10
+ emailVerifiedAt: Date | null;
11
+ createdAt: Date;
12
+ adminRole: AdminRole | null;
13
+ }
14
+ export interface AdministratorGrantSummary {
15
+ grantId: string;
16
+ role: AdminRole;
17
+ createdAt: Date;
18
+ revokedAt: Date | null;
19
+ user: AdministratorUserIdentity;
20
+ }
21
+ //# sourceMappingURL=administrator-read-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"administrator-read-model.d.ts","sourceRoot":"","sources":["../../../src/core/entities/administrator-read-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,WAAW,CAAC;AAE1C,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,wBAAyB,SAAQ,yBAAyB;IACzE,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,yBAAyB,CAAC;CACjC"}
@@ -0,0 +1,3 @@
1
+ export { };
2
+
3
+ //# sourceMappingURL=administrator-read-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/core/entities/administrator-read-model.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport type {UserStatus} from './user.js';\n\nexport interface AdministratorUserIdentity {\n id: string;\n email: string;\n name: string | null;\n status: UserStatus;\n}\n\nexport interface AdministratorUserSummary extends AdministratorUserIdentity {\n emailVerifiedAt: Date | null;\n createdAt: Date;\n adminRole: AdminRole | null;\n}\n\nexport interface AdministratorGrantSummary {\n grantId: string;\n role: AdminRole;\n createdAt: Date;\n revokedAt: Date | null;\n user: AdministratorUserIdentity;\n}\n"],"names":[],"mappings":"AAgBA,WAMC"}
@@ -1,12 +1,27 @@
1
1
  import type { AdminRole } from '@shipfox/api-auth-dto';
2
2
  import type { AdministrationActionEvent } from '@shipfox/api-common-dto';
3
+ import { type TimestampIdCursor } from '@shipfox/node-drizzle';
3
4
  import type { AdminGrant } from '#core/entities/admin-grant.js';
5
+ import type { AdministratorGrantSummary } from '#core/entities/administrator-read-model.js';
4
6
  export interface CreateAdminGrantParams {
5
7
  userId: string;
6
8
  role: AdminRole;
7
9
  }
8
10
  export declare function createAdminGrant(params: CreateAdminGrantParams): Promise<AdminGrant>;
9
- export declare function listAdminGrants(): Promise<AdminGrant[]>;
11
+ export interface AdministratorGrantSummaryRecord {
12
+ id: string;
13
+ role: AdminRole;
14
+ createdAt: Date;
15
+ revokedAt: Date | null;
16
+ user: AdministratorGrantSummary['user'];
17
+ }
18
+ export declare function listAdminGrantSummaries(params: {
19
+ limit: number;
20
+ cursor?: TimestampIdCursor;
21
+ }): Promise<{
22
+ rows: AdministratorGrantSummaryRecord[];
23
+ nextCursor: TimestampIdCursor | null;
24
+ }>;
10
25
  export declare function findCurrentAdminRole(params: {
11
26
  userId: string;
12
27
  }): Promise<AdminRole | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"admin-grants.d.ts","sourceRoot":"","sources":["../../src/db/admin-grants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EACV,yBAAyB,EAE1B,MAAM,yBAAyB,CAAC;AAIjC,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,+BAA+B,CAAC;AAqB9D,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ1F;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAG7D;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAc9F;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAqCjG;AAED,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB,EAAE,MAAM,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,yBAAyB,CAAC;CAClC;AAoFD,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,yBAAyB,GAAG;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACnD,OAAO,CAAC,UAAU,CAAC,CA6CrB;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,yBAAyB,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,GACpE,OAAO,CAAC,UAAU,CAAC,CA4CrB;AAED,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,yBAAyB,GAAG;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACpD,OAAO,CAAC,UAAU,CAAC,CA+CrB"}
1
+ {"version":3,"file":"admin-grants.d.ts","sourceRoot":"","sources":["../../src/db/admin-grants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EACV,yBAAyB,EAE1B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,uBAAuB,CAAC;AAI/B,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,4CAA4C,CAAC;AAqB1F,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ1F;AAED,MAAM,WAAW,+BAA+B;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,yBAAyB,CAAC,MAAM,CAAC,CAAC;CACzC;AAED,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B,GAAG,OAAO,CAAC;IACV,IAAI,EAAE,+BAA+B,EAAE,CAAC;IACxC,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACtC,CAAC,CA+BD;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAc9F;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAqCjG;AAED,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB,EAAE,MAAM,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,yBAAyB,CAAC;CAClC;AAoFD,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,yBAAyB,GAAG;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACnD,OAAO,CAAC,UAAU,CAAC,CA6CrB;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,yBAAyB,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,GACpE,OAAO,CAAC,UAAU,CAAC,CA4CrB;AAED,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,yBAAyB,GAAG;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACpD,OAAO,CAAC,UAAU,CAAC,CA+CrB"}
@@ -1,5 +1,6 @@
1
+ import { paginateTimestampIdRows, timestampIdCursorWhere } from '@shipfox/node-drizzle';
1
2
  import { writeOutboxEvent } from '@shipfox/node-outbox';
2
- import { and, asc, eq, isNull, sql } from 'drizzle-orm';
3
+ import { and, desc, eq, isNull, sql } from 'drizzle-orm';
3
4
  import { highestAdminRole } from '#core/admin-role-model.js';
4
5
  import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, LastAdminOwnerError, UserNotFoundError } from '#core/errors.js';
5
6
  import { db } from './db.js';
@@ -16,9 +17,36 @@ export async function createAdminGrant(params) {
16
17
  if (!row) throw new Error('Insert returned no rows');
17
18
  return toAdminGrant(row);
18
19
  }
19
- export async function listAdminGrants() {
20
- const rows = await db().select().from(adminGrants).orderBy(asc(adminGrants.createdAt));
21
- return rows.map(toAdminGrant);
20
+ export async function listAdminGrantSummaries(params) {
21
+ const cursorCondition = timestampIdCursorWhere({
22
+ timestampColumn: adminGrants.createdAt,
23
+ idColumn: adminGrants.id,
24
+ cursor: params.cursor
25
+ });
26
+ const conditions = cursorCondition ? [
27
+ cursorCondition
28
+ ] : [];
29
+ const rows = await db().select({
30
+ id: adminGrants.id,
31
+ role: adminGrants.role,
32
+ createdAt: adminGrants.createdAt,
33
+ revokedAt: adminGrants.revokedAt,
34
+ user: {
35
+ id: users.id,
36
+ email: users.email,
37
+ name: users.name,
38
+ status: users.status
39
+ }
40
+ }).from(adminGrants).innerJoin(users, eq(adminGrants.userId, users.id)).where(and(...conditions)).orderBy(desc(adminGrants.createdAt), desc(adminGrants.id)).limit(params.limit + 1);
41
+ const page = paginateTimestampIdRows({
42
+ rows,
43
+ limit: params.limit,
44
+ timestampKey: 'createdAt'
45
+ });
46
+ return {
47
+ rows: page.pageRows,
48
+ nextCursor: page.nextCursor
49
+ };
22
50
  }
23
51
  export async function findCurrentAdminRole(params) {
24
52
  const rows = await db().select({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/admin-grants.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport type {\n AdministrationActionEvent,\n AdministrationActionEventMap,\n} from '@shipfox/api-common-dto';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport {and, asc, eq, isNull, sql} from 'drizzle-orm';\nimport {highestAdminRole} from '#core/admin-role-model.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {db} from './db.js';\nimport {\n type AdminCommandResultDb,\n adminCommandResults,\n type StoredAdminGrant,\n} from './schema/admin-command-results.js';\nimport {adminGrants, toAdminGrant} from './schema/admin-grants.js';\nimport {authOutbox} from './schema/outbox.js';\nimport {users} from './schema/users.js';\n\ntype Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];\n\nexport interface CreateAdminGrantParams {\n userId: string;\n role: AdminRole;\n}\n\nexport async function createAdminGrant(params: CreateAdminGrantParams): Promise<AdminGrant> {\n const rows = await db()\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toAdminGrant(row);\n}\n\nexport async function listAdminGrants(): Promise<AdminGrant[]> {\n const rows = await db().select().from(adminGrants).orderBy(asc(adminGrants.createdAt));\n return rows.map(toAdminGrant);\n}\n\nexport async function findCurrentAdminRole(params: {userId: string}): Promise<AdminRole | null> {\n const rows = await db()\n .select({role: adminGrants.role})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n\n return highestAdminRole(rows.map(({role}) => role));\n}\n\nexport async function revokeAdminGrant(params: {grantId: string}): Promise<AdminGrant | undefined> {\n return await db().transaction(async (tx) => {\n // All owner grant changes share one lock so concurrent revocations cannot\n // both observe the same final owner and leave the instance ownerless.\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) return undefined;\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const row = updated[0];\n return row ? toAdminGrant(row) : undefined;\n });\n}\n\ninterface AuditedAdminCommandParams {\n actorId: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n event: AdministrationActionEvent;\n}\n\nfunction toStoredAdminGrant(grant: AdminGrant): StoredAdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt?.toISOString() ?? null,\n createdAt: grant.createdAt.toISOString(),\n updatedAt: grant.updatedAt.toISOString(),\n };\n}\n\nfunction fromStoredAdminGrant(grant: StoredAdminGrant): AdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt ? new Date(grant.revokedAt) : null,\n createdAt: new Date(grant.createdAt),\n updatedAt: new Date(grant.updatedAt),\n };\n}\n\nasync function findCommandResult(\n tx: Tx,\n params: Pick<\n AuditedAdminCommandParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n > & {\n command: string;\n },\n): Promise<AdminGrant | undefined> {\n const rows = await tx\n .select()\n .from(adminCommandResults)\n .where(\n and(\n eq(adminCommandResults.actorId, params.actorId),\n eq(adminCommandResults.idempotencyKeyFingerprint, params.idempotencyKeyFingerprint),\n ),\n )\n .limit(1);\n const result: AdminCommandResultDb | undefined = rows[0];\n if (!result) return undefined;\n if (\n result.command !== params.command ||\n result.requestFingerprint !== params.requestFingerprint\n ) {\n throw new AdminIdempotencyKeyReuseError();\n }\n return fromStoredAdminGrant(result.result.grant);\n}\n\nasync function storeCommandResult(\n tx: Tx,\n params: AuditedAdminCommandParams,\n grant: AdminGrant,\n): Promise<void> {\n await tx.insert(adminCommandResults).values({\n actorId: params.actorId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n command: params.event.command,\n requestFingerprint: params.requestFingerprint,\n result: {grant: toStoredAdminGrant(grant)},\n });\n}\n\nasync function writeAdminAction(tx: Tx, event: AdministrationActionEvent): Promise<void> {\n await writeOutboxEvent<AdministrationActionEventMap>(tx, authOutbox, {\n type: 'administration.action.performed',\n payload: event,\n });\n}\n\nasync function lockAdminCommand(\n tx: Tx,\n params: {actorId: string; idempotencyKeyFingerprint: string},\n) {\n await tx.execute(\n sql`select pg_advisory_xact_lock(hashtext(${`auth_admin_command:${params.actorId}:${params.idempotencyKeyFingerprint}`}))`,\n );\n}\n\nexport async function bootstrapFirstAdminOwner(\n params: AuditedAdminCommandParams & {userId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const activeOwners = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n )\n .limit(1);\n if (activeOwners.length > 0) throw new AdminBootstrapClosedError();\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: 'admin-owner'})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Bootstrap grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function grantAdminRoleWithAudit(\n params: AuditedAdminCommandParams & {userId: string; role: AdminRole},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const activeRows = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n eq(adminGrants.role, params.role),\n isNull(adminGrants.revokedAt),\n ),\n )\n .limit(1);\n if (activeRows.length > 0) throw new AdminGrantAlreadyExistsError();\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Administrator grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function revokeAdminGrantWithAudit(\n params: AuditedAdminCommandParams & {grantId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) throw new AdminGrantNotFoundError();\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const updatedRow = updated[0];\n if (!updatedRow) throw new AdminGrantNotFoundError();\n\n const revoked = toAdminGrant(updatedRow);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, revoked);\n return revoked;\n });\n}\n"],"names":["writeOutboxEvent","and","asc","eq","isNull","sql","highestAdminRole","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","LastAdminOwnerError","UserNotFoundError","db","adminCommandResults","adminGrants","toAdminGrant","authOutbox","users","createAdminGrant","params","rows","insert","values","userId","role","returning","row","Error","listAdminGrants","select","from","orderBy","createdAt","map","findCurrentAdminRole","innerJoin","id","where","revokedAt","status","revokeAdminGrant","transaction","tx","execute","grantId","limit","grant","undefined","activeOwners","length","updated","update","set","Date","updatedAt","toStoredAdminGrant","toISOString","fromStoredAdminGrant","findCommandResult","actorId","idempotencyKeyFingerprint","result","command","requestFingerprint","storeCommandResult","event","writeAdminAction","type","payload","lockAdminCommand","bootstrapFirstAdminOwner","existing","userRows","user","grantAdminRoleWithAudit","activeRows","revokeAdminGrantWithAudit","updatedRow","revoked"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/db/admin-grants.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport type {\n AdministrationActionEvent,\n AdministrationActionEventMap,\n} from '@shipfox/api-common-dto';\nimport {\n paginateTimestampIdRows,\n type TimestampIdCursor,\n timestampIdCursorWhere,\n} from '@shipfox/node-drizzle';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport {and, desc, eq, isNull, sql} from 'drizzle-orm';\nimport {highestAdminRole} from '#core/admin-role-model.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {AdministratorGrantSummary} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {db} from './db.js';\nimport {\n type AdminCommandResultDb,\n adminCommandResults,\n type StoredAdminGrant,\n} from './schema/admin-command-results.js';\nimport {adminGrants, toAdminGrant} from './schema/admin-grants.js';\nimport {authOutbox} from './schema/outbox.js';\nimport {users} from './schema/users.js';\n\ntype Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];\n\nexport interface CreateAdminGrantParams {\n userId: string;\n role: AdminRole;\n}\n\nexport async function createAdminGrant(params: CreateAdminGrantParams): Promise<AdminGrant> {\n const rows = await db()\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toAdminGrant(row);\n}\n\nexport interface AdministratorGrantSummaryRecord {\n id: string;\n role: AdminRole;\n createdAt: Date;\n revokedAt: Date | null;\n user: AdministratorGrantSummary['user'];\n}\n\nexport async function listAdminGrantSummaries(params: {\n limit: number;\n cursor?: TimestampIdCursor;\n}): Promise<{\n rows: AdministratorGrantSummaryRecord[];\n nextCursor: TimestampIdCursor | null;\n}> {\n const cursorCondition = timestampIdCursorWhere({\n timestampColumn: adminGrants.createdAt,\n idColumn: adminGrants.id,\n cursor: params.cursor,\n });\n const conditions = cursorCondition ? [cursorCondition] : [];\n const rows = await db()\n .select({\n id: adminGrants.id,\n role: adminGrants.role,\n createdAt: adminGrants.createdAt,\n revokedAt: adminGrants.revokedAt,\n user: {\n id: users.id,\n email: users.email,\n name: users.name,\n status: users.status,\n },\n })\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(and(...conditions))\n .orderBy(desc(adminGrants.createdAt), desc(adminGrants.id))\n .limit(params.limit + 1);\n\n const page = paginateTimestampIdRows({rows, limit: params.limit, timestampKey: 'createdAt'});\n return {\n rows: page.pageRows,\n nextCursor: page.nextCursor,\n };\n}\n\nexport async function findCurrentAdminRole(params: {userId: string}): Promise<AdminRole | null> {\n const rows = await db()\n .select({role: adminGrants.role})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n\n return highestAdminRole(rows.map(({role}) => role));\n}\n\nexport async function revokeAdminGrant(params: {grantId: string}): Promise<AdminGrant | undefined> {\n return await db().transaction(async (tx) => {\n // All owner grant changes share one lock so concurrent revocations cannot\n // both observe the same final owner and leave the instance ownerless.\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) return undefined;\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const row = updated[0];\n return row ? toAdminGrant(row) : undefined;\n });\n}\n\ninterface AuditedAdminCommandParams {\n actorId: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n event: AdministrationActionEvent;\n}\n\nfunction toStoredAdminGrant(grant: AdminGrant): StoredAdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt?.toISOString() ?? null,\n createdAt: grant.createdAt.toISOString(),\n updatedAt: grant.updatedAt.toISOString(),\n };\n}\n\nfunction fromStoredAdminGrant(grant: StoredAdminGrant): AdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt ? new Date(grant.revokedAt) : null,\n createdAt: new Date(grant.createdAt),\n updatedAt: new Date(grant.updatedAt),\n };\n}\n\nasync function findCommandResult(\n tx: Tx,\n params: Pick<\n AuditedAdminCommandParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n > & {\n command: string;\n },\n): Promise<AdminGrant | undefined> {\n const rows = await tx\n .select()\n .from(adminCommandResults)\n .where(\n and(\n eq(adminCommandResults.actorId, params.actorId),\n eq(adminCommandResults.idempotencyKeyFingerprint, params.idempotencyKeyFingerprint),\n ),\n )\n .limit(1);\n const result: AdminCommandResultDb | undefined = rows[0];\n if (!result) return undefined;\n if (\n result.command !== params.command ||\n result.requestFingerprint !== params.requestFingerprint\n ) {\n throw new AdminIdempotencyKeyReuseError();\n }\n return fromStoredAdminGrant(result.result.grant);\n}\n\nasync function storeCommandResult(\n tx: Tx,\n params: AuditedAdminCommandParams,\n grant: AdminGrant,\n): Promise<void> {\n await tx.insert(adminCommandResults).values({\n actorId: params.actorId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n command: params.event.command,\n requestFingerprint: params.requestFingerprint,\n result: {grant: toStoredAdminGrant(grant)},\n });\n}\n\nasync function writeAdminAction(tx: Tx, event: AdministrationActionEvent): Promise<void> {\n await writeOutboxEvent<AdministrationActionEventMap>(tx, authOutbox, {\n type: 'administration.action.performed',\n payload: event,\n });\n}\n\nasync function lockAdminCommand(\n tx: Tx,\n params: {actorId: string; idempotencyKeyFingerprint: string},\n) {\n await tx.execute(\n sql`select pg_advisory_xact_lock(hashtext(${`auth_admin_command:${params.actorId}:${params.idempotencyKeyFingerprint}`}))`,\n );\n}\n\nexport async function bootstrapFirstAdminOwner(\n params: AuditedAdminCommandParams & {userId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const activeOwners = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n )\n .limit(1);\n if (activeOwners.length > 0) throw new AdminBootstrapClosedError();\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: 'admin-owner'})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Bootstrap grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function grantAdminRoleWithAudit(\n params: AuditedAdminCommandParams & {userId: string; role: AdminRole},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const activeRows = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n eq(adminGrants.role, params.role),\n isNull(adminGrants.revokedAt),\n ),\n )\n .limit(1);\n if (activeRows.length > 0) throw new AdminGrantAlreadyExistsError();\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Administrator grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function revokeAdminGrantWithAudit(\n params: AuditedAdminCommandParams & {grantId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) throw new AdminGrantNotFoundError();\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const updatedRow = updated[0];\n if (!updatedRow) throw new AdminGrantNotFoundError();\n\n const revoked = toAdminGrant(updatedRow);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, revoked);\n return revoked;\n });\n}\n"],"names":["paginateTimestampIdRows","timestampIdCursorWhere","writeOutboxEvent","and","desc","eq","isNull","sql","highestAdminRole","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","LastAdminOwnerError","UserNotFoundError","db","adminCommandResults","adminGrants","toAdminGrant","authOutbox","users","createAdminGrant","params","rows","insert","values","userId","role","returning","row","Error","listAdminGrantSummaries","cursorCondition","timestampColumn","createdAt","idColumn","id","cursor","conditions","select","revokedAt","user","email","name","status","from","innerJoin","where","orderBy","limit","page","timestampKey","pageRows","nextCursor","findCurrentAdminRole","map","revokeAdminGrant","transaction","tx","execute","grantId","grant","undefined","activeOwners","length","updated","update","set","Date","updatedAt","toStoredAdminGrant","toISOString","fromStoredAdminGrant","findCommandResult","actorId","idempotencyKeyFingerprint","result","command","requestFingerprint","storeCommandResult","event","writeAdminAction","type","payload","lockAdminCommand","bootstrapFirstAdminOwner","existing","userRows","grantAdminRoleWithAudit","activeRows","revokeAdminGrantWithAudit","updatedRow","revoked"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { AdminRole } from '@shipfox/api-auth-dto';
2
+ import type { UserStatus } from '#core/entities/user.js';
3
+ export interface AdministratorUserRecord {
4
+ id: string;
5
+ email: string;
6
+ name: string | null;
7
+ emailVerifiedAt: Date | null;
8
+ status: UserStatus;
9
+ createdAt: Date;
10
+ adminRole: AdminRole | null;
11
+ }
12
+ type AdministratorUserLookup = {
13
+ id: string;
14
+ email?: never;
15
+ } | {
16
+ email: string;
17
+ id?: never;
18
+ };
19
+ export declare function findAdministratorUser(params: AdministratorUserLookup): Promise<AdministratorUserRecord | undefined>;
20
+ export {};
21
+ //# sourceMappingURL=admin-users.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-users.d.ts","sourceRoot":"","sources":["../../src/db/admin-users.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAGrD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,wBAAwB,CAAC;AAKvD,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;CAC7B;AAED,KAAK,uBAAuB,GAAG;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;AAEzF,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAAC,CAmC9C"}
@@ -0,0 +1,32 @@
1
+ import { and, eq, isNull } from 'drizzle-orm';
2
+ import { highestAdminRole } from '#core/admin-role-model.js';
3
+ import { db } from './db.js';
4
+ import { adminGrants } from './schema/admin-grants.js';
5
+ import { users } from './schema/users.js';
6
+ export async function findAdministratorUser(params) {
7
+ const identifier = 'id' in params ? eq(users.id, params.id) : eq(users.email, params.email);
8
+ const rows = await db().select({
9
+ id: users.id,
10
+ email: users.email,
11
+ name: users.name,
12
+ emailVerifiedAt: users.emailVerifiedAt,
13
+ status: users.status,
14
+ createdAt: users.createdAt,
15
+ adminRole: adminGrants.role
16
+ }).from(users).leftJoin(adminGrants, and(eq(adminGrants.userId, users.id), isNull(adminGrants.revokedAt), eq(users.status, 'active'))).where(identifier);
17
+ const first = rows[0];
18
+ if (!first) return undefined;
19
+ return {
20
+ id: first.id,
21
+ email: first.email,
22
+ name: first.name,
23
+ emailVerifiedAt: first.emailVerifiedAt,
24
+ status: first.status,
25
+ createdAt: first.createdAt,
26
+ adminRole: highestAdminRole(rows.flatMap(({ adminRole })=>adminRole ? [
27
+ adminRole
28
+ ] : []))
29
+ };
30
+ }
31
+
32
+ //# sourceMappingURL=admin-users.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/db/admin-users.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport {and, eq, isNull} from 'drizzle-orm';\nimport {highestAdminRole} from '#core/admin-role-model.js';\nimport type {UserStatus} from '#core/entities/user.js';\nimport {db} from './db.js';\nimport {adminGrants} from './schema/admin-grants.js';\nimport {users} from './schema/users.js';\n\nexport interface AdministratorUserRecord {\n id: string;\n email: string;\n name: string | null;\n emailVerifiedAt: Date | null;\n status: UserStatus;\n createdAt: Date;\n adminRole: AdminRole | null;\n}\n\ntype AdministratorUserLookup = {id: string; email?: never} | {email: string; id?: never};\n\nexport async function findAdministratorUser(\n params: AdministratorUserLookup,\n): Promise<AdministratorUserRecord | undefined> {\n const identifier = 'id' in params ? eq(users.id, params.id) : eq(users.email, params.email);\n const rows = await db()\n .select({\n id: users.id,\n email: users.email,\n name: users.name,\n emailVerifiedAt: users.emailVerifiedAt,\n status: users.status,\n createdAt: users.createdAt,\n adminRole: adminGrants.role,\n })\n .from(users)\n .leftJoin(\n adminGrants,\n and(\n eq(adminGrants.userId, users.id),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n )\n .where(identifier);\n\n const first = rows[0];\n if (!first) return undefined;\n\n return {\n id: first.id,\n email: first.email,\n name: first.name,\n emailVerifiedAt: first.emailVerifiedAt,\n status: first.status,\n createdAt: first.createdAt,\n adminRole: highestAdminRole(rows.flatMap(({adminRole}) => (adminRole ? [adminRole] : []))),\n };\n}\n"],"names":["and","eq","isNull","highestAdminRole","db","adminGrants","users","findAdministratorUser","params","identifier","id","email","rows","select","name","emailVerifiedAt","status","createdAt","adminRole","role","from","leftJoin","userId","revokedAt","where","first","undefined","flatMap"],"mappings":"AACA,SAAQA,GAAG,EAAEC,EAAE,EAAEC,MAAM,QAAO,cAAc;AAC5C,SAAQC,gBAAgB,QAAO,4BAA4B;AAE3D,SAAQC,EAAE,QAAO,UAAU;AAC3B,SAAQC,WAAW,QAAO,2BAA2B;AACrD,SAAQC,KAAK,QAAO,oBAAoB;AAcxC,OAAO,eAAeC,sBACpBC,MAA+B;IAE/B,MAAMC,aAAa,QAAQD,SAASP,GAAGK,MAAMI,EAAE,EAAEF,OAAOE,EAAE,IAAIT,GAAGK,MAAMK,KAAK,EAAEH,OAAOG,KAAK;IAC1F,MAAMC,OAAO,MAAMR,KAChBS,MAAM,CAAC;QACNH,IAAIJ,MAAMI,EAAE;QACZC,OAAOL,MAAMK,KAAK;QAClBG,MAAMR,MAAMQ,IAAI;QAChBC,iBAAiBT,MAAMS,eAAe;QACtCC,QAAQV,MAAMU,MAAM;QACpBC,WAAWX,MAAMW,SAAS;QAC1BC,WAAWb,YAAYc,IAAI;IAC7B,GACCC,IAAI,CAACd,OACLe,QAAQ,CACPhB,aACAL,IACEC,GAAGI,YAAYiB,MAAM,EAAEhB,MAAMI,EAAE,GAC/BR,OAAOG,YAAYkB,SAAS,GAC5BtB,GAAGK,MAAMU,MAAM,EAAE,YAGpBQ,KAAK,CAACf;IAET,MAAMgB,QAAQb,IAAI,CAAC,EAAE;IACrB,IAAI,CAACa,OAAO,OAAOC;IAEnB,OAAO;QACLhB,IAAIe,MAAMf,EAAE;QACZC,OAAOc,MAAMd,KAAK;QAClBG,MAAMW,MAAMX,IAAI;QAChBC,iBAAiBU,MAAMV,eAAe;QACtCC,QAAQS,MAAMT,MAAM;QACpBC,WAAWQ,MAAMR,SAAS;QAC1BC,WAAWf,iBAAiBS,KAAKe,OAAO,CAAC,CAAC,EAACT,SAAS,EAAC,GAAMA,YAAY;gBAACA;aAAU,GAAG,EAAE;IACzF;AACF"}
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { ShipfoxModule } from '@shipfox/node-module';
2
2
  import type { SignupPolicy } from '#core/ports.js';
3
3
  export type { AdminRole, JobLeaseTokenClaims, RunnerSessionTokenClaims } from '@shipfox/api-auth-dto';
4
4
  export { ADMIN_ROLES, getCurrentAdminRole, hasMinimumAdminRole, highestAdminRole, requireAdminRole, revokeAdminGrant, } from '#core/admin-role.js';
5
- export { bootstrapFirstAdminOwner, grantAdministratorRole, listAdministratorGrants, revokeAdministratorGrant, } from '#core/administration.js';
5
+ export { bootstrapFirstAdminOwner, grantAdministratorRole, revokeAdministratorGrant, } from '#core/administration.js';
6
6
  export type { CreateSessionForUserError, CreateSessionForUserParams, CreateSessionForUserResult, ProvisionUserParams, } from '#core/auth.js';
7
7
  export { createSessionForUser, provisionUser } from '#core/auth.js';
8
8
  export type { EmailOwner, FindUserByEmailParams } from '#core/email-owner.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,sBAAsB,CAAC;AAGxD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAiBjD,YAAY,EAAC,SAAS,EAAE,mBAAmB,EAAE,wBAAwB,EAAC,MAAM,uBAAuB,CAAC;AACpG,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,0BAA0B,EAC1B,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAC,oBAAoB,EAAE,aAAa,EAAC,MAAM,eAAe,CAAC;AAClE,YAAY,EAAC,UAAU,EAAE,qBAAqB,EAAC,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAC,eAAe,EAAC,MAAM,sBAAsB,CAAC;AACrD,YAAY,EAAC,UAAU,EAAC,MAAM,+BAA+B,CAAC;AAC9D,YAAY,EAAC,IAAI,EAAE,UAAU,EAAC,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EACL,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC7B,sBAAsB,EACtB,8BAA8B,EAC9B,qBAAqB,EACrB,+BAA+B,EAC/B,uBAAuB,EACvB,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AACjD,OAAO,EACL,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,6BAA6B,EAC7B,kCAAkC,GACnC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,2BAA2B,EAChC,mBAAmB,EACnB,8BAA8B,EAC9B,KAAK,gBAAgB,EACrB,KAAK,MAAM,GACZ,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAC,0BAA0B,EAAC,MAAM,wCAAwC,CAAC;AAClF,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAC,6BAA6B,EAAC,MAAM,2CAA2C,CAAC;AAIxF,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,OAAO,0CAA0C,EAAE,2BAA2B,CAAC;IAC3F,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,YAA8C,GAC/C,EAAE,uBAAuB,GAAG,aAAa,CAezC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,sBAAsB,CAAC;AAGxD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAoBjD,YAAY,EAAC,SAAS,EAAE,mBAAmB,EAAE,wBAAwB,EAAC,MAAM,uBAAuB,CAAC;AACpG,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,0BAA0B,EAC1B,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAC,oBAAoB,EAAE,aAAa,EAAC,MAAM,eAAe,CAAC;AAClE,YAAY,EAAC,UAAU,EAAE,qBAAqB,EAAC,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAC,eAAe,EAAC,MAAM,sBAAsB,CAAC;AACrD,YAAY,EAAC,UAAU,EAAC,MAAM,+BAA+B,CAAC;AAC9D,YAAY,EAAC,IAAI,EAAE,UAAU,EAAC,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EACL,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC7B,sBAAsB,EACtB,8BAA8B,EAC9B,qBAAqB,EACrB,+BAA+B,EAC/B,uBAAuB,EACvB,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AACjD,OAAO,EACL,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,6BAA6B,EAC7B,kCAAkC,GACnC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,2BAA2B,EAChC,mBAAmB,EACnB,8BAA8B,EAC9B,KAAK,gBAAgB,EACrB,KAAK,MAAM,GACZ,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAC,0BAA0B,EAAC,MAAM,wCAAwC,CAAC;AAClF,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAC,6BAA6B,EAAC,MAAM,2CAA2C,CAAC;AAIxF,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,OAAO,0CAA0C,EAAE,2BAA2B,CAAC;IAC3F,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,YAA8C,GAC/C,EAAE,uBAAuB,GAAG,aAAa,CAgBzC"}
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { createLeaseTokenAuthMethod } from '#presentation/auth/lease-token-auth.
11
11
  import { createRunnerSessionAuthMethod } from '#presentation/auth/runner-session-auth.js';
12
12
  import { createAuthE2eRoutes } from '#presentation/e2eRoutes/index.js';
13
13
  import { createAuthInterModulePresentation } from '#presentation/inter-module.js';
14
- import { administrationRoutes } from '#presentation/routes/administration.js';
14
+ import { administrationRoutes, administrationUserRoutes } from '#presentation/routes/administration.js';
15
15
  import { buildAuthRoutes } from '#presentation/routes/index.js';
16
16
  import { onPasswordResetSendRequested } from '#presentation/subscribers/index.js';
17
17
  import { passwordLoginMethods } from './login-methods.js';
@@ -20,7 +20,7 @@ const authPublisherEventSchemas = {
20
20
  ...administrationActionEventSchemas
21
21
  };
22
22
  export { ADMIN_ROLES, getCurrentAdminRole, hasMinimumAdminRole, highestAdminRole, requireAdminRole, revokeAdminGrant } from '#core/admin-role.js';
23
- export { bootstrapFirstAdminOwner, grantAdministratorRole, listAdministratorGrants, revokeAdministratorGrant } from '#core/administration.js';
23
+ export { bootstrapFirstAdminOwner, grantAdministratorRole, revokeAdministratorGrant } from '#core/administration.js';
24
24
  export { createSessionForUser, provisionUser } from '#core/auth.js';
25
25
  export { findUserByEmail } from '#core/email-owner.js';
26
26
  export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, AuthDependencyUnavailableError, EmailNotVerifiedError, InvalidAdminBootstrapTokenError, InvalidCredentialsError, LastAdminOwnerError, SignupNotAllowedError, UserNotFoundError } from '#core/errors.js';
@@ -48,7 +48,8 @@ export function createAuthModule({ workspaces, signupPolicy = createEnvironmentS
48
48
  loginMethods: passwordLoginMethods(config.AUTH_PASSWORD_ENABLED),
49
49
  routes: [
50
50
  buildAuthRoutes(config.AUTH_PASSWORD_ENABLED, workspaces, signupPolicy),
51
- administrationRoutes
51
+ administrationRoutes,
52
+ administrationUserRoutes
52
53
  ],
53
54
  e2eRoutes: [
54
55
  createAuthE2eRoutes(workspaces)