@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 +8 -0
- package/README.md +1 -1
- package/dist/core/auth/sessionCookie.d.ts +8 -2
- package/dist/core/contracts/authUserDirectory.d.ts +1 -0
- package/dist/entries/auth/sessionCookie.js +17 -2
- package/dist/entries/auth/sessionGuard.js +21 -5
- package/dist/entries/jobs/dispatchWebhookJob.js +5 -1
- package/dist/entries/openapi/generator.js +2 -1
- package/package.json +1 -1
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.
|
|
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, };
|
|
@@ -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
|
|
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
|
|
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
|
|
186
|
-
if (!
|
|
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/")) {
|