@shipfox/api-auth 15.0.0 → 17.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 (101) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +41 -0
  4. package/dist/config.d.ts +1 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +4 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/administration.d.ts +24 -2
  9. package/dist/core/administration.d.ts.map +1 -1
  10. package/dist/core/administration.js +126 -3
  11. package/dist/core/administration.js.map +1 -1
  12. package/dist/core/auth.d.ts +31 -0
  13. package/dist/core/auth.d.ts.map +1 -1
  14. package/dist/core/auth.js +70 -2
  15. package/dist/core/auth.js.map +1 -1
  16. package/dist/core/errors.d.ts +15 -0
  17. package/dist/core/errors.d.ts.map +1 -1
  18. package/dist/core/errors.js +30 -0
  19. package/dist/core/errors.js.map +1 -1
  20. package/dist/core/jwt.d.ts +2 -0
  21. package/dist/core/jwt.d.ts.map +1 -1
  22. package/dist/core/jwt.js +22 -1
  23. package/dist/core/jwt.js.map +1 -1
  24. package/dist/db/admin-command.d.ts +12 -4
  25. package/dist/db/admin-command.d.ts.map +1 -1
  26. package/dist/db/admin-command.js +10 -1
  27. package/dist/db/admin-command.js.map +1 -1
  28. package/dist/db/admin-grants.d.ts.map +1 -1
  29. package/dist/db/admin-grants.js +4 -1
  30. package/dist/db/admin-grants.js.map +1 -1
  31. package/dist/db/admin-user-moderation.d.ts +3 -0
  32. package/dist/db/admin-user-moderation.d.ts.map +1 -1
  33. package/dist/db/admin-user-moderation.js +6 -2
  34. package/dist/db/admin-user-moderation.js.map +1 -1
  35. package/dist/db/impersonation.d.ts +55 -0
  36. package/dist/db/impersonation.d.ts.map +1 -0
  37. package/dist/db/impersonation.js +212 -0
  38. package/dist/db/impersonation.js.map +1 -0
  39. package/dist/db/schema/admin-command-results.d.ts +16 -0
  40. package/dist/db/schema/admin-command-results.d.ts.map +1 -1
  41. package/dist/db/schema/admin-command-results.js.map +1 -1
  42. package/dist/index.d.ts +5 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +5 -5
  45. package/dist/index.js.map +1 -1
  46. package/dist/metrics/index.d.ts +1 -1
  47. package/dist/metrics/index.d.ts.map +1 -1
  48. package/dist/metrics/index.js +1 -1
  49. package/dist/metrics/index.js.map +1 -1
  50. package/dist/metrics/instance.d.ts +4 -2
  51. package/dist/metrics/instance.d.ts.map +1 -1
  52. package/dist/metrics/instance.js +8 -0
  53. package/dist/metrics/instance.js.map +1 -1
  54. package/dist/presentation/auth/bearer-token-auth.d.ts +2 -2
  55. package/dist/presentation/auth/bearer-token-auth.d.ts.map +1 -1
  56. package/dist/presentation/auth/bearer-token-auth.js +2 -2
  57. package/dist/presentation/auth/bearer-token-auth.js.map +1 -1
  58. package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
  59. package/dist/presentation/auth/jwt-auth.js +26 -2
  60. package/dist/presentation/auth/jwt-auth.js.map +1 -1
  61. package/dist/presentation/routes/administration.d.ts +2 -1
  62. package/dist/presentation/routes/administration.d.ts.map +1 -1
  63. package/dist/presentation/routes/administration.js +127 -19
  64. package/dist/presentation/routes/administration.js.map +1 -1
  65. package/dist/presentation/routes/rate-limit.d.ts +8 -0
  66. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  67. package/dist/presentation/routes/rate-limit.js +44 -0
  68. package/dist/presentation/routes/rate-limit.js.map +1 -1
  69. package/dist/presentation/routes/session/me.d.ts.map +1 -1
  70. package/dist/presentation/routes/session/me.js +2 -1
  71. package/dist/presentation/routes/session/me.js.map +1 -1
  72. package/dist/tsconfig.test.tsbuildinfo +1 -1
  73. package/package.json +5 -5
  74. package/src/config.ts +4 -0
  75. package/src/core/administration.ts +172 -1
  76. package/src/core/auth.test.ts +79 -0
  77. package/src/core/auth.ts +98 -1
  78. package/src/core/errors.ts +35 -0
  79. package/src/core/jwt.test.ts +92 -0
  80. package/src/core/jwt.ts +38 -9
  81. package/src/db/admin-command.ts +35 -6
  82. package/src/db/admin-grants.ts +5 -1
  83. package/src/db/admin-user-moderation.ts +8 -2
  84. package/src/db/impersonation.ts +323 -0
  85. package/src/db/schema/admin-command-results.ts +18 -0
  86. package/src/index.test.ts +1 -1
  87. package/src/index.ts +12 -3
  88. package/src/metrics/index.ts +2 -0
  89. package/src/metrics/instance.ts +13 -2
  90. package/src/presentation/auth/bearer-token-auth.ts +4 -4
  91. package/src/presentation/auth/jwt-auth.test.ts +191 -0
  92. package/src/presentation/auth/jwt-auth.ts +19 -1
  93. package/src/presentation/routes/administration.test.ts +828 -3
  94. package/src/presentation/routes/administration.ts +130 -11
  95. package/src/presentation/routes/rate-limit.ts +40 -0
  96. package/src/presentation/routes/session/login.test.ts +5 -0
  97. package/src/presentation/routes/session/me.test.ts +24 -0
  98. package/src/presentation/routes/session/me.ts +1 -0
  99. package/src/presentation/routes/session/refresh.test.ts +11 -0
  100. package/test/routes.ts +43 -16
  101. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 67 files with swc (406.61ms)
2
+ Successfully compiled: 68 files with swc (429.99ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @shipfox/api-auth
2
2
 
3
+ ## 17.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a4f56ff: Adds the `requireAdministrationActor` guard to `@shipfox/api-auth-context`. The guard rejects impersonated sessions (`UserContext` carrying `impersonatorId`) with the `admin-role-required` failure on every `/admin` route in the auth, projects, workspaces, and runners modules. It protects the first-owner bootstrap route before the system checks roles.
8
+ - 918d84a: Adds the impersonation mint command and admin route: `POST /admin/auth/users/:user_id/impersonate` mints a short-lived, marked, audited impersonated session for an active, verified, non-administrator user, gated behind the opt-in `AUTH_IMPERSONATION_ENABLED` flag (defaults to `false`). The response DTO `impersonateResponseSchema` carries `token`, `expires_at`, `server_time`, `impersonator_id`, and the target user.
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies [a4f56ff]
13
+ - Updated dependencies [5ae8b3d]
14
+ - Updated dependencies [a591e8a]
15
+ - Updated dependencies [918d84a]
16
+ - Updated dependencies [9f898d9]
17
+ - @shipfox/api-auth-context@17.0.0
18
+ - @shipfox/api-auth-dto@17.0.0
19
+ - @shipfox/node-postgres@0.5.1
20
+ - @shipfox/api-email-challenges@1.1.11
21
+ - @shipfox/node-outbox@0.2.6
22
+
3
23
  ## 15.0.0
4
24
 
5
25
  ### Patch Changes
package/README.md CHANGED
@@ -57,6 +57,7 @@ Required environment:
57
57
  | --- | --- | --- |
58
58
  | `AUTH_ROOT_KEY` | none | Canonical base64 for 32 random bytes used to derive separate authentication and email-challenge keys. Generate it with `openssl rand -base64 32`. |
59
59
  | `AUTH_JWT_EXPIRES_IN` | `15m` | Access token lifetime. |
60
+ | `AUTH_IMPERSONATION_ENABLED` | `false` | Enables the administrator impersonation mint command. Defaults to `false` because the source-available client ships no impersonation banner; enable it only where every signed-in surface renders one. |
60
61
  | `AUTH_JOB_LEASE_TOKEN_EXPIRES_IN` | `90m` | Job lease token lifetime. |
61
62
  | `AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN` | `1h` | Runner session token lifetime. |
62
63
  | `AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS` | `14` | Refresh token and cookie lifetime. |
@@ -143,6 +144,41 @@ itself.
143
144
  issuance or refresh. Accepted because the recommended lifetime is 15 minutes,
144
145
  so the staleness window is bounded and a per-request membership read is avoided.
145
146
 
147
+ ### Impersonated session token
148
+
149
+ An impersonated session is a signed user access token and nothing else. It is
150
+ minted by `POST /admin/auth/users/:userId/impersonate` (see
151
+ [Administrator user moderation](#administrator-user-moderation)) and carries the
152
+ administrator's user ID in the signed `impersonatorId` claim, so the mark cannot
153
+ be removed or forged by a client.
154
+
155
+ - **Design:** the target user's ordinary access token (same `sub` and real
156
+ membership claims as login) plus the `impersonatorId` claim. The token carries
157
+ **no `refreshSessionId`**, and the command creates no refresh session and no
158
+ cookie, so nothing persisted can resurrect the session after the window.
159
+ - **Lifetime:** TTL is `min(AUTH_JWT_EXPIRES_IN, 15 minutes)`. The mint,
160
+ replay, and renewal responses carry `expires_at` and a `server_time` anchor
161
+ from the issuer; the client never decodes the JWT.
162
+ - **Invariants:** the session cannot be refreshed (the refresh cookie still
163
+ belongs to the administrator and can only restore their identity); it expires
164
+ at most 15 minutes after issuance; and every `/admin` route rejects a request
165
+ whose context carries `impersonatorId` with `admin-role-required`, before
166
+ roles are consulted. The durable-artefact deny-list (runner registration
167
+ tokens, provisioner tokens, and workspace invitations) rejects impersonated
168
+ sessions with `impersonation-not-permitted`; the list is enumerated and
169
+ documented in ADR 0014, and adding a credential-issuing or grant-creating
170
+ route means adding it there.
171
+ - **No token at rest:** the idempotent command result stores only the target
172
+ user ID, the expiry, and a list of SHA-256 fingerprints of the tokens issued
173
+ under the key. The raw token is never logged or persisted, and a token
174
+ recovered from a log or proxy capture can be hashed and matched back to the
175
+ command that minted it.
176
+ - **Irrevocability window:** like every user access token, an issued
177
+ impersonated token is not revocable before `exp`. Suspending the target,
178
+ revoking the administrator's grant, or disabling `AUTH_IMPERSONATION_ENABLED`
179
+ blocks the next mint, replay, or renewal but does not kill an already-issued
180
+ token; the ≤15-minute window bounds the residual risk.
181
+
146
182
  ### Runner session token
147
183
 
148
184
  A runner session token is the data-plane identity for a running manual runner.
@@ -285,6 +321,7 @@ The public auth endpoints include an application-layer abuse baseline for open s
285
321
  | `POST /auth/verify-email/resend` | Shared with password reset | Shared with password reset |
286
322
  | `POST /admin/auth/admin-grants/bootstrap` | 5 attempts per 15 minutes | n/a |
287
323
  | `GET /admin/auth/users` | 60 attempts per 5 minutes | n/a |
324
+ | `POST /admin/auth/users/:userId/impersonate` | 20 attempts per 15 minutes | n/a |
288
325
 
289
326
  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.
290
327
 
@@ -307,6 +344,9 @@ All routes are mounted under `/admin/auth/users` and require an authenticated be
307
344
  | `POST` | `/:userId/suspend` | `admin-operator` | Suspends the user, revokes all refresh sessions, and returns the final safe user summary with a correlation ID. Issued access tokens remain valid until `exp`. |
308
345
  | `POST` | `/:userId/reactivate` | `admin-operator` | Restores sign-in eligibility without restoring revoked sessions. |
309
346
  | `POST` | `/:userId/revoke-sessions` | `admin-operator` | Revokes all refresh sessions without changing account status and returns the number of affected sessions. Issued access tokens remain valid until `exp`. |
347
+ | `POST` | `/:userId/impersonate` | `admin-operator` | Mints a short-lived, marked, audited impersonated session for an active, verified, non-administrator user. See below. |
348
+
349
+ `POST /:userId/impersonate` requires an `Idempotency-Key` and a bounded reason, and is rate limited under the `impersonate` bucket by source IP and by actor. It returns `{token, expires_at, server_time, impersonator_id, user}` and sets no cookie. The target must be active with a verified email and hold no active administrator grant; the actor must not be the target. The mint, a same-key in-window replay, and a renewal each re-run the full authorization and eligibility ladder and publish their own `administration.action.performed` event. A replay re-signs for the same target with the original `expires_at` and never extends the window; after the window passes, the key is consumed for good: every further replay returns `410 impersonation-expired`, and a fresh `Idempotency-Key` is required for a new mint. The stored command result holds only SHA-256 fingerprints of the issued tokens. Failures are audited from their own committed transaction when the actor holds an identifiable administrator role (the strict event schema carries the actor role); role-less denials (a revoked grant, a suspended actor, or a non-admin probing the route) fail closed without an event, and client-contract errors (`404 not-found` for an unknown target, `409 idempotency-key-reused`) are reported to the caller and never enter the failure stream.
310
350
 
311
351
  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`.
312
352
 
@@ -336,6 +376,7 @@ It also exports lower-level pieces for tests and advanced integration:
336
376
  - `issueJobLeaseToken(claims)` / `verifyJobLeaseToken(token)`: mint and verify job lease tokens.
337
377
  - `createEnvironmentSignupPolicy()`: creates the environment-backed signup policy used by default. Pass `signupPolicy` to `createAuthModule` to replace it. A denied custom policy can opt into sanitized Markdown with `format: 'markdown'`; without a format, its message stays plain text.
338
378
  - `getClientContext(request)`: reads the authenticated user context from a Fastify request.
379
+ - `createImpersonatedSessionToken({targetUserId, impersonatorId, workspaces})`: mints an access-token-only impersonated session (capped TTL, `impersonatorId` claim, no refresh material). The `impersonateUser` administration command owns the authorization ladder, idempotency, and audit flow.
339
380
  - `getAuthenticatedSessionContext(request)`: reads the user ID and required refresh-session ID from verified access-token claims. It does not check whether the refresh session is still active.
340
381
  - `findUserByEmail({email})`: read-only lookup of the current owner of a normalized email; see below.
341
382
  - Entity types: `User`, `UserStatus`, `RefreshToken`, `PasswordReset`, and `EmailOwner`.
package/dist/config.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export declare const config: Readonly<{
2
2
  ADMIN_BOOTSTRAP_TOKEN: string | undefined;
3
3
  AUTH_JWT_EXPIRES_IN: string;
4
+ AUTH_IMPERSONATION_ENABLED: boolean;
4
5
  AUTH_JOB_LEASE_TOKEN_EXPIRES_IN: string;
5
6
  AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN: string;
6
7
  AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: number;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;kDAqDjB,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;kDAyDjB,CAAC"}
package/dist/config.js CHANGED
@@ -9,6 +9,10 @@ export const config = createConfig({
9
9
  desc: 'How long an access token stays valid. Accepts a duration string such as 15m, 1h, or 7d.',
10
10
  default: '15m'
11
11
  }),
12
+ AUTH_IMPERSONATION_ENABLED: bool({
13
+ desc: 'Whether administrators can mint impersonated sessions for target users. Defaults to false: the source-available client ships no impersonation banner, so enable it only where every signed-in surface renders one.',
14
+ default: false
15
+ }),
12
16
  AUTH_JOB_LEASE_TOKEN_EXPIRES_IN: str({
13
17
  desc: 'How long a job lease token stays valid. Set it longer than the longest job (JOB_MAX_DURATION is 60 minutes) plus a safety margin.',
14
18
  default: '90m'
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {bool, createConfig, num, str} from '@shipfox/config';\nimport {SIGNUP_DENIAL_MESSAGE_MAX_LENGTH} from '#core/ports.js';\n\nexport const config = createConfig({\n ADMIN_BOOTSTRAP_TOKEN: str({\n desc: 'Deployment secret accepted once to create the first administrator owner. Set it before bootstrap and remove or rotate it after successful bootstrap.',\n default: undefined,\n }),\n AUTH_JWT_EXPIRES_IN: str({\n desc: 'How long an access token stays valid. Accepts a duration string such as 15m, 1h, or 7d.',\n default: '15m',\n }),\n AUTH_JOB_LEASE_TOKEN_EXPIRES_IN: str({\n desc: 'How long a job lease token stays valid. Set it longer than the longest job (JOB_MAX_DURATION is 60 minutes) plus a safety margin.',\n default: '90m',\n }),\n AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN: str({\n desc: 'How long a runner session token stays valid. A revoked registration token can leave existing sessions usable until this lifetime ends.',\n default: '1h',\n }),\n AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: num({\n desc: 'How many days a refresh token stays valid before the user must sign in again.',\n default: 14,\n }),\n AUTH_REFRESH_ROTATION_GRACE_SECONDS: num({\n desc: 'Window after a refresh token is rotated during which the now-rotated token is still accepted, so concurrent refreshes from parallel tabs do not log the user out. Reuse past this window is treated as a compromise and revokes the session.',\n default: 30,\n }),\n AUTH_REFRESH_COOKIE_NAME: str({\n desc: 'Name of the browser cookie that stores the refresh token.',\n default: 'shipfox_refresh_token',\n }),\n AUTH_PASSWORD_ENABLED: bool({\n desc: 'Whether password login is available. Use true or false. Defaults to true. When false, password and email-verification routes are not registered, and server startup requires another module to contribute a login method.',\n default: true,\n }),\n AUTH_SIGNUP_GATE_ENABLED: bool({\n desc: 'Whether new account creation is restricted to the configured signup email allowlist. Defaults to false, which allows every signup.',\n default: false,\n }),\n AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: str({\n desc: 'Comma-separated email domains allowed to create accounts, such as shipfox.io,acme.com. Required when AUTH_SIGNUP_GATE_ENABLED is true unless AUTH_SIGNUP_ALLOWED_EMAILS is set.',\n default: '',\n }),\n AUTH_SIGNUP_ALLOWED_EMAILS: str({\n desc: 'Comma-separated exact email addresses allowed to create accounts. Required when AUTH_SIGNUP_GATE_ENABLED is true unless AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS is set.',\n default: '',\n }),\n AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: str({\n desc: 'Markdown message returned when the signup gate blocks an account. Use at most 500 characters. Optional. The default is This Shipfox deployment does not accept new accounts right now.',\n default: undefined,\n }),\n CLIENT_BASE_URL: str({\n desc: 'Base URL of the client app. Used to build links in emails such as password resets.',\n default: 'http://localhost:5173',\n }),\n});\n\nif (\n config.AUTH_SIGNUP_GATE_ENABLED &&\n !hasSignupAllowlistEntry(config.AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS) &&\n !hasSignupAllowlistEntry(config.AUTH_SIGNUP_ALLOWED_EMAILS)\n) {\n throw new Error(\n 'AUTH_SIGNUP_GATE_ENABLED requires AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS or AUTH_SIGNUP_ALLOWED_EMAILS to be set.',\n );\n}\n\nif (\n config.AUTH_SIGNUP_NOT_ALLOWED_MESSAGE !== undefined &&\n config.AUTH_SIGNUP_NOT_ALLOWED_MESSAGE.length > SIGNUP_DENIAL_MESSAGE_MAX_LENGTH\n) {\n throw new Error(\n `AUTH_SIGNUP_NOT_ALLOWED_MESSAGE must contain at most ${SIGNUP_DENIAL_MESSAGE_MAX_LENGTH} characters.`,\n );\n}\n\nfunction hasSignupAllowlistEntry(value: string): boolean {\n return value.split(',').some((entry) => entry.trim().length > 0);\n}\n"],"names":["bool","createConfig","num","str","SIGNUP_DENIAL_MESSAGE_MAX_LENGTH","config","ADMIN_BOOTSTRAP_TOKEN","desc","default","undefined","AUTH_JWT_EXPIRES_IN","AUTH_JOB_LEASE_TOKEN_EXPIRES_IN","AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN","AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS","AUTH_REFRESH_ROTATION_GRACE_SECONDS","AUTH_REFRESH_COOKIE_NAME","AUTH_PASSWORD_ENABLED","AUTH_SIGNUP_GATE_ENABLED","AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS","AUTH_SIGNUP_ALLOWED_EMAILS","AUTH_SIGNUP_NOT_ALLOWED_MESSAGE","CLIENT_BASE_URL","hasSignupAllowlistEntry","Error","length","value","split","some","entry","trim"],"mappings":"AAAA,SAAQA,IAAI,EAAEC,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAC7D,SAAQC,gCAAgC,QAAO,iBAAiB;AAEhE,OAAO,MAAMC,SAASJ,aAAa;IACjCK,uBAAuBH,IAAI;QACzBI,MAAM;QACNC,SAASC;IACX;IACAC,qBAAqBP,IAAI;QACvBI,MAAM;QACNC,SAAS;IACX;IACAG,iCAAiCR,IAAI;QACnCI,MAAM;QACNC,SAAS;IACX;IACAI,sCAAsCT,IAAI;QACxCI,MAAM;QACNC,SAAS;IACX;IACAK,oCAAoCX,IAAI;QACtCK,MAAM;QACNC,SAAS;IACX;IACAM,qCAAqCZ,IAAI;QACvCK,MAAM;QACNC,SAAS;IACX;IACAO,0BAA0BZ,IAAI;QAC5BI,MAAM;QACNC,SAAS;IACX;IACAQ,uBAAuBhB,KAAK;QAC1BO,MAAM;QACNC,SAAS;IACX;IACAS,0BAA0BjB,KAAK;QAC7BO,MAAM;QACNC,SAAS;IACX;IACAU,mCAAmCf,IAAI;QACrCI,MAAM;QACNC,SAAS;IACX;IACAW,4BAA4BhB,IAAI;QAC9BI,MAAM;QACNC,SAAS;IACX;IACAY,iCAAiCjB,IAAI;QACnCI,MAAM;QACNC,SAASC;IACX;IACAY,iBAAiBlB,IAAI;QACnBI,MAAM;QACNC,SAAS;IACX;AACF,GAAG;AAEH,IACEH,OAAOY,wBAAwB,IAC/B,CAACK,wBAAwBjB,OAAOa,iCAAiC,KACjE,CAACI,wBAAwBjB,OAAOc,0BAA0B,GAC1D;IACA,MAAM,IAAII,MACR;AAEJ;AAEA,IACElB,OAAOe,+BAA+B,KAAKX,aAC3CJ,OAAOe,+BAA+B,CAACI,MAAM,GAAGpB,kCAChD;IACA,MAAM,IAAImB,MACR,CAAC,qDAAqD,EAAEnB,iCAAiC,YAAY,CAAC;AAE1G;AAEA,SAASkB,wBAAwBG,KAAa;IAC5C,OAAOA,MAAMC,KAAK,CAAC,KAAKC,IAAI,CAAC,CAACC,QAAUA,MAAMC,IAAI,GAAGL,MAAM,GAAG;AAChE"}
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {bool, createConfig, num, str} from '@shipfox/config';\nimport {SIGNUP_DENIAL_MESSAGE_MAX_LENGTH} from '#core/ports.js';\n\nexport const config = createConfig({\n ADMIN_BOOTSTRAP_TOKEN: str({\n desc: 'Deployment secret accepted once to create the first administrator owner. Set it before bootstrap and remove or rotate it after successful bootstrap.',\n default: undefined,\n }),\n AUTH_JWT_EXPIRES_IN: str({\n desc: 'How long an access token stays valid. Accepts a duration string such as 15m, 1h, or 7d.',\n default: '15m',\n }),\n AUTH_IMPERSONATION_ENABLED: bool({\n desc: 'Whether administrators can mint impersonated sessions for target users. Defaults to false: the source-available client ships no impersonation banner, so enable it only where every signed-in surface renders one.',\n default: false,\n }),\n AUTH_JOB_LEASE_TOKEN_EXPIRES_IN: str({\n desc: 'How long a job lease token stays valid. Set it longer than the longest job (JOB_MAX_DURATION is 60 minutes) plus a safety margin.',\n default: '90m',\n }),\n AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN: str({\n desc: 'How long a runner session token stays valid. A revoked registration token can leave existing sessions usable until this lifetime ends.',\n default: '1h',\n }),\n AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS: num({\n desc: 'How many days a refresh token stays valid before the user must sign in again.',\n default: 14,\n }),\n AUTH_REFRESH_ROTATION_GRACE_SECONDS: num({\n desc: 'Window after a refresh token is rotated during which the now-rotated token is still accepted, so concurrent refreshes from parallel tabs do not log the user out. Reuse past this window is treated as a compromise and revokes the session.',\n default: 30,\n }),\n AUTH_REFRESH_COOKIE_NAME: str({\n desc: 'Name of the browser cookie that stores the refresh token.',\n default: 'shipfox_refresh_token',\n }),\n AUTH_PASSWORD_ENABLED: bool({\n desc: 'Whether password login is available. Use true or false. Defaults to true. When false, password and email-verification routes are not registered, and server startup requires another module to contribute a login method.',\n default: true,\n }),\n AUTH_SIGNUP_GATE_ENABLED: bool({\n desc: 'Whether new account creation is restricted to the configured signup email allowlist. Defaults to false, which allows every signup.',\n default: false,\n }),\n AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: str({\n desc: 'Comma-separated email domains allowed to create accounts, such as shipfox.io,acme.com. Required when AUTH_SIGNUP_GATE_ENABLED is true unless AUTH_SIGNUP_ALLOWED_EMAILS is set.',\n default: '',\n }),\n AUTH_SIGNUP_ALLOWED_EMAILS: str({\n desc: 'Comma-separated exact email addresses allowed to create accounts. Required when AUTH_SIGNUP_GATE_ENABLED is true unless AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS is set.',\n default: '',\n }),\n AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: str({\n desc: 'Markdown message returned when the signup gate blocks an account. Use at most 500 characters. Optional. The default is This Shipfox deployment does not accept new accounts right now.',\n default: undefined,\n }),\n CLIENT_BASE_URL: str({\n desc: 'Base URL of the client app. Used to build links in emails such as password resets.',\n default: 'http://localhost:5173',\n }),\n});\n\nif (\n config.AUTH_SIGNUP_GATE_ENABLED &&\n !hasSignupAllowlistEntry(config.AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS) &&\n !hasSignupAllowlistEntry(config.AUTH_SIGNUP_ALLOWED_EMAILS)\n) {\n throw new Error(\n 'AUTH_SIGNUP_GATE_ENABLED requires AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS or AUTH_SIGNUP_ALLOWED_EMAILS to be set.',\n );\n}\n\nif (\n config.AUTH_SIGNUP_NOT_ALLOWED_MESSAGE !== undefined &&\n config.AUTH_SIGNUP_NOT_ALLOWED_MESSAGE.length > SIGNUP_DENIAL_MESSAGE_MAX_LENGTH\n) {\n throw new Error(\n `AUTH_SIGNUP_NOT_ALLOWED_MESSAGE must contain at most ${SIGNUP_DENIAL_MESSAGE_MAX_LENGTH} characters.`,\n );\n}\n\nfunction hasSignupAllowlistEntry(value: string): boolean {\n return value.split(',').some((entry) => entry.trim().length > 0);\n}\n"],"names":["bool","createConfig","num","str","SIGNUP_DENIAL_MESSAGE_MAX_LENGTH","config","ADMIN_BOOTSTRAP_TOKEN","desc","default","undefined","AUTH_JWT_EXPIRES_IN","AUTH_IMPERSONATION_ENABLED","AUTH_JOB_LEASE_TOKEN_EXPIRES_IN","AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN","AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS","AUTH_REFRESH_ROTATION_GRACE_SECONDS","AUTH_REFRESH_COOKIE_NAME","AUTH_PASSWORD_ENABLED","AUTH_SIGNUP_GATE_ENABLED","AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS","AUTH_SIGNUP_ALLOWED_EMAILS","AUTH_SIGNUP_NOT_ALLOWED_MESSAGE","CLIENT_BASE_URL","hasSignupAllowlistEntry","Error","length","value","split","some","entry","trim"],"mappings":"AAAA,SAAQA,IAAI,EAAEC,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAC7D,SAAQC,gCAAgC,QAAO,iBAAiB;AAEhE,OAAO,MAAMC,SAASJ,aAAa;IACjCK,uBAAuBH,IAAI;QACzBI,MAAM;QACNC,SAASC;IACX;IACAC,qBAAqBP,IAAI;QACvBI,MAAM;QACNC,SAAS;IACX;IACAG,4BAA4BX,KAAK;QAC/BO,MAAM;QACNC,SAAS;IACX;IACAI,iCAAiCT,IAAI;QACnCI,MAAM;QACNC,SAAS;IACX;IACAK,sCAAsCV,IAAI;QACxCI,MAAM;QACNC,SAAS;IACX;IACAM,oCAAoCZ,IAAI;QACtCK,MAAM;QACNC,SAAS;IACX;IACAO,qCAAqCb,IAAI;QACvCK,MAAM;QACNC,SAAS;IACX;IACAQ,0BAA0Bb,IAAI;QAC5BI,MAAM;QACNC,SAAS;IACX;IACAS,uBAAuBjB,KAAK;QAC1BO,MAAM;QACNC,SAAS;IACX;IACAU,0BAA0BlB,KAAK;QAC7BO,MAAM;QACNC,SAAS;IACX;IACAW,mCAAmChB,IAAI;QACrCI,MAAM;QACNC,SAAS;IACX;IACAY,4BAA4BjB,IAAI;QAC9BI,MAAM;QACNC,SAAS;IACX;IACAa,iCAAiClB,IAAI;QACnCI,MAAM;QACNC,SAASC;IACX;IACAa,iBAAiBnB,IAAI;QACnBI,MAAM;QACNC,SAAS;IACX;AACF,GAAG;AAEH,IACEH,OAAOa,wBAAwB,IAC/B,CAACK,wBAAwBlB,OAAOc,iCAAiC,KACjE,CAACI,wBAAwBlB,OAAOe,0BAA0B,GAC1D;IACA,MAAM,IAAII,MACR;AAEJ;AAEA,IACEnB,OAAOgB,+BAA+B,KAAKZ,aAC3CJ,OAAOgB,+BAA+B,CAACI,MAAM,GAAGrB,kCAChD;IACA,MAAM,IAAIoB,MACR,CAAC,qDAAqD,EAAEpB,iCAAiC,YAAY,CAAC;AAE1G;AAEA,SAASmB,wBAAwBG,KAAa;IAC5C,OAAOA,MAAMC,KAAK,CAAC,KAAKC,IAAI,CAAC,CAACC,QAAUA,MAAMC,IAAI,GAAGL,MAAM,GAAG;AAChE"}
@@ -1,8 +1,10 @@
1
1
  import type { AdminRole } from '@shipfox/api-auth-dto';
2
+ import type { WorkspacesInterModuleClient } from '@shipfox/api-workspaces-dto/inter-module';
2
3
  import type { TimestampIdCursor } from '@shipfox/node-drizzle';
4
+ import { type ImpersonationResult } from '#db/impersonation.js';
3
5
  import type { AdminGrant } from './entities/admin-grant.js';
4
6
  import type { AdministratorGrantSummary, AdministratorUserSummary } from './entities/administrator-read-model.js';
5
- import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from './errors.js';
7
+ import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, CannotImpersonateAdministratorError, CannotImpersonateSelfError, ImpersonationDisabledError, ImpersonationExpiredError, ImpersonationTargetNotActiveError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from './errors.js';
6
8
  export declare function administrationCommandFingerprint(command: string, input: unknown): string;
7
9
  export interface AdministrationMutationContext {
8
10
  actorId: string;
@@ -56,5 +58,25 @@ export declare function revokeAdministratorUserSessions(params: AdministrationMu
56
58
  userId: string;
57
59
  reason?: string;
58
60
  }): Promise<AdministratorUserMutationResult>;
59
- export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError, };
61
+ export interface ImpersonateUserParams extends AdministrationMutationContext {
62
+ targetUserId: string;
63
+ reason: string;
64
+ /**
65
+ * The actor's own session mark, from the verified request context. The
66
+ * positional `/admin` guard rejects an impersonated actor before this
67
+ * command runs; the command refuses the same mark defensively.
68
+ */
69
+ actorImpersonatorId?: string | undefined;
70
+ workspaces: WorkspacesInterModuleClient;
71
+ }
72
+ /**
73
+ * Mints a short-lived, marked, audited impersonated session for a target
74
+ * user. Enforces the authorization and eligibility ladder (rules 1-6), the
75
+ * fingerprint-only idempotency flow (replay re-runs the ladder and re-signs
76
+ * with the original expiry), and the audit event contract: success and replay
77
+ * events commit atomically with the command result, and failure events commit
78
+ * in their own transaction after the rollback.
79
+ */
80
+ export declare function impersonateUser(params: ImpersonateUserParams): Promise<ImpersonationResult>;
81
+ export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, CannotImpersonateAdministratorError, CannotImpersonateSelfError, ImpersonationDisabledError, ImpersonationExpiredError, ImpersonationTargetNotActiveError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError, };
60
82
  //# sourceMappingURL=administration.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"administration.d.ts","sourceRoot":"","sources":["../../src/core/administration.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"administration.d.ts","sourceRoot":"","sources":["../../src/core/administration.ts"],"names":[],"mappings":"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"}
@@ -5,8 +5,10 @@ import { config } from '#config.js';
5
5
  import { bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb, grantAdminRoleWithAudit, hasActiveAdminOwner, listAdminGrantSummaries, revokeAdminGrantWithAudit } from '#db/admin-grants.js';
6
6
  import { reactivateUserWithAudit, revokeUserSessionsWithAudit, suspendUserWithAudit } from '#db/admin-user-moderation.js';
7
7
  import { findAdministratorUser as findAdministratorUserInDb } from '#db/admin-users.js';
8
- import { requireAdminRole } from './admin-role.js';
9
- import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from './errors.js';
8
+ import { impersonateUserWithAudit, impersonationSucceededEventExists, publishImpersonationFailure } from '#db/impersonation.js';
9
+ import { recordImpersonationOutcome } from '#metrics/index.js';
10
+ import { getCurrentAdminRole, requireAdminRole } from './admin-role.js';
11
+ import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, AdminRoleRequiredError, CannotImpersonateAdministratorError, CannotImpersonateSelfError, EmailNotVerifiedError, ImpersonationDisabledError, ImpersonationExpiredError, ImpersonationTargetNotActiveError, InvalidAdminBootstrapTokenError, InvalidCredentialsError, LastAdminOwnerError, UserNotFoundError } from './errors.js';
10
12
  const ADMIN_OWNER_ROLE = 'admin-owner';
11
13
  const ADMIN_OBSERVER_ROLE = 'admin-observer';
12
14
  const ADMIN_OPERATOR_ROLE = 'admin-operator';
@@ -16,6 +18,22 @@ const REVOKE_COMMAND = 'auth.admin_grant.revoke';
16
18
  const SUSPEND_USER_COMMAND = 'auth.user.suspend';
17
19
  const REACTIVATE_USER_COMMAND = 'auth.user.reactivate';
18
20
  const REVOKE_USER_SESSIONS_COMMAND = 'auth.user.revoke-sessions';
21
+ const IMPERSONATE_COMMAND = 'auth.user.impersonate';
22
+ /**
23
+ * Client-contract errors are reported to the caller, not the denial stream: a
24
+ * 404 for an unknown target or a 409 for a reused key is a request mistake,
25
+ * and auditing each retry would drown the `failed` event stream under durable
26
+ * rows that a security review cannot distinguish from genuine denials.
27
+ */ function isImpersonationClientContractError(error) {
28
+ return error instanceof AdminIdempotencyKeyReuseError || error instanceof UserNotFoundError;
29
+ }
30
+ /**
31
+ * Deterministic authorization and eligibility denials. They are raised before
32
+ * the command transaction writes anything, so the transaction is known to have
33
+ * rolled back and the failure event is an unambiguous audit of the denial.
34
+ */ function isImpersonationDenial(error) {
35
+ return error instanceof ImpersonationDisabledError || error instanceof AdminRoleRequiredError || error instanceof CannotImpersonateSelfError || error instanceof CannotImpersonateAdministratorError || error instanceof ImpersonationTargetNotActiveError || error instanceof ImpersonationExpiredError || error instanceof EmailNotVerifiedError || error instanceof InvalidCredentialsError;
36
+ }
19
37
  export function administrationCommandFingerprint(command, input) {
20
38
  return hashOpaqueToken(`${command}:${JSON.stringify(input)}`);
21
39
  }
@@ -243,6 +261,111 @@ export async function revokeAdministratorUserSessions(params) {
243
261
  })
244
262
  }));
245
263
  }
246
- export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError };
264
+ /**
265
+ * Mints a short-lived, marked, audited impersonated session for a target
266
+ * user. Enforces the authorization and eligibility ladder (rules 1-6), the
267
+ * fingerprint-only idempotency flow (replay re-runs the ladder and re-signs
268
+ * with the original expiry), and the audit event contract: success and replay
269
+ * events commit atomically with the command result, and failure events commit
270
+ * in their own transaction after the rollback.
271
+ */ export async function impersonateUser(params) {
272
+ const idempotencyKeyFingerprint = hashOpaqueToken(params.idempotencyKey);
273
+ const requestFingerprint = administrationCommandFingerprint(IMPERSONATE_COMMAND, {
274
+ targetUserId: params.targetUserId,
275
+ reason: params.reason
276
+ });
277
+ try {
278
+ // Rule 1: the capability is an explicit opt-in; the flag is a kill switch
279
+ // and must hold on the replay path as well as the initial mint.
280
+ if (!config.AUTH_IMPERSONATION_ENABLED) throw new ImpersonationDisabledError();
281
+ // Rule 3: an impersonated actor cannot impersonate (nested impersonation).
282
+ if (params.actorImpersonatorId !== undefined) {
283
+ throw new AdminRoleRequiredError(ADMIN_OPERATOR_ROLE);
284
+ }
285
+ // Rule 2: minimum admin-operator, the same bar as suspension and session
286
+ // revocation. Re-checked inside the transaction on every path, replay
287
+ // included, so a revocation mid-window ends the capability immediately.
288
+ await requireAdminRole({
289
+ userId: params.actorId,
290
+ minimumRole: ADMIN_OPERATOR_ROLE
291
+ });
292
+ const result = await impersonateUserWithAudit({
293
+ actorId: params.actorId,
294
+ targetUserId: params.targetUserId,
295
+ reason: params.reason,
296
+ idempotencyKeyFingerprint,
297
+ requestFingerprint,
298
+ correlationId: params.correlationId,
299
+ workspaces: params.workspaces
300
+ });
301
+ recordImpersonationOutcome('succeeded');
302
+ return result;
303
+ } catch (error) {
304
+ // The command did not hand out a token, so the attempt is a failed outcome
305
+ // regardless of why: mint-volume and denial-spike alerts key off this.
306
+ recordImpersonationOutcome('failed');
307
+ // Client-contract errors (unknown target, reused key) are reported to the
308
+ // caller and never enter the denial stream.
309
+ if (isImpersonationClientContractError(error)) throw error;
310
+ // Deterministic denials always audit: their transaction rolled back with
311
+ // nothing written, so the `failed` event is unambiguous even when a
312
+ // previous mint under the same key left a committed result row.
313
+ if (isImpersonationDenial(error)) {
314
+ await publishImpersonationFailureForActor(params, {
315
+ idempotencyKeyFingerprint,
316
+ correlationId: params.correlationId
317
+ });
318
+ throw error;
319
+ }
320
+ // An unexpected error may be an ambiguous COMMIT: the mint transaction
321
+ // committed (result row and `succeeded` event are durable) but the driver
322
+ // raised on the acknowledgement. Publishing a `failed` event then would
323
+ // contradict the committed trail, so reconcile against the committed
324
+ // `succeeded` event for THIS invocation before writing anything: the event
325
+ // is written atomically with the result row, and its correlationId is
326
+ // unique per request, so a result row committed by an earlier mint or
327
+ // replay under the same key is never mistaken for this invocation's
328
+ // commit. The reconcile is best-effort: never mask the original error.
329
+ let committed = false;
330
+ try {
331
+ committed = await impersonationSucceededEventExists({
332
+ actorId: params.actorId,
333
+ idempotencyKeyFingerprint,
334
+ correlationId: params.correlationId
335
+ });
336
+ } catch {
337
+ // Fall through: publish the failure event rather than losing the denial.
338
+ }
339
+ if (!committed) {
340
+ await publishImpersonationFailureForActor(params, {
341
+ idempotencyKeyFingerprint,
342
+ correlationId: params.correlationId
343
+ });
344
+ }
345
+ throw error;
346
+ }
347
+ }
348
+ async function publishImpersonationFailureForActor(params, audit) {
349
+ // Failures publish from a separate committed transaction after the rollback:
350
+ // the main transaction is gone, so an event written inside it would
351
+ // disappear, and the role check runs before it even opens.
352
+ let actorRole = null;
353
+ try {
354
+ actorRole = await getCurrentAdminRole({
355
+ userId: params.actorId
356
+ });
357
+ } catch {
358
+ // The failure event is best-effort; never mask the original error.
359
+ }
360
+ await publishImpersonationFailure({
361
+ actorId: params.actorId,
362
+ targetUserId: params.targetUserId,
363
+ reason: params.reason,
364
+ actorRole,
365
+ idempotencyKeyFingerprint: audit.idempotencyKeyFingerprint,
366
+ correlationId: audit.correlationId
367
+ });
368
+ }
369
+ export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, CannotImpersonateAdministratorError, CannotImpersonateSelfError, ImpersonationDisabledError, ImpersonationExpiredError, ImpersonationTargetNotActiveError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError };
247
370
 
248
371
  //# sourceMappingURL=administration.js.map
@@ -1 +1 @@
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
+ {"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 {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';\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 {\n type ImpersonationResult,\n impersonateUserWithAudit,\n impersonationSucceededEventExists,\n publishImpersonationFailure,\n} from '#db/impersonation.js';\nimport {recordImpersonationOutcome} from '#metrics/index.js';\nimport {getCurrentAdminRole, 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 AdminRoleRequiredError,\n CannotImpersonateAdministratorError,\n CannotImpersonateSelfError,\n EmailNotVerifiedError,\n ImpersonationDisabledError,\n ImpersonationExpiredError,\n ImpersonationTargetNotActiveError,\n InvalidAdminBootstrapTokenError,\n InvalidCredentialsError,\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';\nconst IMPERSONATE_COMMAND = 'auth.user.impersonate';\n\n/**\n * Client-contract errors are reported to the caller, not the denial stream: a\n * 404 for an unknown target or a 409 for a reused key is a request mistake,\n * and auditing each retry would drown the `failed` event stream under durable\n * rows that a security review cannot distinguish from genuine denials.\n */\nfunction isImpersonationClientContractError(error: unknown): boolean {\n return error instanceof AdminIdempotencyKeyReuseError || error instanceof UserNotFoundError;\n}\n\n/**\n * Deterministic authorization and eligibility denials. They are raised before\n * the command transaction writes anything, so the transaction is known to have\n * rolled back and the failure event is an unambiguous audit of the denial.\n */\nfunction isImpersonationDenial(error: unknown): boolean {\n return (\n error instanceof ImpersonationDisabledError ||\n error instanceof AdminRoleRequiredError ||\n error instanceof CannotImpersonateSelfError ||\n error instanceof CannotImpersonateAdministratorError ||\n error instanceof ImpersonationTargetNotActiveError ||\n error instanceof ImpersonationExpiredError ||\n error instanceof EmailNotVerifiedError ||\n error instanceof InvalidCredentialsError\n );\n}\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 interface ImpersonateUserParams extends AdministrationMutationContext {\n targetUserId: string;\n reason: string;\n /**\n * The actor's own session mark, from the verified request context. The\n * positional `/admin` guard rejects an impersonated actor before this\n * command runs; the command refuses the same mark defensively.\n */\n actorImpersonatorId?: string | undefined;\n workspaces: WorkspacesInterModuleClient;\n}\n\n/**\n * Mints a short-lived, marked, audited impersonated session for a target\n * user. Enforces the authorization and eligibility ladder (rules 1-6), the\n * fingerprint-only idempotency flow (replay re-runs the ladder and re-signs\n * with the original expiry), and the audit event contract: success and replay\n * events commit atomically with the command result, and failure events commit\n * in their own transaction after the rollback.\n */\nexport async function impersonateUser(params: ImpersonateUserParams): Promise<ImpersonationResult> {\n const idempotencyKeyFingerprint = hashOpaqueToken(params.idempotencyKey);\n const requestFingerprint = administrationCommandFingerprint(IMPERSONATE_COMMAND, {\n targetUserId: params.targetUserId,\n reason: params.reason,\n });\n try {\n // Rule 1: the capability is an explicit opt-in; the flag is a kill switch\n // and must hold on the replay path as well as the initial mint.\n if (!config.AUTH_IMPERSONATION_ENABLED) throw new ImpersonationDisabledError();\n // Rule 3: an impersonated actor cannot impersonate (nested impersonation).\n if (params.actorImpersonatorId !== undefined) {\n throw new AdminRoleRequiredError(ADMIN_OPERATOR_ROLE);\n }\n // Rule 2: minimum admin-operator, the same bar as suspension and session\n // revocation. Re-checked inside the transaction on every path, replay\n // included, so a revocation mid-window ends the capability immediately.\n await requireAdminRole({\n userId: params.actorId,\n minimumRole: ADMIN_OPERATOR_ROLE,\n });\n const result = await impersonateUserWithAudit({\n actorId: params.actorId,\n targetUserId: params.targetUserId,\n reason: params.reason,\n idempotencyKeyFingerprint,\n requestFingerprint,\n correlationId: params.correlationId,\n workspaces: params.workspaces,\n });\n recordImpersonationOutcome('succeeded');\n return result;\n } catch (error) {\n // The command did not hand out a token, so the attempt is a failed outcome\n // regardless of why: mint-volume and denial-spike alerts key off this.\n recordImpersonationOutcome('failed');\n // Client-contract errors (unknown target, reused key) are reported to the\n // caller and never enter the denial stream.\n if (isImpersonationClientContractError(error)) throw error;\n // Deterministic denials always audit: their transaction rolled back with\n // nothing written, so the `failed` event is unambiguous even when a\n // previous mint under the same key left a committed result row.\n if (isImpersonationDenial(error)) {\n await publishImpersonationFailureForActor(params, {\n idempotencyKeyFingerprint,\n correlationId: params.correlationId,\n });\n throw error;\n }\n // An unexpected error may be an ambiguous COMMIT: the mint transaction\n // committed (result row and `succeeded` event are durable) but the driver\n // raised on the acknowledgement. Publishing a `failed` event then would\n // contradict the committed trail, so reconcile against the committed\n // `succeeded` event for THIS invocation before writing anything: the event\n // is written atomically with the result row, and its correlationId is\n // unique per request, so a result row committed by an earlier mint or\n // replay under the same key is never mistaken for this invocation's\n // commit. The reconcile is best-effort: never mask the original error.\n let committed = false;\n try {\n committed = await impersonationSucceededEventExists({\n actorId: params.actorId,\n idempotencyKeyFingerprint,\n correlationId: params.correlationId,\n });\n } catch {\n // Fall through: publish the failure event rather than losing the denial.\n }\n if (!committed) {\n await publishImpersonationFailureForActor(params, {\n idempotencyKeyFingerprint,\n correlationId: params.correlationId,\n });\n }\n throw error;\n }\n}\n\nasync function publishImpersonationFailureForActor(\n params: ImpersonateUserParams,\n audit: {idempotencyKeyFingerprint: string; correlationId: string},\n): Promise<void> {\n // Failures publish from a separate committed transaction after the rollback:\n // the main transaction is gone, so an event written inside it would\n // disappear, and the role check runs before it even opens.\n let actorRole: AdminRole | null = null;\n try {\n actorRole = await getCurrentAdminRole({userId: params.actorId});\n } catch {\n // The failure event is best-effort; never mask the original error.\n }\n await publishImpersonationFailure({\n actorId: params.actorId,\n targetUserId: params.targetUserId,\n reason: params.reason,\n actorRole,\n idempotencyKeyFingerprint: audit.idempotencyKeyFingerprint,\n correlationId: audit.correlationId,\n });\n}\n\nexport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n CannotImpersonateAdministratorError,\n CannotImpersonateSelfError,\n ImpersonationDisabledError,\n ImpersonationExpiredError,\n ImpersonationTargetNotActiveError,\n InvalidAdminBootstrapTokenError,\n LastAdminOwnerError,\n UserNotFoundError,\n};\n"],"names":["timingSafeEqual","createAdministrationActionEvent","hashOpaqueToken","config","bootstrapFirstAdminOwner","bootstrapFirstAdminOwnerInDb","grantAdminRoleWithAudit","hasActiveAdminOwner","listAdminGrantSummaries","revokeAdminGrantWithAudit","reactivateUserWithAudit","revokeUserSessionsWithAudit","suspendUserWithAudit","findAdministratorUser","findAdministratorUserInDb","impersonateUserWithAudit","impersonationSucceededEventExists","publishImpersonationFailure","recordImpersonationOutcome","getCurrentAdminRole","requireAdminRole","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","AdminRoleRequiredError","CannotImpersonateAdministratorError","CannotImpersonateSelfError","EmailNotVerifiedError","ImpersonationDisabledError","ImpersonationExpiredError","ImpersonationTargetNotActiveError","InvalidAdminBootstrapTokenError","InvalidCredentialsError","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","IMPERSONATE_COMMAND","isImpersonationClientContractError","error","isImpersonationDenial","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","impersonateUser","targetUserId","AUTH_IMPERSONATION_ENABLED","actorImpersonatorId","workspaces","publishImpersonationFailureForActor","committed","audit"],"mappings":"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"}
@@ -73,6 +73,37 @@ export interface CreateSessionForUserResult {
73
73
  }
74
74
  export type CreateSessionForUserError = AuthDependencyUnavailableError | EmailNotVerifiedError | InvalidCredentialsError | UserNotFoundError;
75
75
  export declare function createSessionForUser(params: CreateSessionForUserParams): Promise<CreateSessionForUserResult>;
76
+ /**
77
+ * Cap for impersonated session tokens: the TTL is min(`AUTH_JWT_EXPIRES_IN`,
78
+ * this), so an impersonated window can never outlive 15 minutes no matter how
79
+ * the deployment configures ordinary access tokens.
80
+ */
81
+ export declare const IMPERSONATION_MAX_TTL_SECONDS: number;
82
+ export interface CreateImpersonatedSessionTokenParams {
83
+ targetUserId: string;
84
+ /** The administrator the token is minted for; signed into the `impersonatorId` claim. */
85
+ impersonatorId: string;
86
+ workspaces: WorkspacesInterModuleClient;
87
+ /**
88
+ * Re-sign lifetime override used by an idempotent replay: the remaining
89
+ * lifetime to the stored `expires_at`, so a replay never extends the window.
90
+ * Defaults to min(`AUTH_JWT_EXPIRES_IN`, 15 minutes).
91
+ */
92
+ expiresIn?: string | undefined;
93
+ }
94
+ export interface CreateImpersonatedSessionTokenResult {
95
+ token: string;
96
+ expiresAt: Date;
97
+ user: User;
98
+ }
99
+ /**
100
+ * Mints an access-token-only impersonated session for a target user: the same
101
+ * eligibility as login (active account, verified email), the target's real
102
+ * membership claims, a capped TTL, the `impersonatorId` claim, and **no**
103
+ * `refreshSessionId`. It creates no refresh session and sets no cookie, so
104
+ * nothing persisted can resurrect the session after the token window.
105
+ */
106
+ export declare function createImpersonatedSessionToken(params: CreateImpersonatedSessionTokenParams): Promise<CreateImpersonatedSessionTokenResult>;
76
107
  export interface RefreshAccessTokenResult {
77
108
  token: string;
78
109
  /** Undefined on a grace-window hit: keep the existing cookie instead of rotating it. */
@@ -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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"}