@getstrata/core 0.5.78 → 0.5.79

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 0.5.79
4
+
5
+ - `readSession()` returns `{ userId, issuedAt }` from the HMAC session cookie. `isSessionInvalidated(issuedAt, session_valid_after)` lets `SessionGuard` reject cookies issued before `AuthUserRecord.session_valid_after` (Jetstream logout-other-devices / password change).
6
+
3
7
  ## 0.5.78
4
8
 
5
9
  - OpenAPI treats `POST /auth/two-factor-challenge` as a public operation (no bearer), including when routes are registered under `API_PREFIX`.
@@ -4,11 +4,17 @@ declare const SESSION_REMEMBER_TTL_SECONDS: number;
4
4
  interface CreateSessionCookieOptions {
5
5
  remember?: boolean;
6
6
  }
7
+ interface SignedSession {
8
+ userId: number;
9
+ issuedAt: number;
10
+ }
7
11
  declare function sessionCookieName(): string;
8
12
  declare function sessionTtlSeconds(): number;
9
13
  declare function sessionRememberTtlSeconds(): number;
14
+ declare function readSession(request: Request): SignedSession | null;
10
15
  declare function readSessionUserId(request: Request): number | null;
16
+ declare function isSessionInvalidated(issuedAt: number, validAfter: Date | string | null | undefined): boolean;
11
17
  declare function createSessionCookie(userId: number, options?: CreateSessionCookieOptions): string;
12
18
  declare function clearSessionCookie(): string;
13
- export type { CreateSessionCookieOptions };
14
- export { clearSessionCookie, createSessionCookie, readSessionUserId, SESSION_COOKIE, SESSION_REMEMBER_TTL_SECONDS, SESSION_TTL_SECONDS, sessionCookieName, sessionRememberTtlSeconds, sessionTtlSeconds, };
19
+ export type { CreateSessionCookieOptions, SignedSession };
20
+ export { clearSessionCookie, createSessionCookie, isSessionInvalidated, readSession, readSessionUserId, SESSION_COOKIE, SESSION_REMEMBER_TTL_SECONDS, SESSION_TTL_SECONDS, sessionCookieName, sessionRememberTtlSeconds, sessionTtlSeconds, };
@@ -4,6 +4,7 @@ interface AuthUserRecord {
4
4
  email?: string | null;
5
5
  role: string;
6
6
  email_verified_at?: Date | string | null;
7
+ session_valid_after?: Date | string | null;
7
8
  }
8
9
  interface AuthUserDirectory {
9
10
  resolveUserFromToken(token: string): Promise<AuthUser | null>;
@@ -62,9 +62,9 @@ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds,
62
62
  if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
63
63
  return null;
64
64
  }
65
- return userId;
65
+ return { userId, issuedAt };
66
66
  }
67
- function readSessionUserId(request) {
67
+ function readSession(request) {
68
68
  const cookieValue = readCookieValue(request, sessionCookieName());
69
69
  if (!cookieValue) {
70
70
  return null;
@@ -80,6 +80,19 @@ function readSessionUserId(request) {
80
80
  const [userIdRaw, issuedAtRaw, cookieSignature] = parts;
81
81
  return readSignedSession(String(userIdRaw), String(issuedAtRaw), cookieSignature, sessionTtlSeconds(), false);
82
82
  }
83
+ function readSessionUserId(request) {
84
+ return readSession(request)?.userId ?? null;
85
+ }
86
+ function isSessionInvalidated(issuedAt, validAfter) {
87
+ if (!validAfter) {
88
+ return false;
89
+ }
90
+ const timestamp = validAfter instanceof Date ? validAfter.getTime() : Date.parse(String(validAfter));
91
+ if (!Number.isFinite(timestamp)) {
92
+ return false;
93
+ }
94
+ return issuedAt < timestamp;
95
+ }
83
96
  function createSessionCookie(userId, options = {}) {
84
97
  const issuedAt = Date.now();
85
98
  const ttlSeconds = options.remember ? sessionRememberTtlSeconds() : sessionTtlSeconds();
@@ -97,6 +110,8 @@ export {
97
110
  SESSION_TTL_SECONDS,
98
111
  clearSessionCookie,
99
112
  createSessionCookie,
113
+ isSessionInvalidated,
114
+ readSession,
100
115
  readSessionUserId,
101
116
  sessionCookieName,
102
117
  sessionRememberTtlSeconds,
@@ -145,9 +145,9 @@ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds,
145
145
  if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
146
146
  return null;
147
147
  }
148
- return userId;
148
+ return { userId, issuedAt };
149
149
  }
150
- function readSessionUserId(request) {
150
+ function readSession(request) {
151
151
  const cookieValue = readCookieValue(request, sessionCookieName());
152
152
  if (!cookieValue) {
153
153
  return null;
@@ -163,6 +163,19 @@ function readSessionUserId(request) {
163
163
  const [userIdRaw, issuedAtRaw, cookieSignature] = parts;
164
164
  return readSignedSession(String(userIdRaw), String(issuedAtRaw), cookieSignature, sessionTtlSeconds(), false);
165
165
  }
166
+ function readSessionUserId(request) {
167
+ return readSession(request)?.userId ?? null;
168
+ }
169
+ function isSessionInvalidated(issuedAt, validAfter) {
170
+ if (!validAfter) {
171
+ return false;
172
+ }
173
+ const timestamp = validAfter instanceof Date ? validAfter.getTime() : Date.parse(String(validAfter));
174
+ if (!Number.isFinite(timestamp)) {
175
+ return false;
176
+ }
177
+ return issuedAt < timestamp;
178
+ }
166
179
  function createSessionCookie(userId, options = {}) {
167
180
  const issuedAt = Date.now();
168
181
  const ttlSeconds = options.remember ? sessionRememberTtlSeconds() : sessionTtlSeconds();
@@ -182,8 +195,8 @@ class SessionGuard {
182
195
  this.container = container;
183
196
  }
184
197
  async resolve(request) {
185
- const userId = readSessionUserId(request);
186
- if (!userId) {
198
+ const session = readSession(request);
199
+ if (!session) {
187
200
  return null;
188
201
  }
189
202
  const tokenService = resolveAuthUserDirectory(this.container);
@@ -191,7 +204,10 @@ class SessionGuard {
191
204
  return null;
192
205
  }
193
206
  try {
194
- const user = await tokenService.findByIdOrThrow(userId);
207
+ const user = await tokenService.findByIdOrThrow(session.userId);
208
+ if (isSessionInvalidated(session.issuedAt, user.session_valid_after)) {
209
+ return null;
210
+ }
195
211
  return {
196
212
  id: user.id,
197
213
  role: user.role,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "0.5.78",
3
+ "version": "0.5.79",
4
4
  "description": "Strata — Laravel-inspired Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",