@getstrata/core 0.5.77 → 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,13 @@
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
+
7
+ ## 0.5.78
8
+
9
+ - OpenAPI treats `POST /auth/two-factor-challenge` as a public operation (no bearer), including when routes are registered under `API_PREFIX`.
10
+
3
11
  ## 0.5.77
4
12
 
5
13
  - `@getstrata/core/security/recoveryCodes` (`generateRecoveryCodes`, `hashRecoveryCode`, `recoveryCodeMatches`). Fortify-style one-time MFA backup codes (`abcd-efgh`).
package/README.md CHANGED
@@ -82,7 +82,7 @@ import type { Migration } from "@getstrata/core/database/migrations/types";
82
82
  Package name: **`@getstrata/core`** (npm org [`@getstrata`](https://www.npmjs.com/org/getstrata)).
83
83
 
84
84
  1. Add `NPM_TOKEN` to GitHub repository secrets.
85
- 2. Tag a release: `git tag v0.5.79 && git push origin v0.5.79`
85
+ 2. Tag a release: `git tag v0.5.80 && git push origin v0.5.80`
86
86
  3. [Release workflow](../../.github/workflows/release.yml) builds and runs `npm publish --access public`.
87
87
 
88
88
  Previously published as `@eyk-workhub/framework@0.1.0`, deprecated in favor of this package.
@@ -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,
@@ -2,6 +2,7 @@
2
2
  // ../../src/modules/webhook/dispatchWebhookJob.ts
3
3
  import { createHmac } from "crypto";
4
4
  import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
5
+ import { BadRequestError } from "@getstrata/core/errors/http";
5
6
  import { Job } from "@getstrata/core/queue";
6
7
  import { webhookSignatureHeader } from "@getstrata/core/runtime/appKeyPrefix";
7
8
  import { safeFetch } from "@getstrata/core/security/safeFetch";
@@ -39,10 +40,10 @@ class DispatchWebhookJob extends Job {
39
40
  const signature = createHmac("sha256", webhook.secret).update(body).digest("hex");
40
41
  const allowHttp = (process.env.APP_ENV ?? "local") !== "production";
41
42
  const signatureHeader = webhookSignatureHeader();
42
- assertSafeOutboundUrl(webhook.url, { allowHttp });
43
43
  let responseStatus = null;
44
44
  let errorMessage = null;
45
45
  try {
46
+ assertSafeOutboundUrl(webhook.url, { allowHttp });
46
47
  const response = await safeFetch(webhook.url, {
47
48
  method: "POST",
48
49
  headers: {
@@ -67,6 +68,9 @@ class DispatchWebhookJob extends Job {
67
68
  ${errorMessage}
68
69
  )
69
70
  `;
71
+ if (error instanceof BadRequestError) {
72
+ return;
73
+ }
70
74
  return error instanceof Error ? error : new Error(errorMessage);
71
75
  }
72
76
  await db`
@@ -51,6 +51,7 @@ function sdkClientClassName() {
51
51
  var PUBLIC_ROUTE_DESCRIPTIONS = {
52
52
  "GET /auth/me": "Current authenticated user",
53
53
  "POST /auth/login": "Login with email and password",
54
+ "POST /auth/two-factor-challenge": "Complete two-factor login challenge",
54
55
  "POST /auth/register": "Register with name, email, and password",
55
56
  "POST /auth/forgot-password": "Request a password reset email",
56
57
  "POST /auth/reset-password": "Reset password with email and token",
@@ -99,7 +100,7 @@ function toRelativeApiPath(path) {
99
100
  }
100
101
  function requiresBearerAuth(path, method) {
101
102
  const relative = toRelativeApiPath(path);
102
- if (relative.startsWith("/auth/login") || relative.startsWith("/auth/register") || relative.startsWith("/auth/forgot-password") || relative.startsWith("/auth/reset-password") || relative.startsWith("/auth/email/verification-notification") || relative.startsWith("/auth/oauth")) {
103
+ if (relative.startsWith("/auth/login") || relative.startsWith("/auth/two-factor-challenge") || relative.startsWith("/auth/register") || relative.startsWith("/auth/forgot-password") || relative.startsWith("/auth/reset-password") || relative.startsWith("/auth/email/verification-notification") || relative.startsWith("/auth/oauth")) {
103
104
  return false;
104
105
  }
105
106
  if (relative.startsWith("/scim/") || relative.startsWith("/billing/webhooks/") || path.startsWith("/scim/") || path.startsWith("/billing/webhooks/")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "0.5.77",
3
+ "version": "0.5.79",
4
4
  "description": "Strata — Laravel-inspired Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",