@getstrata/core 0.5.5 → 0.5.6
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/core/crypto/nonCryptographicHash.d.ts +2 -0
- package/dist/core/http/cookies.d.ts +4 -0
- package/dist/core/http/csrfProtection.d.ts +12 -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 +4 -1
|
@@ -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) {
|
package/dist/entries/http.js
CHANGED
|
@@ -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) {
|
|
@@ -3166,8 +3152,12 @@ function createAuthorizeMiddleware(gate, auth, resource, action) {
|
|
|
3166
3152
|
return await next();
|
|
3167
3153
|
};
|
|
3168
3154
|
}
|
|
3155
|
+
// ../../src/core/crypto/nonCryptographicHash.ts
|
|
3156
|
+
function nonCryptographicDigest(input) {
|
|
3157
|
+
return Bun.hash(input).toString(16);
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3169
3160
|
// ../../src/core/http/etag.ts
|
|
3170
|
-
import { createHash as createHash2 } from "crypto";
|
|
3171
3161
|
function isEtagEnabled() {
|
|
3172
3162
|
return (process.env.FEATURE_ETAG ?? "true") !== "false";
|
|
3173
3163
|
}
|
|
@@ -3175,13 +3165,13 @@ function formatWeakEtag(digest) {
|
|
|
3175
3165
|
return `W/"${digest}"`;
|
|
3176
3166
|
}
|
|
3177
3167
|
function computeEtagFromJson(data) {
|
|
3178
|
-
const digest =
|
|
3168
|
+
const digest = nonCryptographicDigest(JSON.stringify(data)).slice(0, 32);
|
|
3179
3169
|
return formatWeakEtag(digest);
|
|
3180
3170
|
}
|
|
3181
3171
|
function etagFromResource(resource) {
|
|
3182
3172
|
const version = resource.updated_at ?? resource.created_at ?? "";
|
|
3183
3173
|
const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
|
|
3184
|
-
const digest =
|
|
3174
|
+
const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
|
|
3185
3175
|
return formatWeakEtag(digest);
|
|
3186
3176
|
}
|
|
3187
3177
|
function normalizeEtag(value) {
|
|
@@ -1751,26 +1751,26 @@ class Model {
|
|
|
1751
1751
|
throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
|
|
1752
1752
|
}
|
|
1753
1753
|
static primaryKeyField() {
|
|
1754
|
-
return resolveModelRepository(
|
|
1754
|
+
return resolveModelRepository(Model).getTable().primaryKey;
|
|
1755
1755
|
}
|
|
1756
1756
|
static hydrateAttributes(attributes) {
|
|
1757
|
-
const casts = modelStatics(
|
|
1757
|
+
const casts = modelStatics(Model).$casts ?? {};
|
|
1758
1758
|
return applyCasts(attributes, casts, "hydrate");
|
|
1759
1759
|
}
|
|
1760
1760
|
static dehydrateAttributes(attributes) {
|
|
1761
|
-
const casts = modelStatics(
|
|
1761
|
+
const casts = modelStatics(Model).$casts ?? {};
|
|
1762
1762
|
return applyCasts(attributes, casts, "dehydrate");
|
|
1763
1763
|
}
|
|
1764
1764
|
static fromRecord(record, repository, exists = true) {
|
|
1765
|
-
const statics = modelStatics(
|
|
1765
|
+
const statics = modelStatics(Model);
|
|
1766
1766
|
const hydrated = statics.hydrateAttributes(record);
|
|
1767
1767
|
return new statics(hydrated, repository, exists);
|
|
1768
1768
|
}
|
|
1769
1769
|
static boot() {}
|
|
1770
1770
|
static addGlobalScope(_name, scope) {
|
|
1771
|
-
ensureBooted(
|
|
1772
|
-
const existing = modelGlobalScopes.get(
|
|
1773
|
-
modelGlobalScopes.set(
|
|
1771
|
+
ensureBooted(Model);
|
|
1772
|
+
const existing = modelGlobalScopes.get(Model) ?? [];
|
|
1773
|
+
modelGlobalScopes.set(Model, [
|
|
1774
1774
|
...existing,
|
|
1775
1775
|
scope
|
|
1776
1776
|
]);
|
|
@@ -1780,17 +1780,17 @@ class Model {
|
|
|
1780
1780
|
}
|
|
1781
1781
|
static query() {
|
|
1782
1782
|
ensureBooted(this);
|
|
1783
|
-
const repository = resolveModelRepository(
|
|
1783
|
+
const repository = resolveModelRepository(Model);
|
|
1784
1784
|
let query = repository.query();
|
|
1785
|
-
for (const scope of getGlobalScopes(
|
|
1785
|
+
for (const scope of getGlobalScopes(Model)) {
|
|
1786
1786
|
query = scope(query);
|
|
1787
1787
|
}
|
|
1788
1788
|
return query;
|
|
1789
1789
|
}
|
|
1790
1790
|
static async create(attributes) {
|
|
1791
1791
|
const statics = modelStatics(this);
|
|
1792
|
-
ensureBooted(
|
|
1793
|
-
const repository = resolveModelRepository(
|
|
1792
|
+
ensureBooted(Model);
|
|
1793
|
+
const repository = resolveModelRepository(Model);
|
|
1794
1794
|
const table = repository.getTable();
|
|
1795
1795
|
const timestamps = statics.$timestamps ?? true;
|
|
1796
1796
|
const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
|
|
@@ -1800,23 +1800,23 @@ class Model {
|
|
|
1800
1800
|
return statics.fromRecord(record, repository, true);
|
|
1801
1801
|
}
|
|
1802
1802
|
static async find(id) {
|
|
1803
|
-
const statics = modelStatics(
|
|
1804
|
-
const repository = resolveModelRepository(
|
|
1803
|
+
const statics = modelStatics(Model);
|
|
1804
|
+
const repository = resolveModelRepository(Model);
|
|
1805
1805
|
const primaryKey = repository.getTable().primaryKey;
|
|
1806
|
-
const record = await Model.query.call(
|
|
1806
|
+
const record = await Model.query.call(Model).where({ [primaryKey]: id }).first();
|
|
1807
1807
|
return record ? statics.fromRecord(record, repository, true) : null;
|
|
1808
1808
|
}
|
|
1809
1809
|
static async findOrFail(id, errorFactory) {
|
|
1810
|
-
const model = await Model.find.call(
|
|
1810
|
+
const model = await Model.find.call(Model, id);
|
|
1811
1811
|
if (model) {
|
|
1812
1812
|
return model;
|
|
1813
1813
|
}
|
|
1814
|
-
throw errorFactory?.(id) ?? new NotFoundError(`${
|
|
1814
|
+
throw errorFactory?.(id) ?? new NotFoundError(`${Model.name} ${String(id)} not found.`);
|
|
1815
1815
|
}
|
|
1816
1816
|
static async all(options = {}) {
|
|
1817
1817
|
const statics = modelStatics(this);
|
|
1818
|
-
const repository = resolveModelRepository(
|
|
1819
|
-
let query = Model.query.call(
|
|
1818
|
+
const repository = resolveModelRepository(Model);
|
|
1819
|
+
let query = Model.query.call(Model);
|
|
1820
1820
|
if (options.orderBy) {
|
|
1821
1821
|
query = query.orderBy(options.orderBy);
|
|
1822
1822
|
}
|
|
@@ -1828,8 +1828,8 @@ class Model {
|
|
|
1828
1828
|
}
|
|
1829
1829
|
static async firstWhere(where, options = {}) {
|
|
1830
1830
|
const statics = modelStatics(this);
|
|
1831
|
-
const repository = resolveModelRepository(
|
|
1832
|
-
let query = Model.query.call(
|
|
1831
|
+
const repository = resolveModelRepository(Model);
|
|
1832
|
+
let query = Model.query.call(Model).where(where);
|
|
1833
1833
|
if (options.orderBy) {
|
|
1834
1834
|
query = query.orderBy(options.orderBy);
|
|
1835
1835
|
}
|