@shipfox/api-auth 10.2.0 → 11.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 +14 -0
- package/README.md +30 -14
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +16 -8
- package/dist/core/auth.js.map +1 -1
- package/dist/core/jwt.d.ts +10 -0
- package/dist/core/jwt.d.ts.map +1 -1
- package/dist/core/jwt.js +3 -2
- package/dist/core/jwt.js.map +1 -1
- package/dist/db/refresh-tokens.d.ts +0 -4
- package/dist/db/refresh-tokens.d.ts.map +1 -1
- package/dist/db/refresh-tokens.js +0 -6
- package/dist/db/refresh-tokens.js.map +1 -1
- package/dist/presentation/auth/jwt-auth.d.ts +1 -4
- package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
- package/dist/presentation/auth/jwt-auth.js +5 -29
- package/dist/presentation/auth/jwt-auth.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/core/auth.test.ts +29 -10
- package/src/core/auth.ts +27 -12
- package/src/core/jwt.test.ts +20 -4
- package/src/core/jwt.ts +2 -1
- package/src/db/refresh-tokens.test.ts +0 -19
- package/src/db/refresh-tokens.ts +0 -23
- package/src/presentation/auth/jwt-auth.test.ts +33 -8
- package/src/presentation/auth/jwt-auth.ts +3 -26
- package/src/presentation/routes/administration.test.ts +9 -1
- package/src/presentation/routes/password/change-password.test.ts +6 -0
- package/src/presentation/routes/password/password-reset-confirm.test.ts +14 -0
- package/src/presentation/routes/registration/signup.test.ts +2 -2
- package/src/presentation/routes/session/logout.test.ts +6 -0
- package/src/presentation/routes/session/refresh.test.ts +11 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
$ shipfox-swc
|
|
2
|
-
Successfully compiled: 67 files with swc (
|
|
2
|
+
Successfully compiled: 67 files with swc (446.61ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @shipfox/api-auth
|
|
2
2
|
|
|
3
|
+
## 11.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 25158c8: Carry workspace lifecycle status in JWT membership claims and enforce suspended or inactive access at the stateless workspace gate while keeping access-token verification stateless.
|
|
8
|
+
|
|
9
|
+
`getAuthenticatedSessionContext()` now reads refresh-session metadata from verified access-token claims without checking active refresh-session state; revoking a refresh session does not invalidate an already-issued access token.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [25158c8]
|
|
14
|
+
- @shipfox/api-auth-context@11.0.0
|
|
15
|
+
- @shipfox/api-workspaces-dto@11.0.0
|
|
16
|
+
|
|
3
17
|
## 10.2.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -70,6 +70,17 @@ Required environment:
|
|
|
70
70
|
| `CLIENT_BASE_URL` | `http://localhost:5173` | Base URL used in email verification and password reset links. |
|
|
71
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. |
|
|
72
72
|
|
|
73
|
+
The recommended access-token lifetime is 15 minutes. Because user JWTs are
|
|
74
|
+
verified from their claims, a token issued before logout, password changes,
|
|
75
|
+
session revocation, or workspace suspension remains usable until its configured
|
|
76
|
+
`exp` time. Logout and session revocation block refresh-session renewal, while
|
|
77
|
+
password changes revoke other refresh sessions and retain the current session
|
|
78
|
+
when its refresh cookie is valid. Workspace suspension is carried into the next
|
|
79
|
+
status-aware snapshot, where the stateless workspace gate rejects member access;
|
|
80
|
+
none of these actions retroactively invalidate an issued access token. Newly
|
|
81
|
+
issued and refreshed tokens carry workspace lifecycle status so the gate can
|
|
82
|
+
return the stable suspended or inactive access result.
|
|
83
|
+
|
|
73
84
|
Email delivery uses the shared `@shipfox/node-mailer` configuration.
|
|
74
85
|
|
|
75
86
|
The signup gate is off by default. When it is on, provide at least one non-empty
|
|
@@ -99,8 +110,9 @@ itself.
|
|
|
99
110
|
### User session token
|
|
100
111
|
|
|
101
112
|
- **Design:** a stateless session token. It carries the user's identity and
|
|
102
|
-
|
|
103
|
-
a per-request user or
|
|
113
|
+
workspace membership list, including each workspace's lifecycle status, in
|
|
114
|
+
its claims so protected routes can authorize without a per-request user or
|
|
115
|
+
membership lookup.
|
|
104
116
|
- **Audience:** signed-in users on first-party clients (web app, CLI).
|
|
105
117
|
- **Grants:** the user's own account routes and, downstream, any route that
|
|
106
118
|
authorizes against the membership claims. Its scope is "this user, with these
|
|
@@ -111,12 +123,12 @@ itself.
|
|
|
111
123
|
fresh token when it expires.
|
|
112
124
|
- *Store* — held in client memory only, never persisted to disk or local
|
|
113
125
|
storage. Refresh sessions are stored server-side as hashes, never in raw form.
|
|
114
|
-
- *Discard* — expires
|
|
115
|
-
|
|
126
|
+
- *Discard* — expires at the configured `exp` time. Revoking the refresh
|
|
127
|
+
session blocks renewal but does not invalidate the issued access token.
|
|
116
128
|
- **Tradeoff — stale memberships:** because memberships ride in the claims, a
|
|
117
|
-
membership change only takes effect on the next
|
|
118
|
-
|
|
119
|
-
membership read is avoided.
|
|
129
|
+
membership or workspace-status change only takes effect on the next token
|
|
130
|
+
issuance or refresh. Accepted because the recommended lifetime is 15 minutes,
|
|
131
|
+
so the staleness window is bounded and a per-request membership read is avoided.
|
|
120
132
|
|
|
121
133
|
### Runner session token
|
|
122
134
|
|
|
@@ -279,9 +291,9 @@ All routes are mounted under `/admin/auth/users` and require an authenticated be
|
|
|
279
291
|
|
|
280
292
|
| Method | Path | Required role | Result |
|
|
281
293
|
| --- | --- | --- | --- |
|
|
282
|
-
| `POST` | `/:userId/suspend` | `admin-operator` | Suspends the user, revokes all
|
|
294
|
+
| `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`. |
|
|
283
295
|
| `POST` | `/:userId/reactivate` | `admin-operator` | Restores sign-in eligibility without restoring revoked sessions. |
|
|
284
|
-
| `POST` | `/:userId/revoke-sessions` | `admin-operator` | Revokes all
|
|
296
|
+
| `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`. |
|
|
285
297
|
|
|
286
298
|
Each command requires an `Idempotency-Key`. Suspension requires a bounded reason; reactivation and session revocation accept an optional bounded reason. Repeating a successful command returns its committed result. Reusing a key for a different command or request returns `409 idempotency-key-reused`.
|
|
287
299
|
|
|
@@ -311,7 +323,7 @@ It also exports lower-level pieces for tests and advanced integration:
|
|
|
311
323
|
- `issueJobLeaseToken(claims)` / `verifyJobLeaseToken(token)`: mint and verify job lease tokens.
|
|
312
324
|
- `createEnvironmentSignupPolicy()`: creates the environment-backed signup policy used by default. Pass `signupPolicy` to `createAuthModule` to replace it.
|
|
313
325
|
- `getClientContext(request)`: reads the authenticated user context from a Fastify request.
|
|
314
|
-
- `getAuthenticatedSessionContext(request)`:
|
|
326
|
+
- `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.
|
|
315
327
|
- `findUserByEmail({email})`: read-only lookup of the current owner of a normalized email; see below.
|
|
316
328
|
- Entity types: `User`, `UserStatus`, `RefreshToken`, `PasswordReset`, and `EmailOwner`.
|
|
317
329
|
|
|
@@ -416,9 +428,9 @@ const session = await getAuthenticatedSessionContext(request);
|
|
|
416
428
|
```
|
|
417
429
|
|
|
418
430
|
The refresh-session ID remains the same when its refresh token rotates. The
|
|
419
|
-
helper
|
|
420
|
-
|
|
421
|
-
|
|
431
|
+
helper does not query the refresh-session table. It returns the standard `401
|
|
432
|
+
unauthorized` error when the verified request context is missing, inconsistent,
|
|
433
|
+
or has no refresh-session ID. It exposes neither refresh-token material nor
|
|
422
434
|
storage details.
|
|
423
435
|
|
|
424
436
|
## Data Model
|
|
@@ -441,7 +453,11 @@ Passwords use Argon2id. Password reset tokens and refresh tokens are opaque toke
|
|
|
441
453
|
- A denied password signup returns `403` with error code `signup-not-allowed`. The policy message is capped at 500 characters.
|
|
442
454
|
- Invitation signup validates the invitation and bypasses the signup policy. Invitations remain a separate authorization path.
|
|
443
455
|
- Login only succeeds for active users with verified email addresses and a password hash.
|
|
444
|
-
-
|
|
456
|
+
- Login and refresh reject suspended or deleted users. Current-state reads such
|
|
457
|
+
as `/auth/me` still return the persisted suspended-user status. Ordinary JWT
|
|
458
|
+
verification does not query user or refresh-session rows.
|
|
459
|
+
- Revoking a refresh session blocks renewal. An already-issued access token
|
|
460
|
+
remains valid until its configured `exp` time.
|
|
445
461
|
- Refresh tokens rotate on each refresh.
|
|
446
462
|
- Password reset tokens and email challenge proofs are consumed once.
|
|
447
463
|
- Password change revokes other refresh sessions. It keeps the current session when the current refresh cookie is valid.
|
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":"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;AA6I7C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,YAAY,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;CACzC;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACjC,YAAY,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;CACzC;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB9E;AAED,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG;IAChC,cAAc,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,qBAAqB,EAAE,IAAI,CAAA;KAAC,CAAC;CAC3D,CAAC;AAEF,wBAAsB,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CA8C9F;AAED,MAAM,WAAW,0BAA2B,SAAQ,YAAY;IAC9D,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,2BAA2B,CAAC;CACzC;AAED,MAAM,WAAW,0BAA2B,SAAQ,WAAW;IAC7D,UAAU,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAC,GAAG,IAAI,CAAC;IACrE,WAAW,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAC,CAAC;CAC/C;AAED,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,0BAA0B,CAAC,CAiErC;AAED,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAexE;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,2BAA2B,CAAC;CACzC;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;IACX,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAsB,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAwBrE;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,UAAU,EAAE,2BAA2B,CAAC;CACzC;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;IACX,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,MAAM,yBAAyB,GACjC,8BAA8B,GAC9B,qBAAqB,GACrB,uBAAuB,GACvB,iBAAiB,CAAC;AAEtB,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,0BAA0B,CAAC,CAoBrC;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,IAAI,EAAE,IAAI,CAAC;IACX,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,2BAA2B,CAAC;CACzC,GAAG,OAAO,CAAC,wBAAwB,CAAC,CA+DpC;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;IACX,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,6BAA6B;IAC5C,qBAAqB,EAAE,IAAI,CAAC;CAC7B;AAED,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,2BAA2B,CAAC;CACzC,GAAG,OAAO,CAAC,8BAA8B,CAAC,CAqB1C;AAED,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,6BAA6B,CAAC,CAYzC;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBjF;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;IACX,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,2BAA2B,CAAC;CACzC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAsBtC;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBhB;AAED,wBAAsB,MAAM,CAAC,MAAM,EAAE;IAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAOvF;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAO5F"}
|
package/dist/core/auth.js
CHANGED
|
@@ -46,17 +46,20 @@ function daysFromNow(days) {
|
|
|
46
46
|
function recordRefreshOutcome(outcome) {
|
|
47
47
|
recordTokenRefreshed(outcome);
|
|
48
48
|
}
|
|
49
|
-
async function
|
|
49
|
+
async function loadTokenMemberships(userId, workspaces) {
|
|
50
50
|
const memberships = await workspaces.listMembershipsForTokenClaims({
|
|
51
|
-
userId
|
|
51
|
+
userId
|
|
52
52
|
}).catch((error)=>{
|
|
53
53
|
throw new AuthDependencyUnavailableError('workspaces', error);
|
|
54
54
|
});
|
|
55
|
+
return memberships.memberships;
|
|
56
|
+
}
|
|
57
|
+
async function signAccessToken(user, memberships, refreshSessionId) {
|
|
55
58
|
return await signUserToken({
|
|
56
59
|
userId: user.id,
|
|
57
60
|
email: user.email,
|
|
58
61
|
name: user.name,
|
|
59
|
-
memberships
|
|
62
|
+
memberships,
|
|
60
63
|
refreshSessionId,
|
|
61
64
|
secret: userAccessTokenKey(),
|
|
62
65
|
expiresIn: config.AUTH_JWT_EXPIRES_IN
|
|
@@ -83,7 +86,8 @@ async function createRefreshSession(user, refreshSessionId) {
|
|
|
83
86
|
}
|
|
84
87
|
async function createSessionTokens(user, workspaces) {
|
|
85
88
|
const refreshSessionId = crypto.randomUUID();
|
|
86
|
-
const
|
|
89
|
+
const memberships = await loadTokenMemberships(user.id, workspaces);
|
|
90
|
+
const token = await signAccessToken(user, memberships, refreshSessionId);
|
|
87
91
|
const adminRole = await getCurrentAdminRole({
|
|
88
92
|
userId: user.id
|
|
89
93
|
});
|
|
@@ -248,7 +252,7 @@ export async function signupWithInvitation(params) {
|
|
|
248
252
|
};
|
|
249
253
|
}
|
|
250
254
|
}
|
|
251
|
-
// Step 4: Issue session.
|
|
255
|
+
// Step 4: Issue session. createSessionTokens reads memberships through the
|
|
252
256
|
// workspaces module API, so a successful accept is reflected in the JWT.
|
|
253
257
|
const { token, refreshToken, adminRole } = await createSessionTokens(user, params.workspaces);
|
|
254
258
|
if (acceptError) {
|
|
@@ -367,7 +371,8 @@ export async function refreshAccessToken(params) {
|
|
|
367
371
|
// second tab); past it, reuse of a retired token means a compromised session.
|
|
368
372
|
if (current.rotatedAt) {
|
|
369
373
|
if (isWithinRotationGrace(current)) {
|
|
370
|
-
const
|
|
374
|
+
const memberships = await loadTokenMemberships(user.id, params.workspaces);
|
|
375
|
+
const token = await signAccessToken(user, memberships, current.sessionId);
|
|
371
376
|
recordRefreshOutcome('grace');
|
|
372
377
|
return {
|
|
373
378
|
token,
|
|
@@ -382,6 +387,9 @@ export async function refreshAccessToken(params) {
|
|
|
382
387
|
recordRefreshOutcome('rejected');
|
|
383
388
|
throw new TokenInvalidError('Refresh token reused after rotation');
|
|
384
389
|
}
|
|
390
|
+
// Keep the external membership snapshot ahead of rotation so an outage
|
|
391
|
+
// cannot retire a refresh token that the caller can still retry.
|
|
392
|
+
const memberships = await loadTokenMemberships(user.id, params.workspaces);
|
|
385
393
|
// Losing the CAS requires a state check: another request may have rotated,
|
|
386
394
|
// revoked, or expired the token before this refresh could claim it.
|
|
387
395
|
const nextRefreshToken = generateOpaqueToken('refreshToken');
|
|
@@ -407,7 +415,7 @@ export async function refreshAccessToken(params) {
|
|
|
407
415
|
recordRefreshOutcome('rejected');
|
|
408
416
|
throw new TokenInvalidError('Refresh token is invalid or expired');
|
|
409
417
|
}
|
|
410
|
-
const token = await signAccessToken(user,
|
|
418
|
+
const token = await signAccessToken(user, memberships, latest.sessionId);
|
|
411
419
|
recordRefreshOutcome('grace');
|
|
412
420
|
return {
|
|
413
421
|
token,
|
|
@@ -416,7 +424,7 @@ export async function refreshAccessToken(params) {
|
|
|
416
424
|
adminRole
|
|
417
425
|
};
|
|
418
426
|
}
|
|
419
|
-
const token = await signAccessToken(user,
|
|
427
|
+
const token = await signAccessToken(user, memberships, current.sessionId);
|
|
420
428
|
recordRefreshOutcome('rotated');
|
|
421
429
|
return {
|
|
422
430
|
token,
|
package/dist/core/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/auth.ts"],"sourcesContent":["import {type AdminRole, emailSchema} from '@shipfox/api-auth-dto';\nimport {\n confirmEmailChallenge,\n consumeEmailChallengeProof,\n createEmailChallenge,\n resendEmailChallenge,\n} from '@shipfox/api-email-challenges';\nimport {\n type WorkspacesInterModuleClient,\n workspacesInterModuleContract,\n} from '@shipfox/api-workspaces-dto/inter-module';\nimport {isInterModuleKnownError} from '@shipfox/inter-module';\nimport {userAccessTokenKey} from '@shipfox/node-auth-root-key';\nimport {generateOpaqueToken, hashOpaqueToken} from '@shipfox/node-tokens';\nimport {config} from '#config.js';\nimport {consumePasswordReset, createPasswordReset} from '#db/password-resets.js';\nimport {\n createRefreshTokenForActiveUser,\n findActiveRefreshTokenByHash,\n findRefreshTokenByHash,\n revokeRefreshSession,\n revokeRefreshTokensForUser,\n rotateRefreshToken,\n} from '#db/refresh-tokens.js';\nimport {\n createUser as createDbUser,\n findUserByEmail,\n findUserById,\n markEmailVerified,\n provisionUser as provisionDbUser,\n updateUserPassword,\n} from '#db/users.js';\nimport {type AuthTokenRefreshOutcome, recordTokenRefreshed} from '#metrics/index.js';\nimport {getCurrentAdminRole} from './admin-role.js';\nimport type {RefreshToken} from './entities/refresh-token.js';\nimport type {User} from './entities/user.js';\nimport {\n AuthDependencyUnavailableError,\n EmailNotVerifiedError,\n EmailTakenError,\n InvalidCredentialsError,\n InvitationEmailMismatchError,\n SignupNotAllowedError,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError,\n UserNotFoundError,\n} from './errors.js';\nimport {signUserToken} from './jwt.js';\nimport {hashPassword, verifyPassword} from './password.js';\nimport type {SignupPolicy} from './ports.js';\nimport {createEnvironmentSignupPolicy} from './signup-policy.js';\n\nconst RESET_TTL_HOURS = 1;\nconst PASSWORD_VERIFICATION_PURPOSE = 'password-verification';\nconst DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE =\n 'This Shipfox deployment does not accept new accounts right now.';\n\nconst defaultSignupPolicy: SignupPolicy = createEnvironmentSignupPolicy();\n\nasync function assertSignupAllowed(params: {\n signupPolicy?: SignupPolicy | undefined;\n email: string;\n emailVerified: boolean;\n source: string;\n}): Promise<void> {\n const result = await (params.signupPolicy ?? defaultSignupPolicy).isSignupAllowed({\n email: params.email,\n emailVerified: params.emailVerified,\n source: params.source,\n });\n\n if (!result.allowed) {\n throw new SignupNotAllowedError(result.message ?? DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE);\n }\n}\n\nlet dummyHashCache: string | undefined;\nasync function getDummyHash(): Promise<string> {\n if (!dummyHashCache) {\n dummyHashCache = await hashPassword({password: 'dummy-for-timing-parity'});\n }\n return dummyHashCache;\n}\n\nfunction hoursFromNow(hours: number): Date {\n return new Date(Date.now() + hours * 60 * 60 * 1000);\n}\n\nfunction daysFromNow(days: number): Date {\n return new Date(Date.now() + days * 24 * 60 * 60 * 1000);\n}\n\nfunction recordRefreshOutcome(outcome: AuthTokenRefreshOutcome): void {\n recordTokenRefreshed(outcome);\n}\n\nasync function signAccessToken(\n user: User,\n workspaces: WorkspacesInterModuleClient,\n refreshSessionId: string,\n): Promise<string> {\n const memberships = await workspaces\n .listMembershipsForTokenClaims({userId: user.id})\n .catch((error: unknown) => {\n throw new AuthDependencyUnavailableError('workspaces', error);\n });\n return await signUserToken({\n userId: user.id,\n email: user.email,\n name: user.name,\n memberships: memberships.memberships,\n refreshSessionId,\n secret: userAccessTokenKey(),\n expiresIn: config.AUTH_JWT_EXPIRES_IN,\n });\n}\n\nfunction isWithinRotationGrace(refreshToken: RefreshToken): boolean {\n if (!refreshToken.rotatedAt) return false;\n const graceMs = config.AUTH_REFRESH_ROTATION_GRACE_SECONDS * 1000;\n return Date.now() - refreshToken.rotatedAt.getTime() <= graceMs;\n}\n\nasync function createRefreshSession(\n user: User,\n refreshSessionId: string,\n): Promise<{refreshToken: string; refreshSessionId: string}> {\n const refreshToken = generateOpaqueToken('refreshToken');\n const session = await createRefreshTokenForActiveUser({\n sessionId: refreshSessionId,\n userId: user.id,\n hashedToken: hashOpaqueToken(refreshToken),\n expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS),\n });\n if (!session) throw new InvalidCredentialsError();\n return {refreshToken, refreshSessionId: session.sessionId};\n}\n\nasync function createSessionTokens(\n user: User,\n workspaces: WorkspacesInterModuleClient,\n): Promise<{token: string; refreshToken: string; adminRole: AdminRole | null}> {\n const refreshSessionId = crypto.randomUUID();\n const token = await signAccessToken(user, workspaces, refreshSessionId);\n const adminRole = await getCurrentAdminRole({userId: user.id});\n const {refreshToken} = await createRefreshSession(user, refreshSessionId);\n return {token, refreshToken, adminRole};\n}\n\nfunction passwordResetLink(rawToken: string): string {\n return `${config.CLIENT_BASE_URL}/auth/reset?token=${rawToken}`;\n}\n\nfunction invitationErrorFromKnownError(\n error: unknown,\n method: 'preflightInvitationAcceptance' | 'acceptInvitation',\n): Error {\n const contractMethod = workspacesInterModuleContract.methods[method];\n if (!isInterModuleKnownError(contractMethod, error)) return error as Error;\n\n if (error.code === 'invitation-token-invalid')\n return new TokenInvalidError('Invitation token is invalid');\n if (error.code === 'invitation-token-used') return new TokenAlreadyUsedError();\n if (error.code === 'invitation-token-expired') return new TokenExpiredError();\n return new InvitationEmailMismatchError();\n}\n\nfunction invitationErrorMessage(\n code:\n | 'invitation-token-invalid'\n | 'invitation-token-used'\n | 'invitation-token-expired'\n | 'invitation-email-mismatch',\n): string {\n if (code === 'invitation-token-invalid') return 'Invitation token is invalid';\n if (code === 'invitation-token-used') return 'Invitation has already been accepted';\n if (code === 'invitation-token-expired') return 'Invitation has expired';\n return 'Signup email does not match the invitation';\n}\n\nexport interface SignupParams {\n email: string;\n password: string;\n name?: string | undefined;\n signupPolicy?: SignupPolicy | undefined;\n}\n\nexport interface ProvisionUserParams {\n email: string;\n name?: string | null | undefined;\n signupPolicy?: SignupPolicy | undefined;\n}\n\n/**\n * Creates a verified, password-less user for an external identity provider.\n * Existing users are returned unchanged, including their password and profile.\n */\nexport async function provisionUser(params: ProvisionUserParams): Promise<User> {\n const email = emailSchema.parse(params.email);\n const existing = await findUserByEmail({email});\n if (existing) return existing;\n\n await assertSignupAllowed({\n signupPolicy: params.signupPolicy,\n email,\n emailVerified: true,\n source: 'external-identity',\n });\n\n return await provisionDbUser({\n email,\n name: params.name ?? null,\n });\n}\n\nexport type SignupResult = User & {\n emailChallenge: {id: string; nextResendAvailableAt: Date};\n};\n\nexport async function signup(params: SignupParams & {sourceIp?: string}): Promise<SignupResult> {\n const email = emailSchema.parse(params.email);\n const existing = await findUserByEmail({email});\n if (existing) {\n const canResumeVerification =\n existing.status === 'active' &&\n existing.emailVerifiedAt === null &&\n existing.hashedPassword !== null &&\n (await verifyPassword({password: params.password, hash: existing.hashedPassword}));\n if (canResumeVerification) {\n const emailChallenge = await createEmailChallenge({\n email: existing.email,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: existing.id,\n idempotencyKey: existing.id,\n sourceIp: params.sourceIp ?? '0.0.0.0',\n });\n return {...existing, emailChallenge};\n }\n throw new EmailTakenError(email);\n }\n\n await assertSignupAllowed({\n signupPolicy: params.signupPolicy,\n email,\n emailVerified: false,\n source: 'password',\n });\n\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email,\n hashedPassword,\n name: params.name ?? null,\n signedUp: {viaInvitation: false},\n });\n\n const emailChallenge = await createEmailChallenge({\n email: user.email,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: user.id,\n idempotencyKey: user.id,\n sourceIp: params.sourceIp ?? '0.0.0.0',\n });\n\n return {...user, emailChallenge};\n}\n\nexport interface SignupWithInvitationParams extends SignupParams {\n invitationToken: string;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface SignupWithInvitationResult extends LoginResult {\n membership: {id: string; userId: string; workspaceId: string} | null;\n acceptError?: {code: string; message: string};\n}\n\nexport async function signupWithInvitation(\n params: SignupWithInvitationParams,\n): Promise<SignupWithInvitationResult> {\n // Step 1: Pre-validate the invitation BEFORE any user write so an invalid\n // token never produces an orphan user. Re-validation happens again inside\n // acceptWorkspaceInvitation (race-safe) after the user is created.\n try {\n await params.workspaces.preflightInvitationAcceptance({\n token: params.invitationToken,\n email: params.email,\n });\n } catch (error) {\n throw invitationErrorFromKnownError(error, 'preflightInvitationAcceptance');\n }\n\n // Step 2: Create the user as verified. The invitation email is the proof of\n // ownership, and this avoids a second auth-table write just to mark verified.\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n emailVerifiedAt: new Date(),\n signedUp: {viaInvitation: true},\n });\n\n // Step 3: Accept the invitation through the workspaces module boundary.\n // This is intentionally not one database transaction with auth; treat it like\n // calling another service. If it fails after user creation, return a verified\n // signed-in user and an explicit retryable accept error.\n let membership: SignupWithInvitationResult['membership'] = null;\n let acceptError: SignupWithInvitationResult['acceptError'];\n try {\n const result = await params.workspaces.acceptInvitation({\n token: params.invitationToken,\n userId: user.id,\n email: user.email,\n name: user.name,\n });\n membership = {\n id: result.membership.id,\n userId: result.membership.userId,\n workspaceId: result.membership.workspaceId,\n };\n } catch (error) {\n if (isInterModuleKnownError(workspacesInterModuleContract.methods.acceptInvitation, error)) {\n acceptError = {code: error.code, message: invitationErrorMessage(error.code)};\n } else {\n acceptError = {\n code: 'AcceptFailed',\n message: 'Could not accept the invitation; please retry from the invite link.',\n };\n }\n }\n\n // Step 4: Issue session. signAccessToken reads memberships through the\n // workspaces module API, so a successful accept is reflected in the JWT.\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n if (acceptError) {\n return {token, refreshToken, user, membership, acceptError, adminRole};\n }\n return {token, refreshToken, user, membership, adminRole};\n}\n\nexport interface CreateUserParams extends SignupParams {\n verified: boolean;\n}\n\nexport async function createUser(params: CreateUserParams): Promise<User> {\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n emailVerifiedAt: params.verified ? new Date() : null,\n });\n\n return user;\n}\n\nexport interface LoginParams {\n email: string;\n password: string;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface LoginResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport async function login(params: LoginParams): Promise<LoginResult> {\n const user = await findUserByEmail({email: params.email});\n\n if (!user) {\n await verifyPassword({password: params.password, hash: await getDummyHash()});\n throw new InvalidCredentialsError();\n }\n\n const hasPassword = user.hashedPassword !== null;\n const ok = await verifyPassword({\n password: params.password,\n hash: user.hashedPassword ?? (await getDummyHash()),\n });\n if (!hasPassword || !ok || user.status !== 'active') {\n throw new InvalidCredentialsError();\n }\n\n if (user.emailVerifiedAt === null) {\n throw new EmailNotVerifiedError();\n }\n\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n return {token, refreshToken, user, adminRole};\n}\n\nexport interface CreateSessionForUserParams {\n userId?: string | undefined;\n email?: string | undefined;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface CreateSessionForUserResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport type CreateSessionForUserError =\n | AuthDependencyUnavailableError\n | EmailNotVerifiedError\n | InvalidCredentialsError\n | UserNotFoundError;\n\nexport async function createSessionForUser(\n params: CreateSessionForUserParams,\n): Promise<CreateSessionForUserResult> {\n const user = params.userId\n ? await findUserById({id: params.userId})\n : params.email\n ? await findUserByEmail({email: emailSchema.parse(params.email)})\n : undefined;\n\n if (!user) {\n throw new UserNotFoundError(params.userId ?? params.email ?? 'unknown');\n }\n if (user.emailVerifiedAt === null) {\n throw new EmailNotVerifiedError();\n }\n if (user.status !== 'active') {\n throw new InvalidCredentialsError();\n }\n\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n return {token, refreshToken, user, adminRole};\n}\n\nexport interface RefreshAccessTokenResult {\n token: string;\n /** Undefined on a grace-window hit: keep the existing cookie instead of rotating it. */\n refreshToken: string | undefined;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport async function refreshAccessToken(params: {\n refreshToken: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<RefreshAccessTokenResult> {\n const currentHashedToken = hashOpaqueToken(params.refreshToken);\n const current = await findRefreshTokenByHash({hashedToken: currentHashedToken});\n if (!current) {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n\n const user = await findUserById({id: current.userId});\n if (user?.status !== 'active') {\n await revokeRefreshSession({sessionId: current.sessionId, userId: current.userId});\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const adminRole = await getCurrentAdminRole({userId: user.id});\n\n // Within the grace window a rotated token means a concurrent refresh (e.g. a\n // second tab); past it, reuse of a retired token means a compromised session.\n if (current.rotatedAt) {\n if (isWithinRotationGrace(current)) {\n const token = await signAccessToken(user, params.workspaces, current.sessionId);\n recordRefreshOutcome('grace');\n return {token, refreshToken: undefined, user, adminRole};\n }\n await revokeRefreshTokensForUser({userId: user.id});\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token reused after rotation');\n }\n\n // Losing the CAS requires a state check: another request may have rotated,\n // revoked, or expired the token before this refresh could claim it.\n const nextRefreshToken = generateOpaqueToken('refreshToken');\n const rotated = await rotateRefreshToken({\n userId: current.userId,\n id: current.id,\n currentHashedToken,\n nextHashedToken: hashOpaqueToken(nextRefreshToken),\n expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS),\n });\n if (!rotated) {\n const latestUser = await findUserById({id: current.userId});\n if (latestUser?.status !== 'active') {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const latest = await findRefreshTokenByHash({hashedToken: currentHashedToken});\n if (!latest || !isWithinRotationGrace(latest)) {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const token = await signAccessToken(user, params.workspaces, latest.sessionId);\n recordRefreshOutcome('grace');\n return {token, refreshToken: undefined, user, adminRole};\n }\n\n const token = await signAccessToken(user, params.workspaces, current.sessionId);\n recordRefreshOutcome('rotated');\n return {token, refreshToken: nextRefreshToken, user, adminRole};\n}\n\nexport interface ConfirmEmailVerificationResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport interface ResendEmailVerificationResult {\n nextResendAvailableAt: Date;\n}\n\nexport async function confirmEmailVerification(params: {\n email: string;\n challengeId: string;\n code: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<ConfirmEmailVerificationResult> {\n const user = await findUserByEmail({email: emailSchema.parse(params.email)});\n if (!user) throw new TokenInvalidError('Verification code is invalid or expired');\n await confirmEmailChallenge({id: params.challengeId, code: params.code, continuation: user.id});\n await consumeEmailChallengeProof({\n id: params.challengeId,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: user.id,\n });\n\n const verifiedUser = await markEmailVerified({userId: user.id});\n if (verifiedUser?.status !== 'active') {\n throw new TokenInvalidError('Verification code is invalid or expired');\n }\n\n const {token, refreshToken, adminRole} = await createSessionTokens(\n verifiedUser,\n params.workspaces,\n );\n\n return {token, refreshToken, user: verifiedUser, adminRole};\n}\n\nexport async function resendEmailVerification(params: {\n email: string;\n challengeId: string;\n sourceIp: string;\n}): Promise<ResendEmailVerificationResult> {\n const user = await findUserByEmail({email: emailSchema.parse(params.email)});\n if (!user || user.emailVerifiedAt || user.status !== 'active') {\n return {nextResendAvailableAt: new Date()};\n }\n const result = await resendEmailChallenge({\n id: params.challengeId,\n continuation: user.id,\n sourceIp: params.sourceIp,\n });\n\n return {nextResendAvailableAt: result.nextResendAvailableAt};\n}\n\nexport async function requestPasswordReset(params: {email: string}): Promise<void> {\n const user = await findUserByEmail({email: params.email});\n if (user?.status !== 'active' || user.hashedPassword === null) {\n return;\n }\n\n const rawToken = generateOpaqueToken('passwordReset');\n await createPasswordReset({\n userId: user.id,\n hashedToken: hashOpaqueToken(rawToken),\n expiresAt: hoursFromNow(RESET_TTL_HOURS),\n sendEmail: {\n email: user.email,\n resetLink: passwordResetLink(rawToken),\n expiresInHours: RESET_TTL_HOURS,\n },\n });\n}\n\nexport interface ConfirmPasswordResetResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport async function confirmPasswordReset(params: {\n token: string;\n newPassword: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<ConfirmPasswordResetResult> {\n const consumed = await consumePasswordReset({hashedToken: hashOpaqueToken(params.token)});\n if (!consumed) {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n const existingUser = await findUserById({id: consumed.userId});\n if (existingUser?.status !== 'active' || existingUser.hashedPassword === null) {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n const hashedPassword = await hashPassword({password: params.newPassword});\n const user = await updateUserPassword({userId: consumed.userId, hashedPassword});\n if (user?.status !== 'active') {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n await revokeRefreshTokensForUser({userId: consumed.userId});\n\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n return {token, refreshToken, user, adminRole};\n}\n\nexport async function changePassword(params: {\n userId: string;\n currentPassword: string;\n newPassword: string;\n refreshToken?: string | undefined;\n}): Promise<void> {\n const user = await findUserById({id: params.userId});\n if (!user) {\n throw new UserNotFoundError(params.userId);\n }\n\n const hasPassword = user.hashedPassword !== null;\n const ok = await verifyPassword({\n password: params.currentPassword,\n hash: user.hashedPassword ?? (await getDummyHash()),\n });\n if (!hasPassword || !ok) {\n throw new InvalidCredentialsError();\n }\n\n const hashedPassword = await hashPassword({password: params.newPassword});\n const currentRefreshSession = params.refreshToken\n ? await findActiveRefreshTokenByHash({hashedToken: hashOpaqueToken(params.refreshToken)})\n : undefined;\n const exceptRefreshTokenId =\n currentRefreshSession?.userId === user.id ? currentRefreshSession.id : undefined;\n\n await updateUserPassword({userId: user.id, hashedPassword});\n await revokeRefreshTokensForUser({userId: user.id, exceptRefreshTokenId});\n}\n\nexport async function logout(params: {refreshToken?: string | undefined}): Promise<void> {\n if (!params.refreshToken) return;\n const refreshToken = await findRefreshTokenByHash({\n hashedToken: hashOpaqueToken(params.refreshToken),\n });\n if (!refreshToken) return;\n await revokeRefreshSession({sessionId: refreshToken.sessionId, userId: refreshToken.userId});\n}\n\nexport interface GetCurrentUserResult {\n user: User;\n}\n\nexport async function getCurrentUser(params: {userId: string}): Promise<GetCurrentUserResult> {\n const user = await findUserById({id: params.userId});\n if (!user) {\n throw new UserNotFoundError(params.userId);\n }\n\n return {user};\n}\n"],"names":["emailSchema","confirmEmailChallenge","consumeEmailChallengeProof","createEmailChallenge","resendEmailChallenge","workspacesInterModuleContract","isInterModuleKnownError","userAccessTokenKey","generateOpaqueToken","hashOpaqueToken","config","consumePasswordReset","createPasswordReset","createRefreshTokenForActiveUser","findActiveRefreshTokenByHash","findRefreshTokenByHash","revokeRefreshSession","revokeRefreshTokensForUser","rotateRefreshToken","createUser","createDbUser","findUserByEmail","findUserById","markEmailVerified","provisionUser","provisionDbUser","updateUserPassword","recordTokenRefreshed","getCurrentAdminRole","AuthDependencyUnavailableError","EmailNotVerifiedError","EmailTakenError","InvalidCredentialsError","InvitationEmailMismatchError","SignupNotAllowedError","TokenAlreadyUsedError","TokenExpiredError","TokenInvalidError","UserNotFoundError","signUserToken","hashPassword","verifyPassword","createEnvironmentSignupPolicy","RESET_TTL_HOURS","PASSWORD_VERIFICATION_PURPOSE","DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE","defaultSignupPolicy","assertSignupAllowed","params","result","signupPolicy","isSignupAllowed","email","emailVerified","source","allowed","message","dummyHashCache","getDummyHash","password","hoursFromNow","hours","Date","now","daysFromNow","days","recordRefreshOutcome","outcome","signAccessToken","user","workspaces","refreshSessionId","memberships","listMembershipsForTokenClaims","userId","id","catch","error","name","secret","expiresIn","AUTH_JWT_EXPIRES_IN","isWithinRotationGrace","refreshToken","rotatedAt","graceMs","AUTH_REFRESH_ROTATION_GRACE_SECONDS","getTime","createRefreshSession","session","sessionId","hashedToken","expiresAt","AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS","createSessionTokens","crypto","randomUUID","token","adminRole","passwordResetLink","rawToken","CLIENT_BASE_URL","invitationErrorFromKnownError","method","contractMethod","methods","code","invitationErrorMessage","parse","existing","signup","canResumeVerification","status","emailVerifiedAt","hashedPassword","hash","emailChallenge","purpose","continuation","idempotencyKey","sourceIp","signedUp","viaInvitation","signupWithInvitation","preflightInvitationAcceptance","invitationToken","membership","acceptError","acceptInvitation","workspaceId","verified","login","hasPassword","ok","createSessionForUser","undefined","refreshAccessToken","currentHashedToken","current","nextRefreshToken","rotated","nextHashedToken","latestUser","latest","confirmEmailVerification","challengeId","verifiedUser","resendEmailVerification","nextResendAvailableAt","requestPasswordReset","sendEmail","resetLink","expiresInHours","confirmPasswordReset","consumed","existingUser","newPassword","changePassword","currentPassword","currentRefreshSession","exceptRefreshTokenId","logout","getCurrentUser"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/core/auth.ts"],"sourcesContent":["import {type AdminRole, emailSchema} from '@shipfox/api-auth-dto';\nimport {\n confirmEmailChallenge,\n consumeEmailChallengeProof,\n createEmailChallenge,\n resendEmailChallenge,\n} from '@shipfox/api-email-challenges';\nimport {\n type WorkspacesInterModuleClient,\n workspacesInterModuleContract,\n} from '@shipfox/api-workspaces-dto/inter-module';\nimport {isInterModuleKnownError} from '@shipfox/inter-module';\nimport {userAccessTokenKey} from '@shipfox/node-auth-root-key';\nimport {generateOpaqueToken, hashOpaqueToken} from '@shipfox/node-tokens';\nimport {config} from '#config.js';\nimport {consumePasswordReset, createPasswordReset} from '#db/password-resets.js';\nimport {\n createRefreshTokenForActiveUser,\n findActiveRefreshTokenByHash,\n findRefreshTokenByHash,\n revokeRefreshSession,\n revokeRefreshTokensForUser,\n rotateRefreshToken,\n} from '#db/refresh-tokens.js';\nimport {\n createUser as createDbUser,\n findUserByEmail,\n findUserById,\n markEmailVerified,\n provisionUser as provisionDbUser,\n updateUserPassword,\n} from '#db/users.js';\nimport {type AuthTokenRefreshOutcome, recordTokenRefreshed} from '#metrics/index.js';\nimport {getCurrentAdminRole} from './admin-role.js';\nimport type {RefreshToken} from './entities/refresh-token.js';\nimport type {User} from './entities/user.js';\nimport {\n AuthDependencyUnavailableError,\n EmailNotVerifiedError,\n EmailTakenError,\n InvalidCredentialsError,\n InvitationEmailMismatchError,\n SignupNotAllowedError,\n TokenAlreadyUsedError,\n TokenExpiredError,\n TokenInvalidError,\n UserNotFoundError,\n} from './errors.js';\nimport {signUserToken, type TokenMembership} from './jwt.js';\nimport {hashPassword, verifyPassword} from './password.js';\nimport type {SignupPolicy} from './ports.js';\nimport {createEnvironmentSignupPolicy} from './signup-policy.js';\n\nconst RESET_TTL_HOURS = 1;\nconst PASSWORD_VERIFICATION_PURPOSE = 'password-verification';\nconst DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE =\n 'This Shipfox deployment does not accept new accounts right now.';\n\nconst defaultSignupPolicy: SignupPolicy = createEnvironmentSignupPolicy();\n\nasync function assertSignupAllowed(params: {\n signupPolicy?: SignupPolicy | undefined;\n email: string;\n emailVerified: boolean;\n source: string;\n}): Promise<void> {\n const result = await (params.signupPolicy ?? defaultSignupPolicy).isSignupAllowed({\n email: params.email,\n emailVerified: params.emailVerified,\n source: params.source,\n });\n\n if (!result.allowed) {\n throw new SignupNotAllowedError(result.message ?? DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE);\n }\n}\n\nlet dummyHashCache: string | undefined;\nasync function getDummyHash(): Promise<string> {\n if (!dummyHashCache) {\n dummyHashCache = await hashPassword({password: 'dummy-for-timing-parity'});\n }\n return dummyHashCache;\n}\n\nfunction hoursFromNow(hours: number): Date {\n return new Date(Date.now() + hours * 60 * 60 * 1000);\n}\n\nfunction daysFromNow(days: number): Date {\n return new Date(Date.now() + days * 24 * 60 * 60 * 1000);\n}\n\nfunction recordRefreshOutcome(outcome: AuthTokenRefreshOutcome): void {\n recordTokenRefreshed(outcome);\n}\n\ntype TokenMemberships = TokenMembership[];\n\nasync function loadTokenMemberships(\n userId: string,\n workspaces: WorkspacesInterModuleClient,\n): Promise<TokenMemberships> {\n const memberships = await workspaces\n .listMembershipsForTokenClaims({userId})\n .catch((error: unknown) => {\n throw new AuthDependencyUnavailableError('workspaces', error);\n });\n return memberships.memberships;\n}\n\nasync function signAccessToken(\n user: User,\n memberships: TokenMemberships,\n refreshSessionId: string,\n): Promise<string> {\n return await signUserToken({\n userId: user.id,\n email: user.email,\n name: user.name,\n memberships,\n refreshSessionId,\n secret: userAccessTokenKey(),\n expiresIn: config.AUTH_JWT_EXPIRES_IN,\n });\n}\n\nfunction isWithinRotationGrace(refreshToken: RefreshToken): boolean {\n if (!refreshToken.rotatedAt) return false;\n const graceMs = config.AUTH_REFRESH_ROTATION_GRACE_SECONDS * 1000;\n return Date.now() - refreshToken.rotatedAt.getTime() <= graceMs;\n}\n\nasync function createRefreshSession(\n user: User,\n refreshSessionId: string,\n): Promise<{refreshToken: string; refreshSessionId: string}> {\n const refreshToken = generateOpaqueToken('refreshToken');\n const session = await createRefreshTokenForActiveUser({\n sessionId: refreshSessionId,\n userId: user.id,\n hashedToken: hashOpaqueToken(refreshToken),\n expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS),\n });\n if (!session) throw new InvalidCredentialsError();\n return {refreshToken, refreshSessionId: session.sessionId};\n}\n\nasync function createSessionTokens(\n user: User,\n workspaces: WorkspacesInterModuleClient,\n): Promise<{token: string; refreshToken: string; adminRole: AdminRole | null}> {\n const refreshSessionId = crypto.randomUUID();\n const memberships = await loadTokenMemberships(user.id, workspaces);\n const token = await signAccessToken(user, memberships, refreshSessionId);\n const adminRole = await getCurrentAdminRole({userId: user.id});\n const {refreshToken} = await createRefreshSession(user, refreshSessionId);\n return {token, refreshToken, adminRole};\n}\n\nfunction passwordResetLink(rawToken: string): string {\n return `${config.CLIENT_BASE_URL}/auth/reset?token=${rawToken}`;\n}\n\nfunction invitationErrorFromKnownError(\n error: unknown,\n method: 'preflightInvitationAcceptance' | 'acceptInvitation',\n): Error {\n const contractMethod = workspacesInterModuleContract.methods[method];\n if (!isInterModuleKnownError(contractMethod, error)) return error as Error;\n\n if (error.code === 'invitation-token-invalid')\n return new TokenInvalidError('Invitation token is invalid');\n if (error.code === 'invitation-token-used') return new TokenAlreadyUsedError();\n if (error.code === 'invitation-token-expired') return new TokenExpiredError();\n return new InvitationEmailMismatchError();\n}\n\nfunction invitationErrorMessage(\n code:\n | 'invitation-token-invalid'\n | 'invitation-token-used'\n | 'invitation-token-expired'\n | 'invitation-email-mismatch',\n): string {\n if (code === 'invitation-token-invalid') return 'Invitation token is invalid';\n if (code === 'invitation-token-used') return 'Invitation has already been accepted';\n if (code === 'invitation-token-expired') return 'Invitation has expired';\n return 'Signup email does not match the invitation';\n}\n\nexport interface SignupParams {\n email: string;\n password: string;\n name?: string | undefined;\n signupPolicy?: SignupPolicy | undefined;\n}\n\nexport interface ProvisionUserParams {\n email: string;\n name?: string | null | undefined;\n signupPolicy?: SignupPolicy | undefined;\n}\n\n/**\n * Creates a verified, password-less user for an external identity provider.\n * Existing users are returned unchanged, including their password and profile.\n */\nexport async function provisionUser(params: ProvisionUserParams): Promise<User> {\n const email = emailSchema.parse(params.email);\n const existing = await findUserByEmail({email});\n if (existing) return existing;\n\n await assertSignupAllowed({\n signupPolicy: params.signupPolicy,\n email,\n emailVerified: true,\n source: 'external-identity',\n });\n\n return await provisionDbUser({\n email,\n name: params.name ?? null,\n });\n}\n\nexport type SignupResult = User & {\n emailChallenge: {id: string; nextResendAvailableAt: Date};\n};\n\nexport async function signup(params: SignupParams & {sourceIp?: string}): Promise<SignupResult> {\n const email = emailSchema.parse(params.email);\n const existing = await findUserByEmail({email});\n if (existing) {\n const canResumeVerification =\n existing.status === 'active' &&\n existing.emailVerifiedAt === null &&\n existing.hashedPassword !== null &&\n (await verifyPassword({password: params.password, hash: existing.hashedPassword}));\n if (canResumeVerification) {\n const emailChallenge = await createEmailChallenge({\n email: existing.email,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: existing.id,\n idempotencyKey: existing.id,\n sourceIp: params.sourceIp ?? '0.0.0.0',\n });\n return {...existing, emailChallenge};\n }\n throw new EmailTakenError(email);\n }\n\n await assertSignupAllowed({\n signupPolicy: params.signupPolicy,\n email,\n emailVerified: false,\n source: 'password',\n });\n\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email,\n hashedPassword,\n name: params.name ?? null,\n signedUp: {viaInvitation: false},\n });\n\n const emailChallenge = await createEmailChallenge({\n email: user.email,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: user.id,\n idempotencyKey: user.id,\n sourceIp: params.sourceIp ?? '0.0.0.0',\n });\n\n return {...user, emailChallenge};\n}\n\nexport interface SignupWithInvitationParams extends SignupParams {\n invitationToken: string;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface SignupWithInvitationResult extends LoginResult {\n membership: {id: string; userId: string; workspaceId: string} | null;\n acceptError?: {code: string; message: string};\n}\n\nexport async function signupWithInvitation(\n params: SignupWithInvitationParams,\n): Promise<SignupWithInvitationResult> {\n // Step 1: Pre-validate the invitation BEFORE any user write so an invalid\n // token never produces an orphan user. Re-validation happens again inside\n // acceptWorkspaceInvitation (race-safe) after the user is created.\n try {\n await params.workspaces.preflightInvitationAcceptance({\n token: params.invitationToken,\n email: params.email,\n });\n } catch (error) {\n throw invitationErrorFromKnownError(error, 'preflightInvitationAcceptance');\n }\n\n // Step 2: Create the user as verified. The invitation email is the proof of\n // ownership, and this avoids a second auth-table write just to mark verified.\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n emailVerifiedAt: new Date(),\n signedUp: {viaInvitation: true},\n });\n\n // Step 3: Accept the invitation through the workspaces module boundary.\n // This is intentionally not one database transaction with auth; treat it like\n // calling another service. If it fails after user creation, return a verified\n // signed-in user and an explicit retryable accept error.\n let membership: SignupWithInvitationResult['membership'] = null;\n let acceptError: SignupWithInvitationResult['acceptError'];\n try {\n const result = await params.workspaces.acceptInvitation({\n token: params.invitationToken,\n userId: user.id,\n email: user.email,\n name: user.name,\n });\n membership = {\n id: result.membership.id,\n userId: result.membership.userId,\n workspaceId: result.membership.workspaceId,\n };\n } catch (error) {\n if (isInterModuleKnownError(workspacesInterModuleContract.methods.acceptInvitation, error)) {\n acceptError = {code: error.code, message: invitationErrorMessage(error.code)};\n } else {\n acceptError = {\n code: 'AcceptFailed',\n message: 'Could not accept the invitation; please retry from the invite link.',\n };\n }\n }\n\n // Step 4: Issue session. createSessionTokens reads memberships through the\n // workspaces module API, so a successful accept is reflected in the JWT.\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n if (acceptError) {\n return {token, refreshToken, user, membership, acceptError, adminRole};\n }\n return {token, refreshToken, user, membership, adminRole};\n}\n\nexport interface CreateUserParams extends SignupParams {\n verified: boolean;\n}\n\nexport async function createUser(params: CreateUserParams): Promise<User> {\n const existing = await findUserByEmail({email: params.email});\n if (existing) {\n throw new EmailTakenError(params.email);\n }\n\n const hashedPassword = await hashPassword({password: params.password});\n const user = await createDbUser({\n email: params.email,\n hashedPassword,\n name: params.name ?? null,\n emailVerifiedAt: params.verified ? new Date() : null,\n });\n\n return user;\n}\n\nexport interface LoginParams {\n email: string;\n password: string;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface LoginResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport async function login(params: LoginParams): Promise<LoginResult> {\n const user = await findUserByEmail({email: params.email});\n\n if (!user) {\n await verifyPassword({password: params.password, hash: await getDummyHash()});\n throw new InvalidCredentialsError();\n }\n\n const hasPassword = user.hashedPassword !== null;\n const ok = await verifyPassword({\n password: params.password,\n hash: user.hashedPassword ?? (await getDummyHash()),\n });\n if (!hasPassword || !ok || user.status !== 'active') {\n throw new InvalidCredentialsError();\n }\n\n if (user.emailVerifiedAt === null) {\n throw new EmailNotVerifiedError();\n }\n\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n return {token, refreshToken, user, adminRole};\n}\n\nexport interface CreateSessionForUserParams {\n userId?: string | undefined;\n email?: string | undefined;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface CreateSessionForUserResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport type CreateSessionForUserError =\n | AuthDependencyUnavailableError\n | EmailNotVerifiedError\n | InvalidCredentialsError\n | UserNotFoundError;\n\nexport async function createSessionForUser(\n params: CreateSessionForUserParams,\n): Promise<CreateSessionForUserResult> {\n const user = params.userId\n ? await findUserById({id: params.userId})\n : params.email\n ? await findUserByEmail({email: emailSchema.parse(params.email)})\n : undefined;\n\n if (!user) {\n throw new UserNotFoundError(params.userId ?? params.email ?? 'unknown');\n }\n if (user.emailVerifiedAt === null) {\n throw new EmailNotVerifiedError();\n }\n if (user.status !== 'active') {\n throw new InvalidCredentialsError();\n }\n\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n return {token, refreshToken, user, adminRole};\n}\n\nexport interface RefreshAccessTokenResult {\n token: string;\n /** Undefined on a grace-window hit: keep the existing cookie instead of rotating it. */\n refreshToken: string | undefined;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport async function refreshAccessToken(params: {\n refreshToken: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<RefreshAccessTokenResult> {\n const currentHashedToken = hashOpaqueToken(params.refreshToken);\n const current = await findRefreshTokenByHash({hashedToken: currentHashedToken});\n if (!current) {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n\n const user = await findUserById({id: current.userId});\n if (user?.status !== 'active') {\n await revokeRefreshSession({sessionId: current.sessionId, userId: current.userId});\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const adminRole = await getCurrentAdminRole({userId: user.id});\n\n // Within the grace window a rotated token means a concurrent refresh (e.g. a\n // second tab); past it, reuse of a retired token means a compromised session.\n if (current.rotatedAt) {\n if (isWithinRotationGrace(current)) {\n const memberships = await loadTokenMemberships(user.id, params.workspaces);\n const token = await signAccessToken(user, memberships, current.sessionId);\n recordRefreshOutcome('grace');\n return {token, refreshToken: undefined, user, adminRole};\n }\n await revokeRefreshTokensForUser({userId: user.id});\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token reused after rotation');\n }\n\n // Keep the external membership snapshot ahead of rotation so an outage\n // cannot retire a refresh token that the caller can still retry.\n const memberships = await loadTokenMemberships(user.id, params.workspaces);\n\n // Losing the CAS requires a state check: another request may have rotated,\n // revoked, or expired the token before this refresh could claim it.\n const nextRefreshToken = generateOpaqueToken('refreshToken');\n const rotated = await rotateRefreshToken({\n userId: current.userId,\n id: current.id,\n currentHashedToken,\n nextHashedToken: hashOpaqueToken(nextRefreshToken),\n expiresAt: daysFromNow(config.AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS),\n });\n if (!rotated) {\n const latestUser = await findUserById({id: current.userId});\n if (latestUser?.status !== 'active') {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const latest = await findRefreshTokenByHash({hashedToken: currentHashedToken});\n if (!latest || !isWithinRotationGrace(latest)) {\n recordRefreshOutcome('rejected');\n throw new TokenInvalidError('Refresh token is invalid or expired');\n }\n const token = await signAccessToken(user, memberships, latest.sessionId);\n recordRefreshOutcome('grace');\n return {token, refreshToken: undefined, user, adminRole};\n }\n\n const token = await signAccessToken(user, memberships, current.sessionId);\n recordRefreshOutcome('rotated');\n return {token, refreshToken: nextRefreshToken, user, adminRole};\n}\n\nexport interface ConfirmEmailVerificationResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport interface ResendEmailVerificationResult {\n nextResendAvailableAt: Date;\n}\n\nexport async function confirmEmailVerification(params: {\n email: string;\n challengeId: string;\n code: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<ConfirmEmailVerificationResult> {\n const user = await findUserByEmail({email: emailSchema.parse(params.email)});\n if (!user) throw new TokenInvalidError('Verification code is invalid or expired');\n await confirmEmailChallenge({id: params.challengeId, code: params.code, continuation: user.id});\n await consumeEmailChallengeProof({\n id: params.challengeId,\n purpose: PASSWORD_VERIFICATION_PURPOSE,\n continuation: user.id,\n });\n\n const verifiedUser = await markEmailVerified({userId: user.id});\n if (verifiedUser?.status !== 'active') {\n throw new TokenInvalidError('Verification code is invalid or expired');\n }\n\n const {token, refreshToken, adminRole} = await createSessionTokens(\n verifiedUser,\n params.workspaces,\n );\n\n return {token, refreshToken, user: verifiedUser, adminRole};\n}\n\nexport async function resendEmailVerification(params: {\n email: string;\n challengeId: string;\n sourceIp: string;\n}): Promise<ResendEmailVerificationResult> {\n const user = await findUserByEmail({email: emailSchema.parse(params.email)});\n if (!user || user.emailVerifiedAt || user.status !== 'active') {\n return {nextResendAvailableAt: new Date()};\n }\n const result = await resendEmailChallenge({\n id: params.challengeId,\n continuation: user.id,\n sourceIp: params.sourceIp,\n });\n\n return {nextResendAvailableAt: result.nextResendAvailableAt};\n}\n\nexport async function requestPasswordReset(params: {email: string}): Promise<void> {\n const user = await findUserByEmail({email: params.email});\n if (user?.status !== 'active' || user.hashedPassword === null) {\n return;\n }\n\n const rawToken = generateOpaqueToken('passwordReset');\n await createPasswordReset({\n userId: user.id,\n hashedToken: hashOpaqueToken(rawToken),\n expiresAt: hoursFromNow(RESET_TTL_HOURS),\n sendEmail: {\n email: user.email,\n resetLink: passwordResetLink(rawToken),\n expiresInHours: RESET_TTL_HOURS,\n },\n });\n}\n\nexport interface ConfirmPasswordResetResult {\n token: string;\n refreshToken: string;\n user: User;\n adminRole: AdminRole | null;\n}\n\nexport async function confirmPasswordReset(params: {\n token: string;\n newPassword: string;\n workspaces: WorkspacesInterModuleClient;\n}): Promise<ConfirmPasswordResetResult> {\n const consumed = await consumePasswordReset({hashedToken: hashOpaqueToken(params.token)});\n if (!consumed) {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n const existingUser = await findUserById({id: consumed.userId});\n if (existingUser?.status !== 'active' || existingUser.hashedPassword === null) {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n const hashedPassword = await hashPassword({password: params.newPassword});\n const user = await updateUserPassword({userId: consumed.userId, hashedPassword});\n if (user?.status !== 'active') {\n throw new TokenInvalidError('Reset token is invalid or expired');\n }\n\n await revokeRefreshTokensForUser({userId: consumed.userId});\n\n const {token, refreshToken, adminRole} = await createSessionTokens(user, params.workspaces);\n\n return {token, refreshToken, user, adminRole};\n}\n\nexport async function changePassword(params: {\n userId: string;\n currentPassword: string;\n newPassword: string;\n refreshToken?: string | undefined;\n}): Promise<void> {\n const user = await findUserById({id: params.userId});\n if (!user) {\n throw new UserNotFoundError(params.userId);\n }\n\n const hasPassword = user.hashedPassword !== null;\n const ok = await verifyPassword({\n password: params.currentPassword,\n hash: user.hashedPassword ?? (await getDummyHash()),\n });\n if (!hasPassword || !ok) {\n throw new InvalidCredentialsError();\n }\n\n const hashedPassword = await hashPassword({password: params.newPassword});\n const currentRefreshSession = params.refreshToken\n ? await findActiveRefreshTokenByHash({hashedToken: hashOpaqueToken(params.refreshToken)})\n : undefined;\n const exceptRefreshTokenId =\n currentRefreshSession?.userId === user.id ? currentRefreshSession.id : undefined;\n\n await updateUserPassword({userId: user.id, hashedPassword});\n await revokeRefreshTokensForUser({userId: user.id, exceptRefreshTokenId});\n}\n\nexport async function logout(params: {refreshToken?: string | undefined}): Promise<void> {\n if (!params.refreshToken) return;\n const refreshToken = await findRefreshTokenByHash({\n hashedToken: hashOpaqueToken(params.refreshToken),\n });\n if (!refreshToken) return;\n await revokeRefreshSession({sessionId: refreshToken.sessionId, userId: refreshToken.userId});\n}\n\nexport interface GetCurrentUserResult {\n user: User;\n}\n\nexport async function getCurrentUser(params: {userId: string}): Promise<GetCurrentUserResult> {\n const user = await findUserById({id: params.userId});\n if (!user) {\n throw new UserNotFoundError(params.userId);\n }\n\n return {user};\n}\n"],"names":["emailSchema","confirmEmailChallenge","consumeEmailChallengeProof","createEmailChallenge","resendEmailChallenge","workspacesInterModuleContract","isInterModuleKnownError","userAccessTokenKey","generateOpaqueToken","hashOpaqueToken","config","consumePasswordReset","createPasswordReset","createRefreshTokenForActiveUser","findActiveRefreshTokenByHash","findRefreshTokenByHash","revokeRefreshSession","revokeRefreshTokensForUser","rotateRefreshToken","createUser","createDbUser","findUserByEmail","findUserById","markEmailVerified","provisionUser","provisionDbUser","updateUserPassword","recordTokenRefreshed","getCurrentAdminRole","AuthDependencyUnavailableError","EmailNotVerifiedError","EmailTakenError","InvalidCredentialsError","InvitationEmailMismatchError","SignupNotAllowedError","TokenAlreadyUsedError","TokenExpiredError","TokenInvalidError","UserNotFoundError","signUserToken","hashPassword","verifyPassword","createEnvironmentSignupPolicy","RESET_TTL_HOURS","PASSWORD_VERIFICATION_PURPOSE","DEFAULT_SIGNUP_NOT_ALLOWED_MESSAGE","defaultSignupPolicy","assertSignupAllowed","params","result","signupPolicy","isSignupAllowed","email","emailVerified","source","allowed","message","dummyHashCache","getDummyHash","password","hoursFromNow","hours","Date","now","daysFromNow","days","recordRefreshOutcome","outcome","loadTokenMemberships","userId","workspaces","memberships","listMembershipsForTokenClaims","catch","error","signAccessToken","user","refreshSessionId","id","name","secret","expiresIn","AUTH_JWT_EXPIRES_IN","isWithinRotationGrace","refreshToken","rotatedAt","graceMs","AUTH_REFRESH_ROTATION_GRACE_SECONDS","getTime","createRefreshSession","session","sessionId","hashedToken","expiresAt","AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS","createSessionTokens","crypto","randomUUID","token","adminRole","passwordResetLink","rawToken","CLIENT_BASE_URL","invitationErrorFromKnownError","method","contractMethod","methods","code","invitationErrorMessage","parse","existing","signup","canResumeVerification","status","emailVerifiedAt","hashedPassword","hash","emailChallenge","purpose","continuation","idempotencyKey","sourceIp","signedUp","viaInvitation","signupWithInvitation","preflightInvitationAcceptance","invitationToken","membership","acceptError","acceptInvitation","workspaceId","verified","login","hasPassword","ok","createSessionForUser","undefined","refreshAccessToken","currentHashedToken","current","nextRefreshToken","rotated","nextHashedToken","latestUser","latest","confirmEmailVerification","challengeId","verifiedUser","resendEmailVerification","nextResendAvailableAt","requestPasswordReset","sendEmail","resetLink","expiresInHours","confirmPasswordReset","consumed","existingUser","newPassword","changePassword","currentPassword","currentRefreshSession","exceptRefreshTokenId","logout","getCurrentUser"],"mappings":"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"}
|
package/dist/core/jwt.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export declare const tokenMembershipSchema: z.ZodObject<{
|
|
|
4
4
|
role: z.ZodEnum<{
|
|
5
5
|
admin: "admin";
|
|
6
6
|
}>;
|
|
7
|
+
workspaceStatus: z.ZodDefault<z.ZodEnum<{
|
|
8
|
+
active: "active";
|
|
9
|
+
suspended: "suspended";
|
|
10
|
+
deleted: "deleted";
|
|
11
|
+
}>>;
|
|
7
12
|
}, z.core.$strip>;
|
|
8
13
|
export type TokenMembership = z.infer<typeof tokenMembershipSchema>;
|
|
9
14
|
export declare const userTokenClaimsSchema: z.ZodObject<{
|
|
@@ -16,6 +21,11 @@ export declare const userTokenClaimsSchema: z.ZodObject<{
|
|
|
16
21
|
role: z.ZodEnum<{
|
|
17
22
|
admin: "admin";
|
|
18
23
|
}>;
|
|
24
|
+
workspaceStatus: z.ZodDefault<z.ZodEnum<{
|
|
25
|
+
active: "active";
|
|
26
|
+
suspended: "suspended";
|
|
27
|
+
deleted: "deleted";
|
|
28
|
+
}>>;
|
|
19
29
|
}, z.core.$strip>>;
|
|
20
30
|
iat: z.ZodNumber;
|
|
21
31
|
exp: z.ZodNumber;
|
package/dist/core/jwt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/core/jwt.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,eAAO,MAAM,qBAAqB
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/core/jwt.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,eAAO,MAAM,qBAAqB;;;;;;;;;;iBAIhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;iBAQhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACjC,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;CAC7B;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAchF;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAa7F"}
|
package/dist/core/jwt.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { workspaceRoleSchema } from '@shipfox/api-workspaces-dto';
|
|
1
|
+
import { workspaceRoleSchema, workspaceStatusSchema } from '@shipfox/api-workspaces-dto';
|
|
2
2
|
import { signHs256, verifyHs256 } from '@shipfox/node-jwt';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { recordTokenIssued, recordTokenVerified } from '#metrics/index.js';
|
|
5
5
|
export const tokenMembershipSchema = z.object({
|
|
6
6
|
workspaceId: z.string().uuid(),
|
|
7
|
-
role: workspaceRoleSchema
|
|
7
|
+
role: workspaceRoleSchema,
|
|
8
|
+
workspaceStatus: workspaceStatusSchema.default('active')
|
|
8
9
|
});
|
|
9
10
|
export const userTokenClaimsSchema = z.object({
|
|
10
11
|
sub: z.string().uuid(),
|
package/dist/core/jwt.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/jwt.ts"],"sourcesContent":["import {workspaceRoleSchema} from '@shipfox/api-workspaces-dto';\nimport {signHs256, verifyHs256} from '@shipfox/node-jwt';\nimport {z} from 'zod';\nimport {recordTokenIssued, recordTokenVerified} from '#metrics/index.js';\n\nexport const tokenMembershipSchema = z.object({\n workspaceId: z.string().uuid(),\n role: workspaceRoleSchema,\n});\n\nexport type TokenMembership = z.infer<typeof tokenMembershipSchema>;\n\nexport const userTokenClaimsSchema = z.object({\n sub: z.string().uuid(),\n refreshSessionId: z.string().uuid().optional(),\n email: z.string().email(),\n name: z.string().nullable().optional(),\n memberships: z.array(tokenMembershipSchema),\n iat: z.number().int(),\n exp: z.number().int(),\n});\n\nexport type UserTokenClaims = z.infer<typeof userTokenClaimsSchema>;\n\nexport interface SignUserTokenParams {\n refreshSessionId?: string | undefined;\n userId: string;\n email: string;\n name?: string | null | undefined;\n memberships: TokenMembership[];\n secret: string | Uint8Array;\n expiresIn: string;\n}\n\nexport interface VerifyUserTokenParams {\n token: string;\n secret: string | Uint8Array;\n}\n\nexport async function signUserToken(params: SignUserTokenParams): Promise<string> {\n const token = await signHs256({\n payload: {\n email: params.email,\n name: params.name ?? null,\n memberships: params.memberships,\n refreshSessionId: params.refreshSessionId,\n },\n secret: params.secret,\n expiresIn: params.expiresIn,\n subject: params.userId,\n });\n recordTokenIssued('session');\n return token;\n}\n\nexport async function verifyUserToken(params: VerifyUserTokenParams): Promise<UserTokenClaims> {\n try {\n const claims = await verifyHs256({\n token: params.token,\n secret: params.secret,\n schema: userTokenClaimsSchema,\n });\n recordTokenVerified('session', 'ok');\n return claims;\n } catch (error) {\n recordTokenVerified('session', 'rejected');\n throw error;\n }\n}\n"],"names":["workspaceRoleSchema","signHs256","verifyHs256","z","recordTokenIssued","recordTokenVerified","tokenMembershipSchema","object","workspaceId","string","uuid","role","userTokenClaimsSchema","sub","refreshSessionId","optional","email","name","nullable","memberships","array","iat","number","int","exp","signUserToken","params","token","payload","secret","expiresIn","subject","userId","verifyUserToken","claims","schema","error"],"mappings":"AAAA,SAAQA,mBAAmB,QAAO,8BAA8B;
|
|
1
|
+
{"version":3,"sources":["../../src/core/jwt.ts"],"sourcesContent":["import {workspaceRoleSchema, workspaceStatusSchema} from '@shipfox/api-workspaces-dto';\nimport {signHs256, verifyHs256} from '@shipfox/node-jwt';\nimport {z} from 'zod';\nimport {recordTokenIssued, recordTokenVerified} from '#metrics/index.js';\n\nexport const tokenMembershipSchema = z.object({\n workspaceId: z.string().uuid(),\n role: workspaceRoleSchema,\n workspaceStatus: workspaceStatusSchema.default('active'),\n});\n\nexport type TokenMembership = z.infer<typeof tokenMembershipSchema>;\n\nexport const userTokenClaimsSchema = z.object({\n sub: z.string().uuid(),\n refreshSessionId: z.string().uuid().optional(),\n email: z.string().email(),\n name: z.string().nullable().optional(),\n memberships: z.array(tokenMembershipSchema),\n iat: z.number().int(),\n exp: z.number().int(),\n});\n\nexport type UserTokenClaims = z.infer<typeof userTokenClaimsSchema>;\n\nexport interface SignUserTokenParams {\n refreshSessionId?: string | undefined;\n userId: string;\n email: string;\n name?: string | null | undefined;\n memberships: TokenMembership[];\n secret: string | Uint8Array;\n expiresIn: string;\n}\n\nexport interface VerifyUserTokenParams {\n token: string;\n secret: string | Uint8Array;\n}\n\nexport async function signUserToken(params: SignUserTokenParams): Promise<string> {\n const token = await signHs256({\n payload: {\n email: params.email,\n name: params.name ?? null,\n memberships: params.memberships,\n refreshSessionId: params.refreshSessionId,\n },\n secret: params.secret,\n expiresIn: params.expiresIn,\n subject: params.userId,\n });\n recordTokenIssued('session');\n return token;\n}\n\nexport async function verifyUserToken(params: VerifyUserTokenParams): Promise<UserTokenClaims> {\n try {\n const claims = await verifyHs256({\n token: params.token,\n secret: params.secret,\n schema: userTokenClaimsSchema,\n });\n recordTokenVerified('session', 'ok');\n return claims;\n } catch (error) {\n recordTokenVerified('session', 'rejected');\n throw error;\n }\n}\n"],"names":["workspaceRoleSchema","workspaceStatusSchema","signHs256","verifyHs256","z","recordTokenIssued","recordTokenVerified","tokenMembershipSchema","object","workspaceId","string","uuid","role","workspaceStatus","default","userTokenClaimsSchema","sub","refreshSessionId","optional","email","name","nullable","memberships","array","iat","number","int","exp","signUserToken","params","token","payload","secret","expiresIn","subject","userId","verifyUserToken","claims","schema","error"],"mappings":"AAAA,SAAQA,mBAAmB,EAAEC,qBAAqB,QAAO,8BAA8B;AACvF,SAAQC,SAAS,EAAEC,WAAW,QAAO,oBAAoB;AACzD,SAAQC,CAAC,QAAO,MAAM;AACtB,SAAQC,iBAAiB,EAAEC,mBAAmB,QAAO,oBAAoB;AAEzE,OAAO,MAAMC,wBAAwBH,EAAEI,MAAM,CAAC;IAC5CC,aAAaL,EAAEM,MAAM,GAAGC,IAAI;IAC5BC,MAAMZ;IACNa,iBAAiBZ,sBAAsBa,OAAO,CAAC;AACjD,GAAG;AAIH,OAAO,MAAMC,wBAAwBX,EAAEI,MAAM,CAAC;IAC5CQ,KAAKZ,EAAEM,MAAM,GAAGC,IAAI;IACpBM,kBAAkBb,EAAEM,MAAM,GAAGC,IAAI,GAAGO,QAAQ;IAC5CC,OAAOf,EAAEM,MAAM,GAAGS,KAAK;IACvBC,MAAMhB,EAAEM,MAAM,GAAGW,QAAQ,GAAGH,QAAQ;IACpCI,aAAalB,EAAEmB,KAAK,CAAChB;IACrBiB,KAAKpB,EAAEqB,MAAM,GAAGC,GAAG;IACnBC,KAAKvB,EAAEqB,MAAM,GAAGC,GAAG;AACrB,GAAG;AAmBH,OAAO,eAAeE,cAAcC,MAA2B;IAC7D,MAAMC,QAAQ,MAAM5B,UAAU;QAC5B6B,SAAS;YACPZ,OAAOU,OAAOV,KAAK;YACnBC,MAAMS,OAAOT,IAAI,IAAI;YACrBE,aAAaO,OAAOP,WAAW;YAC/BL,kBAAkBY,OAAOZ,gBAAgB;QAC3C;QACAe,QAAQH,OAAOG,MAAM;QACrBC,WAAWJ,OAAOI,SAAS;QAC3BC,SAASL,OAAOM,MAAM;IACxB;IACA9B,kBAAkB;IAClB,OAAOyB;AACT;AAEA,OAAO,eAAeM,gBAAgBP,MAA6B;IACjE,IAAI;QACF,MAAMQ,SAAS,MAAMlC,YAAY;YAC/B2B,OAAOD,OAAOC,KAAK;YACnBE,QAAQH,OAAOG,MAAM;YACrBM,QAAQvB;QACV;QACAT,oBAAoB,WAAW;QAC/B,OAAO+B;IACT,EAAE,OAAOE,OAAO;QACdjC,oBAAoB,WAAW;QAC/B,MAAMiC;IACR;AACF"}
|
|
@@ -19,10 +19,6 @@ export declare function createRefreshTokenForActiveUser(params: CreateRefreshTok
|
|
|
19
19
|
export declare function findActiveRefreshTokenByHash(params: {
|
|
20
20
|
hashedToken: string;
|
|
21
21
|
}): Promise<RefreshToken | undefined>;
|
|
22
|
-
export declare function findActiveRefreshSession(params: {
|
|
23
|
-
sessionId: string;
|
|
24
|
-
userId: string;
|
|
25
|
-
}): Promise<RefreshToken | undefined>;
|
|
26
22
|
/**
|
|
27
23
|
* Looks up a non-revoked, non-expired token by hash, including ones already
|
|
28
24
|
* rotated. Rotation reads {@link RefreshToken.rotatedAt} to decide whether to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh-tokens.d.ts","sourceRoot":"","sources":["../../src/db/refresh-tokens.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAI3B,KAAK,EAAE,GAAG,UAAU,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE7E,wBAAsB,wBAAwB,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpF;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC,CAchG;AAED,wBAAsB,+BAA+B,CACnD,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAuBnC;AAED;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,MAAM,EAAE;IACzD,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAiBpC;AAED
|
|
1
|
+
{"version":3,"file":"refresh-tokens.d.ts","sourceRoot":"","sources":["../../src/db/refresh-tokens.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAI3B,KAAK,EAAE,GAAG,UAAU,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE7E,wBAAsB,wBAAwB,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpF;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC,CAchG;AAED,wBAAsB,+BAA+B,CACnD,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAuBnC;AAED;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,MAAM,EAAE;IACzD,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAiBpC;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAgBpC;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,IAAI,CAAC;CACjB,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAwDpC;AAED,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAK3F;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhB;AAED,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3C,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhB"}
|
|
@@ -45,12 +45,6 @@ export async function createRefreshTokenForActiveUser(params) {
|
|
|
45
45
|
if (!row) return undefined;
|
|
46
46
|
return toRefreshToken(row);
|
|
47
47
|
}
|
|
48
|
-
export async function findActiveRefreshSession(params) {
|
|
49
|
-
const rows = await db().select().from(refreshTokens).where(and(eq(refreshTokens.sessionId, params.sessionId), eq(refreshTokens.userId, params.userId), isNull(refreshTokens.revokedAt), isNull(refreshTokens.rotatedAt), gt(refreshTokens.expiresAt, sql`now()`))).limit(1);
|
|
50
|
-
const row = rows[0];
|
|
51
|
-
if (!row) return undefined;
|
|
52
|
-
return toRefreshToken(row);
|
|
53
|
-
}
|
|
54
48
|
/**
|
|
55
49
|
* Looks up a non-revoked, non-expired token by hash, including ones already
|
|
56
50
|
* rotated. Rotation reads {@link RefreshToken.rotatedAt} to decide whether to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/db/refresh-tokens.ts"],"sourcesContent":["import {and, eq, gt, isNull, ne, sql} from 'drizzle-orm';\nimport type {RefreshToken} from '#core/entities/refresh-token.js';\nimport {db} from './db.js';\nimport {refreshTokens, toRefreshToken} from './schema/refresh-tokens.js';\nimport {users} from './schema/users.js';\n\ntype Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];\n\nexport async function lockUserSessionMutations(tx: Tx, userId: string): Promise<void> {\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${`auth_user_sessions:${userId}`}))`);\n}\n\nexport interface CreateRefreshTokenParams {\n sessionId?: string | undefined;\n userId: string;\n hashedToken: string;\n expiresAt: Date;\n}\n\nexport async function createRefreshToken(params: CreateRefreshTokenParams): Promise<RefreshToken> {\n const rows = await db()\n .insert(refreshTokens)\n .values({\n sessionId: params.sessionId,\n userId: params.userId,\n hashedToken: params.hashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toRefreshToken(row);\n}\n\nexport async function createRefreshTokenForActiveUser(\n params: CreateRefreshTokenParams,\n): Promise<RefreshToken | undefined> {\n return await db().transaction(async (tx) => {\n await lockUserSessionMutations(tx, params.userId);\n const userRows = await tx\n .select({status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n if (userRows[0]?.status !== 'active') return undefined;\n\n const rows = await tx\n .insert(refreshTokens)\n .values({\n sessionId: params.sessionId,\n userId: params.userId,\n hashedToken: params.hashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toRefreshToken(row);\n });\n}\n\n/**\n * Looks up the live session token by hash — one that can still authenticate as\n * the current session. Returns `undefined` for revoked, expired, or already\n * rotated tokens; rotated rows survive in the table only for the grace window\n * and are not \"active\".\n */\nexport async function findActiveRefreshTokenByHash(params: {\n hashedToken: string;\n}): Promise<RefreshToken | undefined> {\n const rows = await db()\n .select()\n .from(refreshTokens)\n .where(\n and(\n eq(refreshTokens.hashedToken, params.hashedToken),\n isNull(refreshTokens.revokedAt),\n isNull(refreshTokens.rotatedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toRefreshToken(row);\n}\n\nexport async function findActiveRefreshSession(params: {\n sessionId: string;\n userId: string;\n}): Promise<RefreshToken | undefined> {\n const rows = await db()\n .select()\n .from(refreshTokens)\n .where(\n and(\n eq(refreshTokens.sessionId, params.sessionId),\n eq(refreshTokens.userId, params.userId),\n isNull(refreshTokens.revokedAt),\n isNull(refreshTokens.rotatedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toRefreshToken(row);\n}\n\n/**\n * Looks up a non-revoked, non-expired token by hash, including ones already\n * rotated. Rotation reads {@link RefreshToken.rotatedAt} to decide whether to\n * rotate, honour the grace window, or reject reuse, so — unlike\n * {@link findActiveRefreshTokenByHash} — it must still see rotated rows.\n */\nexport async function findRefreshTokenByHash(params: {\n hashedToken: string;\n}): Promise<RefreshToken | undefined> {\n const rows = await db()\n .select()\n .from(refreshTokens)\n .where(\n and(\n eq(refreshTokens.hashedToken, params.hashedToken),\n isNull(refreshTokens.revokedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toRefreshToken(row);\n}\n\n/**\n * Atomically claims rotation and inserts the successor token. The predecessor is\n * marked rather than deleted so later reuse of the retired token can be detected.\n * If the successor insert fails, the predecessor rotation rolls back.\n */\nexport async function rotateRefreshToken(params: {\n userId?: string | undefined;\n id: string;\n currentHashedToken: string;\n nextHashedToken: string;\n expiresAt: Date;\n}): Promise<RefreshToken | undefined> {\n return await db().transaction(async (tx) => {\n const userId =\n params.userId ??\n (\n await tx\n .select({userId: refreshTokens.userId})\n .from(refreshTokens)\n .where(eq(refreshTokens.id, params.id))\n .limit(1)\n )[0]?.userId;\n if (!userId) return undefined;\n\n await lockUserSessionMutations(tx, userId);\n const userRows = await tx\n .select({status: users.status})\n .from(users)\n .where(eq(users.id, userId))\n .limit(1);\n if (userRows[0]?.status !== 'active') return undefined;\n\n const rotatedRows = await tx\n .update(refreshTokens)\n .set({\n rotatedAt: sql`now()`,\n lastUsedAt: sql`now()`,\n updatedAt: sql`now()`,\n })\n .where(\n and(\n eq(refreshTokens.id, params.id),\n eq(refreshTokens.hashedToken, params.currentHashedToken),\n isNull(refreshTokens.revokedAt),\n isNull(refreshTokens.rotatedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .returning();\n\n const rotated = rotatedRows[0];\n if (!rotated) return undefined;\n\n const successorRows = await tx\n .insert(refreshTokens)\n .values({\n sessionId: rotated.sessionId,\n userId: rotated.userId,\n hashedToken: params.nextHashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n\n if (!successorRows[0]) throw new Error('Insert returned no rows');\n\n return toRefreshToken(rotated);\n });\n}\n\nexport async function revokeRefreshTokenByHash(params: {hashedToken: string}): Promise<void> {\n await db()\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(and(eq(refreshTokens.hashedToken, params.hashedToken), isNull(refreshTokens.revokedAt)));\n}\n\nexport async function revokeRefreshSession(params: {\n sessionId: string;\n userId: string;\n}): Promise<void> {\n await db()\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(\n and(\n eq(refreshTokens.sessionId, params.sessionId),\n eq(refreshTokens.userId, params.userId),\n isNull(refreshTokens.revokedAt),\n ),\n );\n}\n\nexport async function revokeRefreshTokensForUser(params: {\n userId: string;\n exceptRefreshTokenId?: string | undefined;\n}): Promise<void> {\n await db()\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(\n and(\n eq(refreshTokens.userId, params.userId),\n isNull(refreshTokens.revokedAt),\n params.exceptRefreshTokenId ? ne(refreshTokens.id, params.exceptRefreshTokenId) : undefined,\n ),\n );\n}\n"],"names":["and","eq","gt","isNull","ne","sql","db","refreshTokens","toRefreshToken","users","lockUserSessionMutations","tx","userId","execute","createRefreshToken","params","rows","insert","values","sessionId","hashedToken","expiresAt","returning","row","Error","createRefreshTokenForActiveUser","transaction","userRows","select","status","from","where","id","limit","undefined","findActiveRefreshTokenByHash","revokedAt","rotatedAt","findActiveRefreshSession","findRefreshTokenByHash","rotateRefreshToken","rotatedRows","update","set","lastUsedAt","updatedAt","currentHashedToken","rotated","successorRows","nextHashedToken","revokeRefreshTokenByHash","revokeRefreshSession","revokeRefreshTokensForUser","exceptRefreshTokenId"],"mappings":"AAAA,SAAQA,GAAG,EAAEC,EAAE,EAAEC,EAAE,EAAEC,MAAM,EAAEC,EAAE,EAAEC,GAAG,QAAO,cAAc;AAEzD,SAAQC,EAAE,QAAO,UAAU;AAC3B,SAAQC,aAAa,EAAEC,cAAc,QAAO,6BAA6B;AACzE,SAAQC,KAAK,QAAO,oBAAoB;AAIxC,OAAO,eAAeC,yBAAyBC,EAAM,EAAEC,MAAc;IACnE,MAAMD,GAAGE,OAAO,CAACR,GAAG,CAAC,sCAAsC,EAAE,CAAC,mBAAmB,EAAEO,QAAQ,CAAC,EAAE,CAAC;AACjG;AASA,OAAO,eAAeE,mBAAmBC,MAAgC;IACvE,MAAMC,OAAO,MAAMV,KAChBW,MAAM,CAACV,eACPW,MAAM,CAAC;QACNC,WAAWJ,OAAOI,SAAS;QAC3BP,QAAQG,OAAOH,MAAM;QACrBQ,aAAaL,OAAOK,WAAW;QAC/BC,WAAWN,OAAOM,SAAS;IAC7B,GACCC,SAAS;IAEZ,MAAMC,MAAMP,IAAI,CAAC,EAAE;IACnB,IAAI,CAACO,KAAK,MAAM,IAAIC,MAAM;IAC1B,OAAOhB,eAAee;AACxB;AAEA,OAAO,eAAeE,gCACpBV,MAAgC;IAEhC,OAAO,MAAMT,KAAKoB,WAAW,CAAC,OAAOf;QACnC,MAAMD,yBAAyBC,IAAII,OAAOH,MAAM;QAChD,MAAMe,WAAW,MAAMhB,GACpBiB,MAAM,CAAC;YAACC,QAAQpB,MAAMoB,MAAM;QAAA,GAC5BC,IAAI,CAACrB,OACLsB,KAAK,CAAC9B,GAAGQ,MAAMuB,EAAE,EAAEjB,OAAOH,MAAM,GAChCqB,KAAK,CAAC;QACT,IAAIN,QAAQ,CAAC,EAAE,EAAEE,WAAW,UAAU,OAAOK;QAE7C,MAAMlB,OAAO,MAAML,GAChBM,MAAM,CAACV,eACPW,MAAM,CAAC;YACNC,WAAWJ,OAAOI,SAAS;YAC3BP,QAAQG,OAAOH,MAAM;YACrBQ,aAAaL,OAAOK,WAAW;YAC/BC,WAAWN,OAAOM,SAAS;QAC7B,GACCC,SAAS;QACZ,MAAMC,MAAMP,IAAI,CAAC,EAAE;QACnB,IAAI,CAACO,KAAK,MAAM,IAAIC,MAAM;QAC1B,OAAOhB,eAAee;IACxB;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeY,6BAA6BpB,MAElD;IACC,MAAMC,OAAO,MAAMV,KAChBsB,MAAM,GACNE,IAAI,CAACvB,eACLwB,KAAK,CACJ/B,IACEC,GAAGM,cAAca,WAAW,EAAEL,OAAOK,WAAW,GAChDjB,OAAOI,cAAc6B,SAAS,GAC9BjC,OAAOI,cAAc8B,SAAS,GAC9BnC,GAAGK,cAAcc,SAAS,EAAEhB,GAAG,CAAC,KAAK,CAAC,IAGzC4B,KAAK,CAAC;IAET,MAAMV,MAAMP,IAAI,CAAC,EAAE;IACnB,IAAI,CAACO,KAAK,OAAOW;IACjB,OAAO1B,eAAee;AACxB;AAEA,OAAO,eAAee,yBAAyBvB,MAG9C;IACC,MAAMC,OAAO,MAAMV,KAChBsB,MAAM,GACNE,IAAI,CAACvB,eACLwB,KAAK,CACJ/B,IACEC,GAAGM,cAAcY,SAAS,EAAEJ,OAAOI,SAAS,GAC5ClB,GAAGM,cAAcK,MAAM,EAAEG,OAAOH,MAAM,GACtCT,OAAOI,cAAc6B,SAAS,GAC9BjC,OAAOI,cAAc8B,SAAS,GAC9BnC,GAAGK,cAAcc,SAAS,EAAEhB,GAAG,CAAC,KAAK,CAAC,IAGzC4B,KAAK,CAAC;IAET,MAAMV,MAAMP,IAAI,CAAC,EAAE;IACnB,IAAI,CAACO,KAAK,OAAOW;IACjB,OAAO1B,eAAee;AACxB;AAEA;;;;;CAKC,GACD,OAAO,eAAegB,uBAAuBxB,MAE5C;IACC,MAAMC,OAAO,MAAMV,KAChBsB,MAAM,GACNE,IAAI,CAACvB,eACLwB,KAAK,CACJ/B,IACEC,GAAGM,cAAca,WAAW,EAAEL,OAAOK,WAAW,GAChDjB,OAAOI,cAAc6B,SAAS,GAC9BlC,GAAGK,cAAcc,SAAS,EAAEhB,GAAG,CAAC,KAAK,CAAC,IAGzC4B,KAAK,CAAC;IAET,MAAMV,MAAMP,IAAI,CAAC,EAAE;IACnB,IAAI,CAACO,KAAK,OAAOW;IACjB,OAAO1B,eAAee;AACxB;AAEA;;;;CAIC,GACD,OAAO,eAAeiB,mBAAmBzB,MAMxC;IACC,OAAO,MAAMT,KAAKoB,WAAW,CAAC,OAAOf;QACnC,MAAMC,SACJG,OAAOH,MAAM,IACb,AACE,CAAA,MAAMD,GACHiB,MAAM,CAAC;YAAChB,QAAQL,cAAcK,MAAM;QAAA,GACpCkB,IAAI,CAACvB,eACLwB,KAAK,CAAC9B,GAAGM,cAAcyB,EAAE,EAAEjB,OAAOiB,EAAE,GACpCC,KAAK,CAAC,EAAC,CACX,CAAC,EAAE,EAAErB;QACR,IAAI,CAACA,QAAQ,OAAOsB;QAEpB,MAAMxB,yBAAyBC,IAAIC;QACnC,MAAMe,WAAW,MAAMhB,GACpBiB,MAAM,CAAC;YAACC,QAAQpB,MAAMoB,MAAM;QAAA,GAC5BC,IAAI,CAACrB,OACLsB,KAAK,CAAC9B,GAAGQ,MAAMuB,EAAE,EAAEpB,SACnBqB,KAAK,CAAC;QACT,IAAIN,QAAQ,CAAC,EAAE,EAAEE,WAAW,UAAU,OAAOK;QAE7C,MAAMO,cAAc,MAAM9B,GACvB+B,MAAM,CAACnC,eACPoC,GAAG,CAAC;YACHN,WAAWhC,GAAG,CAAC,KAAK,CAAC;YACrBuC,YAAYvC,GAAG,CAAC,KAAK,CAAC;YACtBwC,WAAWxC,GAAG,CAAC,KAAK,CAAC;QACvB,GACC0B,KAAK,CACJ/B,IACEC,GAAGM,cAAcyB,EAAE,EAAEjB,OAAOiB,EAAE,GAC9B/B,GAAGM,cAAca,WAAW,EAAEL,OAAO+B,kBAAkB,GACvD3C,OAAOI,cAAc6B,SAAS,GAC9BjC,OAAOI,cAAc8B,SAAS,GAC9BnC,GAAGK,cAAcc,SAAS,EAAEhB,GAAG,CAAC,KAAK,CAAC,IAGzCiB,SAAS;QAEZ,MAAMyB,UAAUN,WAAW,CAAC,EAAE;QAC9B,IAAI,CAACM,SAAS,OAAOb;QAErB,MAAMc,gBAAgB,MAAMrC,GACzBM,MAAM,CAACV,eACPW,MAAM,CAAC;YACNC,WAAW4B,QAAQ5B,SAAS;YAC5BP,QAAQmC,QAAQnC,MAAM;YACtBQ,aAAaL,OAAOkC,eAAe;YACnC5B,WAAWN,OAAOM,SAAS;QAC7B,GACCC,SAAS;QAEZ,IAAI,CAAC0B,aAAa,CAAC,EAAE,EAAE,MAAM,IAAIxB,MAAM;QAEvC,OAAOhB,eAAeuC;IACxB;AACF;AAEA,OAAO,eAAeG,yBAAyBnC,MAA6B;IAC1E,MAAMT,KACHoC,MAAM,CAACnC,eACPoC,GAAG,CAAC;QAACP,WAAW/B,GAAG,CAAC,KAAK,CAAC;QAAEwC,WAAWxC,GAAG,CAAC,KAAK,CAAC;IAAA,GACjD0B,KAAK,CAAC/B,IAAIC,GAAGM,cAAca,WAAW,EAAEL,OAAOK,WAAW,GAAGjB,OAAOI,cAAc6B,SAAS;AAChG;AAEA,OAAO,eAAee,qBAAqBpC,MAG1C;IACC,MAAMT,KACHoC,MAAM,CAACnC,eACPoC,GAAG,CAAC;QAACP,WAAW/B,GAAG,CAAC,KAAK,CAAC;QAAEwC,WAAWxC,GAAG,CAAC,KAAK,CAAC;IAAA,GACjD0B,KAAK,CACJ/B,IACEC,GAAGM,cAAcY,SAAS,EAAEJ,OAAOI,SAAS,GAC5ClB,GAAGM,cAAcK,MAAM,EAAEG,OAAOH,MAAM,GACtCT,OAAOI,cAAc6B,SAAS;AAGtC;AAEA,OAAO,eAAegB,2BAA2BrC,MAGhD;IACC,MAAMT,KACHoC,MAAM,CAACnC,eACPoC,GAAG,CAAC;QAACP,WAAW/B,GAAG,CAAC,KAAK,CAAC;QAAEwC,WAAWxC,GAAG,CAAC,KAAK,CAAC;IAAA,GACjD0B,KAAK,CACJ/B,IACEC,GAAGM,cAAcK,MAAM,EAAEG,OAAOH,MAAM,GACtCT,OAAOI,cAAc6B,SAAS,GAC9BrB,OAAOsC,oBAAoB,GAAGjD,GAAGG,cAAcyB,EAAE,EAAEjB,OAAOsC,oBAAoB,IAAInB;AAG1F"}
|
|
1
|
+
{"version":3,"sources":["../../src/db/refresh-tokens.ts"],"sourcesContent":["import {and, eq, gt, isNull, ne, sql} from 'drizzle-orm';\nimport type {RefreshToken} from '#core/entities/refresh-token.js';\nimport {db} from './db.js';\nimport {refreshTokens, toRefreshToken} from './schema/refresh-tokens.js';\nimport {users} from './schema/users.js';\n\ntype Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];\n\nexport async function lockUserSessionMutations(tx: Tx, userId: string): Promise<void> {\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${`auth_user_sessions:${userId}`}))`);\n}\n\nexport interface CreateRefreshTokenParams {\n sessionId?: string | undefined;\n userId: string;\n hashedToken: string;\n expiresAt: Date;\n}\n\nexport async function createRefreshToken(params: CreateRefreshTokenParams): Promise<RefreshToken> {\n const rows = await db()\n .insert(refreshTokens)\n .values({\n sessionId: params.sessionId,\n userId: params.userId,\n hashedToken: params.hashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toRefreshToken(row);\n}\n\nexport async function createRefreshTokenForActiveUser(\n params: CreateRefreshTokenParams,\n): Promise<RefreshToken | undefined> {\n return await db().transaction(async (tx) => {\n await lockUserSessionMutations(tx, params.userId);\n const userRows = await tx\n .select({status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n if (userRows[0]?.status !== 'active') return undefined;\n\n const rows = await tx\n .insert(refreshTokens)\n .values({\n sessionId: params.sessionId,\n userId: params.userId,\n hashedToken: params.hashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toRefreshToken(row);\n });\n}\n\n/**\n * Looks up the live session token by hash — one that can still authenticate as\n * the current session. Returns `undefined` for revoked, expired, or already\n * rotated tokens; rotated rows survive in the table only for the grace window\n * and are not \"active\".\n */\nexport async function findActiveRefreshTokenByHash(params: {\n hashedToken: string;\n}): Promise<RefreshToken | undefined> {\n const rows = await db()\n .select()\n .from(refreshTokens)\n .where(\n and(\n eq(refreshTokens.hashedToken, params.hashedToken),\n isNull(refreshTokens.revokedAt),\n isNull(refreshTokens.rotatedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toRefreshToken(row);\n}\n\n/**\n * Looks up a non-revoked, non-expired token by hash, including ones already\n * rotated. Rotation reads {@link RefreshToken.rotatedAt} to decide whether to\n * rotate, honour the grace window, or reject reuse, so — unlike\n * {@link findActiveRefreshTokenByHash} — it must still see rotated rows.\n */\nexport async function findRefreshTokenByHash(params: {\n hashedToken: string;\n}): Promise<RefreshToken | undefined> {\n const rows = await db()\n .select()\n .from(refreshTokens)\n .where(\n and(\n eq(refreshTokens.hashedToken, params.hashedToken),\n isNull(refreshTokens.revokedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toRefreshToken(row);\n}\n\n/**\n * Atomically claims rotation and inserts the successor token. The predecessor is\n * marked rather than deleted so later reuse of the retired token can be detected.\n * If the successor insert fails, the predecessor rotation rolls back.\n */\nexport async function rotateRefreshToken(params: {\n userId?: string | undefined;\n id: string;\n currentHashedToken: string;\n nextHashedToken: string;\n expiresAt: Date;\n}): Promise<RefreshToken | undefined> {\n return await db().transaction(async (tx) => {\n const userId =\n params.userId ??\n (\n await tx\n .select({userId: refreshTokens.userId})\n .from(refreshTokens)\n .where(eq(refreshTokens.id, params.id))\n .limit(1)\n )[0]?.userId;\n if (!userId) return undefined;\n\n await lockUserSessionMutations(tx, userId);\n const userRows = await tx\n .select({status: users.status})\n .from(users)\n .where(eq(users.id, userId))\n .limit(1);\n if (userRows[0]?.status !== 'active') return undefined;\n\n const rotatedRows = await tx\n .update(refreshTokens)\n .set({\n rotatedAt: sql`now()`,\n lastUsedAt: sql`now()`,\n updatedAt: sql`now()`,\n })\n .where(\n and(\n eq(refreshTokens.id, params.id),\n eq(refreshTokens.hashedToken, params.currentHashedToken),\n isNull(refreshTokens.revokedAt),\n isNull(refreshTokens.rotatedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .returning();\n\n const rotated = rotatedRows[0];\n if (!rotated) return undefined;\n\n const successorRows = await tx\n .insert(refreshTokens)\n .values({\n sessionId: rotated.sessionId,\n userId: rotated.userId,\n hashedToken: params.nextHashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n\n if (!successorRows[0]) throw new Error('Insert returned no rows');\n\n return toRefreshToken(rotated);\n });\n}\n\nexport async function revokeRefreshTokenByHash(params: {hashedToken: string}): Promise<void> {\n await db()\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(and(eq(refreshTokens.hashedToken, params.hashedToken), isNull(refreshTokens.revokedAt)));\n}\n\nexport async function revokeRefreshSession(params: {\n sessionId: string;\n userId: string;\n}): Promise<void> {\n await db()\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(\n and(\n eq(refreshTokens.sessionId, params.sessionId),\n eq(refreshTokens.userId, params.userId),\n isNull(refreshTokens.revokedAt),\n ),\n );\n}\n\nexport async function revokeRefreshTokensForUser(params: {\n userId: string;\n exceptRefreshTokenId?: string | undefined;\n}): Promise<void> {\n await db()\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(\n and(\n eq(refreshTokens.userId, params.userId),\n isNull(refreshTokens.revokedAt),\n params.exceptRefreshTokenId ? ne(refreshTokens.id, params.exceptRefreshTokenId) : undefined,\n ),\n );\n}\n"],"names":["and","eq","gt","isNull","ne","sql","db","refreshTokens","toRefreshToken","users","lockUserSessionMutations","tx","userId","execute","createRefreshToken","params","rows","insert","values","sessionId","hashedToken","expiresAt","returning","row","Error","createRefreshTokenForActiveUser","transaction","userRows","select","status","from","where","id","limit","undefined","findActiveRefreshTokenByHash","revokedAt","rotatedAt","findRefreshTokenByHash","rotateRefreshToken","rotatedRows","update","set","lastUsedAt","updatedAt","currentHashedToken","rotated","successorRows","nextHashedToken","revokeRefreshTokenByHash","revokeRefreshSession","revokeRefreshTokensForUser","exceptRefreshTokenId"],"mappings":"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"}
|
|
@@ -14,10 +14,7 @@ export interface CreateJwtAuthMethodOptions {
|
|
|
14
14
|
secret: string;
|
|
15
15
|
}
|
|
16
16
|
export declare function getClientContext(request: FastifyRequest): ClientContext | null;
|
|
17
|
-
/**
|
|
18
|
-
* Resolves the active refresh session backing an authenticated user request.
|
|
19
|
-
* The session check intentionally happens only for consumers that need browser-session binding.
|
|
20
|
-
*/
|
|
17
|
+
/** Returns the refresh-session metadata carried by a verified access token. */
|
|
21
18
|
export declare function getAuthenticatedSessionContext(request: FastifyRequest): Promise<AuthenticatedSessionContext>;
|
|
22
19
|
export declare function createJwtAuthMethod(): AuthMethod;
|
|
23
20
|
//# sourceMappingURL=jwt-auth.d.ts.map
|