@shipfox/api-auth 9.1.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +28 -3
  4. package/dist/config.d.ts +4 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +22 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/admin-role-model.d.ts +5 -0
  9. package/dist/core/admin-role-model.d.ts.map +1 -0
  10. package/dist/core/admin-role-model.js +18 -0
  11. package/dist/core/admin-role-model.js.map +1 -0
  12. package/dist/core/admin-role.d.ts +13 -0
  13. package/dist/core/admin-role.d.ts.map +1 -0
  14. package/dist/core/admin-role.js +21 -0
  15. package/dist/core/admin-role.js.map +1 -0
  16. package/dist/core/auth.d.ts +9 -0
  17. package/dist/core/auth.d.ts.map +1 -1
  18. package/dist/core/auth.js +71 -20
  19. package/dist/core/auth.js.map +1 -1
  20. package/dist/core/entities/admin-grant.d.ts +10 -0
  21. package/dist/core/entities/admin-grant.d.ts.map +1 -0
  22. package/dist/core/entities/admin-grant.js +3 -0
  23. package/dist/core/entities/admin-grant.js.map +1 -0
  24. package/dist/core/errors.d.ts +10 -0
  25. package/dist/core/errors.d.ts.map +1 -1
  26. package/dist/core/errors.js +19 -0
  27. package/dist/core/errors.js.map +1 -1
  28. package/dist/core/ports.d.ts +13 -0
  29. package/dist/core/ports.d.ts.map +1 -0
  30. package/dist/core/ports.js +3 -0
  31. package/dist/core/ports.js.map +1 -0
  32. package/dist/core/signup-policy.d.ts +10 -0
  33. package/dist/core/signup-policy.d.ts.map +1 -0
  34. package/dist/core/signup-policy.js +39 -0
  35. package/dist/core/signup-policy.js.map +1 -0
  36. package/dist/db/admin-grants.d.ts +15 -0
  37. package/dist/db/admin-grants.d.ts.map +1 -0
  38. package/dist/db/admin-grants.js +49 -0
  39. package/dist/db/admin-grants.js.map +1 -0
  40. package/dist/db/db.d.ts +218 -0
  41. package/dist/db/db.d.ts.map +1 -1
  42. package/dist/db/db.js +2 -0
  43. package/dist/db/db.js.map +1 -1
  44. package/dist/db/schema/admin-grants.d.ts +115 -0
  45. package/dist/db/schema/admin-grants.d.ts.map +1 -0
  46. package/dist/db/schema/admin-grants.js +42 -0
  47. package/dist/db/schema/admin-grants.js.map +1 -0
  48. package/dist/index.d.ts +11 -4
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +6 -3
  51. package/dist/index.js.map +1 -1
  52. package/dist/presentation/dto/user.d.ts +2 -1
  53. package/dist/presentation/dto/user.d.ts.map +1 -1
  54. package/dist/presentation/dto/user.js +2 -1
  55. package/dist/presentation/dto/user.js.map +1 -1
  56. package/dist/presentation/inter-module.d.ts.map +1 -1
  57. package/dist/presentation/inter-module.js +26 -2
  58. package/dist/presentation/inter-module.js.map +1 -1
  59. package/dist/presentation/routes/index.d.ts +2 -1
  60. package/dist/presentation/routes/index.d.ts.map +1 -1
  61. package/dist/presentation/routes/index.js +2 -2
  62. package/dist/presentation/routes/index.js.map +1 -1
  63. package/dist/presentation/routes/registration/signup.d.ts +2 -1
  64. package/dist/presentation/routes/registration/signup.d.ts.map +1 -1
  65. package/dist/presentation/routes/registration/signup.js +14 -3
  66. package/dist/presentation/routes/registration/signup.js.map +1 -1
  67. package/dist/presentation/routes/session/me.d.ts.map +1 -1
  68. package/dist/presentation/routes/session/me.js +5 -1
  69. package/dist/presentation/routes/session/me.js.map +1 -1
  70. package/dist/tsconfig.test.tsbuildinfo +1 -1
  71. package/drizzle/0001_brainy_shriek.sql +14 -0
  72. package/drizzle/meta/0001_snapshot.json +702 -0
  73. package/drizzle/meta/_journal.json +7 -0
  74. package/package.json +5 -5
  75. package/src/config.test.ts +39 -0
  76. package/src/config.ts +30 -0
  77. package/src/core/admin-role-model.ts +25 -0
  78. package/src/core/admin-role.test.ts +39 -0
  79. package/src/core/admin-role.ts +25 -0
  80. package/src/core/auth.test.ts +110 -0
  81. package/src/core/auth.ts +78 -21
  82. package/src/core/entities/admin-grant.ts +10 -0
  83. package/src/core/errors.ts +24 -0
  84. package/src/core/jwt.test.ts +1 -0
  85. package/src/core/ports.ts +7 -0
  86. package/src/core/signup-policy.test.ts +78 -0
  87. package/src/core/signup-policy.ts +42 -0
  88. package/src/db/admin-grants.test.ts +57 -0
  89. package/src/db/admin-grants.ts +83 -0
  90. package/src/db/db.ts +2 -0
  91. package/src/db/schema/admin-grants.ts +49 -0
  92. package/src/index.test.ts +53 -0
  93. package/src/index.ts +28 -5
  94. package/src/presentation/dto/user.ts +7 -2
  95. package/src/presentation/inter-module.test.ts +47 -0
  96. package/src/presentation/inter-module.ts +22 -1
  97. package/src/presentation/routes/index.ts +3 -1
  98. package/src/presentation/routes/registration/signup.test.ts +26 -0
  99. package/src/presentation/routes/registration/signup.ts +22 -2
  100. package/src/presentation/routes/session/login.test.ts +1 -0
  101. package/src/presentation/routes/session/me.test.ts +18 -2
  102. package/src/presentation/routes/session/me.ts +5 -1
  103. package/test/globalSetup.ts +1 -1
  104. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 52 files with swc (491.07ms)
2
+ Successfully compiled: 59 files with swc (416.7ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @shipfox/api-auth
2
2
 
3
+ ## 9.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bd7c46b: Thread an optional signup policy through API Auth module composition in preparation for signup enforcement.
8
+ - 456c884: Add Auth-owned local administrator grants and server-side role evaluation.
9
+ - 36adf25: Guards password and external-identity account creation with the signup policy.
10
+ - f31bee6: Adds the environment-variable signup policy and its Auth-prefixed configuration.
11
+ - 63c0e5b: Adds the signup policy port and denial error to API Auth.
12
+
13
+ ### Patch Changes
14
+
15
+ - b321700: Default the Auth module to its environment-backed signup policy when no custom policy is provided.
16
+ - ad36e26: Return a bounded signup-not-allowed message from the password signup route.
17
+ - Updated dependencies [456c884]
18
+ - @shipfox/api-auth-dto@9.2.0
19
+ - @shipfox/api-email-challenges@1.1.4
20
+ - @shipfox/api-workspaces-dto@9.2.0
21
+ - @shipfox/api-auth-context@9.2.0
22
+
3
23
  ## 9.1.0
4
24
 
5
25
  ### Patch Changes
package/README.md CHANGED
@@ -63,10 +63,21 @@ Required environment:
63
63
  | `AUTH_REFRESH_ROTATION_GRACE_SECONDS` | `30` | Grace window for accepting a just-rotated refresh token during concurrent refreshes. |
64
64
  | `AUTH_REFRESH_COOKIE_NAME` | `shipfox_refresh_token` | HTTP cookie name for refresh sessions. |
65
65
  | `AUTH_PASSWORD_ENABLED` | `true` | Enables password and email-verification routes. Set to `false` when another module provides login. Server construction fails if no login method is available. |
66
+ | `AUTH_SIGNUP_GATE_ENABLED` | `false` | Restricts new account creation to the configured signup email allowlist. Requires `AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS` or `AUTH_SIGNUP_ALLOWED_EMAILS` to be set. |
67
+ | `AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS` | none | Comma-separated email domains allowed to create accounts, such as `shipfox.io,acme.com`. |
68
+ | `AUTH_SIGNUP_ALLOWED_EMAILS` | none | Comma-separated exact email addresses allowed to create accounts. |
69
+ | `AUTH_SIGNUP_NOT_ALLOWED_MESSAGE` | none | Description returned when the signup gate blocks an account. Defaults to "This Shipfox deployment does not accept new accounts right now." |
66
70
  | `CLIENT_BASE_URL` | `http://localhost:5173` | Base URL used in email verification and password reset links. |
67
71
 
68
72
  Email delivery uses the shared `@shipfox/node-mailer` configuration.
69
73
 
74
+ The signup gate is off by default. When it is on, provide at least one non-empty
75
+ entry in `AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS` or `AUTH_SIGNUP_ALLOWED_EMAILS`.
76
+ Startup fails when both lists are empty. Values are trimmed and lowercased.
77
+ The policy matches exact email addresses or exact domains. It does not match
78
+ subdomains or wildcards. `createAuthModule` applies these settings by default;
79
+ pass a `signupPolicy` to replace the environment-backed policy.
80
+
70
81
  When `AUTH_PASSWORD_ENABLED=false`, the module does not register signup, login, password-reset, password-change, or email-verification routes. Refresh, logout, and current-session routes stay available. Another module must contribute a login method before the API server starts.
71
82
 
72
83
  ## Security model
@@ -202,7 +213,7 @@ All routes are mounted under `/auth`.
202
213
 
203
214
  | Method | Path | Auth | Result |
204
215
  | --- | --- | --- | --- |
205
- | `POST` | `/signup` | none | Creates a user and sends an eight-digit email verification code. |
216
+ | `POST` | `/signup` | none | Creates a user and sends an eight-digit email verification code. A denied new signup returns `403` with error code `signup-not-allowed` and the policy message. |
206
217
  | `POST` | `/verify-email/confirm` | none | Confirms a verification code, returns an access token, and sets the refresh cookie. |
207
218
  | `POST` | `/verify-email/resend` | none | Sends a new verification code when the account is eligible. |
208
219
  | `POST` | `/login` | none | Returns an access token and sets the refresh cookie. |
@@ -244,7 +255,9 @@ application composition:
244
255
  ```ts
245
256
  import {createAuthModule} from '@shipfox/api-auth';
246
257
 
247
- const authModule = createAuthModule({workspaces});
258
+ const authModule = createAuthModule({
259
+ workspaces,
260
+ });
248
261
  ```
249
262
 
250
263
  It also exports lower-level pieces for tests and advanced integration:
@@ -256,6 +269,7 @@ It also exports lower-level pieces for tests and advanced integration:
256
269
  - `createLeaseTokenAuthMethod()`: the Fastify auth method for job lease tokens.
257
270
  - `issueRunnerSessionToken(claims)` / `verifyRunnerSessionToken(token)`: mint and verify runner session tokens.
258
271
  - `issueJobLeaseToken(claims)` / `verifyJobLeaseToken(token)`: mint and verify job lease tokens.
272
+ - `createEnvironmentSignupPolicy()`: creates the environment-backed signup policy used by default. Pass `signupPolicy` to `createAuthModule` to replace it.
259
273
  - `getClientContext(request)`: reads the authenticated user context from a Fastify request.
260
274
  - `getAuthenticatedSessionContext(request)`: resolves an authenticated request to its user ID and active refresh-session ID.
261
275
  - `findUserByEmail({email})`: read-only lookup of the current owner of a normalized email; see below.
@@ -270,18 +284,20 @@ create a normal Shipfox session and set its refresh cookie:
270
284
  import {
271
285
  authCookiePlugin,
272
286
  createSessionForUser,
287
+ createEnvironmentSignupPolicy,
273
288
  provisionUser,
274
289
  setRefreshTokenCookie,
275
290
  } from '@shipfox/api-auth';
276
291
  import type {FastifyReply} from 'fastify';
277
292
 
278
293
  export const callbackRoutePlugins = [authCookiePlugin];
294
+ const signupPolicy = createEnvironmentSignupPolicy();
279
295
 
280
296
  export async function completeProviderCallback(
281
297
  reply: FastifyReply,
282
298
  profile: {email: string; name?: string},
283
299
  ): Promise<string> {
284
- const user = await provisionUser(profile);
300
+ const user = await provisionUser({...profile, signupPolicy});
285
301
  const session = await createSessionForUser({userId: user.id});
286
302
 
287
303
  setRefreshTokenCookie(reply, session.refreshToken);
@@ -295,6 +311,12 @@ For OAuth providers, require a verified-email claim such as `email_verified`.
295
311
  session for any active, verified account. An unverified provider email could
296
312
  otherwise sign in as an existing password account with the same address.
297
313
 
314
+ Pass the same `signupPolicy` to `provisionUser` when signup gating is enabled.
315
+ The function checks for an existing user before it calls the policy, so existing
316
+ users keep signing in even when their address is no longer allowed. For a new
317
+ user, a denied policy throws `SignupNotAllowedError`. Call
318
+ `createSessionForUser` only after provisioning succeeds.
319
+
298
320
  Add `authCookiePlugin` to the callback route group before calling a cookie
299
321
  helper. `getRefreshTokenCookie`, `setRefreshTokenCookie`, and
300
322
  `clearRefreshTokenCookie` use the configured cookie name and the `/auth` path.
@@ -373,6 +395,9 @@ Passwords use Argon2id. Password reset tokens and refresh tokens are opaque toke
373
395
  ## Behavior Notes
374
396
 
375
397
  - Signup sends an eight-digit verification code and returns the new user with its challenge ID.
398
+ - Signup gating blocks account creation only. Existing users keep signing in, even when their address is not on the current allowlist.
399
+ - A denied password signup returns `403` with error code `signup-not-allowed`. The policy message is capped at 500 characters.
400
+ - Invitation signup validates the invitation and bypasses the signup policy. Invitations remain a separate authorization path.
376
401
  - Login only succeeds for active users with verified email addresses and a password hash.
377
402
  - Refresh tokens rotate on each refresh.
378
403
  - Password reset tokens and email challenge proofs are consumed once.
package/dist/config.d.ts CHANGED
@@ -6,6 +6,10 @@ export declare const config: Readonly<{
6
6
  AUTH_REFRESH_ROTATION_GRACE_SECONDS: number;
7
7
  AUTH_REFRESH_COOKIE_NAME: string;
8
8
  AUTH_PASSWORD_ENABLED: boolean;
9
+ AUTH_SIGNUP_GATE_ENABLED: boolean;
10
+ AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: string;
11
+ AUTH_SIGNUP_ALLOWED_EMAILS: string;
12
+ AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: string | undefined;
9
13
  CLIENT_BASE_URL: string;
10
14
  } & import("@shipfox/config").CleanedEnvAccessors>;
11
15
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;;;;kDAiCjB,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;;;;;;;;kDAiDjB,CAAC"}
package/dist/config.js CHANGED
@@ -28,10 +28,32 @@ export const config = createConfig({
28
28
  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.',
29
29
  default: true
30
30
  }),
31
+ AUTH_SIGNUP_GATE_ENABLED: bool({
32
+ desc: 'Whether new account creation is restricted to the configured signup email allowlist. Defaults to false, which allows every signup.',
33
+ default: false
34
+ }),
35
+ AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS: str({
36
+ 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.',
37
+ default: ''
38
+ }),
39
+ AUTH_SIGNUP_ALLOWED_EMAILS: str({
40
+ 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.',
41
+ default: ''
42
+ }),
43
+ AUTH_SIGNUP_NOT_ALLOWED_MESSAGE: str({
44
+ desc: 'Description returned when the signup gate blocks an account. Optional. The default is This Shipfox deployment does not accept new accounts right now.',
45
+ default: undefined
46
+ }),
31
47
  CLIENT_BASE_URL: str({
32
48
  desc: 'Base URL of the client app. Used to build links in emails such as password resets.',
33
49
  default: 'http://localhost:5173'
34
50
  })
35
51
  });
52
+ if (config.AUTH_SIGNUP_GATE_ENABLED && !hasSignupAllowlistEntry(config.AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS) && !hasSignupAllowlistEntry(config.AUTH_SIGNUP_ALLOWED_EMAILS)) {
53
+ throw new Error('AUTH_SIGNUP_GATE_ENABLED requires AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS or AUTH_SIGNUP_ALLOWED_EMAILS to be set.');
54
+ }
55
+ function hasSignupAllowlistEntry(value) {
56
+ return value.split(',').some((entry)=>entry.trim().length > 0);
57
+ }
36
58
 
37
59
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {bool, createConfig, num, str} from '@shipfox/config';\n\nexport const config = createConfig({\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 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"],"names":["bool","createConfig","num","str","config","AUTH_JWT_EXPIRES_IN","desc","default","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","CLIENT_BASE_URL"],"mappings":"AAAA,SAAQA,IAAI,EAAEC,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAE7D,OAAO,MAAMC,SAASH,aAAa;IACjCI,qBAAqBF,IAAI;QACvBG,MAAM;QACNC,SAAS;IACX;IACAC,iCAAiCL,IAAI;QACnCG,MAAM;QACNC,SAAS;IACX;IACAE,sCAAsCN,IAAI;QACxCG,MAAM;QACNC,SAAS;IACX;IACAG,oCAAoCR,IAAI;QACtCI,MAAM;QACNC,SAAS;IACX;IACAI,qCAAqCT,IAAI;QACvCI,MAAM;QACNC,SAAS;IACX;IACAK,0BAA0BT,IAAI;QAC5BG,MAAM;QACNC,SAAS;IACX;IACAM,uBAAuBb,KAAK;QAC1BM,MAAM;QACNC,SAAS;IACX;IACAO,iBAAiBX,IAAI;QACnBG,MAAM;QACNC,SAAS;IACX;AACF,GAAG"}
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {bool, createConfig, num, str} from '@shipfox/config';\n\nexport const config = createConfig({\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: 'Description returned when the signup gate blocks an account. 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\nfunction hasSignupAllowlistEntry(value: string): boolean {\n return value.split(',').some((entry) => entry.trim().length > 0);\n}\n"],"names":["bool","createConfig","num","str","config","AUTH_JWT_EXPIRES_IN","desc","default","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","undefined","CLIENT_BASE_URL","hasSignupAllowlistEntry","Error","value","split","some","entry","trim","length"],"mappings":"AAAA,SAAQA,IAAI,EAAEC,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAE7D,OAAO,MAAMC,SAASH,aAAa;IACjCI,qBAAqBF,IAAI;QACvBG,MAAM;QACNC,SAAS;IACX;IACAC,iCAAiCL,IAAI;QACnCG,MAAM;QACNC,SAAS;IACX;IACAE,sCAAsCN,IAAI;QACxCG,MAAM;QACNC,SAAS;IACX;IACAG,oCAAoCR,IAAI;QACtCI,MAAM;QACNC,SAAS;IACX;IACAI,qCAAqCT,IAAI;QACvCI,MAAM;QACNC,SAAS;IACX;IACAK,0BAA0BT,IAAI;QAC5BG,MAAM;QACNC,SAAS;IACX;IACAM,uBAAuBb,KAAK;QAC1BM,MAAM;QACNC,SAAS;IACX;IACAO,0BAA0Bd,KAAK;QAC7BM,MAAM;QACNC,SAAS;IACX;IACAQ,mCAAmCZ,IAAI;QACrCG,MAAM;QACNC,SAAS;IACX;IACAS,4BAA4Bb,IAAI;QAC9BG,MAAM;QACNC,SAAS;IACX;IACAU,iCAAiCd,IAAI;QACnCG,MAAM;QACNC,SAASW;IACX;IACAC,iBAAiBhB,IAAI;QACnBG,MAAM;QACNC,SAAS;IACX;AACF,GAAG;AAEH,IACEH,OAAOU,wBAAwB,IAC/B,CAACM,wBAAwBhB,OAAOW,iCAAiC,KACjE,CAACK,wBAAwBhB,OAAOY,0BAA0B,GAC1D;IACA,MAAM,IAAIK,MACR;AAEJ;AAEA,SAASD,wBAAwBE,KAAa;IAC5C,OAAOA,MAAMC,KAAK,CAAC,KAAKC,IAAI,CAAC,CAACC,QAAUA,MAAMC,IAAI,GAAGC,MAAM,GAAG;AAChE"}
@@ -0,0 +1,5 @@
1
+ import type { AdminRole } from '@shipfox/api-auth-dto';
2
+ export declare const ADMIN_ROLES: readonly AdminRole[];
3
+ export declare function hasMinimumAdminRole(role: AdminRole, minimumRole: AdminRole): boolean;
4
+ export declare function highestAdminRole(roles: readonly AdminRole[]): AdminRole | null;
5
+ //# sourceMappingURL=admin-role-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-role-model.d.ts","sourceRoot":"","sources":["../../src/core/admin-role-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAErD,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAI3C,CAAC;AAQF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,GAAG,OAAO,CAEpF;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,GAAG,IAAI,CAM9E"}
@@ -0,0 +1,18 @@
1
+ export const ADMIN_ROLES = [
2
+ 'admin-observer',
3
+ 'admin-operator',
4
+ 'admin-owner'
5
+ ];
6
+ const ADMIN_ROLE_RANK = {
7
+ 'admin-observer': 1,
8
+ 'admin-operator': 2,
9
+ 'admin-owner': 3
10
+ };
11
+ export function hasMinimumAdminRole(role, minimumRole) {
12
+ return ADMIN_ROLE_RANK[role] >= ADMIN_ROLE_RANK[minimumRole];
13
+ }
14
+ export function highestAdminRole(roles) {
15
+ return roles.reduce((highest, role)=>highest === null || ADMIN_ROLE_RANK[role] > ADMIN_ROLE_RANK[highest] ? role : highest, null);
16
+ }
17
+
18
+ //# sourceMappingURL=admin-role-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/admin-role-model.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\n\nexport const ADMIN_ROLES: readonly AdminRole[] = [\n 'admin-observer',\n 'admin-operator',\n 'admin-owner',\n];\n\nconst ADMIN_ROLE_RANK: Record<AdminRole, number> = {\n 'admin-observer': 1,\n 'admin-operator': 2,\n 'admin-owner': 3,\n};\n\nexport function hasMinimumAdminRole(role: AdminRole, minimumRole: AdminRole): boolean {\n return ADMIN_ROLE_RANK[role] >= ADMIN_ROLE_RANK[minimumRole];\n}\n\nexport function highestAdminRole(roles: readonly AdminRole[]): AdminRole | null {\n return roles.reduce<AdminRole | null>(\n (highest, role) =>\n highest === null || ADMIN_ROLE_RANK[role] > ADMIN_ROLE_RANK[highest] ? role : highest,\n null,\n );\n}\n"],"names":["ADMIN_ROLES","ADMIN_ROLE_RANK","hasMinimumAdminRole","role","minimumRole","highestAdminRole","roles","reduce","highest"],"mappings":"AAEA,OAAO,MAAMA,cAAoC;IAC/C;IACA;IACA;CACD,CAAC;AAEF,MAAMC,kBAA6C;IACjD,kBAAkB;IAClB,kBAAkB;IAClB,eAAe;AACjB;AAEA,OAAO,SAASC,oBAAoBC,IAAe,EAAEC,WAAsB;IACzE,OAAOH,eAAe,CAACE,KAAK,IAAIF,eAAe,CAACG,YAAY;AAC9D;AAEA,OAAO,SAASC,iBAAiBC,KAA2B;IAC1D,OAAOA,MAAMC,MAAM,CACjB,CAACC,SAASL,OACRK,YAAY,QAAQP,eAAe,CAACE,KAAK,GAAGF,eAAe,CAACO,QAAQ,GAAGL,OAAOK,SAChF;AAEJ"}
@@ -0,0 +1,13 @@
1
+ import type { AdminRole } from '@shipfox/api-auth-dto';
2
+ export { ADMIN_ROLES, hasMinimumAdminRole, highestAdminRole } from './admin-role-model.js';
3
+ export declare function getCurrentAdminRole(params: {
4
+ userId: string;
5
+ }): Promise<AdminRole | null>;
6
+ export declare function requireAdminRole(params: {
7
+ userId: string;
8
+ minimumRole: AdminRole;
9
+ }): Promise<AdminRole>;
10
+ export declare function revokeAdminGrant(params: {
11
+ grantId: string;
12
+ }): Promise<import("./entities/admin-grant.js").AdminGrant | undefined>;
13
+ //# sourceMappingURL=admin-role.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-role.d.ts","sourceRoot":"","sources":["../../src/core/admin-role.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAKrD,OAAO,EAAC,WAAW,EAAE,mBAAmB,EAAE,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AAEzF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAE7F;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,SAAS,CAAC;CACxB,GAAG,OAAO,CAAC,SAAS,CAAC,CAMrB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,uEAE/D"}
@@ -0,0 +1,21 @@
1
+ import { findCurrentAdminRole, revokeAdminGrant as revokeAdminGrantInDb } from '#db/admin-grants.js';
2
+ import { hasMinimumAdminRole } from './admin-role-model.js';
3
+ import { AdminRoleRequiredError } from './errors.js';
4
+ export { ADMIN_ROLES, hasMinimumAdminRole, highestAdminRole } from './admin-role-model.js';
5
+ export async function getCurrentAdminRole(params) {
6
+ return await findCurrentAdminRole(params);
7
+ }
8
+ export async function requireAdminRole(params) {
9
+ const role = await getCurrentAdminRole({
10
+ userId: params.userId
11
+ });
12
+ if (!role || !hasMinimumAdminRole(role, params.minimumRole)) {
13
+ throw new AdminRoleRequiredError(params.minimumRole);
14
+ }
15
+ return role;
16
+ }
17
+ export async function revokeAdminGrant(params) {
18
+ return await revokeAdminGrantInDb(params);
19
+ }
20
+
21
+ //# sourceMappingURL=admin-role.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/admin-role.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport {findCurrentAdminRole, revokeAdminGrant as revokeAdminGrantInDb} from '#db/admin-grants.js';\nimport {hasMinimumAdminRole} from './admin-role-model.js';\nimport {AdminRoleRequiredError} from './errors.js';\n\nexport {ADMIN_ROLES, hasMinimumAdminRole, highestAdminRole} from './admin-role-model.js';\n\nexport async function getCurrentAdminRole(params: {userId: string}): Promise<AdminRole | null> {\n return await findCurrentAdminRole(params);\n}\n\nexport async function requireAdminRole(params: {\n userId: string;\n minimumRole: AdminRole;\n}): Promise<AdminRole> {\n const role = await getCurrentAdminRole({userId: params.userId});\n if (!role || !hasMinimumAdminRole(role, params.minimumRole)) {\n throw new AdminRoleRequiredError(params.minimumRole);\n }\n return role;\n}\n\nexport async function revokeAdminGrant(params: {grantId: string}) {\n return await revokeAdminGrantInDb(params);\n}\n"],"names":["findCurrentAdminRole","revokeAdminGrant","revokeAdminGrantInDb","hasMinimumAdminRole","AdminRoleRequiredError","ADMIN_ROLES","highestAdminRole","getCurrentAdminRole","params","requireAdminRole","role","userId","minimumRole"],"mappings":"AACA,SAAQA,oBAAoB,EAAEC,oBAAoBC,oBAAoB,QAAO,sBAAsB;AACnG,SAAQC,mBAAmB,QAAO,wBAAwB;AAC1D,SAAQC,sBAAsB,QAAO,cAAc;AAEnD,SAAQC,WAAW,EAAEF,mBAAmB,EAAEG,gBAAgB,QAAO,wBAAwB;AAEzF,OAAO,eAAeC,oBAAoBC,MAAwB;IAChE,OAAO,MAAMR,qBAAqBQ;AACpC;AAEA,OAAO,eAAeC,iBAAiBD,MAGtC;IACC,MAAME,OAAO,MAAMH,oBAAoB;QAACI,QAAQH,OAAOG,MAAM;IAAA;IAC7D,IAAI,CAACD,QAAQ,CAACP,oBAAoBO,MAAMF,OAAOI,WAAW,GAAG;QAC3D,MAAM,IAAIR,uBAAuBI,OAAOI,WAAW;IACrD;IACA,OAAOF;AACT;AAEA,OAAO,eAAeT,iBAAiBO,MAAyB;IAC9D,OAAO,MAAMN,qBAAqBM;AACpC"}
@@ -1,14 +1,18 @@
1
+ import { type AdminRole } from '@shipfox/api-auth-dto';
1
2
  import { type WorkspacesInterModuleClient } from '@shipfox/api-workspaces-dto/inter-module';
2
3
  import type { User } from './entities/user.js';
3
4
  import { AuthDependencyUnavailableError, EmailNotVerifiedError, InvalidCredentialsError, UserNotFoundError } from './errors.js';
5
+ import type { SignupPolicy } from './ports.js';
4
6
  export interface SignupParams {
5
7
  email: string;
6
8
  password: string;
7
9
  name?: string | undefined;
10
+ signupPolicy?: SignupPolicy | undefined;
8
11
  }
9
12
  export interface ProvisionUserParams {
10
13
  email: string;
11
14
  name?: string | null | undefined;
15
+ signupPolicy?: SignupPolicy | undefined;
12
16
  }
13
17
  /**
14
18
  * Creates a verified, password-less user for an external identity provider.
@@ -53,6 +57,7 @@ export interface LoginResult {
53
57
  token: string;
54
58
  refreshToken: string;
55
59
  user: User;
60
+ adminRole: AdminRole | null;
56
61
  }
57
62
  export declare function login(params: LoginParams): Promise<LoginResult>;
58
63
  export interface CreateSessionForUserParams {
@@ -64,6 +69,7 @@ export interface CreateSessionForUserResult {
64
69
  token: string;
65
70
  refreshToken: string;
66
71
  user: User;
72
+ adminRole: AdminRole | null;
67
73
  }
68
74
  export type CreateSessionForUserError = AuthDependencyUnavailableError | EmailNotVerifiedError | InvalidCredentialsError | UserNotFoundError;
69
75
  export declare function createSessionForUser(params: CreateSessionForUserParams): Promise<CreateSessionForUserResult>;
@@ -72,6 +78,7 @@ export interface RefreshAccessTokenResult {
72
78
  /** Undefined on a grace-window hit: keep the existing cookie instead of rotating it. */
73
79
  refreshToken: string | undefined;
74
80
  user: User;
81
+ adminRole: AdminRole | null;
75
82
  }
76
83
  export declare function refreshAccessToken(params: {
77
84
  refreshToken: string;
@@ -81,6 +88,7 @@ export interface ConfirmEmailVerificationResult {
81
88
  token: string;
82
89
  refreshToken: string;
83
90
  user: User;
91
+ adminRole: AdminRole | null;
84
92
  }
85
93
  export interface ResendEmailVerificationResult {
86
94
  nextResendAvailableAt: Date;
@@ -103,6 +111,7 @@ export interface ConfirmPasswordResetResult {
103
111
  token: string;
104
112
  refreshToken: string;
105
113
  user: User;
114
+ adminRole: AdminRole | null;
106
115
  }
107
116
  export declare function confirmPasswordReset(params: {
108
117
  token: string;
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"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"}
package/dist/core/auth.js CHANGED
@@ -9,11 +9,28 @@ import { consumePasswordReset, createPasswordReset } from '#db/password-resets.j
9
9
  import { createRefreshToken, findActiveRefreshTokenByHash, findRefreshTokenByHash, revokeRefreshSession, revokeRefreshTokensForUser, rotateRefreshToken } from '#db/refresh-tokens.js';
10
10
  import { createUser as createDbUser, findUserByEmail, findUserById, markEmailVerified, provisionUser as provisionDbUser, updateUserPassword } from '#db/users.js';
11
11
  import { recordTokenRefreshed } from '#metrics/index.js';
12
- import { AuthDependencyUnavailableError, EmailNotVerifiedError, EmailTakenError, InvalidCredentialsError, InvitationEmailMismatchError, TokenAlreadyUsedError, TokenExpiredError, TokenInvalidError, UserNotFoundError } from './errors.js';
12
+ import { getCurrentAdminRole } from './admin-role.js';
13
+ import { AuthDependencyUnavailableError, EmailNotVerifiedError, EmailTakenError, InvalidCredentialsError, InvitationEmailMismatchError, SignupNotAllowedError, TokenAlreadyUsedError, TokenExpiredError, TokenInvalidError, UserNotFoundError } from './errors.js';
13
14
  import { signUserToken } from './jwt.js';
14
15
  import { hashPassword, verifyPassword } from './password.js';
15
16
  const RESET_TTL_HOURS = 1;
16
17
  const PASSWORD_VERIFICATION_PURPOSE = 'password-verification';
18
+ const DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE = 'This Shipfox deployment does not accept new accounts right now.';
19
+ const defaultSignupPolicy = {
20
+ isSignupAllowed: async ()=>({
21
+ allowed: true
22
+ })
23
+ };
24
+ async function assertSignupAllowed(params) {
25
+ const result = await (params.signupPolicy ?? defaultSignupPolicy).isSignupAllowed({
26
+ email: params.email,
27
+ emailVerified: params.emailVerified,
28
+ source: params.source
29
+ });
30
+ if (!result.allowed) {
31
+ throw new SignupNotAllowedError(result.message ?? DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE);
32
+ }
33
+ }
17
34
  let dummyHashCache;
18
35
  async function getDummyHash() {
19
36
  if (!dummyHashCache) {
@@ -69,10 +86,14 @@ async function createRefreshSession(user, refreshSessionId) {
69
86
  async function createSessionTokens(user, workspaces) {
70
87
  const refreshSessionId = crypto.randomUUID();
71
88
  const token = await signAccessToken(user, workspaces, refreshSessionId);
89
+ const adminRole = await getCurrentAdminRole({
90
+ userId: user.id
91
+ });
72
92
  const { refreshToken } = await createRefreshSession(user, refreshSessionId);
73
93
  return {
74
94
  token,
75
- refreshToken
95
+ refreshToken,
96
+ adminRole
76
97
  };
77
98
  }
78
99
  function passwordResetLink(rawToken) {
@@ -96,14 +117,26 @@ function invitationErrorMessage(code) {
96
117
  * Creates a verified, password-less user for an external identity provider.
97
118
  * Existing users are returned unchanged, including their password and profile.
98
119
  */ export async function provisionUser(params) {
120
+ const email = emailSchema.parse(params.email);
121
+ const existing = await findUserByEmail({
122
+ email
123
+ });
124
+ if (existing) return existing;
125
+ await assertSignupAllowed({
126
+ signupPolicy: params.signupPolicy,
127
+ email,
128
+ emailVerified: true,
129
+ source: 'external-identity'
130
+ });
99
131
  return await provisionDbUser({
100
- email: emailSchema.parse(params.email),
132
+ email,
101
133
  name: params.name ?? null
102
134
  });
103
135
  }
104
136
  export async function signup(params) {
137
+ const email = emailSchema.parse(params.email);
105
138
  const existing = await findUserByEmail({
106
- email: params.email
139
+ email
107
140
  });
108
141
  if (existing) {
109
142
  const canResumeVerification = existing.status === 'active' && existing.emailVerifiedAt === null && existing.hashedPassword !== null && await verifyPassword({
@@ -123,13 +156,19 @@ export async function signup(params) {
123
156
  emailChallenge
124
157
  };
125
158
  }
126
- throw new EmailTakenError(params.email);
159
+ throw new EmailTakenError(email);
127
160
  }
161
+ await assertSignupAllowed({
162
+ signupPolicy: params.signupPolicy,
163
+ email,
164
+ emailVerified: false,
165
+ source: 'password'
166
+ });
128
167
  const hashedPassword = await hashPassword({
129
168
  password: params.password
130
169
  });
131
170
  const user = await createDbUser({
132
- email: params.email,
171
+ email,
133
172
  hashedPassword,
134
173
  name: params.name ?? null,
135
174
  signedUp: {
@@ -213,21 +252,23 @@ export async function signupWithInvitation(params) {
213
252
  }
214
253
  // Step 4: Issue session. signAccessToken reads memberships through the
215
254
  // workspaces module API, so a successful accept is reflected in the JWT.
216
- const { token, refreshToken } = await createSessionTokens(user, params.workspaces);
255
+ const { token, refreshToken, adminRole } = await createSessionTokens(user, params.workspaces);
217
256
  if (acceptError) {
218
257
  return {
219
258
  token,
220
259
  refreshToken,
221
260
  user,
222
261
  membership,
223
- acceptError
262
+ acceptError,
263
+ adminRole
224
264
  };
225
265
  }
226
266
  return {
227
267
  token,
228
268
  refreshToken,
229
269
  user,
230
- membership
270
+ membership,
271
+ adminRole
231
272
  };
232
273
  }
233
274
  export async function createUser(params) {
@@ -270,11 +311,12 @@ export async function login(params) {
270
311
  if (user.emailVerifiedAt === null) {
271
312
  throw new EmailNotVerifiedError();
272
313
  }
273
- const { token, refreshToken } = await createSessionTokens(user, params.workspaces);
314
+ const { token, refreshToken, adminRole } = await createSessionTokens(user, params.workspaces);
274
315
  return {
275
316
  token,
276
317
  refreshToken,
277
- user
318
+ user,
319
+ adminRole
278
320
  };
279
321
  }
280
322
  export async function createSessionForUser(params) {
@@ -292,11 +334,12 @@ export async function createSessionForUser(params) {
292
334
  if (user.status !== 'active') {
293
335
  throw new InvalidCredentialsError();
294
336
  }
295
- const { token, refreshToken } = await createSessionTokens(user, params.workspaces);
337
+ const { token, refreshToken, adminRole } = await createSessionTokens(user, params.workspaces);
296
338
  return {
297
339
  token,
298
340
  refreshToken,
299
- user
341
+ user,
342
+ adminRole
300
343
  };
301
344
  }
302
345
  export async function refreshAccessToken(params) {
@@ -319,6 +362,9 @@ export async function refreshAccessToken(params) {
319
362
  recordRefreshOutcome('rejected');
320
363
  throw new TokenInvalidError('Refresh token is invalid or expired');
321
364
  }
365
+ const adminRole = await getCurrentAdminRole({
366
+ userId: user.id
367
+ });
322
368
  // Within the grace window a rotated token means a concurrent refresh (e.g. a
323
369
  // second tab); past it, reuse of a retired token means a compromised session.
324
370
  if (current.rotatedAt) {
@@ -328,7 +374,8 @@ export async function refreshAccessToken(params) {
328
374
  return {
329
375
  token,
330
376
  refreshToken: undefined,
331
- user
377
+ user,
378
+ adminRole
332
379
  };
333
380
  }
334
381
  await revokeRefreshTokensForUser({
@@ -359,7 +406,8 @@ export async function refreshAccessToken(params) {
359
406
  return {
360
407
  token,
361
408
  refreshToken: undefined,
362
- user
409
+ user,
410
+ adminRole
363
411
  };
364
412
  }
365
413
  const token = await signAccessToken(user, params.workspaces, current.sessionId);
@@ -367,7 +415,8 @@ export async function refreshAccessToken(params) {
367
415
  return {
368
416
  token,
369
417
  refreshToken: nextRefreshToken,
370
- user
418
+ user,
419
+ adminRole
371
420
  };
372
421
  }
373
422
  export async function confirmEmailVerification(params) {
@@ -391,11 +440,12 @@ export async function confirmEmailVerification(params) {
391
440
  if (verifiedUser?.status !== 'active') {
392
441
  throw new TokenInvalidError('Verification code is invalid or expired');
393
442
  }
394
- const { token, refreshToken } = await createSessionTokens(verifiedUser, params.workspaces);
443
+ const { token, refreshToken, adminRole } = await createSessionTokens(verifiedUser, params.workspaces);
395
444
  return {
396
445
  token,
397
446
  refreshToken,
398
- user: verifiedUser
447
+ user: verifiedUser,
448
+ adminRole
399
449
  };
400
450
  }
401
451
  export async function resendEmailVerification(params) {
@@ -461,11 +511,12 @@ export async function confirmPasswordReset(params) {
461
511
  await revokeRefreshTokensForUser({
462
512
  userId: consumed.userId
463
513
  });
464
- const { token, refreshToken } = await createSessionTokens(user, params.workspaces);
514
+ const { token, refreshToken, adminRole } = await createSessionTokens(user, params.workspaces);
465
515
  return {
466
516
  token,
467
517
  refreshToken,
468
- user
518
+ user,
519
+ adminRole
469
520
  };
470
521
  }
471
522
  export async function changePassword(params) {