@getstrata/core 0.5.41 → 0.5.43

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.
Files changed (46) hide show
  1. package/dist/core/database/baseRepository.d.ts +1 -0
  2. package/dist/core/queue/failedJobRepository.d.ts +1 -0
  3. package/dist/entries/admin/formatValue.js +32 -0
  4. package/dist/entries/admin/registry.js +32 -0
  5. package/dist/entries/audit/exportAuditLogs.js +18 -0
  6. package/dist/entries/audit/siemFormatter.js +37 -0
  7. package/dist/entries/auth/scimAuthMiddleware.js +241 -0
  8. package/dist/entries/auth/sessionGuard.js +501 -0
  9. package/dist/entries/database/baseRepository.js +1388 -0
  10. package/dist/entries/database/bindConnection.js +22 -0
  11. package/dist/entries/database/boundConnection.js +19 -0
  12. package/dist/entries/database/connection.js +12 -0
  13. package/dist/entries/database/errors.js +128 -0
  14. package/dist/entries/database/model.js +948 -0
  15. package/dist/entries/database/query.js +436 -0
  16. package/dist/entries/database/relationships.js +162 -0
  17. package/dist/entries/database/schema.js +1054 -0
  18. package/dist/entries/database/table.js +8 -0
  19. package/dist/entries/database/transaction.js +129 -0
  20. package/dist/entries/http/authMiddleware.js +47 -0
  21. package/dist/entries/http/authorizeMiddleware.js +104 -0
  22. package/dist/entries/http/metricsMiddleware.js +91 -0
  23. package/dist/entries/http/parseMultipartUpload.js +144 -0
  24. package/dist/entries/http/securedRouteModelBinding.js +6 -0
  25. package/dist/entries/http/webErrorResponse.js +501 -0
  26. package/dist/entries/http/webFormRequest.js +6 -0
  27. package/dist/entries/jobs/dispatchWebhookJob.js +18 -0
  28. package/dist/entries/mail/mailer.js +208 -0
  29. package/dist/entries/mail/markdownMail.js +63 -0
  30. package/dist/entries/mail/markdownMailable.js +78 -0
  31. package/dist/entries/notifications.js +152 -0
  32. package/dist/entries/openapi/generator.js +178 -0
  33. package/dist/entries/openapi/validate.js +28 -0
  34. package/dist/entries/queue/createAppQueue.js +507 -0
  35. package/dist/entries/queue/failedJobRepository.js +2364 -0
  36. package/dist/entries/queue/publicQueue.js +507 -0
  37. package/dist/entries/queue/queueMetrics.js +507 -0
  38. package/dist/entries/queue/redisQueue.js +232 -0
  39. package/dist/entries/runtime/asyncContextStore.js +17 -0
  40. package/dist/entries/security/safeFetch.js +211 -0
  41. package/dist/entries/security/scimTenantTokens.js +51 -0
  42. package/dist/entries/security/timingSafeCompare.js +14 -0
  43. package/dist/entries/tenant/databaseTenantContext.js +116 -0
  44. package/dist/entries/tenant/tenantDatabaseScope.js +113 -0
  45. package/dist/entries/view.js +501 -0
  46. package/package.json +167 -2
@@ -0,0 +1,8 @@
1
+ // @bun
2
+ // ../../src/core/database/table.ts
3
+ function defineTable(definition) {
4
+ return definition;
5
+ }
6
+ export {
7
+ defineTable
8
+ };
@@ -0,0 +1,129 @@
1
+ // @bun
2
+ // ../../src/core/database/connection.ts
3
+ function createDatabaseConnection(source) {
4
+ return {
5
+ async unsafe(query, params = []) {
6
+ return await source.unsafe(query, params);
7
+ }
8
+ };
9
+ }
10
+
11
+ // ../../src/core/database/boundConnection.ts
12
+ var boundConnectionHolder = {
13
+ connection: null
14
+ };
15
+ function bindDatabaseConnection(connection) {
16
+ boundConnectionHolder.connection = connection;
17
+ }
18
+ function getBoundDatabaseConnection() {
19
+ return boundConnectionHolder.connection;
20
+ }
21
+ function resetBoundDatabaseConnection() {
22
+ boundConnectionHolder.connection = null;
23
+ }
24
+
25
+ // ../../src/core/runtime/asyncContextStore.ts
26
+ import { AsyncLocalStorage } from "async_hooks";
27
+ function createAsyncContextStore(key) {
28
+ const symbol = Symbol.for(key);
29
+ const globalRecord = globalThis;
30
+ const existing = globalRecord[symbol];
31
+ if (existing) {
32
+ return existing;
33
+ }
34
+ const store = new AsyncLocalStorage;
35
+ globalRecord[symbol] = store;
36
+ return store;
37
+ }
38
+
39
+ // ../../src/core/database/connectionContext.ts
40
+ var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
41
+ function runWithDatabaseConnection(connection, callback) {
42
+ return activeConnection.run(connection, callback);
43
+ }
44
+ function getActiveDatabaseConnection(fallback) {
45
+ return activeConnection.getStore() ?? fallback;
46
+ }
47
+ function hasActiveDatabaseConnection() {
48
+ return activeConnection.getStore() !== undefined;
49
+ }
50
+
51
+ // ../../src/core/database/queryProxy.ts
52
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
53
+ function createDatabaseQueryProxy(pool) {
54
+ function resolveDatabase() {
55
+ return getActiveDatabaseConnection(pool);
56
+ }
57
+ function resolveDatabaseForProperty(property) {
58
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
59
+ return pool;
60
+ }
61
+ return resolveDatabase();
62
+ }
63
+ return new Proxy(function database() {}, {
64
+ apply(_target, _thisArg, args) {
65
+ return resolveDatabase()(...args);
66
+ },
67
+ get(_target, property) {
68
+ const connection = resolveDatabaseForProperty(property);
69
+ const value = connection[property];
70
+ return typeof value === "function" ? value.bind(connection) : value;
71
+ }
72
+ });
73
+ }
74
+
75
+ // ../../src/core/database/defaultConnection.ts
76
+ var defaultPool = {
77
+ connection: null
78
+ };
79
+ var defaultQuery = {
80
+ connection: null
81
+ };
82
+ function registerDefaultDatabasePool(connection) {
83
+ defaultPool.connection = connection;
84
+ defaultQuery.connection = createDatabaseQueryProxy(connection);
85
+ }
86
+ function getDefaultDatabasePool() {
87
+ if (!defaultPool.connection) {
88
+ throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
89
+ }
90
+ return defaultPool.connection;
91
+ }
92
+ function getDefaultDatabaseQuery() {
93
+ if (!defaultQuery.connection) {
94
+ throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
95
+ }
96
+ return defaultQuery.connection;
97
+ }
98
+
99
+ // ../../src/core/database/repositoryConnection.ts
100
+ function resolveRepositoryConnection() {
101
+ return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
102
+ }
103
+ var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
104
+ apply(_target, _thisArg, args) {
105
+ return resolveRepositoryConnection()(...args);
106
+ },
107
+ get(_target, property) {
108
+ const connection = resolveRepositoryConnection();
109
+ const value = connection[property];
110
+ return typeof value === "function" ? value.bind(connection) : value;
111
+ }
112
+ });
113
+
114
+ // ../../src/core/database/transaction.ts
115
+ function supportsTransactions(connection) {
116
+ return typeof connection.begin === "function";
117
+ }
118
+ async function runInTransaction(operation) {
119
+ const pool = resolveRepositoryConnection();
120
+ if (!supportsTransactions(pool)) {
121
+ throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
122
+ }
123
+ return await pool.begin(async (transaction) => {
124
+ return await operation(createDatabaseConnection(transaction));
125
+ });
126
+ }
127
+ export {
128
+ runInTransaction
129
+ };
@@ -0,0 +1,47 @@
1
+ // @bun
2
+ // ../../src/core/runtime/asyncContextStore.ts
3
+ import { AsyncLocalStorage } from "async_hooks";
4
+ function createAsyncContextStore(key) {
5
+ const symbol = Symbol.for(key);
6
+ const globalRecord = globalThis;
7
+ const existing = globalRecord[symbol];
8
+ if (existing) {
9
+ return existing;
10
+ }
11
+ const store = new AsyncLocalStorage;
12
+ globalRecord[symbol] = store;
13
+ return store;
14
+ }
15
+
16
+ // ../../src/core/auth/authContext.ts
17
+ var authContext = createAsyncContextStore("@getstrata/authContext");
18
+ function runWithAuthUser(user, callback) {
19
+ return authContext.run(user, callback);
20
+ }
21
+ function currentAuthUser() {
22
+ return authContext.getStore() ?? null;
23
+ }
24
+
25
+ // ../../src/core/http/authMiddleware.ts
26
+ function createAuthMiddleware(auth) {
27
+ return async (request, next) => {
28
+ const user = await auth.resolve(request);
29
+ return await runWithAuthUser(user, async () => {
30
+ const response = await next();
31
+ if (user) {
32
+ const headers = new Headers(response.headers);
33
+ headers.set("x-authenticated-user-id", String(user.id));
34
+ return new Response(response.body, {
35
+ status: response.status,
36
+ statusText: response.statusText,
37
+ headers
38
+ });
39
+ }
40
+ return response;
41
+ });
42
+ };
43
+ }
44
+ export {
45
+ createAuthMiddleware,
46
+ authContext
47
+ };
@@ -0,0 +1,104 @@
1
+ // @bun
2
+ // ../../src/core/runtime/asyncContextStore.ts
3
+ import { AsyncLocalStorage } from "async_hooks";
4
+ function createAsyncContextStore(key) {
5
+ const symbol = Symbol.for(key);
6
+ const globalRecord = globalThis;
7
+ const existing = globalRecord[symbol];
8
+ if (existing) {
9
+ return existing;
10
+ }
11
+ const store = new AsyncLocalStorage;
12
+ globalRecord[symbol] = store;
13
+ return store;
14
+ }
15
+
16
+ // ../../src/core/auth/authContext.ts
17
+ var authContext = createAsyncContextStore("@getstrata/authContext");
18
+ function runWithAuthUser(user, callback) {
19
+ return authContext.run(user, callback);
20
+ }
21
+ function currentAuthUser() {
22
+ return authContext.getStore() ?? null;
23
+ }
24
+
25
+ // ../../src/core/errors/http.ts
26
+ class HttpError extends Error {
27
+ status;
28
+ details;
29
+ constructor(status, message, details) {
30
+ super(message);
31
+ this.name = new.target.name;
32
+ this.status = status;
33
+ this.details = details;
34
+ }
35
+ }
36
+
37
+ class BadRequestError extends HttpError {
38
+ constructor(message = "Bad Request", details) {
39
+ super(400, message, details);
40
+ }
41
+ }
42
+
43
+ class NotFoundError extends HttpError {
44
+ constructor(message = "Not Found", details) {
45
+ super(404, message, details);
46
+ }
47
+ }
48
+
49
+ class ConflictError extends HttpError {
50
+ constructor(message = "Conflict", details) {
51
+ super(409, message, details);
52
+ }
53
+ }
54
+
55
+ class UnprocessableEntityError extends HttpError {
56
+ constructor(message = "Unprocessable Entity", details) {
57
+ super(422, message, details);
58
+ }
59
+ }
60
+
61
+ class ValidationError extends HttpError {
62
+ constructor(message = "Validation failed", details) {
63
+ super(422, message, details);
64
+ }
65
+ }
66
+
67
+ class ForbiddenError extends HttpError {
68
+ constructor(message = "Forbidden", details) {
69
+ super(403, message, details);
70
+ }
71
+ }
72
+
73
+ class UnauthorizedError extends HttpError {
74
+ constructor(message = "Unauthorized", details) {
75
+ super(401, message, details);
76
+ }
77
+ }
78
+
79
+ class PayloadTooLargeError extends HttpError {
80
+ constructor(message = "Payload Too Large", details) {
81
+ super(413, message, details);
82
+ }
83
+ }
84
+
85
+ class PreconditionFailedError extends HttpError {
86
+ constructor(message = "Precondition Failed", details) {
87
+ super(412, message, details);
88
+ }
89
+ }
90
+
91
+ // ../../src/core/http/authorizeMiddleware.ts
92
+ function createAuthorizeMiddleware(gate, auth, resource, action) {
93
+ return async (request, next) => {
94
+ const user = currentAuthUser() ?? await auth.resolve(request);
95
+ if (!gate.allows(resource, action, user)) {
96
+ const error = new ForbiddenError;
97
+ return Response.json({ error: error.message }, { status: error.status });
98
+ }
99
+ return await next();
100
+ };
101
+ }
102
+ export {
103
+ createAuthorizeMiddleware
104
+ };
@@ -0,0 +1,91 @@
1
+ // @bun
2
+ // ../../src/core/metrics/prometheus.ts
3
+ class PrometheusRegistry {
4
+ httpRequestsTotal = new Map;
5
+ httpRequestDurationMs = new Map;
6
+ incrementHttpRequest(labels) {
7
+ const key = this.metricKey(labels);
8
+ this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
9
+ }
10
+ observeHttpDuration(labels, durationMs) {
11
+ const key = this.metricKey(labels);
12
+ const samples = this.httpRequestDurationMs.get(key) ?? [];
13
+ samples.push(durationMs);
14
+ this.httpRequestDurationMs.set(key, samples);
15
+ }
16
+ renderMetrics() {
17
+ const lines = [
18
+ "# HELP http_requests_total Total HTTP requests processed.",
19
+ "# TYPE http_requests_total counter"
20
+ ];
21
+ for (const [key, value] of this.httpRequestsTotal) {
22
+ lines.push(`http_requests_total{${key}} ${value}`);
23
+ }
24
+ lines.push("# HELP http_request_duration_ms_sum Sum of HTTP request durations in milliseconds.", "# TYPE http_request_duration_ms_sum counter");
25
+ for (const [key, samples] of this.httpRequestDurationMs) {
26
+ const sum = samples.reduce((total, sample) => total + sample, 0);
27
+ lines.push(`http_request_duration_ms_sum{${key}} ${sum}`);
28
+ }
29
+ return `${lines.join(`
30
+ `)}
31
+ `;
32
+ }
33
+ resetForTests() {
34
+ this.httpRequestsTotal.clear();
35
+ this.httpRequestDurationMs.clear();
36
+ }
37
+ getHttpRequestSummary() {
38
+ const byStatus = {};
39
+ const pathCounts = new Map;
40
+ let totalRequests = 0;
41
+ for (const [key, count] of this.httpRequestsTotal) {
42
+ totalRequests += count;
43
+ const method = key.match(/method="([^"]+)"/)?.[1] ?? "GET";
44
+ const path = key.match(/path="([^"]+)"/)?.[1] ?? "/";
45
+ const status = key.match(/status="([^"]+)"/)?.[1] ?? "200";
46
+ byStatus[status] = (byStatus[status] ?? 0) + count;
47
+ const pathKey = `${method} ${path}`;
48
+ const existing = pathCounts.get(pathKey);
49
+ if (existing) {
50
+ existing.count += count;
51
+ } else {
52
+ pathCounts.set(pathKey, { method, path, count });
53
+ }
54
+ }
55
+ const topPaths = Array.from(pathCounts.values()).sort((left, right) => right.count - left.count).slice(0, 10);
56
+ return {
57
+ totalRequests,
58
+ byStatus,
59
+ topPaths
60
+ };
61
+ }
62
+ metricKey(labels) {
63
+ return `method="${labels.method}",path="${labels.path}",status="${labels.status}"`;
64
+ }
65
+ }
66
+ var prometheusRegistry = new PrometheusRegistry;
67
+
68
+ // ../../src/core/http/metricsMiddleware.ts
69
+ function normalizeMetricPath(pathname) {
70
+ return pathname.replace(/\/\d+/g, "/:id").replace(/\/[0-9a-f-]{36}/gi, "/:id");
71
+ }
72
+ function createMetricsMiddleware() {
73
+ return async (request, next) => {
74
+ const startedAt = performance.now();
75
+ const response = await next();
76
+ const durationMs = performance.now() - startedAt;
77
+ const path = normalizeMetricPath(new URL(request.url).pathname);
78
+ const labels = {
79
+ method: request.method,
80
+ path,
81
+ status: String(response.status)
82
+ };
83
+ prometheusRegistry.incrementHttpRequest(labels);
84
+ prometheusRegistry.observeHttpDuration(labels, durationMs);
85
+ return response;
86
+ };
87
+ }
88
+ export {
89
+ normalizeMetricPath,
90
+ createMetricsMiddleware
91
+ };
@@ -0,0 +1,144 @@
1
+ // @bun
2
+ // ../../src/config/uploads.ts
3
+ var DEFAULT_MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
4
+ var ALLOWED_UPLOAD_MIME_TYPES = new Set([
5
+ "application/pdf",
6
+ "application/json",
7
+ "application/zip",
8
+ "application/x-zip-compressed",
9
+ "image/jpeg",
10
+ "image/png",
11
+ "image/gif",
12
+ "image/webp",
13
+ "text/plain",
14
+ "text/csv"
15
+ ]);
16
+ function resolveMaxUploadBytes() {
17
+ const raw = process.env.MAX_UPLOAD_BYTES?.trim() ?? process.env.MAX_REQUEST_BODY_BYTES?.trim();
18
+ if (!raw) {
19
+ return DEFAULT_MAX_UPLOAD_BYTES;
20
+ }
21
+ const parsed = Number.parseInt(raw, 10);
22
+ if (!Number.isInteger(parsed) || parsed <= 0) {
23
+ return DEFAULT_MAX_UPLOAD_BYTES;
24
+ }
25
+ return parsed;
26
+ }
27
+ function normalizeMimeType(mimeType) {
28
+ return mimeType.split(";")[0]?.trim().toLowerCase() ?? "";
29
+ }
30
+ function isAllowedMimeType(mimeType) {
31
+ const normalized = normalizeMimeType(mimeType);
32
+ if (!normalized || normalized === "application/octet-stream") {
33
+ return true;
34
+ }
35
+ return ALLOWED_UPLOAD_MIME_TYPES.has(normalized);
36
+ }
37
+
38
+ // ../../src/core/errors/http.ts
39
+ class HttpError extends Error {
40
+ status;
41
+ details;
42
+ constructor(status, message, details) {
43
+ super(message);
44
+ this.name = new.target.name;
45
+ this.status = status;
46
+ this.details = details;
47
+ }
48
+ }
49
+
50
+ class BadRequestError extends HttpError {
51
+ constructor(message = "Bad Request", details) {
52
+ super(400, message, details);
53
+ }
54
+ }
55
+
56
+ class NotFoundError extends HttpError {
57
+ constructor(message = "Not Found", details) {
58
+ super(404, message, details);
59
+ }
60
+ }
61
+
62
+ class ConflictError extends HttpError {
63
+ constructor(message = "Conflict", details) {
64
+ super(409, message, details);
65
+ }
66
+ }
67
+
68
+ class UnprocessableEntityError extends HttpError {
69
+ constructor(message = "Unprocessable Entity", details) {
70
+ super(422, message, details);
71
+ }
72
+ }
73
+
74
+ class ValidationError extends HttpError {
75
+ constructor(message = "Validation failed", details) {
76
+ super(422, message, details);
77
+ }
78
+ }
79
+
80
+ class ForbiddenError extends HttpError {
81
+ constructor(message = "Forbidden", details) {
82
+ super(403, message, details);
83
+ }
84
+ }
85
+
86
+ class UnauthorizedError extends HttpError {
87
+ constructor(message = "Unauthorized", details) {
88
+ super(401, message, details);
89
+ }
90
+ }
91
+
92
+ class PayloadTooLargeError extends HttpError {
93
+ constructor(message = "Payload Too Large", details) {
94
+ super(413, message, details);
95
+ }
96
+ }
97
+
98
+ class PreconditionFailedError extends HttpError {
99
+ constructor(message = "Precondition Failed", details) {
100
+ super(412, message, details);
101
+ }
102
+ }
103
+
104
+ // ../../src/core/http/parseMultipartUpload.ts
105
+ function normalizeMimeType2(mimeType) {
106
+ return mimeType.split(";")[0]?.trim().toLowerCase() || "application/octet-stream";
107
+ }
108
+ function sanitizeUploadFileName(name) {
109
+ const base = name.split(/[/\\]/).pop()?.trim() ?? "upload";
110
+ const sanitized = base.replace(/[^\w.\-()+ ]+/g, "_").slice(0, 200);
111
+ return sanitized.length > 0 ? sanitized : "upload";
112
+ }
113
+ async function parseMultipartUpload(request, fieldName = "file") {
114
+ const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
115
+ if (!contentType.includes("multipart/form-data")) {
116
+ throw new BadRequestError("Expected multipart form data.");
117
+ }
118
+ const formData = await request.formData();
119
+ const value = formData.get(fieldName);
120
+ if (!(value instanceof File)) {
121
+ throw new BadRequestError(`Missing upload field "${fieldName}".`);
122
+ }
123
+ if (value.size <= 0) {
124
+ throw new BadRequestError("Uploaded file is empty.");
125
+ }
126
+ const maxBytes = resolveMaxUploadBytes();
127
+ if (value.size > maxBytes) {
128
+ throw new PayloadTooLargeError(`Upload exceeds the ${maxBytes} byte limit.`);
129
+ }
130
+ const mimeType = normalizeMimeType2(value.type.trim() || "application/octet-stream");
131
+ if (!isAllowedMimeType(mimeType)) {
132
+ throw new BadRequestError(`File type "${mimeType}" is not allowed.`);
133
+ }
134
+ return {
135
+ fileName: sanitizeUploadFileName(value.name),
136
+ mimeType,
137
+ size: value.size,
138
+ contents: new Uint8Array(await value.arrayBuffer())
139
+ };
140
+ }
141
+ export {
142
+ sanitizeUploadFileName,
143
+ parseMultipartUpload
144
+ };
@@ -15,6 +15,9 @@ function createAsyncContextStore(key) {
15
15
 
16
16
  // ../../src/core/auth/authContext.ts
17
17
  var authContext = createAsyncContextStore("@getstrata/authContext");
18
+ function runWithAuthUser(user, callback) {
19
+ return authContext.run(user, callback);
20
+ }
18
21
  function currentAuthUser() {
19
22
  return authContext.getStore() ?? null;
20
23
  }
@@ -276,6 +279,9 @@ function applyConditionalGet(request, response, etag) {
276
279
 
277
280
  // ../../src/core/tenant/tenantContext.ts
278
281
  var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
282
+ function runWithTenant(tenant, callback) {
283
+ return tenantContext.run(tenant, callback);
284
+ }
279
285
  function currentTenant() {
280
286
  return tenantContext.getStore() ?? null;
281
287
  }