@getstrata/core 0.5.5 → 0.5.7
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/README.md +3 -0
- package/dist/bootstrap/discoverModules.d.ts +4 -0
- package/dist/bootstrap/modules.d.ts +1 -0
- package/dist/bootstrap/routeRegistry.d.ts +14 -0
- package/dist/core/audit/exportAuditLogs.d.ts +8 -0
- package/dist/core/audit/siemFormatter.d.ts +30 -0
- package/dist/core/auth/membershipScope.d.ts +16 -0
- package/dist/core/auth/membershipService.d.ts +20 -0
- package/dist/core/auth/scimAuthMiddleware.d.ts +3 -0
- package/dist/core/auth/sessionCookie.d.ts +6 -0
- package/dist/core/auth/sessionGuard.d.ts +9 -0
- package/dist/core/cache/createCacheStore.d.ts +11 -0
- package/dist/core/cache/keys.d.ts +2 -0
- package/dist/core/cache/modelCacheTags.d.ts +3 -0
- package/dist/core/cache/redisCacheStore.d.ts +23 -0
- package/dist/core/cache/simpleCache.d.ts +23 -0
- package/dist/core/cache/simpleCacheStore.d.ts +16 -0
- package/dist/core/config/envSchema.d.ts +12 -0
- package/dist/core/crypto/nonCryptographicHash.d.ts +2 -0
- package/dist/core/database/factory.d.ts +6 -0
- package/dist/core/http/cookies.d.ts +4 -0
- package/dist/core/http/csrfProtection.d.ts +12 -0
- package/dist/core/http/scimThrottleMiddleware.d.ts +9 -0
- package/dist/core/jobs/dispatchWebhookJob.d.ts +14 -0
- package/dist/core/jobs/invalidateCacheTagsJob.d.ts +12 -0
- package/dist/core/openapi/generator.d.ts +22 -0
- package/dist/core/openapi/validate.d.ts +3 -0
- package/dist/core/queue/createAppQueue.d.ts +6 -0
- package/dist/core/queue/queueMetrics.d.ts +15 -0
- package/dist/core/security/oauthState.d.ts +8 -0
- package/dist/core/security/safeFetch.d.ts +8 -0
- package/dist/core/security/safeUrl.d.ts +5 -0
- package/dist/core/security/scimTenantTokens.d.ts +3 -0
- package/dist/core/security/stripeWebhook.d.ts +2 -0
- package/dist/core/security/timingSafeCompare.d.ts +2 -0
- package/dist/domain/scim.d.ts +9 -0
- package/dist/entries/auth/guard.js +20 -20
- package/dist/entries/database.js +20 -20
- package/dist/entries/http/csrfToken.js +49 -65
- package/dist/entries/http/etag.js +6 -4
- package/dist/entries/http/webErrorResponse.js +73 -87
- package/dist/entries/http.js +80 -90
- package/dist/entries/queue/createAppQueue.js +20 -20
- package/dist/entries/queue/publicQueue.js +20 -20
- package/dist/entries/queue/queueMetrics.js +20 -20
- package/dist/entries/view.js +73 -87
- package/dist/framework/public-api.d.ts +4 -0
- package/dist/index.js +121 -107
- package/package.json +61 -58
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/http/csrfToken.ts
|
|
3
|
-
import {
|
|
3
|
+
import { timingSafeEqual } from "crypto";
|
|
4
|
+
|
|
5
|
+
// ../../src/core/http/cookies.ts
|
|
6
|
+
function readRequestCookie(request, name) {
|
|
7
|
+
const cookies = request.cookies;
|
|
8
|
+
if (cookies && typeof cookies.get === "function") {
|
|
9
|
+
const value = cookies.get(name);
|
|
10
|
+
if (value) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const header = request.headers.get("cookie");
|
|
15
|
+
if (!header) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
for (const part of header.split(";")) {
|
|
19
|
+
const idx = part.indexOf("=");
|
|
20
|
+
if (idx === -1)
|
|
21
|
+
continue;
|
|
22
|
+
const cookieName = part.slice(0, idx).trim();
|
|
23
|
+
if (cookieName !== name)
|
|
24
|
+
continue;
|
|
25
|
+
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function readBunRequestCookie(request, name) {
|
|
30
|
+
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
31
|
+
}
|
|
4
32
|
|
|
5
33
|
// ../../src/core/http/requestMetaContext.ts
|
|
6
34
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -21,70 +49,28 @@ var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
|
21
49
|
function resolveCsrfSecret() {
|
|
22
50
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
23
51
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
const signature = createHmac("sha256", resolveCsrfSecret()).update(payload).digest("hex");
|
|
27
|
-
return `${payload}.${signature}`;
|
|
52
|
+
function csrfVerifyOptions() {
|
|
53
|
+
return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
|
|
28
54
|
}
|
|
29
|
-
function
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
for (const part of cookieHeader.split(";")) {
|
|
35
|
-
const [name, ...rest] = part.trim().split("=");
|
|
36
|
-
if (name === CSRF_COOKIE) {
|
|
37
|
-
return decodeURIComponent(rest.join("="));
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
function parseSignedCsrfValue(cookieValue) {
|
|
43
|
-
const parts = cookieValue.split(".");
|
|
44
|
-
if (parts.length !== 3) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
const [token, issuedAtRaw, cookieSignature] = parts;
|
|
48
|
-
if (!token || !issuedAtRaw || !cookieSignature) {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
52
|
-
if (!Number.isFinite(issuedAt)) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
if (Date.now() - issuedAt > CSRF_TTL_MS) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
const expectedSignature = signCsrfToken(token, issuedAt).split(".").pop();
|
|
59
|
-
if (!expectedSignature) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
const expectedBuffer = Buffer.from(expectedSignature);
|
|
63
|
-
const actualBuffer = Buffer.from(cookieSignature);
|
|
64
|
-
if (expectedBuffer.length !== actualBuffer.length) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
|
|
68
|
-
return null;
|
|
55
|
+
function tokensMatch(left, right) {
|
|
56
|
+
const leftBuffer = Buffer.from(left);
|
|
57
|
+
const rightBuffer = Buffer.from(right);
|
|
58
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
59
|
+
return false;
|
|
69
60
|
}
|
|
70
|
-
return
|
|
61
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
71
62
|
}
|
|
72
63
|
function createCsrfTokenCookie() {
|
|
73
|
-
const token =
|
|
74
|
-
const issuedAt = Date.now();
|
|
75
|
-
const value = signCsrfToken(token, issuedAt);
|
|
64
|
+
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
76
65
|
return {
|
|
77
66
|
token,
|
|
78
|
-
cookie: `${CSRF_COOKIE}=${encodeURIComponent(
|
|
67
|
+
cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
|
|
79
68
|
};
|
|
80
69
|
}
|
|
81
70
|
function resolveCsrfToken(request) {
|
|
82
|
-
const cookieValue =
|
|
83
|
-
if (cookieValue) {
|
|
84
|
-
|
|
85
|
-
if (parsed) {
|
|
86
|
-
return { token: parsed.token };
|
|
87
|
-
}
|
|
71
|
+
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
72
|
+
if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
|
|
73
|
+
return { token: cookieValue };
|
|
88
74
|
}
|
|
89
75
|
return createCsrfTokenCookie();
|
|
90
76
|
}
|
|
@@ -107,6 +93,10 @@ async function readSubmittedCsrfTokenFromBody(request) {
|
|
|
107
93
|
if (typeof field === "string" && field.trim().length > 0) {
|
|
108
94
|
return field.trim();
|
|
109
95
|
}
|
|
96
|
+
const legacyField = formData.get("_csrf");
|
|
97
|
+
if (typeof legacyField === "string" && legacyField.trim().length > 0) {
|
|
98
|
+
return legacyField.trim();
|
|
99
|
+
}
|
|
110
100
|
}
|
|
111
101
|
return null;
|
|
112
102
|
}
|
|
@@ -114,20 +104,14 @@ function verifyCsrfToken(request, submittedToken) {
|
|
|
114
104
|
if (!submittedToken) {
|
|
115
105
|
return false;
|
|
116
106
|
}
|
|
117
|
-
const cookieValue =
|
|
107
|
+
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
118
108
|
if (!cookieValue) {
|
|
119
109
|
return false;
|
|
120
110
|
}
|
|
121
|
-
|
|
122
|
-
if (!parsed) {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
const submittedBuffer = Buffer.from(submittedToken);
|
|
126
|
-
const expectedBuffer = Buffer.from(parsed.token);
|
|
127
|
-
if (submittedBuffer.length !== expectedBuffer.length) {
|
|
111
|
+
if (!tokensMatch(submittedToken, cookieValue)) {
|
|
128
112
|
return false;
|
|
129
113
|
}
|
|
130
|
-
return
|
|
114
|
+
return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
|
|
131
115
|
}
|
|
132
116
|
function resolveCsrfTokenForRequest(request) {
|
|
133
117
|
const metaToken = currentRequestMeta().csrfToken;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// ../../src/core/
|
|
3
|
-
|
|
2
|
+
// ../../src/core/crypto/nonCryptographicHash.ts
|
|
3
|
+
function nonCryptographicDigest(input) {
|
|
4
|
+
return Bun.hash(input).toString(16);
|
|
5
|
+
}
|
|
4
6
|
|
|
5
7
|
// ../../src/core/errors/http.ts
|
|
6
8
|
class HttpError extends Error {
|
|
@@ -76,13 +78,13 @@ function formatWeakEtag(digest) {
|
|
|
76
78
|
return `W/"${digest}"`;
|
|
77
79
|
}
|
|
78
80
|
function computeEtagFromJson(data) {
|
|
79
|
-
const digest =
|
|
81
|
+
const digest = nonCryptographicDigest(JSON.stringify(data)).slice(0, 32);
|
|
80
82
|
return formatWeakEtag(digest);
|
|
81
83
|
}
|
|
82
84
|
function etagFromResource(resource) {
|
|
83
85
|
const version = resource.updated_at ?? resource.created_at ?? "";
|
|
84
86
|
const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
|
|
85
|
-
const digest =
|
|
87
|
+
const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
|
|
86
88
|
return formatWeakEtag(digest);
|
|
87
89
|
}
|
|
88
90
|
function normalizeEtag(value) {
|
|
@@ -1577,26 +1577,26 @@ class Model {
|
|
|
1577
1577
|
throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
|
|
1578
1578
|
}
|
|
1579
1579
|
static primaryKeyField() {
|
|
1580
|
-
return resolveModelRepository(
|
|
1580
|
+
return resolveModelRepository(Model).getTable().primaryKey;
|
|
1581
1581
|
}
|
|
1582
1582
|
static hydrateAttributes(attributes) {
|
|
1583
|
-
const casts = modelStatics(
|
|
1583
|
+
const casts = modelStatics(Model).$casts ?? {};
|
|
1584
1584
|
return applyCasts(attributes, casts, "hydrate");
|
|
1585
1585
|
}
|
|
1586
1586
|
static dehydrateAttributes(attributes) {
|
|
1587
|
-
const casts = modelStatics(
|
|
1587
|
+
const casts = modelStatics(Model).$casts ?? {};
|
|
1588
1588
|
return applyCasts(attributes, casts, "dehydrate");
|
|
1589
1589
|
}
|
|
1590
1590
|
static fromRecord(record, repository, exists = true) {
|
|
1591
|
-
const statics = modelStatics(
|
|
1591
|
+
const statics = modelStatics(Model);
|
|
1592
1592
|
const hydrated = statics.hydrateAttributes(record);
|
|
1593
1593
|
return new statics(hydrated, repository, exists);
|
|
1594
1594
|
}
|
|
1595
1595
|
static boot() {}
|
|
1596
1596
|
static addGlobalScope(_name, scope) {
|
|
1597
|
-
ensureBooted(
|
|
1598
|
-
const existing = modelGlobalScopes.get(
|
|
1599
|
-
modelGlobalScopes.set(
|
|
1597
|
+
ensureBooted(Model);
|
|
1598
|
+
const existing = modelGlobalScopes.get(Model) ?? [];
|
|
1599
|
+
modelGlobalScopes.set(Model, [
|
|
1600
1600
|
...existing,
|
|
1601
1601
|
scope
|
|
1602
1602
|
]);
|
|
@@ -1606,17 +1606,17 @@ class Model {
|
|
|
1606
1606
|
}
|
|
1607
1607
|
static query() {
|
|
1608
1608
|
ensureBooted(this);
|
|
1609
|
-
const repository = resolveModelRepository(
|
|
1609
|
+
const repository = resolveModelRepository(Model);
|
|
1610
1610
|
let query = repository.query();
|
|
1611
|
-
for (const scope of getGlobalScopes(
|
|
1611
|
+
for (const scope of getGlobalScopes(Model)) {
|
|
1612
1612
|
query = scope(query);
|
|
1613
1613
|
}
|
|
1614
1614
|
return query;
|
|
1615
1615
|
}
|
|
1616
1616
|
static async create(attributes) {
|
|
1617
1617
|
const statics = modelStatics(this);
|
|
1618
|
-
ensureBooted(
|
|
1619
|
-
const repository = resolveModelRepository(
|
|
1618
|
+
ensureBooted(Model);
|
|
1619
|
+
const repository = resolveModelRepository(Model);
|
|
1620
1620
|
const table = repository.getTable();
|
|
1621
1621
|
const timestamps = statics.$timestamps ?? true;
|
|
1622
1622
|
const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
|
|
@@ -1626,23 +1626,23 @@ class Model {
|
|
|
1626
1626
|
return statics.fromRecord(record, repository, true);
|
|
1627
1627
|
}
|
|
1628
1628
|
static async find(id) {
|
|
1629
|
-
const statics = modelStatics(
|
|
1630
|
-
const repository = resolveModelRepository(
|
|
1629
|
+
const statics = modelStatics(Model);
|
|
1630
|
+
const repository = resolveModelRepository(Model);
|
|
1631
1631
|
const primaryKey = repository.getTable().primaryKey;
|
|
1632
|
-
const record = await Model.query.call(
|
|
1632
|
+
const record = await Model.query.call(Model).where({ [primaryKey]: id }).first();
|
|
1633
1633
|
return record ? statics.fromRecord(record, repository, true) : null;
|
|
1634
1634
|
}
|
|
1635
1635
|
static async findOrFail(id, errorFactory) {
|
|
1636
|
-
const model = await Model.find.call(
|
|
1636
|
+
const model = await Model.find.call(Model, id);
|
|
1637
1637
|
if (model) {
|
|
1638
1638
|
return model;
|
|
1639
1639
|
}
|
|
1640
|
-
throw errorFactory?.(id) ?? new NotFoundError(`${
|
|
1640
|
+
throw errorFactory?.(id) ?? new NotFoundError(`${Model.name} ${String(id)} not found.`);
|
|
1641
1641
|
}
|
|
1642
1642
|
static async all(options = {}) {
|
|
1643
1643
|
const statics = modelStatics(this);
|
|
1644
|
-
const repository = resolveModelRepository(
|
|
1645
|
-
let query = Model.query.call(
|
|
1644
|
+
const repository = resolveModelRepository(Model);
|
|
1645
|
+
let query = Model.query.call(Model);
|
|
1646
1646
|
if (options.orderBy) {
|
|
1647
1647
|
query = query.orderBy(options.orderBy);
|
|
1648
1648
|
}
|
|
@@ -1654,8 +1654,8 @@ class Model {
|
|
|
1654
1654
|
}
|
|
1655
1655
|
static async firstWhere(where, options = {}) {
|
|
1656
1656
|
const statics = modelStatics(this);
|
|
1657
|
-
const repository = resolveModelRepository(
|
|
1658
|
-
let query = Model.query.call(
|
|
1657
|
+
const repository = resolveModelRepository(Model);
|
|
1658
|
+
let query = Model.query.call(Model).where(where);
|
|
1659
1659
|
if (options.orderBy) {
|
|
1660
1660
|
query = query.orderBy(options.orderBy);
|
|
1661
1661
|
}
|
|
@@ -2817,76 +2817,64 @@ function hashApiToken(token) {
|
|
|
2817
2817
|
var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
2818
2818
|
|
|
2819
2819
|
// ../../src/core/http/csrfToken.ts
|
|
2820
|
-
import {
|
|
2820
|
+
import { timingSafeEqual } from "crypto";
|
|
2821
|
+
|
|
2822
|
+
// ../../src/core/http/cookies.ts
|
|
2823
|
+
function readRequestCookie(request, name) {
|
|
2824
|
+
const cookies = request.cookies;
|
|
2825
|
+
if (cookies && typeof cookies.get === "function") {
|
|
2826
|
+
const value = cookies.get(name);
|
|
2827
|
+
if (value) {
|
|
2828
|
+
return value;
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
const header = request.headers.get("cookie");
|
|
2832
|
+
if (!header) {
|
|
2833
|
+
return null;
|
|
2834
|
+
}
|
|
2835
|
+
for (const part of header.split(";")) {
|
|
2836
|
+
const idx = part.indexOf("=");
|
|
2837
|
+
if (idx === -1)
|
|
2838
|
+
continue;
|
|
2839
|
+
const cookieName = part.slice(0, idx).trim();
|
|
2840
|
+
if (cookieName !== name)
|
|
2841
|
+
continue;
|
|
2842
|
+
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
2843
|
+
}
|
|
2844
|
+
return null;
|
|
2845
|
+
}
|
|
2846
|
+
function readBunRequestCookie(request, name) {
|
|
2847
|
+
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// ../../src/core/http/csrfToken.ts
|
|
2821
2851
|
var CSRF_COOKIE = "workhub_csrf";
|
|
2822
2852
|
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
2823
2853
|
function resolveCsrfSecret() {
|
|
2824
2854
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
2825
2855
|
}
|
|
2826
|
-
function
|
|
2827
|
-
|
|
2828
|
-
const signature = createHmac4("sha256", resolveCsrfSecret()).update(payload).digest("hex");
|
|
2829
|
-
return `${payload}.${signature}`;
|
|
2856
|
+
function csrfVerifyOptions() {
|
|
2857
|
+
return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
|
|
2830
2858
|
}
|
|
2831
|
-
function
|
|
2832
|
-
const
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
for (const part of cookieHeader.split(";")) {
|
|
2837
|
-
const [name, ...rest] = part.trim().split("=");
|
|
2838
|
-
if (name === CSRF_COOKIE) {
|
|
2839
|
-
return decodeURIComponent(rest.join("="));
|
|
2840
|
-
}
|
|
2841
|
-
}
|
|
2842
|
-
return null;
|
|
2843
|
-
}
|
|
2844
|
-
function parseSignedCsrfValue(cookieValue) {
|
|
2845
|
-
const parts = cookieValue.split(".");
|
|
2846
|
-
if (parts.length !== 3) {
|
|
2847
|
-
return null;
|
|
2848
|
-
}
|
|
2849
|
-
const [token, issuedAtRaw, cookieSignature] = parts;
|
|
2850
|
-
if (!token || !issuedAtRaw || !cookieSignature) {
|
|
2851
|
-
return null;
|
|
2852
|
-
}
|
|
2853
|
-
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
2854
|
-
if (!Number.isFinite(issuedAt)) {
|
|
2855
|
-
return null;
|
|
2856
|
-
}
|
|
2857
|
-
if (Date.now() - issuedAt > CSRF_TTL_MS) {
|
|
2858
|
-
return null;
|
|
2859
|
-
}
|
|
2860
|
-
const expectedSignature = signCsrfToken(token, issuedAt).split(".").pop();
|
|
2861
|
-
if (!expectedSignature) {
|
|
2862
|
-
return null;
|
|
2863
|
-
}
|
|
2864
|
-
const expectedBuffer = Buffer.from(expectedSignature);
|
|
2865
|
-
const actualBuffer = Buffer.from(cookieSignature);
|
|
2866
|
-
if (expectedBuffer.length !== actualBuffer.length) {
|
|
2867
|
-
return null;
|
|
2868
|
-
}
|
|
2869
|
-
if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
|
|
2870
|
-
return null;
|
|
2859
|
+
function tokensMatch(left, right) {
|
|
2860
|
+
const leftBuffer = Buffer.from(left);
|
|
2861
|
+
const rightBuffer = Buffer.from(right);
|
|
2862
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
2863
|
+
return false;
|
|
2871
2864
|
}
|
|
2872
|
-
return
|
|
2865
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
2873
2866
|
}
|
|
2874
2867
|
function createCsrfTokenCookie() {
|
|
2875
|
-
const token =
|
|
2876
|
-
const issuedAt = Date.now();
|
|
2877
|
-
const value = signCsrfToken(token, issuedAt);
|
|
2868
|
+
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
2878
2869
|
return {
|
|
2879
2870
|
token,
|
|
2880
|
-
cookie: `${CSRF_COOKIE}=${encodeURIComponent(
|
|
2871
|
+
cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
|
|
2881
2872
|
};
|
|
2882
2873
|
}
|
|
2883
2874
|
function resolveCsrfToken(request) {
|
|
2884
|
-
const cookieValue =
|
|
2885
|
-
if (cookieValue) {
|
|
2886
|
-
|
|
2887
|
-
if (parsed) {
|
|
2888
|
-
return { token: parsed.token };
|
|
2889
|
-
}
|
|
2875
|
+
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
2876
|
+
if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
|
|
2877
|
+
return { token: cookieValue };
|
|
2890
2878
|
}
|
|
2891
2879
|
return createCsrfTokenCookie();
|
|
2892
2880
|
}
|
|
@@ -2909,6 +2897,10 @@ async function readSubmittedCsrfTokenFromBody(request) {
|
|
|
2909
2897
|
if (typeof field === "string" && field.trim().length > 0) {
|
|
2910
2898
|
return field.trim();
|
|
2911
2899
|
}
|
|
2900
|
+
const legacyField = formData.get("_csrf");
|
|
2901
|
+
if (typeof legacyField === "string" && legacyField.trim().length > 0) {
|
|
2902
|
+
return legacyField.trim();
|
|
2903
|
+
}
|
|
2912
2904
|
}
|
|
2913
2905
|
return null;
|
|
2914
2906
|
}
|
|
@@ -2916,20 +2908,14 @@ function verifyCsrfToken(request, submittedToken) {
|
|
|
2916
2908
|
if (!submittedToken) {
|
|
2917
2909
|
return false;
|
|
2918
2910
|
}
|
|
2919
|
-
const cookieValue =
|
|
2911
|
+
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
2920
2912
|
if (!cookieValue) {
|
|
2921
2913
|
return false;
|
|
2922
2914
|
}
|
|
2923
|
-
|
|
2924
|
-
if (!parsed) {
|
|
2925
|
-
return false;
|
|
2926
|
-
}
|
|
2927
|
-
const submittedBuffer = Buffer.from(submittedToken);
|
|
2928
|
-
const expectedBuffer = Buffer.from(parsed.token);
|
|
2929
|
-
if (submittedBuffer.length !== expectedBuffer.length) {
|
|
2915
|
+
if (!tokensMatch(submittedToken, cookieValue)) {
|
|
2930
2916
|
return false;
|
|
2931
2917
|
}
|
|
2932
|
-
return
|
|
2918
|
+
return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
|
|
2933
2919
|
}
|
|
2934
2920
|
function resolveCsrfTokenForRequest(request) {
|
|
2935
2921
|
const metaToken = currentRequestMeta().csrfToken;
|
|
@@ -2940,14 +2926,14 @@ function resolveCsrfTokenForRequest(request) {
|
|
|
2940
2926
|
}
|
|
2941
2927
|
|
|
2942
2928
|
// ../../src/core/http/flashSession.ts
|
|
2943
|
-
import { createHmac as
|
|
2929
|
+
import { createHmac as createHmac4, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
2944
2930
|
var FLASH_COOKIE = "workhub_flash";
|
|
2945
2931
|
var FLASH_TTL_MS = 60 * 1000;
|
|
2946
2932
|
function resolveFlashSecret() {
|
|
2947
2933
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
2948
2934
|
}
|
|
2949
2935
|
function signFlashPayload(payload, issuedAt) {
|
|
2950
|
-
const signature =
|
|
2936
|
+
const signature = createHmac4("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
2951
2937
|
return `${payload}.${issuedAt}.${signature}`;
|
|
2952
2938
|
}
|
|
2953
2939
|
function readFlashCookie(request) {
|