@getstrata/core 0.4.0 → 0.5.1
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/dist/bootstrap/httpKernel.d.ts +2 -0
- package/dist/config/queue.d.ts +8 -0
- package/dist/config/rateLimit.d.ts +2 -1
- package/dist/core/database/baseRepository.d.ts +16 -4
- package/dist/core/database/index.d.ts +10 -4
- package/dist/core/database/migrations/advisoryLock.d.ts +6 -0
- package/dist/core/database/migrations/runner.d.ts +8 -2
- package/dist/core/database/migrations/types.d.ts +1 -0
- package/dist/core/database/model.d.ts +46 -17
- package/dist/core/database/query.d.ts +25 -14
- package/dist/core/database/relationships.d.ts +32 -2
- package/dist/core/database/repositoryQuery.d.ts +16 -1
- package/dist/core/database/schema/blueprint.d.ts +47 -0
- package/dist/core/database/schema/columnDefinition.d.ts +36 -0
- package/dist/core/database/schema/driver.d.ts +8 -0
- package/dist/core/database/schema/errors.d.ts +4 -0
- package/dist/core/database/schema/grammars/compileStatements.d.ts +8 -0
- package/dist/core/database/schema/grammars/createGrammar.d.ts +4 -0
- package/dist/core/database/schema/grammars/grammar.d.ts +10 -0
- package/dist/core/database/schema/grammars/index.d.ts +7 -0
- package/dist/core/database/schema/grammars/mysqlGrammar.d.ts +2 -0
- package/dist/core/database/schema/grammars/postgresGrammar.d.ts +2 -0
- package/dist/core/database/schema/grammars/sqliteGrammar.d.ts +2 -0
- package/dist/core/database/schema/index.d.ts +12 -0
- package/dist/core/database/schema/schema.d.ts +21 -0
- package/dist/core/database/seeders/runner.d.ts +6 -0
- package/dist/core/database/seeders/types.d.ts +8 -0
- package/dist/core/database/types.d.ts +39 -2
- package/dist/core/database/whereBuilder.d.ts +17 -0
- package/dist/core/http/index.d.ts +1 -1
- package/dist/core/http/securedRouteModelBinding.d.ts +2 -1
- package/dist/core/lifecycle/gracefulShutdown.d.ts +6 -0
- package/dist/core/pagination/index.d.ts +11 -1
- package/dist/core/queue/failedJobRepository.d.ts +6 -0
- package/dist/core/queue/failedJobService.d.ts +15 -0
- package/dist/core/queue/failedJobTable.d.ts +3 -0
- package/dist/core/queue/jobRegistry.d.ts +14 -0
- package/dist/core/queue/jobRunner.d.ts +9 -0
- package/dist/core/queue/publicQueue.d.ts +15 -0
- package/dist/core/queue/redisQueue.d.ts +29 -0
- package/dist/core/queue/resilientQueue.d.ts +9 -0
- package/dist/core/queue/types.d.ts +8 -0
- package/dist/core/scheduler/schedule.d.ts +15 -0
- package/dist/entries/auth/accessControl.js +113 -0
- package/dist/entries/auth/authContext.js +15 -0
- package/dist/entries/auth/guard.js +2871 -0
- package/dist/entries/auth/membershipContext.js +276 -0
- package/dist/entries/auth/membershipScope.js +390 -0
- package/dist/entries/auth/membershipService.js +477 -0
- package/dist/entries/auth/oauth/oidcProvider.js +49 -0
- package/dist/entries/auth/oauth/providers.js +71 -0
- package/dist/entries/auth/oauth/samlProvider.js +26 -0
- package/dist/entries/auth/oauth/types.js +1 -0
- package/dist/entries/auth/password.js +15 -0
- package/dist/entries/auth/policy.js +134 -0
- package/dist/entries/auth/sessionCookie.js +75 -0
- package/dist/entries/auth/tokenHash.js +17 -0
- package/dist/entries/cache/tags.js +13 -0
- package/dist/entries/crypto/fieldEncryption.js +93 -0
- package/dist/entries/crypto/mfaSecret.js +105 -0
- package/dist/entries/database/factory.js +17 -0
- package/dist/entries/database/seeders.js +30 -0
- package/dist/entries/database/types.js +1 -0
- package/dist/entries/database.js +2219 -0
- package/dist/entries/errors/http.js +78 -0
- package/dist/entries/http/contentNegotiation.js +26 -0
- package/dist/entries/http/csrfToken.js +147 -0
- package/dist/entries/http/etag.js +170 -0
- package/dist/entries/http/flashSession.js +106 -0
- package/dist/entries/http/middleware.js +68 -0
- package/dist/entries/http/parseFormBody.js +88 -0
- package/dist/entries/http/requestMetaContext.js +18 -0
- package/dist/entries/http/resources.js +19 -0
- package/dist/entries/http/webErrorResponse.js +3144 -0
- package/dist/entries/http/webFormRequest.js +293 -0
- package/dist/entries/http.js +3925 -0
- package/dist/entries/jobs/dispatchWebhookJob.js +342 -0
- package/dist/entries/lifecycle/gracefulShutdown.js +50 -0
- package/dist/entries/metrics/prometheus.js +70 -0
- package/dist/entries/pagination.js +14 -0
- package/dist/entries/queue/createAppQueue.js +2849 -0
- package/dist/entries/queue/failedJobService.js +38 -0
- package/dist/entries/queue/jobRegistry.js +32 -0
- package/dist/entries/queue/jobRunner.js +75 -0
- package/dist/entries/queue/publicQueue.js +2477 -0
- package/dist/entries/queue/queueMetrics.js +2899 -0
- package/dist/entries/queue/types.js +1 -0
- package/dist/entries/security/oauthState.js +75 -0
- package/dist/entries/security/publicReads.js +33 -0
- package/dist/entries/security/safeUrl.js +143 -0
- package/dist/entries/security/securityEvents.js +41 -0
- package/dist/entries/security/stripeWebhook.js +115 -0
- package/dist/entries/security/tokenExpiry.js +16 -0
- package/dist/entries/security/totp.js +51 -0
- package/dist/entries/storage/storage.js +123 -0
- package/dist/entries/tenant/tenantContext.js +30 -0
- package/dist/entries/tenant/tenantMiddleware.js +312 -0
- package/dist/entries/tracing/traceContext.js +15 -0
- package/dist/entries/validation/rules.js +232 -0
- package/dist/entries/view.js +3073 -0
- package/dist/framework/public-api.d.ts +65 -38
- package/dist/index.js +3199 -308
- package/dist/modules/user/apiTokenRepository.d.ts +1 -1
- package/dist/modules/user/apiTokenTable.d.ts +1 -1
- package/dist/modules/user/authService.d.ts +1 -1
- package/dist/modules/user/notificationRepository.d.ts +1 -1
- package/dist/modules/user/notificationService.d.ts +1 -1
- package/dist/modules/user/notificationTable.d.ts +1 -1
- package/dist/modules/user/oauthIdentityRepository.d.ts +2 -2
- package/dist/modules/user/provider.d.ts +1 -1
- package/dist/modules/user/repository.d.ts +1 -1
- package/dist/modules/user/table.d.ts +1 -1
- package/dist/modules/user/tokenService.d.ts +1 -1
- package/package.json +289 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/security/oauthState.ts
|
|
3
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
4
|
+
var OAUTH_STATE_COOKIE = "oauth_state";
|
|
5
|
+
var OAUTH_STATE_TTL_MS = 10 * 60 * 1000;
|
|
6
|
+
function resolveOAuthStateSecret() {
|
|
7
|
+
return process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-oauth-state-secret";
|
|
8
|
+
}
|
|
9
|
+
function signOAuthState(state, issuedAt) {
|
|
10
|
+
const payload = `${state}.${issuedAt}`;
|
|
11
|
+
const signature = createHmac("sha256", resolveOAuthStateSecret()).update(payload).digest("hex");
|
|
12
|
+
return `${payload}.${signature}`;
|
|
13
|
+
}
|
|
14
|
+
function createOAuthStateCookie() {
|
|
15
|
+
const state = randomBytes(24).toString("hex");
|
|
16
|
+
const issuedAt = Date.now();
|
|
17
|
+
const value = signOAuthState(state, issuedAt);
|
|
18
|
+
return {
|
|
19
|
+
state,
|
|
20
|
+
cookie: `${OAUTH_STATE_COOKIE}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function readOAuthStateCookie(request) {
|
|
24
|
+
const cookieHeader = request.headers.get("cookie");
|
|
25
|
+
if (!cookieHeader) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
for (const part of cookieHeader.split(";")) {
|
|
29
|
+
const [name, ...rest] = part.trim().split("=");
|
|
30
|
+
if (name === OAUTH_STATE_COOKIE) {
|
|
31
|
+
return decodeURIComponent(rest.join("="));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function verifyOAuthState(request, returnedState) {
|
|
37
|
+
if (!returnedState || returnedState.trim().length === 0) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const cookieValue = readOAuthStateCookie(request);
|
|
41
|
+
if (!cookieValue) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const parts = cookieValue.split(".");
|
|
45
|
+
if (parts.length !== 3) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const [cookieState, issuedAtRaw, cookieSignature] = parts;
|
|
49
|
+
if (!cookieState || !issuedAtRaw || !cookieSignature) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
53
|
+
if (cookieState !== returnedState || !Number.isFinite(issuedAt)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (Date.now() - issuedAt > OAUTH_STATE_TTL_MS) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const expectedSignature = signOAuthState(cookieState, issuedAt).split(".").pop();
|
|
60
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
61
|
+
const actualBuffer = Buffer.from(cookieSignature);
|
|
62
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
66
|
+
}
|
|
67
|
+
function clearOAuthStateCookie() {
|
|
68
|
+
return `${OAUTH_STATE_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
verifyOAuthState,
|
|
72
|
+
createOAuthStateCookie,
|
|
73
|
+
clearOAuthStateCookie,
|
|
74
|
+
OAUTH_STATE_COOKIE
|
|
75
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/config/features.ts
|
|
3
|
+
function readFeatureFlags() {
|
|
4
|
+
return {
|
|
5
|
+
webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
|
|
6
|
+
fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
|
|
7
|
+
auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
|
|
8
|
+
oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
|
|
9
|
+
samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
|
|
10
|
+
scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
|
|
11
|
+
billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
|
|
12
|
+
siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
|
|
13
|
+
publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
|
|
14
|
+
emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
|
|
15
|
+
mfa: (process.env.FEATURE_MFA ?? "false") === "true"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
var featureFlags = readFeatureFlags();
|
|
19
|
+
function isFeatureEnabled(feature) {
|
|
20
|
+
return readFeatureFlags()[feature];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ../../src/core/security/publicReads.ts
|
|
24
|
+
function isPublicReadsEnabled() {
|
|
25
|
+
return isFeatureEnabled("publicReads");
|
|
26
|
+
}
|
|
27
|
+
function guestCanViewResource() {
|
|
28
|
+
return isPublicReadsEnabled();
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
isPublicReadsEnabled,
|
|
32
|
+
guestCanViewResource
|
|
33
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/errors/http.ts
|
|
3
|
+
class HttpError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
details;
|
|
6
|
+
constructor(status, message, details) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = new.target.name;
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.details = details;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class BadRequestError extends HttpError {
|
|
15
|
+
constructor(message = "Bad Request", details) {
|
|
16
|
+
super(400, message, details);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class NotFoundError extends HttpError {
|
|
21
|
+
constructor(message = "Not Found", details) {
|
|
22
|
+
super(404, message, details);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class ConflictError extends HttpError {
|
|
27
|
+
constructor(message = "Conflict", details) {
|
|
28
|
+
super(409, message, details);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class UnprocessableEntityError extends HttpError {
|
|
33
|
+
constructor(message = "Unprocessable Entity", details) {
|
|
34
|
+
super(422, message, details);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class ValidationError extends HttpError {
|
|
39
|
+
constructor(message = "Validation failed", details) {
|
|
40
|
+
super(422, message, details);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class ForbiddenError extends HttpError {
|
|
45
|
+
constructor(message = "Forbidden", details) {
|
|
46
|
+
super(403, message, details);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class UnauthorizedError extends HttpError {
|
|
51
|
+
constructor(message = "Unauthorized", details) {
|
|
52
|
+
super(401, message, details);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class PayloadTooLargeError extends HttpError {
|
|
57
|
+
constructor(message = "Payload Too Large", details) {
|
|
58
|
+
super(413, message, details);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class PreconditionFailedError extends HttpError {
|
|
63
|
+
constructor(message = "Precondition Failed", details) {
|
|
64
|
+
super(412, message, details);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ../../src/core/security/safeUrl.ts
|
|
69
|
+
var BLOCKED_HOSTNAMES = new Set([
|
|
70
|
+
"localhost",
|
|
71
|
+
"127.0.0.1",
|
|
72
|
+
"0.0.0.0",
|
|
73
|
+
"::1",
|
|
74
|
+
"metadata.google.internal"
|
|
75
|
+
]);
|
|
76
|
+
function isPrivateIpv4(hostname) {
|
|
77
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
78
|
+
if (!match) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
82
|
+
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
const [a = 0, b = 0] = octets;
|
|
86
|
+
if (a === 10) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (a === 127) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (a === 0) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (a === 169 && b === 254) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (a === 192 && b === 168) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
function isBlockedHostname(hostname) {
|
|
107
|
+
const normalized = hostname.trim().toLowerCase();
|
|
108
|
+
if (normalized.length === 0) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (normalized.includes(":")) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return isPrivateIpv4(normalized);
|
|
121
|
+
}
|
|
122
|
+
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = new URL(rawUrl);
|
|
126
|
+
} catch {
|
|
127
|
+
throw new BadRequestError("Webhook URL is invalid.");
|
|
128
|
+
}
|
|
129
|
+
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
130
|
+
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
131
|
+
}
|
|
132
|
+
if (parsed.username || parsed.password) {
|
|
133
|
+
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
134
|
+
}
|
|
135
|
+
if (isBlockedHostname(parsed.hostname)) {
|
|
136
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
137
|
+
}
|
|
138
|
+
return parsed;
|
|
139
|
+
}
|
|
140
|
+
export {
|
|
141
|
+
isBlockedHostname,
|
|
142
|
+
assertSafeOutboundUrl
|
|
143
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/auth/authContext.ts
|
|
3
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
4
|
+
var authContext = new AsyncLocalStorage;
|
|
5
|
+
function runWithAuthUser(user, callback) {
|
|
6
|
+
return authContext.run(user, callback);
|
|
7
|
+
}
|
|
8
|
+
function currentAuthUser() {
|
|
9
|
+
return authContext.getStore() ?? null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ../../src/core/http/requestMetaContext.ts
|
|
13
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
14
|
+
var requestMetaContext = new AsyncLocalStorage2;
|
|
15
|
+
function runWithRequestMeta(meta, callback) {
|
|
16
|
+
return requestMetaContext.run(meta, callback);
|
|
17
|
+
}
|
|
18
|
+
function currentRequestMeta() {
|
|
19
|
+
return requestMetaContext.getStore() ?? {
|
|
20
|
+
ipAddress: null,
|
|
21
|
+
userAgent: null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ../../src/core/security/securityEvents.ts
|
|
26
|
+
function logSecurityEvent(event, details = {}) {
|
|
27
|
+
const meta = currentRequestMeta();
|
|
28
|
+
const user = currentAuthUser();
|
|
29
|
+
console.log(JSON.stringify({
|
|
30
|
+
level: "security",
|
|
31
|
+
event,
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
ip_address: meta.ipAddress ?? null,
|
|
34
|
+
user_agent: meta.userAgent ?? null,
|
|
35
|
+
user_id: user?.id ?? null,
|
|
36
|
+
...details
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
logSecurityEvent
|
|
41
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/security/stripeWebhook.ts
|
|
3
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
4
|
+
|
|
5
|
+
// ../../src/core/errors/http.ts
|
|
6
|
+
class HttpError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
details;
|
|
9
|
+
constructor(status, message, details) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = new.target.name;
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.details = details;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class BadRequestError extends HttpError {
|
|
18
|
+
constructor(message = "Bad Request", details) {
|
|
19
|
+
super(400, message, details);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class NotFoundError extends HttpError {
|
|
24
|
+
constructor(message = "Not Found", details) {
|
|
25
|
+
super(404, message, details);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class ConflictError extends HttpError {
|
|
30
|
+
constructor(message = "Conflict", details) {
|
|
31
|
+
super(409, message, details);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class UnprocessableEntityError extends HttpError {
|
|
36
|
+
constructor(message = "Unprocessable Entity", details) {
|
|
37
|
+
super(422, message, details);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class ValidationError extends HttpError {
|
|
42
|
+
constructor(message = "Validation failed", details) {
|
|
43
|
+
super(422, message, details);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class ForbiddenError extends HttpError {
|
|
48
|
+
constructor(message = "Forbidden", details) {
|
|
49
|
+
super(403, message, details);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class UnauthorizedError extends HttpError {
|
|
54
|
+
constructor(message = "Unauthorized", details) {
|
|
55
|
+
super(401, message, details);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class PayloadTooLargeError extends HttpError {
|
|
60
|
+
constructor(message = "Payload Too Large", details) {
|
|
61
|
+
super(413, message, details);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class PreconditionFailedError extends HttpError {
|
|
66
|
+
constructor(message = "Precondition Failed", details) {
|
|
67
|
+
super(412, message, details);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ../../src/core/security/stripeWebhook.ts
|
|
72
|
+
function verifyStripeWebhookSignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
|
|
73
|
+
if (!signatureHeader?.trim()) {
|
|
74
|
+
throw new UnauthorizedError("Missing Stripe signature.");
|
|
75
|
+
}
|
|
76
|
+
const entries = signatureHeader.split(",").map((part) => part.trim());
|
|
77
|
+
let timestamp = null;
|
|
78
|
+
const signatures = [];
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const [key, value] = entry.split("=");
|
|
81
|
+
if (key === "t" && value) {
|
|
82
|
+
timestamp = value;
|
|
83
|
+
}
|
|
84
|
+
if (key === "v1" && value) {
|
|
85
|
+
signatures.push(value);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!timestamp || signatures.length === 0) {
|
|
89
|
+
throw new UnauthorizedError("Invalid Stripe signature header.");
|
|
90
|
+
}
|
|
91
|
+
const timestampNumber = Number.parseInt(timestamp, 10);
|
|
92
|
+
if (!Number.isFinite(timestampNumber)) {
|
|
93
|
+
throw new UnauthorizedError("Invalid Stripe signature timestamp.");
|
|
94
|
+
}
|
|
95
|
+
const ageSeconds = Math.floor(Date.now() / 1000) - timestampNumber;
|
|
96
|
+
if (ageSeconds > toleranceSeconds) {
|
|
97
|
+
throw new UnauthorizedError("Stripe signature timestamp is too old.");
|
|
98
|
+
}
|
|
99
|
+
const payload = `${timestamp}.${rawBody}`;
|
|
100
|
+
const expected = createHmac("sha256", secret).update(payload, "utf8").digest("hex");
|
|
101
|
+
const expectedBuffer = Buffer.from(expected);
|
|
102
|
+
const matched = signatures.some((signature) => {
|
|
103
|
+
const actualBuffer = Buffer.from(signature);
|
|
104
|
+
if (actualBuffer.length !== expectedBuffer.length) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return timingSafeEqual(actualBuffer, expectedBuffer);
|
|
108
|
+
});
|
|
109
|
+
if (!matched) {
|
|
110
|
+
throw new UnauthorizedError("Invalid Stripe webhook signature.");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export {
|
|
114
|
+
verifyStripeWebhookSignature
|
|
115
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/security/tokenExpiry.ts
|
|
3
|
+
function resolveDefaultTokenExpiryDays() {
|
|
4
|
+
const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
|
|
5
|
+
if (!raw) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const parsed = Number.parseInt(raw, 10);
|
|
9
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
resolveDefaultTokenExpiryDays
|
|
16
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/security/totp.ts
|
|
3
|
+
import { createHmac } from "crypto";
|
|
4
|
+
function decodeBase32(input) {
|
|
5
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
6
|
+
const normalized = input.replace(/=+$/u, "").toUpperCase();
|
|
7
|
+
let bits = "";
|
|
8
|
+
for (const char of normalized) {
|
|
9
|
+
const value = alphabet.indexOf(char);
|
|
10
|
+
if (value === -1) {
|
|
11
|
+
throw new Error("Invalid base32 character in MFA secret.");
|
|
12
|
+
}
|
|
13
|
+
bits += value.toString(2).padStart(5, "0");
|
|
14
|
+
}
|
|
15
|
+
const bytes = [];
|
|
16
|
+
for (let index = 0;index + 8 <= bits.length; index += 8) {
|
|
17
|
+
bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
|
|
18
|
+
}
|
|
19
|
+
return Buffer.from(bytes);
|
|
20
|
+
}
|
|
21
|
+
function generateTotp(secret, counter, digits = 6) {
|
|
22
|
+
const key = decodeBase32(secret);
|
|
23
|
+
const buffer = Buffer.alloc(8);
|
|
24
|
+
buffer.writeBigUInt64BE(BigInt(counter));
|
|
25
|
+
const digest = createHmac("sha1", key).update(buffer).digest();
|
|
26
|
+
const lastByte = digest[digest.length - 1] ?? 0;
|
|
27
|
+
const offset = lastByte & 15;
|
|
28
|
+
const b0 = digest[offset] ?? 0;
|
|
29
|
+
const b1 = digest[offset + 1] ?? 0;
|
|
30
|
+
const b2 = digest[offset + 2] ?? 0;
|
|
31
|
+
const b3 = digest[offset + 3] ?? 0;
|
|
32
|
+
const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
|
|
33
|
+
return String(code % 10 ** digits).padStart(digits, "0");
|
|
34
|
+
}
|
|
35
|
+
function verifyTotp(secret, token, window = 1) {
|
|
36
|
+
const normalized = token.trim();
|
|
37
|
+
if (!/^\d{6}$/u.test(normalized)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const timestep = Math.floor(Date.now() / 30000);
|
|
41
|
+
for (let offset = -window;offset <= window; offset += 1) {
|
|
42
|
+
if (generateTotp(secret, timestep + offset) === normalized) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
verifyTotp,
|
|
50
|
+
generateTotp
|
|
51
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/storage/storage.ts
|
|
3
|
+
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
var {S3Client } = globalThis.Bun;
|
|
6
|
+
|
|
7
|
+
class LocalStorageDriver {
|
|
8
|
+
rootDirectory;
|
|
9
|
+
constructor(rootDirectory) {
|
|
10
|
+
this.rootDirectory = rootDirectory;
|
|
11
|
+
}
|
|
12
|
+
resolvePath(path) {
|
|
13
|
+
return join(this.rootDirectory, path.replace(/^\/+/, ""));
|
|
14
|
+
}
|
|
15
|
+
async put(path, contents) {
|
|
16
|
+
const absolutePath = this.resolvePath(path);
|
|
17
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
18
|
+
await writeFile(absolutePath, contents);
|
|
19
|
+
return path;
|
|
20
|
+
}
|
|
21
|
+
async get(path) {
|
|
22
|
+
try {
|
|
23
|
+
return await readFile(this.resolvePath(path));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async delete(path) {
|
|
29
|
+
try {
|
|
30
|
+
await unlink(this.resolvePath(path));
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class S3StorageDriver {
|
|
39
|
+
client;
|
|
40
|
+
constructor(client) {
|
|
41
|
+
this.client = client;
|
|
42
|
+
}
|
|
43
|
+
async put(path, contents) {
|
|
44
|
+
await this.client.write(path.replace(/^\/+/, ""), contents);
|
|
45
|
+
return path;
|
|
46
|
+
}
|
|
47
|
+
async get(path) {
|
|
48
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
49
|
+
const file = this.client.file(normalizedPath);
|
|
50
|
+
if (!await file.exists()) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
54
|
+
}
|
|
55
|
+
async delete(path) {
|
|
56
|
+
try {
|
|
57
|
+
await this.client.unlink(path.replace(/^\/+/, ""));
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class StorageManager {
|
|
66
|
+
driver;
|
|
67
|
+
constructor(driver) {
|
|
68
|
+
this.driver = driver;
|
|
69
|
+
}
|
|
70
|
+
put(path, contents) {
|
|
71
|
+
return this.driver.put(path, contents);
|
|
72
|
+
}
|
|
73
|
+
get(path) {
|
|
74
|
+
return this.driver.get(path);
|
|
75
|
+
}
|
|
76
|
+
delete(path) {
|
|
77
|
+
return this.driver.delete(path);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function resolveS3Config() {
|
|
81
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
|
|
82
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
|
|
83
|
+
const bucket = process.env.AWS_BUCKET?.trim();
|
|
84
|
+
if (!accessKeyId || !secretAccessKey || !bucket) {
|
|
85
|
+
throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
accessKeyId,
|
|
89
|
+
secretAccessKey,
|
|
90
|
+
bucket,
|
|
91
|
+
...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
|
|
92
|
+
...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function createS3Client(config = resolveS3Config()) {
|
|
96
|
+
return new S3Client({
|
|
97
|
+
accessKeyId: config.accessKeyId,
|
|
98
|
+
secretAccessKey: config.secretAccessKey,
|
|
99
|
+
bucket: config.bucket,
|
|
100
|
+
...config.region ? { region: config.region } : {},
|
|
101
|
+
...config.endpoint ? { endpoint: config.endpoint } : {}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function createStorageDriver() {
|
|
105
|
+
const driver = process.env.STORAGE_DRIVER ?? "local";
|
|
106
|
+
if (driver === "s3") {
|
|
107
|
+
return new S3StorageDriver(createS3Client());
|
|
108
|
+
}
|
|
109
|
+
return new LocalStorageDriver(process.env.STORAGE_PATH ?? "storage");
|
|
110
|
+
}
|
|
111
|
+
var defaultStorage = new StorageManager(createStorageDriver());
|
|
112
|
+
function storage() {
|
|
113
|
+
return defaultStorage;
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
storage,
|
|
117
|
+
resolveS3Config,
|
|
118
|
+
createStorageDriver,
|
|
119
|
+
createS3Client,
|
|
120
|
+
StorageManager,
|
|
121
|
+
S3StorageDriver,
|
|
122
|
+
LocalStorageDriver
|
|
123
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/tenant/tenantContext.ts
|
|
3
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
4
|
+
var tenantContext = new AsyncLocalStorage;
|
|
5
|
+
function runWithTenant(tenant, callback) {
|
|
6
|
+
return tenantContext.run(tenant, callback);
|
|
7
|
+
}
|
|
8
|
+
function currentTenant() {
|
|
9
|
+
return tenantContext.getStore() ?? null;
|
|
10
|
+
}
|
|
11
|
+
function currentTenantId() {
|
|
12
|
+
return currentTenant()?.id ?? 1;
|
|
13
|
+
}
|
|
14
|
+
function rateLimitMultiplierForPlan(plan) {
|
|
15
|
+
switch (plan) {
|
|
16
|
+
case "enterprise":
|
|
17
|
+
return 4;
|
|
18
|
+
case "pro":
|
|
19
|
+
return 2;
|
|
20
|
+
default:
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
tenantContext,
|
|
26
|
+
runWithTenant,
|
|
27
|
+
rateLimitMultiplierForPlan,
|
|
28
|
+
currentTenantId,
|
|
29
|
+
currentTenant
|
|
30
|
+
};
|