@getstrata/bootstrap 0.2.6 → 0.2.8
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/contracts.d.ts +2 -0
- package/dist/bootstrap/http/securedRouteModelBinding.d.ts +11 -0
- package/dist/bootstrap/providers/storage.d.ts +3 -0
- package/dist/bootstrap/public-api.d.ts +1 -0
- package/dist/bootstrap/scimRoutes.d.ts +1 -1
- package/dist/core/auth/membershipScope.d.ts +16 -0
- package/dist/core/auth/membershipService.d.ts +24 -0
- package/dist/core/database/baseRepository.d.ts +6 -1
- package/dist/core/database/connectionContext.d.ts +2 -1
- package/dist/core/database/defaultConnection.d.ts +6 -0
- package/dist/core/database/queryProxy.d.ts +3 -0
- package/dist/core/database/repositoryConnection.d.ts +3 -3
- package/dist/core/http/securedRouteModelBinding.d.ts +2 -11
- package/dist/core/jobs/dispatchWebhookJob.d.ts +0 -1
- package/dist/core/queue/queueMetrics.d.ts +15 -0
- package/dist/core/security/safeFetch.d.ts +2 -0
- package/dist/core/security/safeUrl.d.ts +16 -1
- package/dist/core/tenant/tenantDatabaseScope.d.ts +2 -1
- package/dist/db/connection/index.d.ts +1 -1
- package/dist/domain/workhub.d.ts +35 -0
- package/dist/entries/applicationRegistry.js +185 -0
- package/dist/entries/config.js +42 -0
- package/dist/entries/context.js +4213 -0
- package/dist/entries/contracts.js +92 -0
- package/dist/entries/createWebRoutes.js +996 -0
- package/dist/entries/http/securedRouteModelBinding.js +383 -0
- package/dist/entries/httpKernel.js +264 -0
- package/dist/entries/providers/view.js +635 -0
- package/dist/entries/providers.js +4099 -0
- package/dist/framework/public-api.d.ts +30 -6
- package/dist/index.js +455 -110
- package/dist/modules/organization/repository.d.ts +14 -0
- package/dist/modules/organization/table.d.ts +3 -0
- package/dist/modules/organization/types.d.ts +10 -0
- package/dist/modules/scim/controller.d.ts +2 -1
- package/dist/modules/scim/scimResponse.d.ts +1 -1
- package/dist/modules/scim/service.d.ts +4 -7
- package/dist/modules/user/repository.d.ts +1 -0
- package/package.json +10 -4
package/dist/index.js
CHANGED
|
@@ -65,18 +65,13 @@ var appConfig = {
|
|
|
65
65
|
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
// ../../src/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
72
|
-
}
|
|
73
|
-
var databaseConfig = {
|
|
74
|
-
url: process.env.DATABASE_URL ?? "",
|
|
75
|
-
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
76
|
-
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
77
|
-
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
78
|
-
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
68
|
+
// ../../src/core/database/boundConnection.ts
|
|
69
|
+
var boundConnectionHolder = {
|
|
70
|
+
connection: null
|
|
79
71
|
};
|
|
72
|
+
function getBoundDatabaseConnection() {
|
|
73
|
+
return boundConnectionHolder.connection;
|
|
74
|
+
}
|
|
80
75
|
|
|
81
76
|
// ../../src/core/database/connectionContext.ts
|
|
82
77
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -85,84 +80,65 @@ function getActiveDatabaseConnection(fallback) {
|
|
|
85
80
|
return activeConnection.getStore() ?? fallback;
|
|
86
81
|
}
|
|
87
82
|
|
|
88
|
-
// ../../src/
|
|
89
|
-
var
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
// ../../src/core/database/queryProxy.ts
|
|
84
|
+
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
85
|
+
function createDatabaseQueryProxy(pool) {
|
|
86
|
+
function resolveDatabase() {
|
|
87
|
+
return getActiveDatabaseConnection(pool);
|
|
93
88
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
89
|
+
function resolveDatabaseForProperty(property) {
|
|
90
|
+
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
91
|
+
return pool;
|
|
92
|
+
}
|
|
93
|
+
return resolveDatabase();
|
|
94
|
+
}
|
|
95
|
+
return new Proxy(function database() {}, {
|
|
96
|
+
apply(_target, _thisArg, args) {
|
|
97
|
+
return resolveDatabase()(...args);
|
|
98
|
+
},
|
|
99
|
+
get(_target, property) {
|
|
100
|
+
const connection = resolveDatabaseForProperty(property);
|
|
101
|
+
const value = connection[property];
|
|
102
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
103
|
+
}
|
|
100
104
|
});
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
// ../../src/
|
|
104
|
-
var
|
|
107
|
+
// ../../src/core/database/defaultConnection.ts
|
|
108
|
+
var defaultPool = {
|
|
105
109
|
connection: null
|
|
106
110
|
};
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
114
|
-
function resolveDatabase() {
|
|
115
|
-
return getActiveDatabaseConnection(getDatabase());
|
|
111
|
+
var defaultQuery = {
|
|
112
|
+
connection: null
|
|
113
|
+
};
|
|
114
|
+
function registerDefaultDatabasePool(connection) {
|
|
115
|
+
defaultPool.connection = connection;
|
|
116
|
+
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
116
117
|
}
|
|
117
|
-
function
|
|
118
|
-
if (
|
|
119
|
-
|
|
118
|
+
function getDefaultDatabaseQuery() {
|
|
119
|
+
if (!defaultQuery.connection) {
|
|
120
|
+
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
120
121
|
}
|
|
121
|
-
return
|
|
122
|
+
return defaultQuery.connection;
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
+
|
|
125
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
126
|
+
function resolveRepositoryConnection() {
|
|
127
|
+
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
128
|
+
}
|
|
129
|
+
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
124
130
|
apply(_target, _thisArg, args) {
|
|
125
|
-
return
|
|
131
|
+
return resolveRepositoryConnection()(...args);
|
|
126
132
|
},
|
|
127
133
|
get(_target, property) {
|
|
128
|
-
const connection =
|
|
134
|
+
const connection = resolveRepositoryConnection();
|
|
129
135
|
const value = connection[property];
|
|
130
136
|
return typeof value === "function" ? value.bind(connection) : value;
|
|
131
137
|
}
|
|
132
138
|
});
|
|
133
|
-
var connection_default = db;
|
|
134
139
|
|
|
135
|
-
// ../../src/core/security/
|
|
136
|
-
|
|
137
|
-
async function safeFetch(input, init = {}, options = {}) {
|
|
138
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
139
|
-
const maxRedirects = options.maxRedirects ?? 0;
|
|
140
|
-
const controller = new AbortController;
|
|
141
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
142
|
-
try {
|
|
143
|
-
let currentUrl = input;
|
|
144
|
-
let redirectCount = 0;
|
|
145
|
-
while (true) {
|
|
146
|
-
const response = await fetch(currentUrl, {
|
|
147
|
-
...init,
|
|
148
|
-
signal: controller.signal,
|
|
149
|
-
redirect: "manual"
|
|
150
|
-
});
|
|
151
|
-
if (response.status >= 300 && response.status < 400) {
|
|
152
|
-
const location = response.headers.get("location");
|
|
153
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
154
|
-
return response;
|
|
155
|
-
}
|
|
156
|
-
currentUrl = new URL(location, currentUrl).toString();
|
|
157
|
-
redirectCount += 1;
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
return response;
|
|
161
|
-
}
|
|
162
|
-
} finally {
|
|
163
|
-
clearTimeout(timeout);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
140
|
+
// ../../src/core/security/safeUrl.ts
|
|
141
|
+
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
166
142
|
|
|
167
143
|
// ../../src/core/errors/http.ts
|
|
168
144
|
class HttpError extends Error {
|
|
@@ -203,8 +179,14 @@ class UnauthorizedError extends HttpError {
|
|
|
203
179
|
super(401, message, details);
|
|
204
180
|
}
|
|
205
181
|
}
|
|
182
|
+
class PreconditionFailedError extends HttpError {
|
|
183
|
+
constructor(message = "Precondition Failed", details) {
|
|
184
|
+
super(412, message, details);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
206
187
|
|
|
207
188
|
// ../../src/core/security/safeUrl.ts
|
|
189
|
+
var dnsLookup = dnsLookupImpl;
|
|
208
190
|
var BLOCKED_HOSTNAMES = new Set([
|
|
209
191
|
"localhost",
|
|
210
192
|
"127.0.0.1",
|
|
@@ -276,14 +258,63 @@ function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
|
276
258
|
}
|
|
277
259
|
return parsed;
|
|
278
260
|
}
|
|
261
|
+
function isBlockedIpAddress(address) {
|
|
262
|
+
return isBlockedHostname(address.trim().toLowerCase());
|
|
263
|
+
}
|
|
264
|
+
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
265
|
+
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
266
|
+
if (options.resolveDns === false) {
|
|
267
|
+
return parsed;
|
|
268
|
+
}
|
|
269
|
+
const hostname = parsed.hostname.trim().toLowerCase();
|
|
270
|
+
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
271
|
+
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
272
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
273
|
+
}
|
|
274
|
+
return parsed;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ../../src/core/security/safeFetch.ts
|
|
278
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
279
|
+
async function safeFetch(input, init = {}, options = {}) {
|
|
280
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
281
|
+
const maxRedirects = options.maxRedirects ?? 0;
|
|
282
|
+
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
283
|
+
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
284
|
+
const controller = new AbortController;
|
|
285
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
286
|
+
try {
|
|
287
|
+
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
288
|
+
let redirectCount = 0;
|
|
289
|
+
while (true) {
|
|
290
|
+
const response = await fetch(currentUrl, {
|
|
291
|
+
...init,
|
|
292
|
+
signal: controller.signal,
|
|
293
|
+
redirect: "manual"
|
|
294
|
+
});
|
|
295
|
+
if (response.status >= 300 && response.status < 400) {
|
|
296
|
+
const location = response.headers.get("location");
|
|
297
|
+
if (!location || redirectCount >= maxRedirects) {
|
|
298
|
+
return response;
|
|
299
|
+
}
|
|
300
|
+
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
301
|
+
redirectCount += 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
return response;
|
|
305
|
+
}
|
|
306
|
+
} finally {
|
|
307
|
+
clearTimeout(timeout);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
279
310
|
|
|
280
311
|
// ../../src/core/tenant/databaseTenantContext.ts
|
|
281
312
|
async function runWithMigrationBypass(callback) {
|
|
282
|
-
await
|
|
313
|
+
await repositoryConnection`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
283
314
|
try {
|
|
284
315
|
return await callback();
|
|
285
316
|
} finally {
|
|
286
|
-
await
|
|
317
|
+
await repositoryConnection`SELECT set_config('app.bypass_rls', 'false', false)`;
|
|
287
318
|
}
|
|
288
319
|
}
|
|
289
320
|
|
|
@@ -340,7 +371,7 @@ async function exportPendingAuditLogs() {
|
|
|
340
371
|
return 0;
|
|
341
372
|
}
|
|
342
373
|
return await runWithMigrationBypass(async () => {
|
|
343
|
-
const rows = await
|
|
374
|
+
const rows = await repositoryConnection`
|
|
344
375
|
SELECT
|
|
345
376
|
id,
|
|
346
377
|
user_id,
|
|
@@ -384,13 +415,13 @@ async function exportPendingAuditLogs() {
|
|
|
384
415
|
...process.env.SIEM_EXPORT_TOKEN ? { authorization: `Bearer ${process.env.SIEM_EXPORT_TOKEN}` } : {}
|
|
385
416
|
},
|
|
386
417
|
body
|
|
387
|
-
});
|
|
418
|
+
}, { allowHttp: appConfig.env !== "production" });
|
|
388
419
|
if (!response.ok) {
|
|
389
420
|
throw new Error(`SIEM export failed with status ${response.status}.`);
|
|
390
421
|
}
|
|
391
422
|
const ids = rows.map((row) => row.id);
|
|
392
423
|
for (const id of ids) {
|
|
393
|
-
await
|
|
424
|
+
await repositoryConnection`UPDATE audit_log SET exported_at = NOW() WHERE id = ${id}`;
|
|
394
425
|
}
|
|
395
426
|
return rows.length;
|
|
396
427
|
});
|
|
@@ -550,7 +581,8 @@ class ConfigStore {
|
|
|
550
581
|
}
|
|
551
582
|
var requiredDependencyKeys = [
|
|
552
583
|
"container",
|
|
553
|
-
"cache"
|
|
584
|
+
"cache",
|
|
585
|
+
"storage"
|
|
554
586
|
];
|
|
555
587
|
function getRequiredDependency(dependencies, key) {
|
|
556
588
|
const dependency = dependencies[key];
|
|
@@ -643,6 +675,60 @@ import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
|
|
|
643
675
|
// ../../src/modules/user/apiTokenRepository.ts
|
|
644
676
|
import { BaseRepository } from "@getstrata/core/database";
|
|
645
677
|
|
|
678
|
+
// ../../src/config/database.ts
|
|
679
|
+
function readInteger(name, fallback) {
|
|
680
|
+
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
681
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
682
|
+
}
|
|
683
|
+
var databaseConfig = {
|
|
684
|
+
url: process.env.DATABASE_URL ?? "",
|
|
685
|
+
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
686
|
+
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
687
|
+
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
688
|
+
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
// ../../src/db/connection/createConnection.ts
|
|
692
|
+
var {SQL } = globalThis.Bun;
|
|
693
|
+
function createDatabaseConnection(config) {
|
|
694
|
+
if (!config.url) {
|
|
695
|
+
throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
|
|
696
|
+
}
|
|
697
|
+
return new SQL({
|
|
698
|
+
url: config.url,
|
|
699
|
+
max: config.poolMax,
|
|
700
|
+
idleTimeout: config.idleTimeoutSeconds,
|
|
701
|
+
maxLifetime: config.maxLifetimeSeconds,
|
|
702
|
+
connectionTimeout: config.connectionTimeoutSeconds
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ../../src/db/connection/index.ts
|
|
707
|
+
var connectionHolder = {
|
|
708
|
+
connection: null
|
|
709
|
+
};
|
|
710
|
+
function getDatabase() {
|
|
711
|
+
if (!connectionHolder.connection) {
|
|
712
|
+
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
713
|
+
registerDefaultDatabasePool(connectionHolder.connection);
|
|
714
|
+
}
|
|
715
|
+
return connectionHolder.connection;
|
|
716
|
+
}
|
|
717
|
+
function getDb() {
|
|
718
|
+
getDatabase();
|
|
719
|
+
return getDefaultDatabaseQuery();
|
|
720
|
+
}
|
|
721
|
+
var db = new Proxy(function database() {}, {
|
|
722
|
+
apply(_target, _thisArg, args) {
|
|
723
|
+
return getDb()(...args);
|
|
724
|
+
},
|
|
725
|
+
get(_target, property) {
|
|
726
|
+
const connection = getDb();
|
|
727
|
+
const value = connection[property];
|
|
728
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
|
|
646
732
|
// ../../src/modules/user/apiTokenTable.ts
|
|
647
733
|
import { defineTable } from "@getstrata/core/database";
|
|
648
734
|
var apiTokenTable = defineTable({
|
|
@@ -1737,17 +1823,20 @@ function resolveApplicationCache() {
|
|
|
1737
1823
|
function resolveApplicationQueue() {
|
|
1738
1824
|
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
1739
1825
|
}
|
|
1826
|
+
function resolveApplicationAuth() {
|
|
1827
|
+
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
|
|
1828
|
+
}
|
|
1829
|
+
function resolveApplicationPolicyGate() {
|
|
1830
|
+
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
1831
|
+
}
|
|
1740
1832
|
|
|
1741
1833
|
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
1742
1834
|
import { createHmac as createHmac2 } from "crypto";
|
|
1743
1835
|
class DispatchWebhookJob extends Job {
|
|
1744
|
-
constructor() {
|
|
1745
|
-
super();
|
|
1746
|
-
}
|
|
1747
1836
|
maxAttempts = 3;
|
|
1748
1837
|
backoffMs = 2000;
|
|
1749
1838
|
async handle(payload) {
|
|
1750
|
-
const rows = await
|
|
1839
|
+
const rows = await repositoryConnection`
|
|
1751
1840
|
SELECT id, url, secret
|
|
1752
1841
|
FROM webhook
|
|
1753
1842
|
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
@@ -1770,14 +1859,14 @@ class DispatchWebhookJob extends Job {
|
|
|
1770
1859
|
"x-workhub-signature": signature
|
|
1771
1860
|
},
|
|
1772
1861
|
body
|
|
1773
|
-
});
|
|
1862
|
+
}, { allowHttp: appConfig.env !== "production" });
|
|
1774
1863
|
responseStatus = response.status;
|
|
1775
1864
|
if (!response.ok) {
|
|
1776
1865
|
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
1777
1866
|
}
|
|
1778
1867
|
} catch (error) {
|
|
1779
1868
|
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1780
|
-
await
|
|
1869
|
+
await repositoryConnection`
|
|
1781
1870
|
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
1782
1871
|
VALUES (
|
|
1783
1872
|
${webhook.id},
|
|
@@ -1789,7 +1878,7 @@ class DispatchWebhookJob extends Job {
|
|
|
1789
1878
|
`;
|
|
1790
1879
|
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1791
1880
|
}
|
|
1792
|
-
await
|
|
1881
|
+
await repositoryConnection`
|
|
1793
1882
|
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
1794
1883
|
VALUES (
|
|
1795
1884
|
${webhook.id},
|
|
@@ -2369,26 +2458,6 @@ function indexMorphToRelation(children, parentsByType, relation) {
|
|
|
2369
2458
|
return result;
|
|
2370
2459
|
}
|
|
2371
2460
|
|
|
2372
|
-
// ../../src/core/database/boundConnection.ts
|
|
2373
|
-
var boundConnectionHolder = {
|
|
2374
|
-
connection: null
|
|
2375
|
-
};
|
|
2376
|
-
function getBoundDatabaseConnection() {
|
|
2377
|
-
return boundConnectionHolder.connection;
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
// ../../src/core/database/repositoryConnection.ts
|
|
2381
|
-
function resolveRepositoryConnection() {
|
|
2382
|
-
return getBoundDatabaseConnection() ?? connection_default;
|
|
2383
|
-
}
|
|
2384
|
-
var repositoryConnection = new Proxy({}, {
|
|
2385
|
-
get(_target, property) {
|
|
2386
|
-
const connection = resolveRepositoryConnection();
|
|
2387
|
-
const value = connection[property];
|
|
2388
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
2389
|
-
}
|
|
2390
|
-
});
|
|
2391
|
-
|
|
2392
2461
|
// ../../src/core/database/whereBuilder.ts
|
|
2393
2462
|
class WhereBuilder {
|
|
2394
2463
|
nodes = [];
|
|
@@ -3816,6 +3885,128 @@ var queueProvider = {
|
|
|
3816
3885
|
};
|
|
3817
3886
|
var queue_default = queueProvider;
|
|
3818
3887
|
|
|
3888
|
+
// ../../src/core/storage/storage.ts
|
|
3889
|
+
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
3890
|
+
import { dirname, join as join3 } from "path";
|
|
3891
|
+
var {S3Client } = globalThis.Bun;
|
|
3892
|
+
|
|
3893
|
+
class LocalStorageDriver {
|
|
3894
|
+
rootDirectory;
|
|
3895
|
+
constructor(rootDirectory) {
|
|
3896
|
+
this.rootDirectory = rootDirectory;
|
|
3897
|
+
}
|
|
3898
|
+
resolveRootDirectory() {
|
|
3899
|
+
return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
|
|
3900
|
+
}
|
|
3901
|
+
resolvePath(path) {
|
|
3902
|
+
return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
|
|
3903
|
+
}
|
|
3904
|
+
async put(path, contents) {
|
|
3905
|
+
const absolutePath = this.resolvePath(path);
|
|
3906
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
3907
|
+
await writeFile(absolutePath, contents);
|
|
3908
|
+
return path;
|
|
3909
|
+
}
|
|
3910
|
+
async get(path) {
|
|
3911
|
+
try {
|
|
3912
|
+
return await readFile(this.resolvePath(path));
|
|
3913
|
+
} catch {
|
|
3914
|
+
return null;
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
async delete(path) {
|
|
3918
|
+
try {
|
|
3919
|
+
await unlink(this.resolvePath(path));
|
|
3920
|
+
return true;
|
|
3921
|
+
} catch {
|
|
3922
|
+
return false;
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
class S3StorageDriver {
|
|
3928
|
+
client;
|
|
3929
|
+
constructor(client) {
|
|
3930
|
+
this.client = client;
|
|
3931
|
+
}
|
|
3932
|
+
async put(path, contents) {
|
|
3933
|
+
await this.client.write(path.replace(/^\/+/, ""), contents);
|
|
3934
|
+
return path;
|
|
3935
|
+
}
|
|
3936
|
+
async get(path) {
|
|
3937
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
3938
|
+
const file = this.client.file(normalizedPath);
|
|
3939
|
+
if (!await file.exists()) {
|
|
3940
|
+
return null;
|
|
3941
|
+
}
|
|
3942
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
3943
|
+
}
|
|
3944
|
+
async delete(path) {
|
|
3945
|
+
try {
|
|
3946
|
+
await this.client.unlink(path.replace(/^\/+/, ""));
|
|
3947
|
+
return true;
|
|
3948
|
+
} catch {
|
|
3949
|
+
return false;
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
class StorageManager {
|
|
3955
|
+
driver;
|
|
3956
|
+
constructor(driver) {
|
|
3957
|
+
this.driver = driver;
|
|
3958
|
+
}
|
|
3959
|
+
put(path, contents) {
|
|
3960
|
+
return this.driver.put(path, contents);
|
|
3961
|
+
}
|
|
3962
|
+
get(path) {
|
|
3963
|
+
return this.driver.get(path);
|
|
3964
|
+
}
|
|
3965
|
+
delete(path) {
|
|
3966
|
+
return this.driver.delete(path);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
function resolveS3Config() {
|
|
3970
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
|
|
3971
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
|
|
3972
|
+
const bucket = process.env.AWS_BUCKET?.trim();
|
|
3973
|
+
if (!accessKeyId || !secretAccessKey || !bucket) {
|
|
3974
|
+
throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
|
|
3975
|
+
}
|
|
3976
|
+
return {
|
|
3977
|
+
accessKeyId,
|
|
3978
|
+
secretAccessKey,
|
|
3979
|
+
bucket,
|
|
3980
|
+
...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
|
|
3981
|
+
...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
|
|
3982
|
+
};
|
|
3983
|
+
}
|
|
3984
|
+
function createS3Client(config = resolveS3Config()) {
|
|
3985
|
+
return new S3Client({
|
|
3986
|
+
accessKeyId: config.accessKeyId,
|
|
3987
|
+
secretAccessKey: config.secretAccessKey,
|
|
3988
|
+
bucket: config.bucket,
|
|
3989
|
+
...config.region ? { region: config.region } : {},
|
|
3990
|
+
...config.endpoint ? { endpoint: config.endpoint } : {}
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
3993
|
+
function createStorageDriver() {
|
|
3994
|
+
const driver = process.env.STORAGE_DRIVER ?? "local";
|
|
3995
|
+
if (driver === "s3") {
|
|
3996
|
+
return new S3StorageDriver(createS3Client());
|
|
3997
|
+
}
|
|
3998
|
+
return new LocalStorageDriver;
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
// ../../src/bootstrap/providers/storage.ts
|
|
4002
|
+
var storageProvider = {
|
|
4003
|
+
name: "core.storage",
|
|
4004
|
+
register({ dependencies }) {
|
|
4005
|
+
dependencies.storage = new StorageManager(createStorageDriver());
|
|
4006
|
+
}
|
|
4007
|
+
};
|
|
4008
|
+
var storage_default = storageProvider;
|
|
4009
|
+
|
|
3819
4010
|
// ../../src/config/frontend.ts
|
|
3820
4011
|
function readFrontendMode() {
|
|
3821
4012
|
const mode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
@@ -3842,9 +4033,9 @@ function currentRequestMeta() {
|
|
|
3842
4033
|
}
|
|
3843
4034
|
|
|
3844
4035
|
// ../../src/core/view/etaViewEngine.ts
|
|
3845
|
-
import { join as
|
|
4036
|
+
import { join as join4 } from "path";
|
|
3846
4037
|
import { Eta } from "eta";
|
|
3847
|
-
var DEFAULT_VIEWS_DIRECTORY =
|
|
4038
|
+
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
3848
4039
|
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
3849
4040
|
|
|
3850
4041
|
class EtaViewEngine {
|
|
@@ -4069,6 +4260,7 @@ var viewProvider = {
|
|
|
4069
4260
|
var coreProviders = [
|
|
4070
4261
|
config_default,
|
|
4071
4262
|
cache_default,
|
|
4263
|
+
storage_default,
|
|
4072
4264
|
auth_default,
|
|
4073
4265
|
events_default,
|
|
4074
4266
|
policy_default,
|
|
@@ -4169,7 +4361,7 @@ function createAppContext() {
|
|
|
4169
4361
|
return appContext;
|
|
4170
4362
|
}
|
|
4171
4363
|
// ../../src/bootstrap/createWebRoutes.ts
|
|
4172
|
-
import { join as
|
|
4364
|
+
import { join as join5 } from "path";
|
|
4173
4365
|
|
|
4174
4366
|
// ../../src/core/http/middleware.ts
|
|
4175
4367
|
function isRouteHandler(value) {
|
|
@@ -4503,7 +4695,7 @@ function createWebRoutes(dependencies) {
|
|
|
4503
4695
|
registerRoute("GET", "/assets/*", ["global", "web"]);
|
|
4504
4696
|
const pathname = new URL(request.url).pathname;
|
|
4505
4697
|
const relativePath = pathname.replace(/^\//, "");
|
|
4506
|
-
const file = Bun.file(
|
|
4698
|
+
const file = Bun.file(join5(process.cwd(), "public", relativePath));
|
|
4507
4699
|
if (!await file.exists()) {
|
|
4508
4700
|
return htmlResponse("Not Found", { status: 404 });
|
|
4509
4701
|
}
|
|
@@ -4521,6 +4713,157 @@ function mergeWebRoutes(dependencies, routes) {
|
|
|
4521
4713
|
...routes
|
|
4522
4714
|
};
|
|
4523
4715
|
}
|
|
4716
|
+
// ../../src/core/crypto/nonCryptographicHash.ts
|
|
4717
|
+
function nonCryptographicDigest(input) {
|
|
4718
|
+
return Bun.hash(input).toString(16);
|
|
4719
|
+
}
|
|
4720
|
+
|
|
4721
|
+
// ../../src/core/http/etag.ts
|
|
4722
|
+
function isEtagEnabled() {
|
|
4723
|
+
return (process.env.FEATURE_ETAG ?? "true") !== "false";
|
|
4724
|
+
}
|
|
4725
|
+
function formatWeakEtag(digest) {
|
|
4726
|
+
return `W/"${digest}"`;
|
|
4727
|
+
}
|
|
4728
|
+
function etagFromResource(resource) {
|
|
4729
|
+
const version = resource.updated_at ?? resource.created_at ?? "";
|
|
4730
|
+
const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
|
|
4731
|
+
const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
|
|
4732
|
+
return formatWeakEtag(digest);
|
|
4733
|
+
}
|
|
4734
|
+
function normalizeEtag(value) {
|
|
4735
|
+
return value.trim();
|
|
4736
|
+
}
|
|
4737
|
+
function etagValuesMatch(left, right) {
|
|
4738
|
+
return normalizeEtag(left) === normalizeEtag(right);
|
|
4739
|
+
}
|
|
4740
|
+
function parseEtagList(header) {
|
|
4741
|
+
if (!header) {
|
|
4742
|
+
return [];
|
|
4743
|
+
}
|
|
4744
|
+
return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
|
|
4745
|
+
}
|
|
4746
|
+
function ifNoneMatchSatisfied(request, etag) {
|
|
4747
|
+
const header = request.headers.get("if-none-match");
|
|
4748
|
+
if (!header) {
|
|
4749
|
+
return false;
|
|
4750
|
+
}
|
|
4751
|
+
if (header.trim() === "*") {
|
|
4752
|
+
return true;
|
|
4753
|
+
}
|
|
4754
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
4755
|
+
}
|
|
4756
|
+
function ifMatchSatisfied(request, etag) {
|
|
4757
|
+
const header = request.headers.get("if-match");
|
|
4758
|
+
if (!header) {
|
|
4759
|
+
return false;
|
|
4760
|
+
}
|
|
4761
|
+
if (header.trim() === "*") {
|
|
4762
|
+
return true;
|
|
4763
|
+
}
|
|
4764
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
4765
|
+
}
|
|
4766
|
+
function assertIfMatch(request, etag, options = {}) {
|
|
4767
|
+
const header = request.headers.get("if-match");
|
|
4768
|
+
if (!header) {
|
|
4769
|
+
if (options.required) {
|
|
4770
|
+
throw new PreconditionFailedError("If-Match header is required.");
|
|
4771
|
+
}
|
|
4772
|
+
return;
|
|
4773
|
+
}
|
|
4774
|
+
if (!ifMatchSatisfied(request, etag)) {
|
|
4775
|
+
throw new PreconditionFailedError("Resource ETag does not match If-Match.");
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
function applyEtagHeaders(headers, etag) {
|
|
4779
|
+
const next = new Headers(headers);
|
|
4780
|
+
next.set("ETag", etag);
|
|
4781
|
+
next.set("Cache-Control", "private, must-revalidate");
|
|
4782
|
+
next.append("Vary", "Authorization");
|
|
4783
|
+
next.append("Vary", "X-Tenant-Id");
|
|
4784
|
+
return next;
|
|
4785
|
+
}
|
|
4786
|
+
function notModifiedResponse(etag) {
|
|
4787
|
+
return new Response(null, {
|
|
4788
|
+
status: 304,
|
|
4789
|
+
headers: applyEtagHeaders(new Headers, etag)
|
|
4790
|
+
});
|
|
4791
|
+
}
|
|
4792
|
+
function applyConditionalGet(request, response, etag) {
|
|
4793
|
+
if (!isEtagEnabled()) {
|
|
4794
|
+
return response;
|
|
4795
|
+
}
|
|
4796
|
+
if (ifNoneMatchSatisfied(request, etag)) {
|
|
4797
|
+
return notModifiedResponse(etag);
|
|
4798
|
+
}
|
|
4799
|
+
const headers = applyEtagHeaders(new Headers(response.headers), etag);
|
|
4800
|
+
return new Response(response.body, {
|
|
4801
|
+
status: response.status,
|
|
4802
|
+
statusText: response.statusText,
|
|
4803
|
+
headers
|
|
4804
|
+
});
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4807
|
+
// ../../src/core/tenant/tenantContext.ts
|
|
4808
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
4809
|
+
var tenantContext = new AsyncLocalStorage4;
|
|
4810
|
+
|
|
4811
|
+
// ../../src/core/http/validation.ts
|
|
4812
|
+
function parsePositiveIntParam(value, name = "id") {
|
|
4813
|
+
const parsed = Number.parseInt(value, 10);
|
|
4814
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4815
|
+
throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
|
|
4816
|
+
}
|
|
4817
|
+
return parsed;
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4820
|
+
// ../../src/bootstrap/http/securedRouteModelBinding.ts
|
|
4821
|
+
function isMutatingPolicyAction(action) {
|
|
4822
|
+
return action === "update" || action === "delete";
|
|
4823
|
+
}
|
|
4824
|
+
function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
4825
|
+
return async (request) => {
|
|
4826
|
+
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
4827
|
+
const model = await resolver(id, request);
|
|
4828
|
+
const gate = resolveApplicationPolicyGate();
|
|
4829
|
+
const auth = resolveApplicationAuth();
|
|
4830
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
4831
|
+
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
4832
|
+
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
4833
|
+
assertIfMatch(request, etagFromResource(model), {
|
|
4834
|
+
required: authorization.requireIfMatch ?? true
|
|
4835
|
+
});
|
|
4836
|
+
}
|
|
4837
|
+
const response = await handler(request, model);
|
|
4838
|
+
if (isEtagEnabled() && authorization.action === "view") {
|
|
4839
|
+
return applyConditionalGet(request, response, etagFromResource(model));
|
|
4840
|
+
}
|
|
4841
|
+
return response;
|
|
4842
|
+
};
|
|
4843
|
+
}
|
|
4844
|
+
function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
4845
|
+
return async (request) => {
|
|
4846
|
+
const key = String(request.params[param] ?? "").trim();
|
|
4847
|
+
if (!key) {
|
|
4848
|
+
throw new BadRequestError(`Missing route parameter "${String(param)}".`);
|
|
4849
|
+
}
|
|
4850
|
+
const model = await resolver(key, request);
|
|
4851
|
+
const gate = resolveApplicationPolicyGate();
|
|
4852
|
+
const auth = resolveApplicationAuth();
|
|
4853
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
4854
|
+
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
4855
|
+
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
4856
|
+
assertIfMatch(request, etagFromResource(model), {
|
|
4857
|
+
required: authorization.requireIfMatch ?? true
|
|
4858
|
+
});
|
|
4859
|
+
}
|
|
4860
|
+
const response = await handler(request, model);
|
|
4861
|
+
if (isEtagEnabled() && authorization.action === "view") {
|
|
4862
|
+
return applyConditionalGet(request, response, etagFromResource(model));
|
|
4863
|
+
}
|
|
4864
|
+
return response;
|
|
4865
|
+
};
|
|
4866
|
+
}
|
|
4524
4867
|
// ../../src/bootstrap/prefixRouteMap.ts
|
|
4525
4868
|
function prefixRouteMap(prefix, routes) {
|
|
4526
4869
|
const normalizedPrefix = prefix.replace(/\/$/, "");
|
|
@@ -4580,7 +4923,7 @@ async function parseFormBody(request) {
|
|
|
4580
4923
|
return { fields, files };
|
|
4581
4924
|
}
|
|
4582
4925
|
// ../../src/bootstrap/web/routing.ts
|
|
4583
|
-
import {
|
|
4926
|
+
import { withErrorHandling } from "@getstrata/core";
|
|
4584
4927
|
function routeParams(request) {
|
|
4585
4928
|
const normalized = {};
|
|
4586
4929
|
const raw = request.params;
|
|
@@ -4755,6 +5098,8 @@ export {
|
|
|
4755
5098
|
toRouteRequest,
|
|
4756
5099
|
slugify,
|
|
4757
5100
|
setActiveApplicationContext2 as setActiveApplicationContext,
|
|
5101
|
+
securedBindRouteModelByKey,
|
|
5102
|
+
securedBindRouteModel,
|
|
4758
5103
|
scheduleRunCommand,
|
|
4759
5104
|
runProviderPhase,
|
|
4760
5105
|
runDueScheduledTasks,
|