@getstrata/bootstrap 0.2.7 → 0.2.9
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/http/securedRouteModelBinding.d.ts +11 -0
- package/dist/bootstrap/membershipService.d.ts +3 -0
- package/dist/bootstrap/public-api.d.ts +4 -1
- package/dist/bootstrap/queue/defaultJobs.d.ts +2 -0
- package/dist/core/auth/membershipService.d.ts +1 -2
- package/dist/core/http/securedRouteModelBinding.d.ts +2 -11
- package/dist/core/queue/createAppQueue.d.ts +3 -3
- package/dist/entries/context.js +692 -684
- package/dist/entries/createWebRoutes.js +1 -0
- package/dist/entries/http/securedRouteModelBinding.js +383 -0
- package/dist/entries/membershipService.js +480 -0
- package/dist/entries/providers/view.js +1 -0
- package/dist/entries/providers.js +1943 -1935
- package/dist/entries/queue/defaultJobs.js +549 -0
- package/dist/framework/public-api.d.ts +1 -0
- package/dist/index.js +447 -118
- package/package.json +18 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// ../../src/bootstrap/public-api.ts
|
|
3
|
-
import { resolveApplicationQueue as resolveApplicationQueue2, setActiveApplicationContext as setActiveApplicationContext2 } from "@getstrata/core";
|
|
4
|
-
|
|
5
2
|
// ../../src/core/scheduler/schedule.ts
|
|
6
3
|
class Schedule {
|
|
7
4
|
tasks = [];
|
|
@@ -179,6 +176,11 @@ class UnauthorizedError extends HttpError {
|
|
|
179
176
|
super(401, message, details);
|
|
180
177
|
}
|
|
181
178
|
}
|
|
179
|
+
class PreconditionFailedError extends HttpError {
|
|
180
|
+
constructor(message = "Precondition Failed", details) {
|
|
181
|
+
super(412, message, details);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
182
184
|
|
|
183
185
|
// ../../src/core/security/safeUrl.ts
|
|
184
186
|
var dnsLookup = dnsLookupImpl;
|
|
@@ -507,8 +509,6 @@ var DEFAULT_CACHE_MAX_ENTRIES = 100;
|
|
|
507
509
|
var DEFAULT_CACHE_DRIVER = "array";
|
|
508
510
|
var DEFAULT_API_TOKEN = "";
|
|
509
511
|
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
510
|
-
// ../../src/bootstrap/context.ts
|
|
511
|
-
import { setActiveApplicationContext } from "@getstrata/core";
|
|
512
512
|
|
|
513
513
|
// ../../src/bootstrap/contracts.ts
|
|
514
514
|
class ServiceContainer {
|
|
@@ -595,6 +595,41 @@ function resolveService(dependencies, token) {
|
|
|
595
595
|
return dependencies.container.resolve(token);
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
+
// ../../src/bootstrap/applicationRegistry.ts
|
|
599
|
+
var activeContext;
|
|
600
|
+
function setActiveApplicationContext(context) {
|
|
601
|
+
activeContext = context;
|
|
602
|
+
}
|
|
603
|
+
function requireActiveApplicationContext() {
|
|
604
|
+
if (!activeContext) {
|
|
605
|
+
throw new Error("The application context has not been bootstrapped.");
|
|
606
|
+
}
|
|
607
|
+
return activeContext;
|
|
608
|
+
}
|
|
609
|
+
function resolveApplicationCache() {
|
|
610
|
+
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
611
|
+
}
|
|
612
|
+
function resolveApplicationQueue() {
|
|
613
|
+
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
614
|
+
}
|
|
615
|
+
function resolveApplicationAuth() {
|
|
616
|
+
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
|
|
617
|
+
}
|
|
618
|
+
function resolveApplicationPolicyGate() {
|
|
619
|
+
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
620
|
+
}
|
|
621
|
+
function resolveApplicationConfig() {
|
|
622
|
+
return requireActiveApplicationContext().config;
|
|
623
|
+
}
|
|
624
|
+
function resolveApplicationLogger() {
|
|
625
|
+
return appLogger;
|
|
626
|
+
}
|
|
627
|
+
function resolveApplicationDependencies() {
|
|
628
|
+
return requireActiveApplicationContext().dependencies;
|
|
629
|
+
}
|
|
630
|
+
// ../../src/bootstrap/context.ts
|
|
631
|
+
import { setActiveApplicationContext as setActiveApplicationContext2 } from "@getstrata/core";
|
|
632
|
+
|
|
598
633
|
// ../../src/bootstrap/discoverModules.ts
|
|
599
634
|
import { readdirSync } from "fs";
|
|
600
635
|
import { join } from "path";
|
|
@@ -723,6 +758,7 @@ var db = new Proxy(function database() {}, {
|
|
|
723
758
|
return typeof value === "function" ? value.bind(connection) : value;
|
|
724
759
|
}
|
|
725
760
|
});
|
|
761
|
+
var connection_default = db;
|
|
726
762
|
|
|
727
763
|
// ../../src/modules/user/apiTokenTable.ts
|
|
728
764
|
import { defineTable } from "@getstrata/core/database";
|
|
@@ -1804,110 +1840,6 @@ class InvalidateCacheTagsJob extends Job {
|
|
|
1804
1840
|
}
|
|
1805
1841
|
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
1806
1842
|
|
|
1807
|
-
// ../../src/bootstrap/applicationRegistry.ts
|
|
1808
|
-
var activeContext;
|
|
1809
|
-
function requireActiveApplicationContext() {
|
|
1810
|
-
if (!activeContext) {
|
|
1811
|
-
throw new Error("The application context has not been bootstrapped.");
|
|
1812
|
-
}
|
|
1813
|
-
return activeContext;
|
|
1814
|
-
}
|
|
1815
|
-
function resolveApplicationCache() {
|
|
1816
|
-
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
1817
|
-
}
|
|
1818
|
-
function resolveApplicationQueue() {
|
|
1819
|
-
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
1823
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
1824
|
-
class DispatchWebhookJob extends Job {
|
|
1825
|
-
maxAttempts = 3;
|
|
1826
|
-
backoffMs = 2000;
|
|
1827
|
-
async handle(payload) {
|
|
1828
|
-
const rows = await repositoryConnection`
|
|
1829
|
-
SELECT id, url, secret
|
|
1830
|
-
FROM webhook
|
|
1831
|
-
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
1832
|
-
LIMIT 1
|
|
1833
|
-
`;
|
|
1834
|
-
const webhook = rows[0];
|
|
1835
|
-
if (!webhook) {
|
|
1836
|
-
return;
|
|
1837
|
-
}
|
|
1838
|
-
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
1839
|
-
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
1840
|
-
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
1841
|
-
let responseStatus = null;
|
|
1842
|
-
let errorMessage = null;
|
|
1843
|
-
try {
|
|
1844
|
-
const response = await safeFetch(webhook.url, {
|
|
1845
|
-
method: "POST",
|
|
1846
|
-
headers: {
|
|
1847
|
-
"content-type": "application/json",
|
|
1848
|
-
"x-workhub-signature": signature
|
|
1849
|
-
},
|
|
1850
|
-
body
|
|
1851
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
1852
|
-
responseStatus = response.status;
|
|
1853
|
-
if (!response.ok) {
|
|
1854
|
-
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
1855
|
-
}
|
|
1856
|
-
} catch (error) {
|
|
1857
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1858
|
-
await repositoryConnection`
|
|
1859
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
1860
|
-
VALUES (
|
|
1861
|
-
${webhook.id},
|
|
1862
|
-
${payload.event},
|
|
1863
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
1864
|
-
${responseStatus},
|
|
1865
|
-
${errorMessage}
|
|
1866
|
-
)
|
|
1867
|
-
`;
|
|
1868
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1869
|
-
}
|
|
1870
|
-
await repositoryConnection`
|
|
1871
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
1872
|
-
VALUES (
|
|
1873
|
-
${webhook.id},
|
|
1874
|
-
${payload.event},
|
|
1875
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
1876
|
-
${responseStatus}
|
|
1877
|
-
)
|
|
1878
|
-
`;
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
1882
|
-
|
|
1883
|
-
// ../../src/core/queue/jobRegistry.ts
|
|
1884
|
-
class JobRegistry {
|
|
1885
|
-
constructor() {}
|
|
1886
|
-
factories = new Map;
|
|
1887
|
-
instances = new WeakMap;
|
|
1888
|
-
register(name, factory) {
|
|
1889
|
-
this.factories.set(name, factory);
|
|
1890
|
-
}
|
|
1891
|
-
resolveName(job) {
|
|
1892
|
-
return this.instances.get(job);
|
|
1893
|
-
}
|
|
1894
|
-
track(name, job) {
|
|
1895
|
-
this.instances.set(job, name);
|
|
1896
|
-
return job;
|
|
1897
|
-
}
|
|
1898
|
-
create(name) {
|
|
1899
|
-
const factory = this.factories.get(name);
|
|
1900
|
-
if (!factory) {
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
return factory();
|
|
1904
|
-
}
|
|
1905
|
-
names() {
|
|
1906
|
-
return [...this.factories.keys()];
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
var jobRegistry = new JobRegistry;
|
|
1910
|
-
|
|
1911
1843
|
// ../../src/core/pagination/index.ts
|
|
1912
1844
|
function buildPaginationMeta(input) {
|
|
1913
1845
|
const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
|
|
@@ -3635,6 +3567,34 @@ class FailedJobService {
|
|
|
3635
3567
|
}
|
|
3636
3568
|
var failedJobService_default = FailedJobService;
|
|
3637
3569
|
|
|
3570
|
+
// ../../src/core/queue/jobRegistry.ts
|
|
3571
|
+
class JobRegistry {
|
|
3572
|
+
constructor() {}
|
|
3573
|
+
factories = new Map;
|
|
3574
|
+
instances = new WeakMap;
|
|
3575
|
+
register(name, factory) {
|
|
3576
|
+
this.factories.set(name, factory);
|
|
3577
|
+
}
|
|
3578
|
+
resolveName(job) {
|
|
3579
|
+
return this.instances.get(job);
|
|
3580
|
+
}
|
|
3581
|
+
track(name, job) {
|
|
3582
|
+
this.instances.set(job, name);
|
|
3583
|
+
return job;
|
|
3584
|
+
}
|
|
3585
|
+
create(name) {
|
|
3586
|
+
const factory = this.factories.get(name);
|
|
3587
|
+
if (!factory) {
|
|
3588
|
+
return;
|
|
3589
|
+
}
|
|
3590
|
+
return factory();
|
|
3591
|
+
}
|
|
3592
|
+
names() {
|
|
3593
|
+
return [...this.factories.keys()];
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
var jobRegistry = new JobRegistry;
|
|
3597
|
+
|
|
3638
3598
|
// ../../src/core/queue/jobRunner.ts
|
|
3639
3599
|
async function runQueueJob(envelope, failedJobs) {
|
|
3640
3600
|
const job = jobRegistry.create(envelope.name);
|
|
@@ -3749,19 +3709,82 @@ function createProductionQueue(driver, options = {}) {
|
|
|
3749
3709
|
return new ResilientQueue(failedJobs, driver === "async");
|
|
3750
3710
|
}
|
|
3751
3711
|
|
|
3752
|
-
// ../../src/core/
|
|
3753
|
-
|
|
3712
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3713
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
3714
|
+
class DispatchWebhookJob extends Job {
|
|
3715
|
+
maxAttempts = 3;
|
|
3716
|
+
backoffMs = 2000;
|
|
3717
|
+
async handle(payload) {
|
|
3718
|
+
const rows = await repositoryConnection`
|
|
3719
|
+
SELECT id, url, secret
|
|
3720
|
+
FROM webhook
|
|
3721
|
+
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
3722
|
+
LIMIT 1
|
|
3723
|
+
`;
|
|
3724
|
+
const webhook = rows[0];
|
|
3725
|
+
if (!webhook) {
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
3728
|
+
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
3729
|
+
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
3730
|
+
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
3731
|
+
let responseStatus = null;
|
|
3732
|
+
let errorMessage = null;
|
|
3733
|
+
try {
|
|
3734
|
+
const response = await safeFetch(webhook.url, {
|
|
3735
|
+
method: "POST",
|
|
3736
|
+
headers: {
|
|
3737
|
+
"content-type": "application/json",
|
|
3738
|
+
"x-workhub-signature": signature
|
|
3739
|
+
},
|
|
3740
|
+
body
|
|
3741
|
+
}, { allowHttp: appConfig.env !== "production" });
|
|
3742
|
+
responseStatus = response.status;
|
|
3743
|
+
if (!response.ok) {
|
|
3744
|
+
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
3745
|
+
}
|
|
3746
|
+
} catch (error) {
|
|
3747
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
3748
|
+
await repositoryConnection`
|
|
3749
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
3750
|
+
VALUES (
|
|
3751
|
+
${webhook.id},
|
|
3752
|
+
${payload.event},
|
|
3753
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
3754
|
+
${responseStatus},
|
|
3755
|
+
${errorMessage}
|
|
3756
|
+
)
|
|
3757
|
+
`;
|
|
3758
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
3759
|
+
}
|
|
3760
|
+
await repositoryConnection`
|
|
3761
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
3762
|
+
VALUES (
|
|
3763
|
+
${webhook.id},
|
|
3764
|
+
${payload.event},
|
|
3765
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
3766
|
+
${responseStatus}
|
|
3767
|
+
)
|
|
3768
|
+
`;
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
3772
|
+
|
|
3773
|
+
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
3754
3774
|
function registerDefaultJobs() {
|
|
3755
3775
|
jobRegistry.register("cache.invalidate-tags", () => {
|
|
3756
3776
|
return new invalidateCacheTagsJob_default(resolveApplicationCache());
|
|
3757
3777
|
});
|
|
3758
3778
|
jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
|
|
3759
3779
|
}
|
|
3760
|
-
|
|
3780
|
+
|
|
3781
|
+
// ../../src/core/queue/createAppQueue.ts
|
|
3782
|
+
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
3783
|
+
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
|
3761
3784
|
return createProductionQueue(driver, {
|
|
3762
3785
|
redisUrl,
|
|
3763
3786
|
failedJobs,
|
|
3764
|
-
registerJobs
|
|
3787
|
+
registerJobs
|
|
3765
3788
|
});
|
|
3766
3789
|
}
|
|
3767
3790
|
|
|
@@ -3869,7 +3892,7 @@ var queueProvider = {
|
|
|
3869
3892
|
config.set("queue.driver", driver);
|
|
3870
3893
|
const failedJobs = createFailedJobService();
|
|
3871
3894
|
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
3872
|
-
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs));
|
|
3895
|
+
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, registerDefaultJobs));
|
|
3873
3896
|
}
|
|
3874
3897
|
};
|
|
3875
3898
|
var queue_default = queueProvider;
|
|
@@ -4346,7 +4369,7 @@ function createAppContext() {
|
|
|
4346
4369
|
config,
|
|
4347
4370
|
dependencies
|
|
4348
4371
|
};
|
|
4349
|
-
|
|
4372
|
+
setActiveApplicationContext2(appContext);
|
|
4350
4373
|
return appContext;
|
|
4351
4374
|
}
|
|
4352
4375
|
// ../../src/bootstrap/createWebRoutes.ts
|
|
@@ -4702,6 +4725,302 @@ function mergeWebRoutes(dependencies, routes) {
|
|
|
4702
4725
|
...routes
|
|
4703
4726
|
};
|
|
4704
4727
|
}
|
|
4728
|
+
// ../../src/core/crypto/nonCryptographicHash.ts
|
|
4729
|
+
function nonCryptographicDigest(input) {
|
|
4730
|
+
return Bun.hash(input).toString(16);
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
// ../../src/core/http/etag.ts
|
|
4734
|
+
function isEtagEnabled() {
|
|
4735
|
+
return (process.env.FEATURE_ETAG ?? "true") !== "false";
|
|
4736
|
+
}
|
|
4737
|
+
function formatWeakEtag(digest) {
|
|
4738
|
+
return `W/"${digest}"`;
|
|
4739
|
+
}
|
|
4740
|
+
function etagFromResource(resource) {
|
|
4741
|
+
const version = resource.updated_at ?? resource.created_at ?? "";
|
|
4742
|
+
const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
|
|
4743
|
+
const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
|
|
4744
|
+
return formatWeakEtag(digest);
|
|
4745
|
+
}
|
|
4746
|
+
function normalizeEtag(value) {
|
|
4747
|
+
return value.trim();
|
|
4748
|
+
}
|
|
4749
|
+
function etagValuesMatch(left, right) {
|
|
4750
|
+
return normalizeEtag(left) === normalizeEtag(right);
|
|
4751
|
+
}
|
|
4752
|
+
function parseEtagList(header) {
|
|
4753
|
+
if (!header) {
|
|
4754
|
+
return [];
|
|
4755
|
+
}
|
|
4756
|
+
return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
|
|
4757
|
+
}
|
|
4758
|
+
function ifNoneMatchSatisfied(request, etag) {
|
|
4759
|
+
const header = request.headers.get("if-none-match");
|
|
4760
|
+
if (!header) {
|
|
4761
|
+
return false;
|
|
4762
|
+
}
|
|
4763
|
+
if (header.trim() === "*") {
|
|
4764
|
+
return true;
|
|
4765
|
+
}
|
|
4766
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
4767
|
+
}
|
|
4768
|
+
function ifMatchSatisfied(request, etag) {
|
|
4769
|
+
const header = request.headers.get("if-match");
|
|
4770
|
+
if (!header) {
|
|
4771
|
+
return false;
|
|
4772
|
+
}
|
|
4773
|
+
if (header.trim() === "*") {
|
|
4774
|
+
return true;
|
|
4775
|
+
}
|
|
4776
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
4777
|
+
}
|
|
4778
|
+
function assertIfMatch(request, etag, options = {}) {
|
|
4779
|
+
const header = request.headers.get("if-match");
|
|
4780
|
+
if (!header) {
|
|
4781
|
+
if (options.required) {
|
|
4782
|
+
throw new PreconditionFailedError("If-Match header is required.");
|
|
4783
|
+
}
|
|
4784
|
+
return;
|
|
4785
|
+
}
|
|
4786
|
+
if (!ifMatchSatisfied(request, etag)) {
|
|
4787
|
+
throw new PreconditionFailedError("Resource ETag does not match If-Match.");
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
function applyEtagHeaders(headers, etag) {
|
|
4791
|
+
const next = new Headers(headers);
|
|
4792
|
+
next.set("ETag", etag);
|
|
4793
|
+
next.set("Cache-Control", "private, must-revalidate");
|
|
4794
|
+
next.append("Vary", "Authorization");
|
|
4795
|
+
next.append("Vary", "X-Tenant-Id");
|
|
4796
|
+
return next;
|
|
4797
|
+
}
|
|
4798
|
+
function notModifiedResponse(etag) {
|
|
4799
|
+
return new Response(null, {
|
|
4800
|
+
status: 304,
|
|
4801
|
+
headers: applyEtagHeaders(new Headers, etag)
|
|
4802
|
+
});
|
|
4803
|
+
}
|
|
4804
|
+
function applyConditionalGet(request, response, etag) {
|
|
4805
|
+
if (!isEtagEnabled()) {
|
|
4806
|
+
return response;
|
|
4807
|
+
}
|
|
4808
|
+
if (ifNoneMatchSatisfied(request, etag)) {
|
|
4809
|
+
return notModifiedResponse(etag);
|
|
4810
|
+
}
|
|
4811
|
+
const headers = applyEtagHeaders(new Headers(response.headers), etag);
|
|
4812
|
+
return new Response(response.body, {
|
|
4813
|
+
status: response.status,
|
|
4814
|
+
statusText: response.statusText,
|
|
4815
|
+
headers
|
|
4816
|
+
});
|
|
4817
|
+
}
|
|
4818
|
+
|
|
4819
|
+
// ../../src/core/tenant/tenantContext.ts
|
|
4820
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
4821
|
+
var tenantContext = new AsyncLocalStorage4;
|
|
4822
|
+
|
|
4823
|
+
// ../../src/core/http/validation.ts
|
|
4824
|
+
function parsePositiveIntParam(value, name = "id") {
|
|
4825
|
+
const parsed = Number.parseInt(value, 10);
|
|
4826
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4827
|
+
throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
|
|
4828
|
+
}
|
|
4829
|
+
return parsed;
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4832
|
+
// ../../src/bootstrap/http/securedRouteModelBinding.ts
|
|
4833
|
+
function isMutatingPolicyAction(action) {
|
|
4834
|
+
return action === "update" || action === "delete";
|
|
4835
|
+
}
|
|
4836
|
+
function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
4837
|
+
return async (request) => {
|
|
4838
|
+
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
4839
|
+
const model = await resolver(id, request);
|
|
4840
|
+
const gate = resolveApplicationPolicyGate();
|
|
4841
|
+
const auth = resolveApplicationAuth();
|
|
4842
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
4843
|
+
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
4844
|
+
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
4845
|
+
assertIfMatch(request, etagFromResource(model), {
|
|
4846
|
+
required: authorization.requireIfMatch ?? true
|
|
4847
|
+
});
|
|
4848
|
+
}
|
|
4849
|
+
const response = await handler(request, model);
|
|
4850
|
+
if (isEtagEnabled() && authorization.action === "view") {
|
|
4851
|
+
return applyConditionalGet(request, response, etagFromResource(model));
|
|
4852
|
+
}
|
|
4853
|
+
return response;
|
|
4854
|
+
};
|
|
4855
|
+
}
|
|
4856
|
+
function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
4857
|
+
return async (request) => {
|
|
4858
|
+
const key = String(request.params[param] ?? "").trim();
|
|
4859
|
+
if (!key) {
|
|
4860
|
+
throw new BadRequestError(`Missing route parameter "${String(param)}".`);
|
|
4861
|
+
}
|
|
4862
|
+
const model = await resolver(key, request);
|
|
4863
|
+
const gate = resolveApplicationPolicyGate();
|
|
4864
|
+
const auth = resolveApplicationAuth();
|
|
4865
|
+
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
4866
|
+
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
4867
|
+
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
4868
|
+
assertIfMatch(request, etagFromResource(model), {
|
|
4869
|
+
required: authorization.requireIfMatch ?? true
|
|
4870
|
+
});
|
|
4871
|
+
}
|
|
4872
|
+
const response = await handler(request, model);
|
|
4873
|
+
if (isEtagEnabled() && authorization.action === "view") {
|
|
4874
|
+
return applyConditionalGet(request, response, etagFromResource(model));
|
|
4875
|
+
}
|
|
4876
|
+
return response;
|
|
4877
|
+
};
|
|
4878
|
+
}
|
|
4879
|
+
// ../../src/core/auth/accessControl.ts
|
|
4880
|
+
var ROLE_RANK = {
|
|
4881
|
+
member: 1,
|
|
4882
|
+
admin: 2,
|
|
4883
|
+
owner: 3
|
|
4884
|
+
};
|
|
4885
|
+
function isGlobalAdmin(user) {
|
|
4886
|
+
return user?.role === "admin";
|
|
4887
|
+
}
|
|
4888
|
+
function hasMinimumOrgRole(role, minimum) {
|
|
4889
|
+
if (!role) {
|
|
4890
|
+
return false;
|
|
4891
|
+
}
|
|
4892
|
+
return ROLE_RANK[role] >= ROLE_RANK[minimum];
|
|
4893
|
+
}
|
|
4894
|
+
function resolveUserId(user) {
|
|
4895
|
+
const userId = typeof user.id === "number" ? user.id : Number(user.id);
|
|
4896
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
4897
|
+
throw new ForbiddenError("Invalid authenticated user.");
|
|
4898
|
+
}
|
|
4899
|
+
return userId;
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4902
|
+
// ../../src/core/auth/membershipContext.ts
|
|
4903
|
+
import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
|
|
4904
|
+
|
|
4905
|
+
// ../../src/modules/organization/memberRepository.ts
|
|
4906
|
+
class OrganizationMemberRepository {
|
|
4907
|
+
constructor() {}
|
|
4908
|
+
async findMembership(userId, organizationId) {
|
|
4909
|
+
const rows = await connection_default`
|
|
4910
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
4911
|
+
FROM organization_member
|
|
4912
|
+
WHERE user_id = ${userId} AND organization_id = ${organizationId}
|
|
4913
|
+
LIMIT 1
|
|
4914
|
+
`;
|
|
4915
|
+
return rows[0] ?? null;
|
|
4916
|
+
}
|
|
4917
|
+
async listForUser(userId) {
|
|
4918
|
+
return await connection_default`
|
|
4919
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
4920
|
+
FROM organization_member
|
|
4921
|
+
WHERE user_id = ${userId}
|
|
4922
|
+
ORDER BY organization_id
|
|
4923
|
+
`;
|
|
4924
|
+
}
|
|
4925
|
+
async listForOrganization(organizationId) {
|
|
4926
|
+
return await connection_default`
|
|
4927
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
4928
|
+
FROM organization_member
|
|
4929
|
+
WHERE organization_id = ${organizationId}
|
|
4930
|
+
ORDER BY id
|
|
4931
|
+
`;
|
|
4932
|
+
}
|
|
4933
|
+
async addMember(input) {
|
|
4934
|
+
const rows = await connection_default`
|
|
4935
|
+
INSERT INTO organization_member (organization_id, user_id, role)
|
|
4936
|
+
VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
|
|
4937
|
+
RETURNING id, organization_id, user_id, role, created_at
|
|
4938
|
+
`;
|
|
4939
|
+
const row = rows[0];
|
|
4940
|
+
if (!row) {
|
|
4941
|
+
throw new Error("Organization member insert did not return a row.");
|
|
4942
|
+
}
|
|
4943
|
+
return row;
|
|
4944
|
+
}
|
|
4945
|
+
async removeMember(organizationId, userId) {
|
|
4946
|
+
const rows = await connection_default`
|
|
4947
|
+
DELETE FROM organization_member
|
|
4948
|
+
WHERE organization_id = ${organizationId} AND user_id = ${userId}
|
|
4949
|
+
RETURNING id
|
|
4950
|
+
`;
|
|
4951
|
+
return rows.length > 0;
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
var memberRepository_default = OrganizationMemberRepository;
|
|
4955
|
+
|
|
4956
|
+
// ../../src/core/auth/membershipContext.ts
|
|
4957
|
+
var membershipContext = new AsyncLocalStorage5;
|
|
4958
|
+
var membershipRepository = new memberRepository_default;
|
|
4959
|
+
|
|
4960
|
+
// ../../src/core/auth/membershipService.ts
|
|
4961
|
+
class MembershipService {
|
|
4962
|
+
members;
|
|
4963
|
+
constructor(members = membershipRepository) {
|
|
4964
|
+
this.members = members;
|
|
4965
|
+
}
|
|
4966
|
+
async listOrganizationIdsForUser(userId) {
|
|
4967
|
+
const memberships = await this.members.listForUser(userId);
|
|
4968
|
+
return memberships.map((membership) => membership.organization_id);
|
|
4969
|
+
}
|
|
4970
|
+
async getOrgRole(userId, organizationId) {
|
|
4971
|
+
const membership = await this.members.findMembership(userId, organizationId);
|
|
4972
|
+
return membership?.role ?? null;
|
|
4973
|
+
}
|
|
4974
|
+
async requireOrgAccess(organizationId, minimumRole = "member", user = currentAuthUser()) {
|
|
4975
|
+
if (!user) {
|
|
4976
|
+
throw new ForbiddenError("Authentication required.");
|
|
4977
|
+
}
|
|
4978
|
+
if (isGlobalAdmin(user)) {
|
|
4979
|
+
return "owner";
|
|
4980
|
+
}
|
|
4981
|
+
const role = await this.getOrgRole(resolveUserId(user), organizationId);
|
|
4982
|
+
if (!role || !hasMinimumOrgRole(role, minimumRole)) {
|
|
4983
|
+
throw new ForbiddenError("Organization membership required.");
|
|
4984
|
+
}
|
|
4985
|
+
return role;
|
|
4986
|
+
}
|
|
4987
|
+
async filterAccessibleOrganizationIds(organizationIds, user = currentAuthUser()) {
|
|
4988
|
+
if (!user) {
|
|
4989
|
+
return [];
|
|
4990
|
+
}
|
|
4991
|
+
if (isGlobalAdmin(user)) {
|
|
4992
|
+
return organizationIds;
|
|
4993
|
+
}
|
|
4994
|
+
const allowed = new Set(await this.listOrganizationIdsForUser(resolveUserId(user)));
|
|
4995
|
+
return organizationIds.filter((organizationId) => allowed.has(organizationId));
|
|
4996
|
+
}
|
|
4997
|
+
async addOwnerOnOrganizationCreate(organizationId, userId) {
|
|
4998
|
+
await this.members.addMember({
|
|
4999
|
+
organizationId,
|
|
5000
|
+
userId,
|
|
5001
|
+
role: "owner"
|
|
5002
|
+
});
|
|
5003
|
+
}
|
|
5004
|
+
listMembersForOrganization(organizationId) {
|
|
5005
|
+
return this.members.listForOrganization(organizationId);
|
|
5006
|
+
}
|
|
5007
|
+
addMember(input) {
|
|
5008
|
+
return this.members.addMember(input);
|
|
5009
|
+
}
|
|
5010
|
+
removeMember(organizationId, userId) {
|
|
5011
|
+
return this.members.removeMember(organizationId, userId);
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
var membershipService_default = MembershipService;
|
|
5015
|
+
|
|
5016
|
+
// ../../src/bootstrap/membershipService.ts
|
|
5017
|
+
function resolveMembershipService() {
|
|
5018
|
+
const dependencies = resolveApplicationDependencies();
|
|
5019
|
+
if (dependencies.container.has("core.membership")) {
|
|
5020
|
+
return dependencies.container.resolve("core.membership");
|
|
5021
|
+
}
|
|
5022
|
+
return new membershipService_default;
|
|
5023
|
+
}
|
|
4705
5024
|
// ../../src/bootstrap/prefixRouteMap.ts
|
|
4706
5025
|
function prefixRouteMap(prefix, routes) {
|
|
4707
5026
|
const normalizedPrefix = prefix.replace(/\/$/, "");
|
|
@@ -4761,7 +5080,7 @@ async function parseFormBody(request) {
|
|
|
4761
5080
|
return { fields, files };
|
|
4762
5081
|
}
|
|
4763
5082
|
// ../../src/bootstrap/web/routing.ts
|
|
4764
|
-
import {
|
|
5083
|
+
import { withErrorHandling } from "@getstrata/core";
|
|
4765
5084
|
function routeParams(request) {
|
|
4766
5085
|
const normalized = {};
|
|
4767
5086
|
const raw = request.params;
|
|
@@ -4935,13 +5254,23 @@ export {
|
|
|
4935
5254
|
wrapSecuredRouteModelByKey,
|
|
4936
5255
|
toRouteRequest,
|
|
4937
5256
|
slugify,
|
|
4938
|
-
|
|
5257
|
+
setActiveApplicationContext,
|
|
5258
|
+
securedBindRouteModelByKey,
|
|
5259
|
+
securedBindRouteModel,
|
|
4939
5260
|
scheduleRunCommand,
|
|
4940
5261
|
runProviderPhase,
|
|
4941
5262
|
runDueScheduledTasks,
|
|
4942
5263
|
routeParams,
|
|
4943
5264
|
resolveService,
|
|
4944
|
-
|
|
5265
|
+
resolveMembershipService,
|
|
5266
|
+
resolveApplicationQueue,
|
|
5267
|
+
resolveApplicationPolicyGate,
|
|
5268
|
+
resolveApplicationLogger,
|
|
5269
|
+
resolveApplicationDependencies,
|
|
5270
|
+
resolveApplicationConfig,
|
|
5271
|
+
resolveApplicationCache,
|
|
5272
|
+
resolveApplicationAuth,
|
|
5273
|
+
registerDefaultJobs,
|
|
4945
5274
|
prefixRouteMap,
|
|
4946
5275
|
parseFormBody,
|
|
4947
5276
|
mergeWebRoutes,
|