@shipfox/api-auth 9.2.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +56 -0
- package/README.md +32 -8
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/core/administration.d.ts +43 -0
- package/dist/core/administration.d.ts.map +1 -0
- package/dist/core/administration.js +152 -0
- package/dist/core/administration.js.map +1 -0
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +2 -5
- package/dist/core/auth.js.map +1 -1
- package/dist/core/entities/administrator-read-model.d.ts +21 -0
- package/dist/core/entities/administrator-read-model.d.ts.map +1 -0
- package/dist/core/entities/administrator-read-model.js +3 -0
- package/dist/core/entities/administrator-read-model.js.map +1 -0
- package/dist/core/errors.d.ts +15 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +30 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/db/admin-grants.d.ts +34 -1
- package/dist/db/admin-grants.d.ts.map +1 -1
- package/dist/db/admin-grants.js +177 -5
- package/dist/db/admin-grants.js.map +1 -1
- package/dist/db/admin-users.d.ts +21 -0
- package/dist/db/admin-users.d.ts.map +1 -0
- package/dist/db/admin-users.js +32 -0
- package/dist/db/admin-users.js.map +1 -0
- package/dist/db/db.d.ts +256 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +2 -0
- package/dist/db/db.js.map +1 -1
- package/dist/db/schema/admin-command-results.d.ts +142 -0
- package/dist/db/schema/admin-command-results.d.ts.map +1 -0
- package/dist/db/schema/admin-command-results.js +21 -0
- package/dist/db/schema/admin-command-results.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +1 -1
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/routes/administration.d.ts +4 -0
- package/dist/presentation/routes/administration.d.ts.map +1 -0
- package/dist/presentation/routes/administration.js +267 -0
- package/dist/presentation/routes/administration.js.map +1 -0
- package/dist/presentation/routes/rate-limit.d.ts +1 -0
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +26 -1
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0002_burly_whizzer.sql +12 -0
- package/drizzle/meta/0002_snapshot.json +791 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +10 -9
- package/src/config.ts +4 -0
- package/src/core/administration.ts +213 -0
- package/src/core/auth-default-signup-policy.test.ts +65 -0
- package/src/core/auth.test.ts +5 -0
- package/src/core/auth.ts +2 -3
- package/src/core/entities/administrator-read-model.ts +23 -0
- package/src/core/errors.ts +35 -0
- package/src/db/admin-grants.test.ts +1 -7
- package/src/db/admin-grants.ts +310 -5
- package/src/db/admin-users.ts +58 -0
- package/src/db/db.ts +2 -0
- package/src/db/schema/admin-command-results.ts +41 -0
- package/src/index.test.ts +6 -2
- package/src/index.ts +23 -2
- package/src/metrics/instance.ts +1 -1
- package/src/presentation/auth/refresh-cookie.test.ts +4 -0
- package/src/presentation/e2eRoutes/index.test.ts +1 -0
- package/src/presentation/routes/administration.test.ts +537 -0
- package/src/presentation/routes/administration.ts +276 -0
- package/src/presentation/routes/index.test.ts +1 -0
- package/src/presentation/routes/rate-limit.ts +26 -2
- package/test/globalSetup.ts +6 -2
- package/test/routes.ts +10 -1
- package/tsconfig.build.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
$ shipfox-swc
|
|
2
|
-
Successfully compiled:
|
|
2
|
+
Successfully compiled: 64 files with swc (349.3ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# @shipfox/api-auth
|
|
2
2
|
|
|
3
|
+
## 10.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 6054364: Add bounded administrator user lookup and safe administrator-grant summaries while removing the unbounded grant reader.
|
|
8
|
+
|
|
9
|
+
### Minor Changes
|
|
10
|
+
|
|
11
|
+
- dd43d63: Moves Auth administration routes to the unversioned `/admin/...` namespace.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [6054364]
|
|
16
|
+
- Updated dependencies [74f9e31]
|
|
17
|
+
- Updated dependencies [e9280fc]
|
|
18
|
+
- @shipfox/api-auth-dto@10.0.0
|
|
19
|
+
- @shipfox/node-fastify@0.4.0
|
|
20
|
+
- @shipfox/api-workspaces-dto@10.0.0
|
|
21
|
+
- @shipfox/api-auth-context@10.0.0
|
|
22
|
+
- @shipfox/node-module@1.0.4
|
|
23
|
+
- @shipfox/api-common-dto@9.2.0
|
|
24
|
+
- @shipfox/api-email-challenges@1.1.6
|
|
25
|
+
- @shipfox/config@1.2.4
|
|
26
|
+
- @shipfox/inter-module@0.2.2
|
|
27
|
+
- @shipfox/node-auth-root-key@0.2.3
|
|
28
|
+
- @shipfox/node-drizzle@0.3.4
|
|
29
|
+
- @shipfox/node-email@0.3.4
|
|
30
|
+
- @shipfox/node-jwt@0.3.2
|
|
31
|
+
- @shipfox/node-mailer@0.2.4
|
|
32
|
+
- @shipfox/node-opentelemetry@0.6.3
|
|
33
|
+
- @shipfox/node-outbox@0.2.6
|
|
34
|
+
- @shipfox/node-postgres@0.4.4
|
|
35
|
+
- @shipfox/node-rate-limit@0.3.2
|
|
36
|
+
- @shipfox/node-tokens@0.3.2
|
|
37
|
+
|
|
38
|
+
## 9.3.0
|
|
39
|
+
|
|
40
|
+
### Minor Changes
|
|
41
|
+
|
|
42
|
+
- 10cf63c: Add first-owner bootstrap and administrator grant management routes.
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- c0046d3: Default low-level account creation to the environment-backed signup policy when no explicit policy is supplied.
|
|
47
|
+
- Updated dependencies [10cf63c]
|
|
48
|
+
- Updated dependencies [4425c6d]
|
|
49
|
+
- Updated dependencies [7b6a409]
|
|
50
|
+
- @shipfox/api-auth-dto@9.3.0
|
|
51
|
+
- @shipfox/node-opentelemetry@0.6.3
|
|
52
|
+
- @shipfox/api-workspaces-dto@9.3.0
|
|
53
|
+
- @shipfox/api-auth-context@9.3.0
|
|
54
|
+
- @shipfox/api-email-challenges@1.1.5
|
|
55
|
+
- @shipfox/node-fastify@0.3.4
|
|
56
|
+
- @shipfox/node-mailer@0.2.4
|
|
57
|
+
- @shipfox/node-module@1.0.3
|
|
58
|
+
|
|
3
59
|
## 9.2.0
|
|
4
60
|
|
|
5
61
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -68,6 +68,7 @@ Required environment:
|
|
|
68
68
|
| `AUTH_SIGNUP_ALLOWED_EMAILS` | none | Comma-separated exact email addresses allowed to create accounts. |
|
|
69
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." |
|
|
70
70
|
| `CLIENT_BASE_URL` | `http://localhost:5173` | Base URL used in email verification and password reset links. |
|
|
71
|
+
| `ADMIN_BOOTSTRAP_TOKEN` | none | Deployment secret accepted once to create the first administrator owner. Set it before bootstrap and remove or rotate it after successful bootstrap. |
|
|
71
72
|
|
|
72
73
|
Email delivery uses the shared `@shipfox/node-mailer` configuration.
|
|
73
74
|
|
|
@@ -231,6 +232,19 @@ When password login is disabled, the password and email-verification rows in thi
|
|
|
231
232
|
> [!IMPORTANT]
|
|
232
233
|
> Refresh cookies are set with `secure: true`, `httpOnly: true`, and `sameSite: "lax"`. Local browser tests need HTTPS or a test path that handles secure cookies.
|
|
233
234
|
|
|
235
|
+
### Administrator grants
|
|
236
|
+
|
|
237
|
+
All routes are mounted under `/admin/auth/admin-grants` and require an authenticated bearer token. Administrator role checks are enforced in the core layer, not by route-level middleware.
|
|
238
|
+
|
|
239
|
+
| Method | Path | Required role | Result |
|
|
240
|
+
| --- | --- | --- | --- |
|
|
241
|
+
| `POST` | `/bootstrap` | none (deployment token) | Claims the `admin-owner` role for the calling user using `ADMIN_BOOTSTRAP_TOKEN`. Fails with `409 bootstrap-closed` once an active owner exists. |
|
|
242
|
+
| `GET` | `/` | `admin-observer` | Lists bounded local administrator grant summaries with embedded safe user identities. |
|
|
243
|
+
| `POST` | `/` | `admin-owner` | Grants a role to an active user. |
|
|
244
|
+
| `DELETE` | `/:grantId` | `admin-owner` | Revokes a grant. Fails with `409 last-owner` when it would remove the final active owner. |
|
|
245
|
+
|
|
246
|
+
`POST` and `DELETE` routes require an `Idempotency-Key` header; a repeated key replays the same result, and reusing a key with a different command returns `409 idempotency-key-reused`. Every mutation writes a redacted `administration.action.performed` outbox event.
|
|
247
|
+
|
|
234
248
|
### Rate limiting
|
|
235
249
|
|
|
236
250
|
The public auth endpoints include an application-layer abuse baseline for open source installs:
|
|
@@ -240,6 +254,8 @@ The public auth endpoints include an application-layer abuse baseline for open s
|
|
|
240
254
|
| `POST /auth/login` | 60 attempts per 5 minutes | 10 attempts per 15 minutes |
|
|
241
255
|
| `POST /auth/password-reset` | 30 email-send attempts per hour | 3 email-send attempts per hour |
|
|
242
256
|
| `POST /auth/verify-email/resend` | Shared with password reset | Shared with password reset |
|
|
257
|
+
| `POST /admin/auth/admin-grants/bootstrap` | 5 attempts per 15 minutes | n/a |
|
|
258
|
+
| `GET /admin/auth/users` | 60 attempts per 5 minutes | n/a |
|
|
243
259
|
|
|
244
260
|
Counters are stored in PostgreSQL as fixed windows in `auth_rate_limits`. IP addresses and email addresses are HMAC-SHA256 values before storage; raw identifiers are not persisted. The identifier key is derived from `AUTH_ROOT_KEY` separately from every token key.
|
|
245
261
|
|
|
@@ -247,6 +263,12 @@ The limiter uses `request.ip`, so production deployments behind a reverse proxy
|
|
|
247
263
|
|
|
248
264
|
This app-layer limiter protects semantic auth work such as Argon2 verification and email sending. It is not a volumetric DDoS control. Public production deployments should still use load balancer, CDN, WAF, or firewall rate limits at the network edge.
|
|
249
265
|
|
|
266
|
+
### Administrator user reads
|
|
267
|
+
|
|
268
|
+
`GET /admin/auth/users` requires `admin-observer` and accepts exactly one checked query filter: `id`, `user_id`, or normalized `email`. The response is a bounded `AdministratorUserSummary` containing only the stable user identity, verified-email timestamp, display name, account status, creation timestamp, and current administrator role.
|
|
269
|
+
|
|
270
|
+
`GET /admin/auth/admin-grants` also requires `admin-observer`. It returns at most 100 newest-first `AdministratorGrantSummary` rows per page, with an opaque cursor and a safe embedded user identity. It does not return credentials, sessions, provider payloads, OAuth tokens, refresh tokens, or raw authentication metadata.
|
|
271
|
+
|
|
250
272
|
## API
|
|
251
273
|
|
|
252
274
|
The package exports a module factory. Pass it the Workspaces inter-module client from the
|
|
@@ -284,20 +306,18 @@ create a normal Shipfox session and set its refresh cookie:
|
|
|
284
306
|
import {
|
|
285
307
|
authCookiePlugin,
|
|
286
308
|
createSessionForUser,
|
|
287
|
-
createEnvironmentSignupPolicy,
|
|
288
309
|
provisionUser,
|
|
289
310
|
setRefreshTokenCookie,
|
|
290
311
|
} from '@shipfox/api-auth';
|
|
291
312
|
import type {FastifyReply} from 'fastify';
|
|
292
313
|
|
|
293
314
|
export const callbackRoutePlugins = [authCookiePlugin];
|
|
294
|
-
const signupPolicy = createEnvironmentSignupPolicy();
|
|
295
315
|
|
|
296
316
|
export async function completeProviderCallback(
|
|
297
317
|
reply: FastifyReply,
|
|
298
318
|
profile: {email: string; name?: string},
|
|
299
319
|
): Promise<string> {
|
|
300
|
-
const user = await provisionUser(
|
|
320
|
+
const user = await provisionUser(profile);
|
|
301
321
|
const session = await createSessionForUser({userId: user.id});
|
|
302
322
|
|
|
303
323
|
setRefreshTokenCookie(reply, session.refreshToken);
|
|
@@ -311,11 +331,13 @@ For OAuth providers, require a verified-email claim such as `email_verified`.
|
|
|
311
331
|
session for any active, verified account. An unverified provider email could
|
|
312
332
|
otherwise sign in as an existing password account with the same address.
|
|
313
333
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
user
|
|
318
|
-
|
|
334
|
+
`provisionUser` uses the environment-backed signup policy by default. Pass an
|
|
335
|
+
explicit `signupPolicy` only when the integration owns a replacement policy,
|
|
336
|
+
such as Cloud's database-backed policy. The function checks for an existing
|
|
337
|
+
user before it calls the policy, so existing users keep signing in even when
|
|
338
|
+
their address is no longer allowed. For a new user, a denied policy throws
|
|
339
|
+
`SignupNotAllowedError`. Call `createSessionForUser` only after provisioning
|
|
340
|
+
succeeds.
|
|
319
341
|
|
|
320
342
|
Add `authCookiePlugin` to the callback route group before calling a cookie
|
|
321
343
|
helper. `getRefreshTokenCookie`, `setRefreshTokenCookie`, and
|
|
@@ -389,6 +411,8 @@ The module creates tables with the `auth_` prefix:
|
|
|
389
411
|
- `auth_refresh_tokens`
|
|
390
412
|
- `auth_password_resets`
|
|
391
413
|
- `auth_rate_limits`
|
|
414
|
+
- `auth_admin_grants`
|
|
415
|
+
- `auth_admin_command_results`
|
|
392
416
|
|
|
393
417
|
Passwords use Argon2id. Password reset tokens and refresh tokens are opaque tokens stored as hashes. Email verification uses the shared email-challenges module.
|
|
394
418
|
|
package/dist/config.d.ts
CHANGED
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;kDAqDjB,CAAC"}
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { bool, createConfig, num, str } from '@shipfox/config';
|
|
2
2
|
export const config = createConfig({
|
|
3
|
+
ADMIN_BOOTSTRAP_TOKEN: str({
|
|
4
|
+
desc: 'Deployment secret accepted once to create the first administrator owner. Set it before bootstrap and remove or rotate it after successful bootstrap.',
|
|
5
|
+
default: undefined
|
|
6
|
+
}),
|
|
3
7
|
AUTH_JWT_EXPIRES_IN: str({
|
|
4
8
|
desc: 'How long an access token stays valid. Accepts a duration string such as 15m, 1h, or 7d.',
|
|
5
9
|
default: '15m'
|
package/dist/config.js.map
CHANGED
|
@@ -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 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","
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {bool, createConfig, num, str} from '@shipfox/config';\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: '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","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","value","split","some","entry","trim","length"],"mappings":"AAAA,SAAQA,IAAI,EAAEC,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAE7D,OAAO,MAAMC,SAASH,aAAa;IACjCI,uBAAuBF,IAAI;QACzBG,MAAM;QACNC,SAASC;IACX;IACAC,qBAAqBN,IAAI;QACvBG,MAAM;QACNC,SAAS;IACX;IACAG,iCAAiCP,IAAI;QACnCG,MAAM;QACNC,SAAS;IACX;IACAI,sCAAsCR,IAAI;QACxCG,MAAM;QACNC,SAAS;IACX;IACAK,oCAAoCV,IAAI;QACtCI,MAAM;QACNC,SAAS;IACX;IACAM,qCAAqCX,IAAI;QACvCI,MAAM;QACNC,SAAS;IACX;IACAO,0BAA0BX,IAAI;QAC5BG,MAAM;QACNC,SAAS;IACX;IACAQ,uBAAuBf,KAAK;QAC1BM,MAAM;QACNC,SAAS;IACX;IACAS,0BAA0BhB,KAAK;QAC7BM,MAAM;QACNC,SAAS;IACX;IACAU,mCAAmCd,IAAI;QACrCG,MAAM;QACNC,SAAS;IACX;IACAW,4BAA4Bf,IAAI;QAC9BG,MAAM;QACNC,SAAS;IACX;IACAY,iCAAiChB,IAAI;QACnCG,MAAM;QACNC,SAASC;IACX;IACAY,iBAAiBjB,IAAI;QACnBG,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,SAASD,wBAAwBE,KAAa;IAC5C,OAAOA,MAAMC,KAAK,CAAC,KAAKC,IAAI,CAAC,CAACC,QAAUA,MAAMC,IAAI,GAAGC,MAAM,GAAG;AAChE"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AdminRole } from '@shipfox/api-auth-dto';
|
|
2
|
+
import type { TimestampIdCursor } from '@shipfox/node-drizzle';
|
|
3
|
+
import type { AdminGrant } from './entities/admin-grant.js';
|
|
4
|
+
import type { AdministratorGrantSummary, AdministratorUserSummary } from './entities/administrator-read-model.js';
|
|
5
|
+
import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from './errors.js';
|
|
6
|
+
export declare function hashAdministrationValue(value: string): string;
|
|
7
|
+
export declare function administrationCommandFingerprint(command: string, input: unknown): string;
|
|
8
|
+
export interface AdministrationMutationContext {
|
|
9
|
+
actorId: string;
|
|
10
|
+
idempotencyKey: string;
|
|
11
|
+
correlationId: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function bootstrapFirstAdminOwner(params: AdministrationMutationContext & {
|
|
14
|
+
bootstrapToken: string;
|
|
15
|
+
}): Promise<AdminGrant>;
|
|
16
|
+
export declare function findAdministratorUserSummary(params: {
|
|
17
|
+
actorId: string;
|
|
18
|
+
} & ({
|
|
19
|
+
id: string;
|
|
20
|
+
email?: never;
|
|
21
|
+
} | {
|
|
22
|
+
email: string;
|
|
23
|
+
id?: never;
|
|
24
|
+
})): Promise<AdministratorUserSummary | undefined>;
|
|
25
|
+
export declare function listAdministratorGrantSummaries(params: {
|
|
26
|
+
actorId: string;
|
|
27
|
+
limit: number;
|
|
28
|
+
cursor?: TimestampIdCursor;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
grants: AdministratorGrantSummary[];
|
|
31
|
+
nextCursor: TimestampIdCursor | null;
|
|
32
|
+
}>;
|
|
33
|
+
export declare function grantAdministratorRole(params: AdministrationMutationContext & {
|
|
34
|
+
userId: string;
|
|
35
|
+
role: AdminRole;
|
|
36
|
+
reason: string;
|
|
37
|
+
}): Promise<AdminGrant>;
|
|
38
|
+
export declare function revokeAdministratorGrant(params: AdministrationMutationContext & {
|
|
39
|
+
grantId: string;
|
|
40
|
+
reason: string;
|
|
41
|
+
}): Promise<AdminGrant>;
|
|
42
|
+
export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError, };
|
|
43
|
+
//# sourceMappingURL=administration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"administration.d.ts","sourceRoot":"","sources":["../../src/core/administration.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAErD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,uBAAuB,CAAC;AAU7D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EACV,yBAAyB,EACzB,wBAAwB,EACzB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EACL,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC7B,+BAA+B,EAC/B,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAQrB,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAExF;AAkCD,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,6BAA6B,GAAG;IAAC,cAAc,EAAE,MAAM,CAAA;CAAC,GAC/D,OAAO,CAAC,UAAU,CAAC,CAuBrB;AAED,wBAAsB,4BAA4B,CAChD,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAAG,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAC,GAAG;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,KAAK,CAAA;CAAC,CAAC,GACtF,OAAO,CAAC,wBAAwB,GAAG,SAAS,CAAC,CAS/C;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B,GAAG,OAAO,CAAC;IACV,MAAM,EAAE,yBAAyB,EAAE,CAAC;IACpC,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACtC,CAAC,CAiBD;AAED,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,6BAA6B,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACxF,OAAO,CAAC,UAAU,CAAC,CA0BrB;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,6BAA6B,GAAG;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACxE,OAAO,CAAC,UAAU,CAAC,CAwBrB;AAED,OAAO,EACL,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC7B,+BAA+B,EAC/B,mBAAmB,EACnB,iBAAiB,GAClB,CAAC"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { createAdministrationActionEvent } from '@shipfox/api-common-dto';
|
|
3
|
+
import { config } from '#config.js';
|
|
4
|
+
import { bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb, grantAdminRoleWithAudit, listAdminGrantSummaries, revokeAdminGrantWithAudit } from '#db/admin-grants.js';
|
|
5
|
+
import { findAdministratorUser as findAdministratorUserInDb } from '#db/admin-users.js';
|
|
6
|
+
import { requireAdminRole } from './admin-role.js';
|
|
7
|
+
import { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError } from './errors.js';
|
|
8
|
+
const ADMIN_OWNER_ROLE = 'admin-owner';
|
|
9
|
+
const ADMIN_OBSERVER_ROLE = 'admin-observer';
|
|
10
|
+
const BOOTSTRAP_COMMAND = 'auth.admin_grant.bootstrap';
|
|
11
|
+
const GRANT_COMMAND = 'auth.admin_grant.grant';
|
|
12
|
+
const REVOKE_COMMAND = 'auth.admin_grant.revoke';
|
|
13
|
+
export function hashAdministrationValue(value) {
|
|
14
|
+
return createHash('sha256').update(value).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
export function administrationCommandFingerprint(command, input) {
|
|
17
|
+
return hashAdministrationValue(`${command}:${JSON.stringify(input)}`);
|
|
18
|
+
}
|
|
19
|
+
function bootstrapTokenMatches(candidate, expected) {
|
|
20
|
+
if (!expected) return false;
|
|
21
|
+
const candidateHash = Buffer.from(hashAdministrationValue(candidate), 'hex');
|
|
22
|
+
const expectedHash = Buffer.from(hashAdministrationValue(expected), 'hex');
|
|
23
|
+
return timingSafeEqual(candidateHash, expectedHash);
|
|
24
|
+
}
|
|
25
|
+
function administrationEvent(params) {
|
|
26
|
+
return createAdministrationActionEvent({
|
|
27
|
+
actorId: params.actorId,
|
|
28
|
+
actorRole: params.actorRole,
|
|
29
|
+
requiredRole: ADMIN_OWNER_ROLE,
|
|
30
|
+
command: params.command,
|
|
31
|
+
targetType: params.targetType,
|
|
32
|
+
targetId: params.targetId,
|
|
33
|
+
reason: params.reason,
|
|
34
|
+
result: 'succeeded',
|
|
35
|
+
correlationId: params.correlationId,
|
|
36
|
+
idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,
|
|
37
|
+
occurredAt: new Date().toISOString()
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export async function bootstrapFirstAdminOwner(params) {
|
|
41
|
+
if (!bootstrapTokenMatches(params.bootstrapToken, config.ADMIN_BOOTSTRAP_TOKEN)) {
|
|
42
|
+
throw new InvalidAdminBootstrapTokenError();
|
|
43
|
+
}
|
|
44
|
+
return await bootstrapFirstAdminOwnerInDb({
|
|
45
|
+
userId: params.actorId,
|
|
46
|
+
actorId: params.actorId,
|
|
47
|
+
idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),
|
|
48
|
+
requestFingerprint: administrationCommandFingerprint(BOOTSTRAP_COMMAND, {
|
|
49
|
+
actorId: params.actorId
|
|
50
|
+
}),
|
|
51
|
+
event: administrationEvent({
|
|
52
|
+
actorId: params.actorId,
|
|
53
|
+
actorRole: ADMIN_OWNER_ROLE,
|
|
54
|
+
command: BOOTSTRAP_COMMAND,
|
|
55
|
+
targetType: 'user',
|
|
56
|
+
targetId: params.actorId,
|
|
57
|
+
reason: 'Initial administrator owner bootstrap',
|
|
58
|
+
correlationId: params.correlationId,
|
|
59
|
+
idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey)
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export async function findAdministratorUserSummary(params) {
|
|
64
|
+
await requireAdminRole({
|
|
65
|
+
userId: params.actorId,
|
|
66
|
+
minimumRole: ADMIN_OBSERVER_ROLE
|
|
67
|
+
});
|
|
68
|
+
const user = await findAdministratorUserInDb('id' in params ? {
|
|
69
|
+
id: params.id
|
|
70
|
+
} : {
|
|
71
|
+
email: params.email
|
|
72
|
+
});
|
|
73
|
+
if (!user) return undefined;
|
|
74
|
+
return user;
|
|
75
|
+
}
|
|
76
|
+
export async function listAdministratorGrantSummaries(params) {
|
|
77
|
+
await requireAdminRole({
|
|
78
|
+
userId: params.actorId,
|
|
79
|
+
minimumRole: ADMIN_OBSERVER_ROLE
|
|
80
|
+
});
|
|
81
|
+
const result = await listAdminGrantSummaries({
|
|
82
|
+
limit: params.limit,
|
|
83
|
+
...params.cursor ? {
|
|
84
|
+
cursor: params.cursor
|
|
85
|
+
} : {}
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
grants: result.rows.map((row)=>({
|
|
89
|
+
grantId: row.id,
|
|
90
|
+
role: row.role,
|
|
91
|
+
createdAt: row.createdAt,
|
|
92
|
+
revokedAt: row.revokedAt,
|
|
93
|
+
user: row.user
|
|
94
|
+
})),
|
|
95
|
+
nextCursor: result.nextCursor
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export async function grantAdministratorRole(params) {
|
|
99
|
+
const actorRole = await requireAdminRole({
|
|
100
|
+
userId: params.actorId,
|
|
101
|
+
minimumRole: ADMIN_OWNER_ROLE
|
|
102
|
+
});
|
|
103
|
+
return await grantAdminRoleWithAudit({
|
|
104
|
+
userId: params.userId,
|
|
105
|
+
role: params.role,
|
|
106
|
+
actorId: params.actorId,
|
|
107
|
+
idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),
|
|
108
|
+
requestFingerprint: administrationCommandFingerprint(GRANT_COMMAND, {
|
|
109
|
+
userId: params.userId,
|
|
110
|
+
role: params.role,
|
|
111
|
+
reason: params.reason
|
|
112
|
+
}),
|
|
113
|
+
event: administrationEvent({
|
|
114
|
+
actorId: params.actorId,
|
|
115
|
+
actorRole,
|
|
116
|
+
command: GRANT_COMMAND,
|
|
117
|
+
targetType: 'user',
|
|
118
|
+
targetId: params.userId,
|
|
119
|
+
reason: params.reason,
|
|
120
|
+
correlationId: params.correlationId,
|
|
121
|
+
idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey)
|
|
122
|
+
})
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export async function revokeAdministratorGrant(params) {
|
|
126
|
+
const actorRole = await requireAdminRole({
|
|
127
|
+
userId: params.actorId,
|
|
128
|
+
minimumRole: ADMIN_OWNER_ROLE
|
|
129
|
+
});
|
|
130
|
+
return await revokeAdminGrantWithAudit({
|
|
131
|
+
grantId: params.grantId,
|
|
132
|
+
actorId: params.actorId,
|
|
133
|
+
idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),
|
|
134
|
+
requestFingerprint: administrationCommandFingerprint(REVOKE_COMMAND, {
|
|
135
|
+
grantId: params.grantId,
|
|
136
|
+
reason: params.reason
|
|
137
|
+
}),
|
|
138
|
+
event: administrationEvent({
|
|
139
|
+
actorId: params.actorId,
|
|
140
|
+
actorRole,
|
|
141
|
+
command: REVOKE_COMMAND,
|
|
142
|
+
targetType: 'admin-grant',
|
|
143
|
+
targetId: params.grantId,
|
|
144
|
+
reason: params.reason,
|
|
145
|
+
correlationId: params.correlationId,
|
|
146
|
+
idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey)
|
|
147
|
+
})
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
export { AdminBootstrapClosedError, AdminGrantAlreadyExistsError, AdminGrantNotFoundError, AdminIdempotencyKeyReuseError, InvalidAdminBootstrapTokenError, LastAdminOwnerError, UserNotFoundError };
|
|
151
|
+
|
|
152
|
+
//# sourceMappingURL=administration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/administration.ts"],"sourcesContent":["import {createHash, timingSafeEqual} from 'node:crypto';\nimport type {AdminRole} from '@shipfox/api-auth-dto';\nimport {createAdministrationActionEvent} from '@shipfox/api-common-dto';\nimport type {TimestampIdCursor} from '@shipfox/node-drizzle';\nimport {config} from '#config.js';\nimport {\n bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb,\n grantAdminRoleWithAudit,\n listAdminGrantSummaries,\n revokeAdminGrantWithAudit,\n} from '#db/admin-grants.js';\nimport {findAdministratorUser as findAdministratorUserInDb} from '#db/admin-users.js';\nimport {requireAdminRole} from './admin-role.js';\nimport type {AdminGrant} from './entities/admin-grant.js';\nimport type {\n AdministratorGrantSummary,\n AdministratorUserSummary,\n} from './entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n InvalidAdminBootstrapTokenError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from './errors.js';\n\nconst ADMIN_OWNER_ROLE: AdminRole = 'admin-owner';\nconst ADMIN_OBSERVER_ROLE: AdminRole = 'admin-observer';\nconst BOOTSTRAP_COMMAND = 'auth.admin_grant.bootstrap';\nconst GRANT_COMMAND = 'auth.admin_grant.grant';\nconst REVOKE_COMMAND = 'auth.admin_grant.revoke';\n\nexport function hashAdministrationValue(value: string): string {\n return createHash('sha256').update(value).digest('hex');\n}\n\nexport function administrationCommandFingerprint(command: string, input: unknown): string {\n return hashAdministrationValue(`${command}:${JSON.stringify(input)}`);\n}\n\nfunction bootstrapTokenMatches(candidate: string, expected: string | undefined): boolean {\n if (!expected) return false;\n const candidateHash = Buffer.from(hashAdministrationValue(candidate), 'hex');\n const expectedHash = Buffer.from(hashAdministrationValue(expected), 'hex');\n return timingSafeEqual(candidateHash, expectedHash);\n}\n\nfunction administrationEvent(params: {\n actorId: string;\n actorRole: AdminRole;\n command: string;\n targetType: string;\n targetId: string;\n reason: string;\n correlationId: string;\n idempotencyKeyFingerprint: string;\n}) {\n return createAdministrationActionEvent({\n actorId: params.actorId,\n actorRole: params.actorRole,\n requiredRole: ADMIN_OWNER_ROLE,\n command: params.command,\n targetType: params.targetType,\n targetId: params.targetId,\n reason: params.reason,\n result: 'succeeded',\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n occurredAt: new Date().toISOString(),\n });\n}\n\nexport interface AdministrationMutationContext {\n actorId: string;\n idempotencyKey: string;\n correlationId: string;\n}\n\nexport async function bootstrapFirstAdminOwner(\n params: AdministrationMutationContext & {bootstrapToken: string},\n): Promise<AdminGrant> {\n if (!bootstrapTokenMatches(params.bootstrapToken, config.ADMIN_BOOTSTRAP_TOKEN)) {\n throw new InvalidAdminBootstrapTokenError();\n }\n\n return await bootstrapFirstAdminOwnerInDb({\n userId: params.actorId,\n actorId: params.actorId,\n idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),\n requestFingerprint: administrationCommandFingerprint(BOOTSTRAP_COMMAND, {\n actorId: params.actorId,\n }),\n event: administrationEvent({\n actorId: params.actorId,\n actorRole: ADMIN_OWNER_ROLE,\n command: BOOTSTRAP_COMMAND,\n targetType: 'user',\n targetId: params.actorId,\n reason: 'Initial administrator owner bootstrap',\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),\n }),\n });\n}\n\nexport async function findAdministratorUserSummary(\n params: {actorId: string} & ({id: string; email?: never} | {email: string; id?: never}),\n): Promise<AdministratorUserSummary | undefined> {\n await requireAdminRole({userId: params.actorId, minimumRole: ADMIN_OBSERVER_ROLE});\n\n const user = await findAdministratorUserInDb(\n 'id' in params ? {id: params.id} : {email: params.email},\n );\n if (!user) return undefined;\n\n return user;\n}\n\nexport async function listAdministratorGrantSummaries(params: {\n actorId: string;\n limit: number;\n cursor?: TimestampIdCursor;\n}): Promise<{\n grants: AdministratorGrantSummary[];\n nextCursor: TimestampIdCursor | null;\n}> {\n await requireAdminRole({userId: params.actorId, minimumRole: ADMIN_OBSERVER_ROLE});\n\n const result = await listAdminGrantSummaries({\n limit: params.limit,\n ...(params.cursor ? {cursor: params.cursor} : {}),\n });\n return {\n grants: result.rows.map((row) => ({\n grantId: row.id,\n role: row.role,\n createdAt: row.createdAt,\n revokedAt: row.revokedAt,\n user: row.user,\n })),\n nextCursor: result.nextCursor,\n };\n}\n\nexport async function grantAdministratorRole(\n params: AdministrationMutationContext & {userId: string; role: AdminRole; reason: string},\n): Promise<AdminGrant> {\n const actorRole = await requireAdminRole({\n userId: params.actorId,\n minimumRole: ADMIN_OWNER_ROLE,\n });\n return await grantAdminRoleWithAudit({\n userId: params.userId,\n role: params.role,\n actorId: params.actorId,\n idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),\n requestFingerprint: administrationCommandFingerprint(GRANT_COMMAND, {\n userId: params.userId,\n role: params.role,\n reason: params.reason,\n }),\n event: administrationEvent({\n actorId: params.actorId,\n actorRole,\n command: GRANT_COMMAND,\n targetType: 'user',\n targetId: params.userId,\n reason: params.reason,\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),\n }),\n });\n}\n\nexport async function revokeAdministratorGrant(\n params: AdministrationMutationContext & {grantId: string; reason: string},\n): Promise<AdminGrant> {\n const actorRole = await requireAdminRole({\n userId: params.actorId,\n minimumRole: ADMIN_OWNER_ROLE,\n });\n return await revokeAdminGrantWithAudit({\n grantId: params.grantId,\n actorId: params.actorId,\n idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),\n requestFingerprint: administrationCommandFingerprint(REVOKE_COMMAND, {\n grantId: params.grantId,\n reason: params.reason,\n }),\n event: administrationEvent({\n actorId: params.actorId,\n actorRole,\n command: REVOKE_COMMAND,\n targetType: 'admin-grant',\n targetId: params.grantId,\n reason: params.reason,\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: hashAdministrationValue(params.idempotencyKey),\n }),\n });\n}\n\nexport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n AdminIdempotencyKeyReuseError,\n InvalidAdminBootstrapTokenError,\n LastAdminOwnerError,\n UserNotFoundError,\n};\n"],"names":["createHash","timingSafeEqual","createAdministrationActionEvent","config","bootstrapFirstAdminOwner","bootstrapFirstAdminOwnerInDb","grantAdminRoleWithAudit","listAdminGrantSummaries","revokeAdminGrantWithAudit","findAdministratorUser","findAdministratorUserInDb","requireAdminRole","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","AdminIdempotencyKeyReuseError","InvalidAdminBootstrapTokenError","LastAdminOwnerError","UserNotFoundError","ADMIN_OWNER_ROLE","ADMIN_OBSERVER_ROLE","BOOTSTRAP_COMMAND","GRANT_COMMAND","REVOKE_COMMAND","hashAdministrationValue","value","update","digest","administrationCommandFingerprint","command","input","JSON","stringify","bootstrapTokenMatches","candidate","expected","candidateHash","Buffer","from","expectedHash","administrationEvent","params","actorId","actorRole","requiredRole","targetType","targetId","reason","result","correlationId","idempotencyKeyFingerprint","occurredAt","Date","toISOString","bootstrapToken","ADMIN_BOOTSTRAP_TOKEN","userId","idempotencyKey","requestFingerprint","event","findAdministratorUserSummary","minimumRole","user","id","email","undefined","listAdministratorGrantSummaries","limit","cursor","grants","rows","map","row","grantId","role","createdAt","revokedAt","nextCursor","grantAdministratorRole","revokeAdministratorGrant"],"mappings":"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"}
|
package/dist/core/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,SAAS,EAAc,MAAM,uBAAuB,CAAC;AAOlE,OAAO,EACL,KAAK,2BAA2B,EAEjC,MAAM,0CAA0C,CAAC;AAyBlD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EACL,8BAA8B,EAC9B,qBAAqB,EAErB,uBAAuB,EAMvB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAGrB,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/core/auth.js
CHANGED
|
@@ -13,14 +13,11 @@ import { getCurrentAdminRole } from './admin-role.js';
|
|
|
13
13
|
import { AuthDependencyUnavailableError, EmailNotVerifiedError, EmailTakenError, InvalidCredentialsError, InvitationEmailMismatchError, SignupNotAllowedError, TokenAlreadyUsedError, TokenExpiredError, TokenInvalidError, UserNotFoundError } from './errors.js';
|
|
14
14
|
import { signUserToken } from './jwt.js';
|
|
15
15
|
import { hashPassword, verifyPassword } from './password.js';
|
|
16
|
+
import { createEnvironmentSignupPolicy } from './signup-policy.js';
|
|
16
17
|
const RESET_TTL_HOURS = 1;
|
|
17
18
|
const PASSWORD_VERIFICATION_PURPOSE = 'password-verification';
|
|
18
19
|
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
|
-
};
|
|
20
|
+
const defaultSignupPolicy = createEnvironmentSignupPolicy();
|
|
24
21
|
async function assertSignupAllowed(params) {
|
|
25
22
|
const result = await (params.signupPolicy ?? defaultSignupPolicy).isSignupAllowed({
|
|
26
23
|
email: params.email,
|