@getstrata/core 0.5.52 → 0.5.54

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 (32) hide show
  1. package/README.md +6 -3
  2. package/dist/core/queue/failedJobRepository.d.ts +1 -1
  3. package/dist/core/queue/failedJobTable.d.ts +1 -1
  4. package/dist/entries/auth/accessControl.js +60 -1
  5. package/dist/entries/auth/membershipMiddleware.js +21 -0
  6. package/dist/entries/auth/membershipScope.js +336 -1
  7. package/dist/entries/auth/membershipService.js +304 -1
  8. package/dist/entries/auth/policy.js +81 -1
  9. package/dist/entries/auth/sessionGuard.js +8 -7
  10. package/dist/entries/database/repositoryQuery.js +655 -0
  11. package/dist/entries/database/whereBuilder.js +32 -0
  12. package/dist/entries/http/formRequest.js +102 -0
  13. package/dist/entries/http/pagination.js +102 -0
  14. package/dist/entries/http/routeModelBinding.js +102 -0
  15. package/dist/entries/http/securedRouteModelBinding.js +102 -0
  16. package/dist/entries/http/validation.js +145 -0
  17. package/dist/entries/http/webErrorResponse.js +8 -7
  18. package/dist/entries/http/webFormRequest.js +102 -0
  19. package/dist/entries/queue/createAppQueue.js +2 -2
  20. package/dist/entries/queue/failedJobRepository.js +2 -2
  21. package/dist/entries/queue/publicQueue.js +2 -2
  22. package/dist/entries/queue/queueMetrics.js +2 -2
  23. package/dist/entries/view.js +8 -7
  24. package/dist/index.js +2215 -2213
  25. package/dist/modules/user/apiTokenRepository.d.ts +1 -1
  26. package/dist/modules/user/apiTokenTable.d.ts +1 -1
  27. package/dist/modules/user/notificationRepository.d.ts +1 -1
  28. package/dist/modules/user/notificationTable.d.ts +1 -1
  29. package/dist/modules/user/oauthIdentityRepository.d.ts +2 -2
  30. package/dist/modules/user/repository.d.ts +1 -1
  31. package/dist/modules/user/table.d.ts +1 -1
  32. package/package.json +17 -2
package/dist/index.js CHANGED
@@ -1530,2580 +1530,2569 @@ class BaseRepository {
1530
1530
  }
1531
1531
  }
1532
1532
  var baseRepository_default = BaseRepository;
1533
- // ../../src/core/database/connection.ts
1534
- function createDatabaseConnection(source) {
1535
- return {
1536
- async unsafe(query, params = []) {
1537
- return await source.unsafe(query, params);
1538
- }
1539
- };
1533
+
1534
+ // ../../src/config/database.ts
1535
+ function readInteger(name, fallback) {
1536
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
1537
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
1540
1538
  }
1541
- // ../../src/core/database/model.ts
1542
- var modelRepositories = new WeakMap;
1543
- var modelGlobalScopes = new WeakMap;
1544
- var modelBooted = new WeakSet;
1545
- function resolveModelRepository(model) {
1546
- const repository = modelRepositories.get(model);
1547
- if (!repository) {
1548
- throw new Error(`${model.name}.repository() is not implemented.`);
1539
+ var databaseConfig = {
1540
+ url: process.env.DATABASE_URL ?? "",
1541
+ poolMax: readInteger("DB_POOL_MAX", 10),
1542
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
1543
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
1544
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
1545
+ };
1546
+
1547
+ // ../../src/db/connection/createConnection.ts
1548
+ var {SQL } = globalThis.Bun;
1549
+ function createDatabaseConnection(config) {
1550
+ if (!config.url) {
1551
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
1549
1552
  }
1550
- return repository;
1551
- }
1552
- function modelStatics(model) {
1553
- return model;
1553
+ return new SQL({
1554
+ url: config.url,
1555
+ max: config.poolMax,
1556
+ idleTimeout: config.idleTimeoutSeconds,
1557
+ maxLifetime: config.maxLifetimeSeconds,
1558
+ connectionTimeout: config.connectionTimeoutSeconds
1559
+ });
1554
1560
  }
1555
- function ensureBooted(model) {
1556
- if (modelBooted.has(model)) {
1557
- return;
1558
- }
1559
- modelBooted.add(model);
1560
- const boot = model.boot;
1561
- if (typeof boot === "function") {
1562
- boot.call(model);
1561
+
1562
+ // ../../src/db/connection/index.ts
1563
+ var connectionHolder = {
1564
+ connection: null
1565
+ };
1566
+ function getDatabase() {
1567
+ if (!connectionHolder.connection) {
1568
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
1569
+ registerDefaultDatabasePool(connectionHolder.connection);
1563
1570
  }
1571
+ return connectionHolder.connection;
1564
1572
  }
1565
- function getGlobalScopes(model) {
1566
- return modelGlobalScopes.get(model) ?? [];
1573
+ function getDb() {
1574
+ getDatabase();
1575
+ return getDefaultDatabaseQuery();
1567
1576
  }
1568
- function hydrateValue(value, cast) {
1569
- if (value === null || value === undefined) {
1570
- return value;
1571
- }
1572
- switch (cast) {
1573
- case "date":
1574
- case "datetime":
1575
- return value instanceof Date ? value : new Date(String(value));
1576
- case "json":
1577
- return typeof value === "string" ? JSON.parse(value) : value;
1578
- case "bool":
1579
- case "boolean":
1580
- return value === true || value === 1 || value === "1" || value === "true";
1581
- default:
1582
- return value;
1577
+ var db = new Proxy(function database() {}, {
1578
+ apply(_target, _thisArg, args) {
1579
+ return getDb()(...args);
1580
+ },
1581
+ get(_target, property) {
1582
+ const connection = getDb();
1583
+ const value = connection[property];
1584
+ return typeof value === "function" ? value.bind(connection) : value;
1583
1585
  }
1586
+ });
1587
+ var connection_default = db;
1588
+
1589
+ // ../../src/core/database/table.ts
1590
+ function defineTable(definition) {
1591
+ return definition;
1584
1592
  }
1585
- function dehydrateValue(value, cast) {
1586
- if (value === null || value === undefined) {
1587
- return value;
1588
- }
1589
- switch (cast) {
1590
- case "date":
1591
- case "datetime":
1592
- return value instanceof Date ? value : new Date(String(value));
1593
- case "json":
1594
- return typeof value === "string" ? value : JSON.stringify(value);
1595
- case "bool":
1596
- case "boolean":
1597
- return Boolean(value);
1598
- default:
1599
- return value;
1600
- }
1593
+
1594
+ // ../../src/modules/user/apiTokenTable.ts
1595
+ var apiTokenTable = defineTable({
1596
+ name: "api_token",
1597
+ primaryKey: "id",
1598
+ columns: [
1599
+ "id",
1600
+ "user_id",
1601
+ "name",
1602
+ "token_hash",
1603
+ "abilities",
1604
+ "last_used_at",
1605
+ "expires_at",
1606
+ "created_at"
1607
+ ],
1608
+ defaultOrderBy: { column: "id", direction: "ASC" }
1609
+ });
1610
+
1611
+ // ../../src/core/auth/password.ts
1612
+ async function verifyPassword(password, passwordHash) {
1613
+ return await Bun.password.verify(password, passwordHash);
1601
1614
  }
1602
- function filterMassAssignable(fillable, guarded, input) {
1603
- const resolvedGuarded = guarded ?? true;
1604
- if (fillable && fillable.length > 0) {
1605
- const allowed = new Set(fillable);
1606
- return Object.fromEntries(Object.entries(input).filter(([key]) => allowed.has(key)));
1615
+
1616
+ // ../../src/core/crypto/fieldEncryption.ts
1617
+ import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
1618
+ var ENCRYPTION_PREFIX = "enc:v1:";
1619
+ var IV_LENGTH = 12;
1620
+ var TAG_LENGTH = 16;
1621
+ function resolveEncryptionKey() {
1622
+ const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
1623
+ if (!raw) {
1624
+ return null;
1607
1625
  }
1608
- if (resolvedGuarded === true || resolvedGuarded.includes("*")) {
1609
- return {};
1626
+ if (/^[0-9a-f]{64}$/i.test(raw)) {
1627
+ return Buffer.from(raw, "hex");
1610
1628
  }
1611
- const blocked = new Set(resolvedGuarded);
1612
- return Object.fromEntries(Object.entries(input).filter(([key]) => !blocked.has(key)));
1629
+ const decoded = Buffer.from(raw, "base64");
1630
+ if (decoded.length === 32) {
1631
+ return decoded;
1632
+ }
1633
+ throw new Error("KMS_ENCRYPTION_KEY must be 32 bytes (hex or base64).");
1613
1634
  }
1614
- function applyCasts(values, casts, direction) {
1615
- if (Object.keys(casts).length === 0) {
1616
- return values;
1635
+ function isFieldEncryptionEnabled() {
1636
+ const featureFlag = process.env.FEATURE_FIELD_ENCRYPTION;
1637
+ if (featureFlag === "false") {
1638
+ return false;
1617
1639
  }
1618
- const result = { ...values };
1619
- const castFn = direction === "hydrate" ? hydrateValue : dehydrateValue;
1620
- for (const [key, cast] of Object.entries(casts)) {
1621
- if (key in result && cast) {
1622
- result[key] = castFn(result[key], cast);
1623
- }
1640
+ if (featureFlag === "true") {
1641
+ return true;
1624
1642
  }
1625
- return result;
1643
+ return (process.env.APP_ENV ?? "local") === "production";
1626
1644
  }
1627
- function applyTimestampsOnCreate(columns, values, enabled) {
1628
- if (!enabled) {
1629
- return values;
1645
+ function decryptField(value, key) {
1646
+ if (!value.startsWith(ENCRYPTION_PREFIX)) {
1647
+ return value;
1630
1648
  }
1631
- const now = new Date;
1632
- const result = { ...values };
1633
- if (columns.includes("created_at")) {
1634
- result.created_at = now;
1649
+ const payload = Buffer.from(value.slice(ENCRYPTION_PREFIX.length), "base64");
1650
+ const iv = payload.subarray(0, IV_LENGTH);
1651
+ const tag = payload.subarray(payload.length - TAG_LENGTH);
1652
+ const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
1653
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
1654
+ decipher.setAuthTag(tag);
1655
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
1656
+ }
1657
+
1658
+ // ../../src/core/crypto/mfaSecret.ts
1659
+ function revealMfaSecret(stored) {
1660
+ if (!stored) {
1661
+ return null;
1635
1662
  }
1636
- if (columns.includes("updated_at")) {
1637
- result.updated_at = now;
1663
+ const key = resolveEncryptionKey();
1664
+ if (!isFieldEncryptionEnabled() || !key || !stored.startsWith("enc:v1:")) {
1665
+ return stored;
1638
1666
  }
1639
- return result;
1667
+ return decryptField(stored, key);
1640
1668
  }
1641
- function applyTimestampsOnUpdate(columns, values, enabled) {
1642
- if (!enabled) {
1643
- return values;
1669
+
1670
+ // ../../src/core/http/requestMetaContext.ts
1671
+ var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
1672
+ function runWithRequestMeta(meta, callback) {
1673
+ return requestMetaContext.run(meta, callback);
1674
+ }
1675
+ function currentRequestMeta() {
1676
+ return requestMetaContext.getStore() ?? {
1677
+ ipAddress: null,
1678
+ userAgent: null
1679
+ };
1680
+ }
1681
+
1682
+ // ../../src/core/security/securityEvents.ts
1683
+ function logSecurityEvent(event, details = {}) {
1684
+ const meta = currentRequestMeta();
1685
+ const user = currentAuthUser();
1686
+ console.log(JSON.stringify({
1687
+ level: "security",
1688
+ event,
1689
+ timestamp: new Date().toISOString(),
1690
+ ip_address: meta.ipAddress ?? null,
1691
+ user_agent: meta.userAgent ?? null,
1692
+ user_id: user?.id ?? null,
1693
+ ...details
1694
+ }));
1695
+ }
1696
+
1697
+ // ../../src/core/security/tokenExpiry.ts
1698
+ function resolveDefaultTokenExpiryDays() {
1699
+ const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
1700
+ if (!raw) {
1701
+ return null;
1644
1702
  }
1645
- const result = { ...values };
1646
- if (columns.includes("updated_at")) {
1647
- result.updated_at = new Date;
1703
+ const parsed = Number.parseInt(raw, 10);
1704
+ if (!Number.isInteger(parsed) || parsed <= 0) {
1705
+ return null;
1648
1706
  }
1649
- return result;
1707
+ return parsed;
1650
1708
  }
1651
1709
 
1652
- class Model {
1653
- attributes;
1654
- repository;
1655
- static $fillable;
1656
- static $guarded;
1657
- static $casts = {};
1658
- static $timestamps = true;
1659
- _exists;
1660
- constructor(attributes, repository, exists = true) {
1661
- this.attributes = attributes;
1662
- this.repository = repository;
1663
- this._exists = exists;
1664
- }
1665
- get $exists() {
1666
- return this._exists;
1667
- }
1668
- get(key) {
1669
- return this.attributes[key];
1670
- }
1671
- get id() {
1672
- return this.attributes[this.primaryKey()];
1673
- }
1674
- toObject() {
1675
- return { ...this.attributes };
1676
- }
1677
- primaryKey() {
1678
- throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
1679
- }
1680
- static primaryKeyField() {
1681
- return resolveModelRepository(this).getTable().primaryKey;
1682
- }
1683
- static hydrateAttributes(attributes) {
1684
- const casts = modelStatics(this).$casts ?? {};
1685
- return applyCasts(attributes, casts, "hydrate");
1710
+ // ../../src/core/security/totp.ts
1711
+ import { createHmac as createHmac2 } from "crypto";
1712
+ function decodeBase32(input) {
1713
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
1714
+ const normalized = input.replace(/=+$/u, "").toUpperCase();
1715
+ let bits = "";
1716
+ for (const char of normalized) {
1717
+ const value = alphabet.indexOf(char);
1718
+ if (value === -1) {
1719
+ throw new Error("Invalid base32 character in MFA secret.");
1720
+ }
1721
+ bits += value.toString(2).padStart(5, "0");
1686
1722
  }
1687
- static dehydrateAttributes(attributes) {
1688
- const casts = modelStatics(this).$casts ?? {};
1689
- return applyCasts(attributes, casts, "dehydrate");
1723
+ const bytes = [];
1724
+ for (let index = 0;index + 8 <= bits.length; index += 8) {
1725
+ bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
1690
1726
  }
1691
- static fromRecord(record, repository, exists = true) {
1692
- const statics = modelStatics(this);
1693
- const hydrated = statics.hydrateAttributes(record);
1694
- return new statics(hydrated, repository, exists);
1727
+ return Buffer.from(bytes);
1728
+ }
1729
+ function generateTotp(secret, counter, digits = 6) {
1730
+ const key = decodeBase32(secret);
1731
+ const buffer = Buffer.alloc(8);
1732
+ buffer.writeBigUInt64BE(BigInt(counter));
1733
+ const digest = createHmac2("sha1", key).update(buffer).digest();
1734
+ const lastByte = digest[digest.length - 1] ?? 0;
1735
+ const offset = lastByte & 15;
1736
+ const b0 = digest[offset] ?? 0;
1737
+ const b1 = digest[offset + 1] ?? 0;
1738
+ const b2 = digest[offset + 2] ?? 0;
1739
+ const b3 = digest[offset + 3] ?? 0;
1740
+ const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
1741
+ return String(code % 10 ** digits).padStart(digits, "0");
1742
+ }
1743
+ function verifyTotp(secret, token, window = 1) {
1744
+ const normalized = token.trim();
1745
+ if (!/^\d{6}$/u.test(normalized)) {
1746
+ return false;
1695
1747
  }
1696
- static boot() {}
1697
- static addGlobalScope(_name, scope) {
1698
- ensureBooted(this);
1699
- const existing = modelGlobalScopes.get(this) ?? [];
1700
- modelGlobalScopes.set(this, [
1701
- ...existing,
1702
- scope
1703
- ]);
1748
+ const timestep = Math.floor(Date.now() / 30000);
1749
+ for (let offset = -window;offset <= window; offset += 1) {
1750
+ if (generateTotp(secret, timestep + offset) === normalized) {
1751
+ return true;
1752
+ }
1704
1753
  }
1705
- static repository() {
1706
- return resolveModelRepository(this);
1754
+ return false;
1755
+ }
1756
+
1757
+ // ../../src/core/tenant/tenantContext.ts
1758
+ var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
1759
+ function runWithTenant(tenant, callback) {
1760
+ return tenantContext.run(tenant, callback);
1761
+ }
1762
+ function currentTenant() {
1763
+ return tenantContext.getStore() ?? null;
1764
+ }
1765
+ function currentTenantId() {
1766
+ return currentTenant()?.id ?? 1;
1767
+ }
1768
+ function rateLimitMultiplierForPlan(plan) {
1769
+ switch (plan) {
1770
+ case "enterprise":
1771
+ return 4;
1772
+ case "pro":
1773
+ return 2;
1774
+ default:
1775
+ return 1;
1707
1776
  }
1708
- static query() {
1709
- ensureBooted(this);
1710
- const repository = resolveModelRepository(this);
1711
- let query = repository.query();
1712
- for (const scope of getGlobalScopes(this)) {
1713
- query = scope(query);
1714
- }
1715
- return query;
1777
+ }
1778
+
1779
+ // ../../src/modules/user/authService.ts
1780
+ class AuthService {
1781
+ users;
1782
+ tokens;
1783
+ oauthIdentities;
1784
+ oauthProviders = new Map;
1785
+ constructor(users, tokens, oauthIdentities) {
1786
+ this.users = users;
1787
+ this.tokens = tokens;
1788
+ this.oauthIdentities = oauthIdentities;
1716
1789
  }
1717
- static async create(attributes) {
1718
- const statics = modelStatics(this);
1719
- ensureBooted(this);
1720
- const repository = resolveModelRepository(this);
1721
- const table = repository.getTable();
1722
- const timestamps = statics.$timestamps ?? true;
1723
- const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
1724
- const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
1725
- const payload = statics.dehydrateAttributes(withTimestamps);
1726
- const record = await repository.create(payload);
1727
- return statics.fromRecord(record, repository, true);
1790
+ registerOAuthProvider(provider) {
1791
+ this.oauthProviders.set(provider.name, provider);
1728
1792
  }
1729
- static async find(id) {
1730
- const statics = modelStatics(this);
1731
- const repository = resolveModelRepository(this);
1732
- const primaryKey = repository.getTable().primaryKey;
1733
- const record = await Model.query.call(this).where({ [primaryKey]: id }).first();
1734
- return record ? statics.fromRecord(record, repository, true) : null;
1793
+ getOAuthProvider(name) {
1794
+ return this.oauthProviders.get(name);
1735
1795
  }
1736
- static async findOrFail(id, errorFactory) {
1737
- const model = await Model.find.call(this, id);
1738
- if (model) {
1739
- return model;
1796
+ async loginWithPassword(email, password, options = {}) {
1797
+ const user = await this.users.findByEmail(email);
1798
+ if (!user?.password_hash) {
1799
+ logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
1800
+ throw new UnauthorizedError("Invalid credentials.");
1740
1801
  }
1741
- throw errorFactory?.(id) ?? new NotFoundError(`${this.name} ${String(id)} not found.`);
1742
- }
1743
- static async all(options = {}) {
1744
- const statics = modelStatics(this);
1745
- const repository = resolveModelRepository(this);
1746
- let query = Model.query.call(this);
1747
- if (options.orderBy) {
1748
- query = query.orderBy(options.orderBy);
1802
+ const valid = await verifyPassword(password, user.password_hash);
1803
+ if (!valid) {
1804
+ logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
1805
+ throw new UnauthorizedError("Invalid credentials.");
1749
1806
  }
1750
- if (options.limit !== undefined) {
1751
- query = query.limit(options.limit);
1807
+ if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
1808
+ logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
1809
+ throw new UnauthorizedError("Email address is not verified.");
1752
1810
  }
1753
- const rows = await query.get();
1754
- return rows.map((row) => statics.fromRecord(row, repository, true));
1755
- }
1756
- static async firstWhere(where, options = {}) {
1757
- const statics = modelStatics(this);
1758
- const repository = resolveModelRepository(this);
1759
- let query = Model.query.call(this).where(where);
1760
- if (options.orderBy) {
1761
- query = query.orderBy(options.orderBy);
1811
+ if (isFeatureEnabled("mfa") && user.mfa_enabled) {
1812
+ const mfaSecret = revealMfaSecret(user.mfa_secret);
1813
+ if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
1814
+ logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
1815
+ throw new UnauthorizedError("Invalid MFA code.");
1816
+ }
1762
1817
  }
1763
- const record = await query.first();
1764
- return record ? statics.fromRecord(record, repository, true) : null;
1818
+ logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
1819
+ return await this.tokens.createToken(user.id, {
1820
+ name: "password-login",
1821
+ abilities: resolveAbilitiesForRole(user.role),
1822
+ expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
1823
+ });
1765
1824
  }
1766
- async save() {
1767
- const ModelClass = modelStatics(this.constructor);
1768
- const timestamps = ModelClass.$timestamps ?? true;
1769
- const casts = ModelClass.$casts ?? {};
1770
- const table = this.repository.getTable();
1771
- if (this.$exists) {
1772
- const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
1773
- const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
1774
- this.attributes = ModelClass.hydrateAttributes(record2);
1775
- return this;
1825
+ async loginWithOAuth(providerName, code) {
1826
+ const provider = this.oauthProviders.get(providerName);
1827
+ if (!provider) {
1828
+ throw new UnauthorizedError("Unsupported OAuth provider.");
1776
1829
  }
1777
- const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
1778
- const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
1779
- const payload = ModelClass.dehydrateAttributes(withTimestamps);
1780
- const record = await this.repository.create(payload);
1781
- this.attributes = ModelClass.hydrateAttributes(record);
1782
- this._exists = true;
1783
- return this;
1784
- }
1785
- async update(changes) {
1786
- const ModelClass = modelStatics(this.constructor);
1787
- const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, changes);
1788
- Object.assign(this.attributes, assignable);
1789
- return await this.save();
1830
+ const profile = await provider.exchangeCode(code);
1831
+ const user = await this.findOrCreateOAuthUser(providerName, profile);
1832
+ logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
1833
+ return await this.tokens.createToken(user.id, {
1834
+ name: `${providerName}-oauth`,
1835
+ abilities: resolveAbilitiesForRole(user.role),
1836
+ expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
1837
+ });
1790
1838
  }
1791
- async delete() {
1792
- if (resolveSoftDeleteColumn(this.repository.getTable())) {
1793
- return await this.repository.deleteById(this.id);
1839
+ buildOAuthAuthorizationUrl(providerName, state) {
1840
+ const provider = this.oauthProviders.get(providerName);
1841
+ if (!provider) {
1842
+ throw new UnauthorizedError("Unsupported OAuth provider.");
1794
1843
  }
1795
- return await this.repository.forceDeleteById(this.id);
1796
- }
1797
- async forceDelete() {
1798
- return await this.repository.forceDeleteById(this.id);
1844
+ return provider.getAuthorizationUrl(state);
1799
1845
  }
1800
- async restore() {
1801
- const ModelClass = modelStatics(this.constructor);
1802
- const record = await this.repository.restoreById(this.id);
1803
- if (!record) {
1804
- return null;
1846
+ async findOrCreateOAuthUser(providerName, profile) {
1847
+ const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
1848
+ if (existingIdentity) {
1849
+ return await this.users.findByIdOrThrow(existingIdentity.user_id);
1805
1850
  }
1806
- this.attributes = ModelClass.hydrateAttributes(record);
1807
- return this;
1808
- }
1809
- async loadHasMany(as, relation, childRepository, options = {}) {
1810
- const grouped = await childRepository.withConnection(this.repository.getConnection()).loadHasManyForParents([this.attributes], relation, options);
1811
- const loaded = grouped.get(this.attributes[relation.localKey]) ?? [];
1812
- return Object.assign(this, { [as]: loaded });
1813
- }
1814
- async loadHasOne(as, relation, childRepository, options = {}) {
1815
- const loaded = await this.loadHasMany(as, relation, childRepository, { ...options, limit: 1 });
1816
- const value = loaded[as]?.[0];
1817
- return Object.assign(this, { [as]: value });
1818
- }
1819
- async loadBelongsTo(as, relation, parentRepository, options = {}) {
1820
- const grouped = await this.repository.loadBelongsToForParents([this.attributes], relation, parentRepository, options);
1821
- const loaded = grouped.get(this.attributes[relation.foreignKey]);
1822
- return Object.assign(this, { [as]: loaded });
1823
- }
1824
- async loadBelongsToMany(as, relation, relatedRepository, options = {}) {
1825
- const connection = this.repository.getConnection();
1826
- const parentId = this.attributes[relation.parentKey];
1827
- const pivotRows = await connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = $1`, [parentId]);
1828
- if (pivotRows.length === 0) {
1829
- return Object.assign(this, { [as]: [] });
1830
- }
1831
- const relatedIds = [
1832
- ...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
1833
- ];
1834
- const relatedRows = await relatedRepository.withConnection(connection).findAll({
1835
- ...options,
1836
- where: {
1837
- [relation.relatedKey]: relatedIds
1838
- }
1851
+ const existingUser = await this.users.findByEmail(profile.email);
1852
+ const user = existingUser ?? await this.users.create({
1853
+ name: profile.name,
1854
+ email: profile.email,
1855
+ role: "member",
1856
+ tenant_id: currentTenantId(),
1857
+ email_verified_at: new Date,
1858
+ created_at: new Date,
1859
+ updated_at: new Date
1839
1860
  });
1840
- const grouped = indexBelongsToManyRelation([this.attributes], pivotRows, relatedRows, relation);
1841
- const loaded = grouped.get(parentId) ?? [];
1842
- return Object.assign(this, { [as]: loaded });
1843
- }
1844
- mergeAttributes(patch) {
1845
- Object.assign(this.attributes, patch);
1846
- return this;
1861
+ await this.oauthIdentities.create({
1862
+ user_id: user.id,
1863
+ provider: providerName,
1864
+ provider_user_id: profile.providerUserId,
1865
+ email: profile.email,
1866
+ created_at: new Date
1867
+ });
1868
+ return user;
1847
1869
  }
1848
1870
  }
1849
- function registerModelRepository(model, repository) {
1850
- modelRepositories.set(model, repository);
1851
- ensureBooted(model);
1852
- return model;
1853
- }
1854
- // ../../src/core/database/schema/columnDefinition.ts
1855
- class ColumnDefinition {
1856
- name;
1857
- kind;
1858
- length;
1859
- isNullable = false;
1860
- isPrimary = false;
1861
- isUnique = false;
1862
- autoIncrement = false;
1863
- defaultValue;
1864
- checkExpression;
1865
- foreignKey;
1866
- constructor(name, kind) {
1867
- this.name = name;
1868
- this.kind = kind;
1871
+
1872
+ // ../../src/modules/user/notificationTable.ts
1873
+ var notificationTable = defineTable({
1874
+ name: "notification",
1875
+ primaryKey: "id",
1876
+ columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
1877
+ defaultOrderBy: { column: "created_at", direction: "DESC" }
1878
+ });
1879
+
1880
+ // ../../src/modules/user/oauthIdentityRepository.ts
1881
+ var oauthIdentityTable = defineTable({
1882
+ name: "oauth_identity",
1883
+ primaryKey: "id",
1884
+ columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
1885
+ });
1886
+
1887
+ // ../../src/modules/user/table.ts
1888
+ var userTable = defineTable({
1889
+ name: "users",
1890
+ primaryKey: "id",
1891
+ columns: [
1892
+ "id",
1893
+ "name",
1894
+ "email",
1895
+ "email_lookup",
1896
+ "role",
1897
+ "tenant_id",
1898
+ "password_hash",
1899
+ "email_verified_at",
1900
+ "mfa_secret",
1901
+ "mfa_enabled",
1902
+ "created_at",
1903
+ "updated_at"
1904
+ ],
1905
+ defaultOrderBy: { column: "id", direction: "ASC" }
1906
+ });
1907
+
1908
+ // ../../src/modules/user/provider.ts
1909
+ var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
1910
+
1911
+ // ../../src/core/auth/guard.ts
1912
+ function devHeaderAbilities(role) {
1913
+ if (role === "admin") {
1914
+ return [...ADMIN_ABILITIES];
1869
1915
  }
1870
- nullable() {
1871
- this.isNullable = true;
1872
- return this;
1916
+ return [...MEMBER_ABILITIES];
1917
+ }
1918
+
1919
+ class GuestGuard {
1920
+ resolve(request) {
1921
+ const userId = request.headers.get("x-authenticated-user-id");
1922
+ if (!userId) {
1923
+ return null;
1924
+ }
1925
+ const role = request.headers.get("x-authenticated-user-role");
1926
+ return {
1927
+ id: userId,
1928
+ abilities: devHeaderAbilities(role),
1929
+ ...role ? { role } : {}
1930
+ };
1873
1931
  }
1874
- notNullable() {
1875
- this.isNullable = false;
1876
- return this;
1932
+ }
1933
+
1934
+ class ApiTokenGuard {
1935
+ options;
1936
+ constructor(options) {
1937
+ this.options = options;
1877
1938
  }
1878
- default(value) {
1879
- if (typeof value === "boolean") {
1880
- this.defaultValue = value ? "TRUE" : "FALSE";
1881
- return this;
1939
+ resolve(request) {
1940
+ const authorization = request.headers.get("authorization");
1941
+ if (!authorization?.startsWith("Bearer ")) {
1942
+ return null;
1882
1943
  }
1883
- if (typeof value === "number") {
1884
- this.defaultValue = String(value);
1885
- return this;
1944
+ const token = authorization.slice("Bearer ".length).trim();
1945
+ if (token !== this.options.token) {
1946
+ return null;
1886
1947
  }
1887
- this.defaultValue = `'${value.replace(/'/g, "''")}'`;
1888
- return this;
1948
+ return this.options.user;
1889
1949
  }
1890
- defaultRaw(expression) {
1891
- this.defaultValue = expression;
1892
- return this;
1950
+ }
1951
+
1952
+ class DatabaseTokenGuard {
1953
+ container;
1954
+ constructor(container) {
1955
+ this.container = container;
1893
1956
  }
1894
- unique() {
1895
- this.isUnique = true;
1896
- return this;
1957
+ async resolve(request) {
1958
+ const authorization = request.headers.get("authorization");
1959
+ if (!authorization?.startsWith("Bearer ")) {
1960
+ return null;
1961
+ }
1962
+ const token = authorization.slice("Bearer ".length).trim();
1963
+ if (!token) {
1964
+ return null;
1965
+ }
1966
+ if (!this.container.has(tokenServiceToken)) {
1967
+ return null;
1968
+ }
1969
+ const tokenService = this.container.resolve(tokenServiceToken);
1970
+ return await tokenService.resolveUserFromToken(token);
1897
1971
  }
1898
- primary() {
1899
- this.isPrimary = true;
1900
- return this;
1972
+ }
1973
+
1974
+ class CompositeGuard {
1975
+ guards;
1976
+ constructor(guards) {
1977
+ this.guards = guards;
1901
1978
  }
1902
- check(expression) {
1903
- this.checkExpression = expression;
1904
- return this;
1979
+ async resolve(request) {
1980
+ for (const guard of this.guards) {
1981
+ const user = await Promise.resolve(guard.resolve(request));
1982
+ if (user) {
1983
+ return user;
1984
+ }
1985
+ }
1986
+ return null;
1905
1987
  }
1906
1988
  }
1907
1989
 
1908
- class ForeignIdColumnDefinition extends ColumnDefinition {
1909
- constructor(name) {
1910
- super(name, "foreignId");
1911
- this.notNullable();
1990
+ class AuthManager {
1991
+ guard;
1992
+ constructor(guard) {
1993
+ this.guard = guard;
1912
1994
  }
1913
- references(table, column = "id") {
1914
- this.foreignKey = {
1915
- referencesTable: table,
1916
- referencesColumn: column
1917
- };
1918
- return this;
1995
+ async resolve(request) {
1996
+ if (request) {
1997
+ return await Promise.resolve(this.guard.resolve(request));
1998
+ }
1999
+ return currentAuthUser();
1919
2000
  }
1920
- constrained(table) {
1921
- const referencesTable = table ?? inferReferencedTable(this.name);
1922
- return this.references(referencesTable);
2001
+ user(request) {
2002
+ return this.resolve(request);
1923
2003
  }
1924
- cascadeOnDelete() {
1925
- if (!this.foreignKey) {
1926
- throw new Error(`Foreign key is not defined for column ${this.name}`);
1927
- }
1928
- this.foreignKey.onDelete = "cascade";
1929
- return this;
2004
+ async check(request) {
2005
+ return await this.user(request) !== null;
1930
2006
  }
1931
- nullOnDelete() {
1932
- if (!this.foreignKey) {
1933
- throw new Error(`Foreign key is not defined for column ${this.name}`);
2007
+ async requireUser(request) {
2008
+ const user = await this.user(request);
2009
+ if (!user) {
2010
+ throw new UnauthorizedError;
1934
2011
  }
1935
- this.foreignKey.onDelete = "set null";
1936
- return this;
1937
- }
1938
- }
1939
- function inferReferencedTable(columnName) {
1940
- if (!columnName.endsWith("_id")) {
1941
- throw new Error(`Cannot infer referenced table from column ${columnName}`);
2012
+ return user;
1942
2013
  }
1943
- return columnName.slice(0, -3);
1944
2014
  }
1945
-
1946
- // ../../src/core/database/schema/blueprint.ts
1947
- class Blueprint {
1948
- table;
1949
- action;
1950
- columns = [];
1951
- indexes = [];
1952
- droppedColumns = [];
1953
- droppedIndexes = [];
1954
- constructor(table, action) {
1955
- this.table = table;
1956
- this.action = action;
2015
+ // ../../src/modules/organization/memberRepository.ts
2016
+ class OrganizationMemberRepository {
2017
+ constructor() {}
2018
+ async findMembership(userId, organizationId) {
2019
+ const rows = await connection_default`
2020
+ SELECT id, organization_id, user_id, role, created_at
2021
+ FROM organization_member
2022
+ WHERE user_id = ${userId} AND organization_id = ${organizationId}
2023
+ LIMIT 1
2024
+ `;
2025
+ return rows[0] ?? null;
1957
2026
  }
1958
- id(name = "id") {
1959
- const column = new ColumnDefinition(name, "id");
1960
- column.primary();
1961
- column.autoIncrement = true;
1962
- this.columns.push(column);
1963
- return column;
2027
+ async listForUser(userId) {
2028
+ return await connection_default`
2029
+ SELECT id, organization_id, user_id, role, created_at
2030
+ FROM organization_member
2031
+ WHERE user_id = ${userId}
2032
+ ORDER BY organization_id
2033
+ `;
1964
2034
  }
1965
- string(name, length) {
1966
- const column = new ColumnDefinition(name, "string");
1967
- column.length = length;
1968
- column.notNullable();
1969
- this.columns.push(column);
1970
- return column;
2035
+ async listForOrganization(organizationId) {
2036
+ return await connection_default`
2037
+ SELECT id, organization_id, user_id, role, created_at
2038
+ FROM organization_member
2039
+ WHERE organization_id = ${organizationId}
2040
+ ORDER BY id
2041
+ `;
1971
2042
  }
1972
- text(name) {
1973
- const column = new ColumnDefinition(name, "text");
1974
- column.notNullable();
1975
- this.columns.push(column);
1976
- return column;
2043
+ async addMember(input) {
2044
+ const rows = await connection_default`
2045
+ INSERT INTO organization_member (organization_id, user_id, role)
2046
+ VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
2047
+ RETURNING id, organization_id, user_id, role, created_at
2048
+ `;
2049
+ const row = rows[0];
2050
+ if (!row) {
2051
+ throw new Error("Organization member insert did not return a row.");
2052
+ }
2053
+ return row;
1977
2054
  }
1978
- boolean(name) {
1979
- const column = new ColumnDefinition(name, "boolean");
1980
- column.notNullable();
1981
- this.columns.push(column);
1982
- return column;
2055
+ async removeMember(organizationId, userId) {
2056
+ const rows = await connection_default`
2057
+ DELETE FROM organization_member
2058
+ WHERE organization_id = ${organizationId} AND user_id = ${userId}
2059
+ RETURNING id
2060
+ `;
2061
+ return rows.length > 0;
1983
2062
  }
1984
- integer(name) {
1985
- const column = new ColumnDefinition(name, "integer");
1986
- column.notNullable();
1987
- this.columns.push(column);
1988
- return column;
2063
+ }
2064
+ var memberRepository_default = OrganizationMemberRepository;
2065
+
2066
+ // ../../src/core/auth/membershipContext.ts
2067
+ var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
2068
+ var membershipRepository = new memberRepository_default;
2069
+ async function runWithMembershipContext(callback) {
2070
+ const user = currentAuthUser();
2071
+ if (!user || isGlobalAdmin(user)) {
2072
+ return await callback();
1989
2073
  }
1990
- bigInteger(name) {
1991
- const column = new ColumnDefinition(name, "bigInteger");
1992
- column.notNullable();
1993
- this.columns.push(column);
1994
- return column;
2074
+ const memberships = await membershipRepository.listForUser(resolveUserId(user));
2075
+ const context = {
2076
+ organizationIds: memberships.map((membership) => membership.organization_id),
2077
+ rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
2078
+ };
2079
+ return await membershipContext.run(context, callback);
2080
+ }
2081
+ function currentOrgRole(organizationId) {
2082
+ return membershipContext.getStore()?.rolesByOrganizationId.get(organizationId) ?? null;
2083
+ }
2084
+ function hasOrgMembership(organizationId) {
2085
+ return currentOrgRole(organizationId) !== null;
2086
+ }
2087
+ function currentOrganizationIds() {
2088
+ return membershipContext.getStore()?.organizationIds ?? [];
2089
+ }
2090
+ function hasMinimumOrgRole2(organizationId, minimum) {
2091
+ const role = currentOrgRole(organizationId);
2092
+ if (!role) {
2093
+ return false;
1995
2094
  }
1996
- timestamp(name) {
1997
- const column = new ColumnDefinition(name, "timestamp");
1998
- column.notNullable();
1999
- this.columns.push(column);
2000
- return column;
2095
+ const ranks = {
2096
+ member: 1,
2097
+ admin: 2,
2098
+ owner: 3
2099
+ };
2100
+ return ranks[role] >= ranks[minimum];
2101
+ }
2102
+ // ../../src/core/auth/membershipContextMiddleware.ts
2103
+ function createMembershipContextMiddleware() {
2104
+ return async (_request, next) => {
2105
+ return await runWithMembershipContext(async () => await next());
2106
+ };
2107
+ }
2108
+
2109
+ // ../../src/core/auth/membershipMiddleware.ts
2110
+ function createMembershipMiddleware() {
2111
+ return createMembershipContextMiddleware();
2112
+ }
2113
+ // ../../src/core/auth/membershipScope.ts
2114
+ function resolveOrganizationScope() {
2115
+ const user = currentAuthUser();
2116
+ if (!user) {
2117
+ return null;
2001
2118
  }
2002
- json(name) {
2003
- const column = new ColumnDefinition(name, "json");
2004
- column.notNullable();
2005
- this.columns.push(column);
2006
- return column;
2119
+ if (isGlobalAdmin(user)) {
2120
+ return null;
2007
2121
  }
2008
- jsonb(name) {
2009
- const column = new ColumnDefinition(name, "jsonb");
2010
- column.notNullable();
2011
- this.columns.push(column);
2012
- return column;
2122
+ return currentOrganizationIds();
2123
+ }
2124
+ function scopedOrganizationIds(requestedOrganizationId) {
2125
+ const scope = resolveOrganizationScope();
2126
+ if (scope === null) {
2127
+ return requestedOrganizationId === undefined ? null : [requestedOrganizationId];
2013
2128
  }
2014
- foreignId(name) {
2015
- const column = new ForeignIdColumnDefinition(name);
2016
- this.columns.push(column);
2017
- return column;
2129
+ if (requestedOrganizationId !== undefined) {
2130
+ return scope.includes(requestedOrganizationId) ? [requestedOrganizationId] : [];
2018
2131
  }
2019
- timestamps() {
2020
- this.timestamp("created_at").defaultRaw("NOW()");
2021
- this.timestamp("updated_at").defaultRaw("NOW()");
2132
+ return scope;
2133
+ }
2134
+ function appendOrganizationScope(where, requestedOrganizationId) {
2135
+ const organizationIds = scopedOrganizationIds(requestedOrganizationId);
2136
+ if (organizationIds === null) {
2137
+ return where;
2022
2138
  }
2023
- softDeletes() {
2024
- this.timestamp("deleted_at").nullable();
2139
+ if (organizationIds.length === 0) {
2140
+ return {
2141
+ ...where,
2142
+ organization_id: [-1]
2143
+ };
2025
2144
  }
2026
- dropColumn(name) {
2027
- this.droppedColumns.push(name);
2145
+ return {
2146
+ ...where,
2147
+ organization_id: organizationIds.length === 1 ? organizationIds[0] : organizationIds
2148
+ };
2149
+ }
2150
+ function appendProjectScope(where, accessibleProjectIds, requestedProjectId) {
2151
+ if (accessibleProjectIds === null) {
2152
+ if (requestedProjectId === undefined) {
2153
+ return where;
2154
+ }
2155
+ return {
2156
+ ...where,
2157
+ project_id: requestedProjectId
2158
+ };
2028
2159
  }
2029
- dropSoftDeletes() {
2030
- this.dropColumn("deleted_at");
2031
- this.dropIndex(`idx_${this.table}_deleted_at`);
2160
+ if (accessibleProjectIds.length === 0) {
2161
+ return {
2162
+ ...where,
2163
+ project_id: [-1]
2164
+ };
2032
2165
  }
2033
- dropIndex(name) {
2034
- this.droppedIndexes.push(name);
2166
+ if (requestedProjectId !== undefined) {
2167
+ return {
2168
+ ...where,
2169
+ project_id: accessibleProjectIds.includes(requestedProjectId) ? requestedProjectId : -1
2170
+ };
2035
2171
  }
2036
- unique(columns, name) {
2037
- this.indexes.push({
2038
- name,
2039
- columns: Array.isArray(columns) ? columns : [columns],
2040
- kind: "unique"
2041
- });
2172
+ return {
2173
+ ...where,
2174
+ project_id: accessibleProjectIds
2175
+ };
2176
+ }
2177
+ function emptyPaginateResult(page, perPage) {
2178
+ return {
2179
+ data: [],
2180
+ meta: {
2181
+ page,
2182
+ per_page: perPage,
2183
+ total: 0,
2184
+ last_page: 1
2185
+ }
2186
+ };
2187
+ }
2188
+ function assertResourceInCurrentTenant(resourceTenantId, resourceLabel, resourceId) {
2189
+ if (resourceTenantId !== currentTenantId()) {
2190
+ throw new NotFoundError(`${resourceLabel} ${resourceId} not found.`);
2042
2191
  }
2043
- index(columns, options = {}) {
2044
- this.indexes.push({
2045
- name: options.name,
2046
- columns: Array.isArray(columns) ? columns : [columns],
2047
- kind: "index",
2048
- order: options.order
2049
- });
2192
+ }
2193
+ function assertOrganizationReadable(organizationId) {
2194
+ const user = currentAuthUser();
2195
+ if (!user || isGlobalAdmin(user)) {
2196
+ return;
2050
2197
  }
2051
- partialIndex(columns, where, nameOrOptions) {
2052
- const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
2053
- this.indexes.push({
2054
- name: options.name,
2055
- columns: Array.isArray(columns) ? columns : [columns],
2056
- kind: options.unique ? "uniquePartial" : "partial",
2057
- where
2058
- });
2059
- }
2060
- fullText(columns, name) {
2061
- this.indexes.push({
2062
- name,
2063
- columns: Array.isArray(columns) ? columns : [columns],
2064
- kind: "fullText"
2065
- });
2198
+ const organizationIds = scopedOrganizationIds();
2199
+ if (organizationIds !== null && !organizationIds.includes(organizationId)) {
2200
+ throw new NotFoundError(`Organization ${organizationId} not found.`);
2066
2201
  }
2067
- ginIndex(column, name) {
2068
- this.indexes.push({
2069
- name,
2070
- columns: [column],
2071
- kind: "gin"
2072
- });
2202
+ }
2203
+ // ../../src/core/contracts/di.ts
2204
+ function getRequiredDependency(dependencies, key) {
2205
+ const dependency = dependencies[key];
2206
+ if (dependency === undefined) {
2207
+ throw new Error(`Required dependency "${key}" is not registered.`);
2073
2208
  }
2209
+ return dependency;
2074
2210
  }
2075
- // ../../src/core/database/schema/driver.ts
2076
- function normalizeConnectionName(connection) {
2077
- const normalized = connection.trim().toLowerCase();
2078
- if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
2079
- return "pgsql";
2211
+ function resolveService(dependencies, token) {
2212
+ return dependencies.container.resolve(token);
2213
+ }
2214
+
2215
+ // ../../src/core/logging/logger.ts
2216
+ class Logger {
2217
+ channel;
2218
+ constructor(channel = "app") {
2219
+ this.channel = channel;
2080
2220
  }
2081
- if (normalized === "mysql" || normalized === "mariadb") {
2082
- return "mysql";
2221
+ write(level, message, context = {}) {
2222
+ const entry = {
2223
+ level,
2224
+ channel: this.channel,
2225
+ message,
2226
+ timestamp: new Date().toISOString(),
2227
+ ...context
2228
+ };
2229
+ const line = JSON.stringify(entry);
2230
+ if (level === "error") {
2231
+ console.error(line);
2232
+ return;
2233
+ }
2234
+ console.log(line);
2083
2235
  }
2084
- if (normalized === "sqlite") {
2085
- return "sqlite";
2236
+ debug(message, context) {
2237
+ this.write("debug", message, context);
2086
2238
  }
2087
- throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
2088
- }
2089
- function resolveDriverFromUrl(url) {
2090
- const normalized = url.trim().toLowerCase();
2091
- if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
2092
- return "pgsql";
2239
+ info(message, context) {
2240
+ this.write("info", message, context);
2093
2241
  }
2094
- if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
2095
- return "mysql";
2242
+ warn(message, context) {
2243
+ this.write("warn", message, context);
2096
2244
  }
2097
- if (normalized.startsWith("sqlite:")) {
2098
- return "sqlite";
2245
+ error(message, context) {
2246
+ this.write("error", message, context);
2099
2247
  }
2100
- return null;
2101
2248
  }
2102
- function resolveDatabaseDriver(options = {}) {
2103
- const connection = options.connection ?? process.env.DB_CONNECTION;
2104
- if (connection) {
2105
- return normalizeConnectionName(connection);
2249
+ var appLogger = new Logger("app");
2250
+
2251
+ // ../../src/core/runtime/applicationRegistry.ts
2252
+ var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
2253
+ var activeContext;
2254
+ function readStoredApplicationContext() {
2255
+ if (activeContext) {
2256
+ return activeContext;
2106
2257
  }
2107
- const url = options.url ?? process.env.DATABASE_URL ?? "";
2108
- const fromUrl = resolveDriverFromUrl(url);
2109
- if (fromUrl) {
2110
- return fromUrl;
2258
+ const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
2259
+ if (globalContext) {
2260
+ activeContext = globalContext;
2111
2261
  }
2112
- return "pgsql";
2262
+ return activeContext;
2113
2263
  }
2114
- // ../../src/core/database/schema/errors.ts
2115
- class UnsupportedSchemaFeatureError extends Error {
2116
- constructor(feature, driver) {
2117
- super(`${feature} is not supported for the ${driver} driver`);
2118
- this.name = "UnsupportedSchemaFeatureError";
2119
- }
2264
+ function setActiveApplicationContext(context) {
2265
+ activeContext = context;
2266
+ globalThis[APPLICATION_CONTEXT_KEY] = context;
2120
2267
  }
2121
- // ../../src/core/database/schema/grammars/grammar.ts
2122
- function compileColumnType(driver, column) {
2123
- switch (column.kind) {
2124
- case "id":
2125
- return compileIdType(driver);
2126
- case "string":
2127
- return compileStringType(driver, column.length);
2128
- case "text":
2129
- return compileTextType(driver);
2130
- case "boolean":
2131
- return compileBooleanType(driver);
2132
- case "integer":
2133
- case "foreignId":
2134
- return compileIntegerType(driver);
2135
- case "bigInteger":
2136
- return compileBigIntegerType(driver);
2137
- case "timestamp":
2138
- return compileTimestampType(driver);
2139
- case "json":
2140
- return compileJsonType(driver);
2141
- case "jsonb":
2142
- return compileJsonbType(driver);
2143
- default:
2144
- throw new Error(`Unsupported column kind: ${column.kind}`);
2268
+ function requireActiveApplicationContext() {
2269
+ const context = readStoredApplicationContext();
2270
+ if (!context) {
2271
+ throw new Error("The application context has not been bootstrapped.");
2145
2272
  }
2273
+ return context;
2146
2274
  }
2147
- function compileIdType(driver) {
2148
- switch (driver) {
2149
- case "pgsql":
2150
- return "SERIAL";
2151
- case "mysql":
2152
- return "BIGINT UNSIGNED";
2153
- case "sqlite":
2154
- return "INTEGER";
2155
- }
2275
+ function resolveApplicationCache() {
2276
+ return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
2156
2277
  }
2157
- function compileStringType(driver, length) {
2158
- switch (driver) {
2159
- case "pgsql":
2160
- return "TEXT";
2161
- case "mysql":
2162
- return length ? `VARCHAR(${length})` : "VARCHAR(255)";
2163
- case "sqlite":
2164
- return "TEXT";
2165
- }
2278
+ function resolveApplicationQueue() {
2279
+ return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
2166
2280
  }
2167
- function compileTextType(driver) {
2168
- switch (driver) {
2169
- case "pgsql":
2170
- case "sqlite":
2171
- return "TEXT";
2172
- case "mysql":
2173
- return "TEXT";
2174
- }
2281
+ function resolveApplicationEventBus() {
2282
+ return requireActiveApplicationContext().container.resolve(CORE_EVENT_BUS_TOKEN);
2175
2283
  }
2176
- function compileBooleanType(driver) {
2177
- switch (driver) {
2178
- case "pgsql":
2179
- return "BOOLEAN";
2180
- case "mysql":
2181
- return "BOOLEAN";
2182
- case "sqlite":
2183
- return "INTEGER";
2184
- }
2284
+ function resolveApplicationAuth() {
2285
+ return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
2185
2286
  }
2186
- function compileIntegerType(driver) {
2187
- switch (driver) {
2188
- case "pgsql":
2189
- return "INTEGER";
2190
- case "mysql":
2191
- return "INT";
2192
- case "sqlite":
2193
- return "INTEGER";
2194
- }
2287
+ function resolveApplicationPolicyGate() {
2288
+ return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
2195
2289
  }
2196
- function compileBigIntegerType(driver) {
2197
- switch (driver) {
2198
- case "pgsql":
2199
- return "BIGINT";
2200
- case "mysql":
2201
- return "BIGINT";
2202
- case "sqlite":
2203
- return "INTEGER";
2204
- }
2290
+ function resolveApplicationConfig() {
2291
+ return requireActiveApplicationContext().config;
2205
2292
  }
2206
- function compileTimestampType(driver) {
2207
- switch (driver) {
2208
- case "pgsql":
2209
- return "TIMESTAMPTZ";
2210
- case "mysql":
2211
- return "TIMESTAMP";
2212
- case "sqlite":
2213
- return "TEXT";
2214
- }
2293
+ function resolveApplicationLogger() {
2294
+ return appLogger;
2215
2295
  }
2216
- function compileJsonType(driver) {
2217
- switch (driver) {
2218
- case "pgsql":
2219
- return "JSONB";
2220
- case "mysql":
2221
- return "JSON";
2222
- case "sqlite":
2223
- return "TEXT";
2224
- }
2296
+ function resolveApplicationDependencies() {
2297
+ return requireActiveApplicationContext().dependencies;
2225
2298
  }
2226
- function compileJsonbType(driver) {
2227
- switch (driver) {
2228
- case "pgsql":
2229
- return "JSONB";
2230
- case "mysql":
2231
- return "JSON";
2232
- case "sqlite":
2233
- return "TEXT";
2299
+
2300
+ // ../../src/core/auth/resolveMembershipService.ts
2301
+ function resolveMembershipService() {
2302
+ const dependencies = resolveApplicationDependencies();
2303
+ if (dependencies.container.has("core.membership")) {
2304
+ return dependencies.container.resolve("core.membership");
2234
2305
  }
2306
+ return new membershipService_default;
2235
2307
  }
2236
2308
 
2237
- // ../../src/core/database/schema/grammars/compileStatements.ts
2238
- function compileCreateTable(driver, blueprint) {
2239
- const table = quoteIdentifier(blueprint.table);
2240
- const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
2241
- for (const index of blueprint.indexes) {
2242
- if (index.kind === "unique" && index.columns.length > 1) {
2243
- const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
2244
- parts.push(`UNIQUE (${columns})`);
2245
- }
2309
+ // ../../src/core/auth/membershipService.ts
2310
+ class MembershipService {
2311
+ members;
2312
+ constructor(members = membershipRepository) {
2313
+ this.members = members;
2246
2314
  }
2247
- const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
2248
- ${parts.join(`,
2249
- `)}
2250
- )`];
2251
- for (const index of blueprint.indexes) {
2252
- if (index.kind === "unique" && index.columns.length === 1) {
2253
- continue;
2315
+ async listOrganizationIdsForUser(userId) {
2316
+ const memberships = await this.members.listForUser(userId);
2317
+ return memberships.map((membership) => membership.organization_id);
2318
+ }
2319
+ async getOrgRole(userId, organizationId) {
2320
+ const membership = await this.members.findMembership(userId, organizationId);
2321
+ return membership?.role ?? null;
2322
+ }
2323
+ async requireOrgAccess(organizationId, minimumRole = "member", user = currentAuthUser()) {
2324
+ if (!user) {
2325
+ throw new ForbiddenError("Authentication required.");
2254
2326
  }
2255
- if (index.kind === "index") {
2256
- statements.push(compileIndex(driver, blueprint.table, index));
2257
- } else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
2258
- statements.push(...compileSpecialIndex(driver, blueprint.table, index));
2327
+ if (isGlobalAdmin(user)) {
2328
+ return "owner";
2259
2329
  }
2330
+ const role = await this.getOrgRole(resolveUserId(user), organizationId);
2331
+ if (!role || !hasMinimumOrgRole(role, minimumRole)) {
2332
+ throw new ForbiddenError("Organization membership required.");
2333
+ }
2334
+ return role;
2260
2335
  }
2261
- return statements;
2262
- }
2263
- function compileAlterTable(driver, blueprint) {
2264
- const statements = [];
2265
- const table = quoteIdentifier(blueprint.table);
2266
- for (const column of blueprint.columns) {
2267
- const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
2268
- statements.push(`ALTER TABLE ${table}
2269
- ${addPrefix} ${compileColumn(driver, column, "alter")}`);
2270
- }
2271
- for (const columnName of blueprint.droppedColumns) {
2272
- const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
2273
- statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
2336
+ async filterAccessibleOrganizationIds(organizationIds, user = currentAuthUser()) {
2337
+ if (!user) {
2338
+ return [];
2339
+ }
2340
+ if (isGlobalAdmin(user)) {
2341
+ return organizationIds;
2342
+ }
2343
+ const allowed = new Set(await this.listOrganizationIdsForUser(resolveUserId(user)));
2344
+ return organizationIds.filter((organizationId) => allowed.has(organizationId));
2274
2345
  }
2275
- for (const indexName of blueprint.droppedIndexes) {
2276
- statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
2346
+ async addOwnerOnOrganizationCreate(organizationId, userId) {
2347
+ await this.members.addMember({
2348
+ organizationId,
2349
+ userId,
2350
+ role: "owner"
2351
+ });
2277
2352
  }
2278
- for (const index of blueprint.indexes) {
2279
- if (index.kind === "index" || index.kind === "unique") {
2280
- statements.push(compileIndex(driver, blueprint.table, index));
2281
- } else {
2282
- statements.push(...compileSpecialIndex(driver, blueprint.table, index));
2283
- }
2353
+ listMembersForOrganization(organizationId) {
2354
+ return this.members.listForOrganization(organizationId);
2284
2355
  }
2285
- return statements;
2286
- }
2287
- function compileDropTable(driver, tableName) {
2288
- const cascade = driver === "pgsql" ? " CASCADE" : "";
2289
- return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
2290
- }
2291
- function compileColumn(driver, column, mode) {
2292
- const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
2293
- if (column.autoIncrement && driver === "mysql") {
2294
- parts[1] = `${parts[1]} AUTO_INCREMENT`;
2356
+ addMember(input) {
2357
+ return this.members.addMember(input);
2295
2358
  }
2296
- if (column.isPrimary && mode === "create") {
2297
- if (driver === "sqlite") {
2298
- parts.push("PRIMARY KEY AUTOINCREMENT");
2299
- } else {
2300
- parts.push("PRIMARY KEY");
2301
- }
2302
- } else if (!column.isNullable) {
2303
- parts.push("NOT NULL");
2304
- } else if (column.isNullable) {
2305
- parts.push("NULL");
2359
+ removeMember(organizationId, userId) {
2360
+ return this.members.removeMember(organizationId, userId);
2306
2361
  }
2307
- if (column.defaultValue !== undefined) {
2308
- parts.push(`DEFAULT ${column.defaultValue}`);
2362
+ }
2363
+ var membershipService_default = MembershipService;
2364
+ // ../../src/core/auth/policy.ts
2365
+ class Policy {
2366
+ constructor() {}
2367
+ view(_user, _resource) {
2368
+ return false;
2309
2369
  }
2310
- if (column.isUnique) {
2311
- parts.push("UNIQUE");
2370
+ create(_user) {
2371
+ return false;
2312
2372
  }
2313
- if (column.checkExpression) {
2314
- parts.push(`CHECK (${column.checkExpression})`);
2373
+ update(_user, _resource) {
2374
+ return false;
2315
2375
  }
2316
- if (column.foreignKey) {
2317
- const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
2318
- const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
2319
- let clause = `REFERENCES ${reference}`;
2320
- if (onDelete === "cascade") {
2321
- clause += " ON DELETE CASCADE";
2322
- } else if (onDelete === "set null") {
2323
- clause += " ON DELETE SET NULL";
2324
- }
2325
- parts.push(clause);
2376
+ delete(_user, _resource) {
2377
+ return false;
2326
2378
  }
2327
- return parts.join(" ");
2328
2379
  }
2329
- function compileIndex(_driver, tableName, index) {
2330
- const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
2331
- const columns = index.columns.map((column) => {
2332
- const quoted = quoteIdentifier(column);
2333
- if (index.order === "desc") {
2334
- return `${quoted} DESC`;
2380
+ var BLOCKED_POLICY_ACTIONS = new Set([
2381
+ "constructor",
2382
+ "toString",
2383
+ "valueOf",
2384
+ "hasOwnProperty",
2385
+ "isPrototypeOf",
2386
+ "propertyIsEnumerable",
2387
+ "__proto__"
2388
+ ]);
2389
+
2390
+ class PolicyGate {
2391
+ constructor() {}
2392
+ policies = new Map;
2393
+ register(resource, policy) {
2394
+ this.policies.set(resource, policy);
2395
+ }
2396
+ allows(resource, action, user, model) {
2397
+ const policy = this.policies.get(resource);
2398
+ if (!policy) {
2399
+ return false;
2335
2400
  }
2336
- return quoted;
2337
- }).join(", ");
2338
- const unique = index.kind === "unique" ? "UNIQUE " : "";
2339
- return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
2340
- }
2341
- function compileSpecialIndex(driver, tableName, index) {
2342
- const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
2343
- const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
2344
- switch (index.kind) {
2345
- case "partial":
2346
- case "uniquePartial": {
2347
- if (driver !== "pgsql") {
2348
- throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
2349
- }
2350
- const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
2351
- return [
2352
- `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
2353
- ];
2401
+ if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
2402
+ return false;
2354
2403
  }
2355
- case "gin": {
2356
- if (driver !== "pgsql") {
2357
- throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
2358
- }
2359
- return [
2360
- `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
2361
- ];
2404
+ const handler = policy[action];
2405
+ if (typeof handler !== "function") {
2406
+ return false;
2362
2407
  }
2363
- case "fullText": {
2364
- if (driver === "mysql") {
2365
- return [
2366
- `CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
2367
- ];
2368
- }
2369
- if (driver === "pgsql") {
2370
- throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
2371
- }
2372
- throw new UnsupportedSchemaFeatureError("fullText()", driver);
2408
+ const resolvedUser = user === undefined ? currentAuthUser() : user;
2409
+ return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
2410
+ }
2411
+ authorize(resource, action, user, model) {
2412
+ if (!this.allows(resource, action, user, model)) {
2413
+ throw new ForbiddenError;
2373
2414
  }
2374
- default:
2375
- return [];
2376
2415
  }
2377
2416
  }
2378
- function defaultIndexName(tableName, columns, kind) {
2379
- return `idx_${tableName}_${columns.join("_")}_${kind}`;
2417
+ // ../../src/core/database/bindConnection.ts
2418
+ function bindDatabaseConnection2(connection) {
2419
+ bindDatabaseConnection(connection);
2380
2420
  }
2381
- function compileBlueprint(driver, blueprint) {
2382
- switch (blueprint.action) {
2383
- case "create":
2384
- return compileCreateTable(driver, blueprint);
2385
- case "alter":
2386
- return compileAlterTable(driver, blueprint);
2387
- case "drop":
2388
- return compileDropTable(driver, blueprint.table);
2389
- default:
2390
- throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
2421
+
2422
+ // ../../src/domain/scim.ts
2423
+ var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
2424
+
2425
+ // ../../src/core/security/timingSafeCompare.ts
2426
+ import { timingSafeEqual } from "crypto";
2427
+ function timingSafeCompareString(left, right) {
2428
+ const leftBuffer = Buffer.from(left);
2429
+ const rightBuffer = Buffer.from(right);
2430
+ if (leftBuffer.length !== rightBuffer.length) {
2431
+ return false;
2391
2432
  }
2433
+ return timingSafeEqual(leftBuffer, rightBuffer);
2392
2434
  }
2393
- // ../../src/core/database/schema/grammars/createGrammar.ts
2394
- function createGrammar(driver) {
2395
- return {
2396
- driver,
2397
- compile(blueprint) {
2398
- return compileBlueprint(driver, blueprint);
2435
+
2436
+ // ../../src/core/security/scimTenantTokens.ts
2437
+ function parseScimTenantTokens(raw) {
2438
+ const tokens = new Map;
2439
+ if (!raw?.trim()) {
2440
+ return tokens;
2441
+ }
2442
+ for (const entry of raw.split(",")) {
2443
+ const [tenantPart, tokenPart] = entry.split(":");
2444
+ if (!tenantPart || !tokenPart) {
2445
+ continue;
2399
2446
  }
2400
- };
2447
+ const tenantId = Number.parseInt(tenantPart.trim(), 10);
2448
+ const token = tokenPart.trim();
2449
+ if (Number.isInteger(tenantId) && tenantId > 0 && token.length > 0) {
2450
+ tokens.set(tenantId, token);
2451
+ }
2452
+ }
2453
+ return tokens;
2454
+ }
2455
+ function resolveScimTenantFromToken(token) {
2456
+ const tenantTokens = parseScimTenantTokens(process.env.SCIM_TENANT_TOKENS);
2457
+ for (const [tenantId, expectedToken] of tenantTokens) {
2458
+ if (timingSafeCompareString(token, expectedToken)) {
2459
+ return tenantId;
2460
+ }
2461
+ }
2462
+ const fallbackToken = process.env.SCIM_BEARER_TOKEN ?? TEST_SCIM_BEARER_TOKEN;
2463
+ if (timingSafeCompareString(token, fallbackToken)) {
2464
+ return 1;
2465
+ }
2466
+ return null;
2401
2467
  }
2402
2468
 
2403
- // ../../src/core/database/schema/grammars/mysqlGrammar.ts
2404
- var MySqlGrammar = createGrammar("mysql");
2405
-
2406
- // ../../src/core/database/schema/grammars/postgresGrammar.ts
2407
- var PostgresGrammar = createGrammar("pgsql");
2408
-
2409
- // ../../src/core/database/schema/grammars/sqliteGrammar.ts
2410
- var SqliteGrammar = createGrammar("sqlite");
2411
-
2412
- // ../../src/core/database/schema/grammars/index.ts
2413
- function grammarForDriver(driver) {
2414
- switch (driver) {
2415
- case "pgsql":
2416
- return PostgresGrammar;
2417
- case "mysql":
2418
- return MySqlGrammar;
2419
- case "sqlite":
2420
- return SqliteGrammar;
2421
- default:
2422
- throw new Error(`Unsupported database driver: ${driver}`);
2423
- }
2424
- }
2425
- // ../../src/core/database/schema/schema.ts
2426
- class SchemaBuilder {
2427
- #driver;
2428
- #statements = [];
2429
- constructor(driver) {
2430
- this.#driver = driver;
2431
- }
2432
- create(table, callback) {
2433
- const blueprint = new Blueprint(table, "create");
2434
- callback(blueprint);
2435
- this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
2436
- return this;
2437
- }
2438
- table(table, callback) {
2439
- const blueprint = new Blueprint(table, "alter");
2440
- callback(blueprint);
2441
- this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
2442
- return this;
2443
- }
2444
- drop(table) {
2445
- const blueprint = new Blueprint(table, "drop");
2446
- this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
2447
- return this;
2448
- }
2449
- toSql() {
2450
- return [...this.#statements];
2451
- }
2452
- async execute(db) {
2453
- for (const statement of this.#statements) {
2454
- await db.unsafe(statement);
2455
- }
2456
- }
2469
+ // ../../src/core/tenant/resolveTenant.ts
2470
+ async function resolveTenant(tenantId) {
2471
+ const rows = await repositoryConnection`
2472
+ SELECT id, slug, plan, region
2473
+ FROM tenant
2474
+ WHERE id = ${tenantId}
2475
+ LIMIT 1
2476
+ `;
2477
+ const row = rows[0];
2478
+ return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
2457
2479
  }
2458
2480
 
2459
- class Schema {
2460
- static builder(driver) {
2461
- return new SchemaBuilder(driver ?? resolveDatabaseDriver());
2462
- }
2463
- static async run(db, driver, callback) {
2464
- const schema = Schema.builder(driver);
2465
- await callback(schema);
2466
- await schema.execute(db);
2467
- }
2468
- }
2469
- function createSchemaBuilder(db, driver) {
2470
- const builder = Schema.builder(driver);
2471
- return Object.assign(builder, {
2472
- async commit() {
2473
- await builder.execute(db);
2474
- }
2475
- });
2476
- }
2477
- // ../../src/core/database/table.ts
2478
- function defineTable(definition) {
2479
- return definition;
2480
- }
2481
- // ../../src/core/database/transaction.ts
2482
- function supportsTransactions(connection) {
2483
- return typeof connection.begin === "function";
2481
+ // ../../src/core/tenant/tenantDatabaseScope.ts
2482
+ async function applyTenantContextToTransaction(transaction, tenantId) {
2483
+ await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
2484
+ await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
2484
2485
  }
2485
- async function runInTransaction(operation) {
2486
- const pool = resolveRepositoryConnection();
2487
- if (!supportsTransactions(pool)) {
2488
- throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
2486
+ async function runWithTenantDatabase(tenant, callback) {
2487
+ if (hasActiveDatabaseConnection()) {
2488
+ const activeConnection2 = getActiveDatabaseConnection(getDefaultDatabasePool());
2489
+ await applyTenantContextToTransaction(activeConnection2, tenant.id);
2490
+ return await runWithTenant(tenant, callback);
2489
2491
  }
2490
- return await pool.begin(async (transaction) => {
2491
- return await operation(createDatabaseConnection(transaction));
2492
+ return await getDefaultDatabasePool().begin(async (transaction) => {
2493
+ await applyTenantContextToTransaction(transaction, tenant.id);
2494
+ return await runWithDatabaseConnection(transaction, async () => {
2495
+ return await runWithTenant(tenant, callback);
2496
+ });
2492
2497
  });
2493
2498
  }
2494
- // ../../src/config/database.ts
2495
- function readInteger(name, fallback) {
2496
- const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
2497
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
2499
+ function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
2500
+ return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
2498
2501
  }
2499
- var databaseConfig = {
2500
- url: process.env.DATABASE_URL ?? "",
2501
- poolMax: readInteger("DB_POOL_MAX", 10),
2502
- idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
2503
- maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
2504
- connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
2505
- };
2506
2502
 
2507
- // ../../src/db/connection/createConnection.ts
2508
- var {SQL } = globalThis.Bun;
2509
- function createDatabaseConnection2(config) {
2510
- if (!config.url) {
2511
- throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
2512
- }
2513
- return new SQL({
2514
- url: config.url,
2515
- max: config.poolMax,
2516
- idleTimeout: config.idleTimeoutSeconds,
2517
- maxLifetime: config.maxLifetimeSeconds,
2518
- connectionTimeout: config.connectionTimeoutSeconds
2503
+ // ../../src/core/auth/scimAuthMiddleware.ts
2504
+ function createScimAuthMiddleware() {
2505
+ return async (request, next) => {
2506
+ const authorization = request.headers.get("authorization");
2507
+ if (!authorization?.startsWith("Bearer ")) {
2508
+ return jsonScimError("SCIM bearer token required.", 401);
2509
+ }
2510
+ const token = authorization.slice("Bearer ".length).trim();
2511
+ const tenantId = resolveScimTenantFromToken(token);
2512
+ if (tenantId === null) {
2513
+ return jsonScimError("Invalid SCIM bearer token.", 401);
2514
+ }
2515
+ const tenant = await resolveTenant(tenantId);
2516
+ if (!tenant) {
2517
+ return jsonScimError("SCIM tenant not found.", 401);
2518
+ }
2519
+ return await runWithTenantDatabase(tenant, async () => {
2520
+ bindDatabaseConnection2(getActiveDatabaseConnection(getDefaultDatabasePool()));
2521
+ try {
2522
+ return await next();
2523
+ } finally {
2524
+ resetBoundDatabaseConnection();
2525
+ }
2526
+ });
2527
+ };
2528
+ }
2529
+ function jsonScimError(detail, status) {
2530
+ return Response.json({
2531
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
2532
+ detail,
2533
+ status: String(status)
2534
+ }, {
2535
+ status,
2536
+ headers: { "content-type": "application/scim+json" }
2519
2537
  });
2520
2538
  }
2539
+ // ../../src/core/cache/redisCacheStore.ts
2540
+ var {RedisClient } = globalThis.Bun;
2541
+ var KEY_PREFIX = "workhub:cache:";
2542
+ var TAG_PREFIX = "workhub:cache:tag:";
2521
2543
 
2522
- // ../../src/db/connection/index.ts
2523
- var connectionHolder = {
2524
- connection: null
2525
- };
2526
- function getDatabase() {
2527
- if (!connectionHolder.connection) {
2528
- connectionHolder.connection = createDatabaseConnection2(databaseConfig);
2529
- registerDefaultDatabasePool(connectionHolder.connection);
2544
+ class RedisCacheStore {
2545
+ ttlMs;
2546
+ maxEntries;
2547
+ client;
2548
+ inflight = new Map;
2549
+ keyTags = new Map;
2550
+ constructor(redisUrl, ttlMs, maxEntries) {
2551
+ this.ttlMs = ttlMs;
2552
+ this.maxEntries = maxEntries;
2553
+ this.client = new RedisClient(redisUrl);
2530
2554
  }
2531
- return connectionHolder.connection;
2532
- }
2533
- function getDb() {
2534
- getDatabase();
2535
- return getDefaultDatabaseQuery();
2536
- }
2537
- var db = new Proxy(function database() {}, {
2538
- apply(_target, _thisArg, args) {
2539
- return getDb()(...args);
2540
- },
2541
- get(_target, property) {
2542
- const connection = getDb();
2543
- const value = connection[property];
2544
- return typeof value === "function" ? value.bind(connection) : value;
2555
+ async get(key) {
2556
+ const raw = await this.client.get(this.storageKey(key));
2557
+ if (raw === null) {
2558
+ return;
2559
+ }
2560
+ return JSON.parse(raw);
2545
2561
  }
2546
- });
2547
- var connection_default = db;
2548
-
2549
- // ../../src/modules/user/apiTokenTable.ts
2550
- var apiTokenTable = defineTable({
2551
- name: "api_token",
2552
- primaryKey: "id",
2553
- columns: [
2554
- "id",
2555
- "user_id",
2556
- "name",
2557
- "token_hash",
2558
- "abilities",
2559
- "last_used_at",
2560
- "expires_at",
2561
- "created_at"
2562
- ],
2563
- defaultOrderBy: { column: "id", direction: "ASC" }
2564
- });
2565
-
2566
- // ../../src/core/auth/password.ts
2567
- async function verifyPassword(password, passwordHash) {
2568
- return await Bun.password.verify(password, passwordHash);
2569
- }
2570
-
2571
- // ../../src/core/crypto/fieldEncryption.ts
2572
- import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
2573
- var ENCRYPTION_PREFIX = "enc:v1:";
2574
- var IV_LENGTH = 12;
2575
- var TAG_LENGTH = 16;
2576
- function resolveEncryptionKey() {
2577
- const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
2578
- if (!raw) {
2579
- return null;
2562
+ async set(key, value, ttlMs) {
2563
+ const resolvedTtlMs = ttlMs ?? this.ttlMs;
2564
+ const payload = JSON.stringify(value);
2565
+ if (resolvedTtlMs > 0) {
2566
+ await this.client.psetex(this.storageKey(key), resolvedTtlMs, payload);
2567
+ } else {
2568
+ await this.client.set(this.storageKey(key), payload);
2569
+ }
2570
+ await this.enforceMaxEntries();
2580
2571
  }
2581
- if (/^[0-9a-f]{64}$/i.test(raw)) {
2582
- return Buffer.from(raw, "hex");
2572
+ async getOrSet(key, loader, ttlMs) {
2573
+ const cached = await this.get(key);
2574
+ if (cached !== undefined) {
2575
+ return cached;
2576
+ }
2577
+ const inflightRequest = this.inflight.get(key);
2578
+ if (inflightRequest) {
2579
+ return inflightRequest;
2580
+ }
2581
+ const pendingRequest = loader().then(async (value) => {
2582
+ await this.set(key, value, ttlMs);
2583
+ return value;
2584
+ }).finally(() => {
2585
+ this.inflight.delete(key);
2586
+ });
2587
+ this.inflight.set(key, pendingRequest);
2588
+ return pendingRequest;
2583
2589
  }
2584
- const decoded = Buffer.from(raw, "base64");
2585
- if (decoded.length === 32) {
2586
- return decoded;
2590
+ async attachTags(key, tags) {
2591
+ if (tags.length === 0) {
2592
+ return;
2593
+ }
2594
+ let tagsForKey = this.keyTags.get(key);
2595
+ if (!tagsForKey) {
2596
+ tagsForKey = new Set;
2597
+ this.keyTags.set(key, tagsForKey);
2598
+ }
2599
+ for (const tag of tags) {
2600
+ tagsForKey.add(tag);
2601
+ await this.client.sadd(this.tagKey(tag), key);
2602
+ }
2587
2603
  }
2588
- throw new Error("KMS_ENCRYPTION_KEY must be 32 bytes (hex or base64).");
2589
- }
2590
- function isFieldEncryptionEnabled() {
2591
- const featureFlag = process.env.FEATURE_FIELD_ENCRYPTION;
2592
- if (featureFlag === "false") {
2593
- return false;
2604
+ async flushTags(tags) {
2605
+ const keysToRemove = new Set;
2606
+ for (const tag of tags) {
2607
+ const members = await this.client.smembers(this.tagKey(tag));
2608
+ for (const member of members) {
2609
+ keysToRemove.add(member);
2610
+ }
2611
+ }
2612
+ let removed = 0;
2613
+ for (const key of keysToRemove) {
2614
+ if (await this.invalidate(key)) {
2615
+ removed += 1;
2616
+ }
2617
+ }
2618
+ for (const tag of tags) {
2619
+ await this.client.del(this.tagKey(tag));
2620
+ }
2621
+ return removed;
2622
+ }
2623
+ async invalidate(key) {
2624
+ const deleted = await this.client.del(this.storageKey(key));
2625
+ await this.detachKeyFromTags(key);
2626
+ return deleted > 0;
2627
+ }
2628
+ async invalidateByPrefix(prefix) {
2629
+ const keys = await this.client.keys(`${KEY_PREFIX}*`);
2630
+ let removed = 0;
2631
+ for (const storageKey of keys) {
2632
+ const key = storageKey.slice(KEY_PREFIX.length);
2633
+ if (key === prefix || key.startsWith(`${prefix}?`)) {
2634
+ if (await this.invalidate(key)) {
2635
+ removed += 1;
2636
+ }
2637
+ }
2638
+ }
2639
+ return removed;
2594
2640
  }
2595
- if (featureFlag === "true") {
2596
- return true;
2641
+ async clear() {
2642
+ const keys = await this.client.keys(`${KEY_PREFIX}*`);
2643
+ if (keys.length > 0) {
2644
+ await this.client.del(...keys);
2645
+ }
2646
+ const tagKeys = await this.client.keys(`${TAG_PREFIX}*`);
2647
+ if (tagKeys.length > 0) {
2648
+ await this.client.del(...tagKeys);
2649
+ }
2650
+ this.inflight.clear();
2651
+ this.keyTags.clear();
2597
2652
  }
2598
- return (process.env.APP_ENV ?? "local") === "production";
2599
- }
2600
- function decryptField(value, key) {
2601
- if (!value.startsWith(ENCRYPTION_PREFIX)) {
2602
- return value;
2653
+ async size() {
2654
+ const keys = await this.client.keys(`${KEY_PREFIX}*`);
2655
+ return keys.length;
2603
2656
  }
2604
- const payload = Buffer.from(value.slice(ENCRYPTION_PREFIX.length), "base64");
2605
- const iv = payload.subarray(0, IV_LENGTH);
2606
- const tag = payload.subarray(payload.length - TAG_LENGTH);
2607
- const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
2608
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
2609
- decipher.setAuthTag(tag);
2610
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
2611
- }
2612
-
2613
- // ../../src/core/crypto/mfaSecret.ts
2614
- function revealMfaSecret(stored) {
2615
- if (!stored) {
2616
- return null;
2657
+ storageKey(key) {
2658
+ return `${KEY_PREFIX}${key}`;
2617
2659
  }
2618
- const key = resolveEncryptionKey();
2619
- if (!isFieldEncryptionEnabled() || !key || !stored.startsWith("enc:v1:")) {
2620
- return stored;
2660
+ tagKey(tag) {
2661
+ return `${TAG_PREFIX}${tag}`;
2621
2662
  }
2622
- return decryptField(stored, key);
2623
- }
2624
-
2625
- // ../../src/core/http/requestMetaContext.ts
2626
- var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
2627
- function runWithRequestMeta(meta, callback) {
2628
- return requestMetaContext.run(meta, callback);
2629
- }
2630
- function currentRequestMeta() {
2631
- return requestMetaContext.getStore() ?? {
2632
- ipAddress: null,
2633
- userAgent: null
2634
- };
2635
- }
2636
-
2637
- // ../../src/core/security/securityEvents.ts
2638
- function logSecurityEvent(event, details = {}) {
2639
- const meta = currentRequestMeta();
2640
- const user = currentAuthUser();
2641
- console.log(JSON.stringify({
2642
- level: "security",
2643
- event,
2644
- timestamp: new Date().toISOString(),
2645
- ip_address: meta.ipAddress ?? null,
2646
- user_agent: meta.userAgent ?? null,
2647
- user_id: user?.id ?? null,
2648
- ...details
2649
- }));
2650
- }
2651
-
2652
- // ../../src/core/security/tokenExpiry.ts
2653
- function resolveDefaultTokenExpiryDays() {
2654
- const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
2655
- if (!raw) {
2656
- return null;
2663
+ async detachKeyFromTags(key) {
2664
+ const tags = this.keyTags.get(key);
2665
+ if (!tags) {
2666
+ return;
2667
+ }
2668
+ for (const tag of tags) {
2669
+ await this.client.srem(this.tagKey(tag), key);
2670
+ }
2671
+ this.keyTags.delete(key);
2657
2672
  }
2658
- const parsed = Number.parseInt(raw, 10);
2659
- if (!Number.isInteger(parsed) || parsed <= 0) {
2660
- return null;
2673
+ async enforceMaxEntries() {
2674
+ const keys = await this.client.keys(`${KEY_PREFIX}*`);
2675
+ if (keys.length <= this.maxEntries) {
2676
+ return;
2677
+ }
2678
+ const overflow = keys.length - this.maxEntries;
2679
+ const keysToRemove = keys.slice(0, overflow);
2680
+ if (keysToRemove.length > 0) {
2681
+ await this.client.del(...keysToRemove);
2682
+ }
2661
2683
  }
2662
- return parsed;
2663
2684
  }
2685
+ var redisCacheStore_default = RedisCacheStore;
2664
2686
 
2665
- // ../../src/core/security/totp.ts
2666
- import { createHmac as createHmac2 } from "crypto";
2667
- function decodeBase32(input) {
2668
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
2669
- const normalized = input.replace(/=+$/u, "").toUpperCase();
2670
- let bits = "";
2671
- for (const char of normalized) {
2672
- const value = alphabet.indexOf(char);
2673
- if (value === -1) {
2674
- throw new Error("Invalid base32 character in MFA secret.");
2687
+ // ../../src/core/cache/simpleCache.ts
2688
+ class SimpleCache {
2689
+ ttlMs;
2690
+ maxEntries;
2691
+ cache = new Map;
2692
+ inflight = new Map;
2693
+ tagIndex = new Map;
2694
+ keyTags = new Map;
2695
+ constructor(ttlMs = 3600000, maxEntries = 100) {
2696
+ this.ttlMs = ttlMs;
2697
+ this.maxEntries = maxEntries;
2698
+ if (!Number.isFinite(ttlMs) || ttlMs < 0) {
2699
+ throw new RangeError("ttlMs must be a non-negative number.");
2700
+ }
2701
+ if (!Number.isInteger(maxEntries) || maxEntries < 1) {
2702
+ throw new RangeError("maxEntries must be a positive integer.");
2675
2703
  }
2676
- bits += value.toString(2).padStart(5, "0");
2677
2704
  }
2678
- const bytes = [];
2679
- for (let index = 0;index + 8 <= bits.length; index += 8) {
2680
- bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
2705
+ get(key) {
2706
+ return this.getFreshEntry(key)?.value;
2681
2707
  }
2682
- return Buffer.from(bytes);
2683
- }
2684
- function generateTotp(secret, counter, digits = 6) {
2685
- const key = decodeBase32(secret);
2686
- const buffer = Buffer.alloc(8);
2687
- buffer.writeBigUInt64BE(BigInt(counter));
2688
- const digest = createHmac2("sha1", key).update(buffer).digest();
2689
- const lastByte = digest[digest.length - 1] ?? 0;
2690
- const offset = lastByte & 15;
2691
- const b0 = digest[offset] ?? 0;
2692
- const b1 = digest[offset + 1] ?? 0;
2693
- const b2 = digest[offset + 2] ?? 0;
2694
- const b3 = digest[offset + 3] ?? 0;
2695
- const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
2696
- return String(code % 10 ** digits).padStart(digits, "0");
2697
- }
2698
- function verifyTotp(secret, token, window = 1) {
2699
- const normalized = token.trim();
2700
- if (!/^\d{6}$/u.test(normalized)) {
2701
- return false;
2708
+ set(key, value, ttlMs) {
2709
+ const now = Date.now();
2710
+ const resolvedTtlMs = ttlMs ?? this.ttlMs;
2711
+ this.cache.set(key, {
2712
+ value,
2713
+ expiresAt: now + resolvedTtlMs,
2714
+ lastAccessedAt: now
2715
+ });
2716
+ this.evictOverflow();
2702
2717
  }
2703
- const timestep = Math.floor(Date.now() / 30000);
2704
- for (let offset = -window;offset <= window; offset += 1) {
2705
- if (generateTotp(secret, timestep + offset) === normalized) {
2706
- return true;
2718
+ async getOrSet(key, loader, ttlMs) {
2719
+ this.pruneExpired();
2720
+ const cachedEntry = this.getFreshEntry(key);
2721
+ if (cachedEntry) {
2722
+ return cachedEntry.value;
2723
+ }
2724
+ const inflightRequest = this.inflight.get(key);
2725
+ if (inflightRequest) {
2726
+ return inflightRequest;
2727
+ }
2728
+ const pendingRequest = loader().then((value) => {
2729
+ this.set(key, value, ttlMs);
2730
+ return value;
2731
+ }).finally(() => {
2732
+ this.inflight.delete(key);
2733
+ });
2734
+ this.inflight.set(key, pendingRequest);
2735
+ return pendingRequest;
2736
+ }
2737
+ attachTags(key, tags) {
2738
+ if (tags.length === 0) {
2739
+ return;
2740
+ }
2741
+ let tagsForKey = this.keyTags.get(key);
2742
+ if (!tagsForKey) {
2743
+ tagsForKey = new Set;
2744
+ this.keyTags.set(key, tagsForKey);
2745
+ }
2746
+ for (const tag of tags) {
2747
+ tagsForKey.add(tag);
2748
+ let keysForTag = this.tagIndex.get(tag);
2749
+ if (!keysForTag) {
2750
+ keysForTag = new Set;
2751
+ this.tagIndex.set(tag, keysForTag);
2752
+ }
2753
+ keysForTag.add(key);
2707
2754
  }
2708
2755
  }
2709
- return false;
2710
- }
2711
-
2712
- // ../../src/core/tenant/tenantContext.ts
2713
- var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
2714
- function runWithTenant(tenant, callback) {
2715
- return tenantContext.run(tenant, callback);
2716
- }
2717
- function currentTenant() {
2718
- return tenantContext.getStore() ?? null;
2719
- }
2720
- function currentTenantId() {
2721
- return currentTenant()?.id ?? 1;
2722
- }
2723
- function rateLimitMultiplierForPlan(plan) {
2724
- switch (plan) {
2725
- case "enterprise":
2726
- return 4;
2727
- case "pro":
2728
- return 2;
2729
- default:
2730
- return 1;
2756
+ flushTags(tags) {
2757
+ const keysToRemove = new Set;
2758
+ for (const tag of tags) {
2759
+ const keys = this.tagIndex.get(tag);
2760
+ if (!keys) {
2761
+ continue;
2762
+ }
2763
+ for (const key of keys) {
2764
+ keysToRemove.add(key);
2765
+ }
2766
+ }
2767
+ let removed = 0;
2768
+ for (const key of keysToRemove) {
2769
+ if (this.invalidate(key)) {
2770
+ removed += 1;
2771
+ }
2772
+ }
2773
+ for (const tag of tags) {
2774
+ this.tagIndex.delete(tag);
2775
+ }
2776
+ return removed;
2731
2777
  }
2732
- }
2733
-
2734
- // ../../src/modules/user/authService.ts
2735
- class AuthService {
2736
- users;
2737
- tokens;
2738
- oauthIdentities;
2739
- oauthProviders = new Map;
2740
- constructor(users, tokens, oauthIdentities) {
2741
- this.users = users;
2742
- this.tokens = tokens;
2743
- this.oauthIdentities = oauthIdentities;
2778
+ invalidate(key) {
2779
+ const removed = this.cache.delete(key);
2780
+ if (removed) {
2781
+ this.detachKeyFromTags(key);
2782
+ }
2783
+ return removed;
2744
2784
  }
2745
- registerOAuthProvider(provider) {
2746
- this.oauthProviders.set(provider.name, provider);
2785
+ invalidateByPrefix(prefix) {
2786
+ let removed = 0;
2787
+ for (const key of [...this.cache.keys()]) {
2788
+ if (key === prefix || key.startsWith(`${prefix}?`)) {
2789
+ if (this.invalidate(key)) {
2790
+ removed += 1;
2791
+ }
2792
+ }
2793
+ }
2794
+ return removed;
2747
2795
  }
2748
- getOAuthProvider(name) {
2749
- return this.oauthProviders.get(name);
2796
+ clear() {
2797
+ this.cache.clear();
2798
+ this.inflight.clear();
2799
+ this.tagIndex.clear();
2800
+ this.keyTags.clear();
2750
2801
  }
2751
- async loginWithPassword(email, password, options = {}) {
2752
- const user = await this.users.findByEmail(email);
2753
- if (!user?.password_hash) {
2754
- logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
2755
- throw new UnauthorizedError("Invalid credentials.");
2756
- }
2757
- const valid = await verifyPassword(password, user.password_hash);
2758
- if (!valid) {
2759
- logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
2760
- throw new UnauthorizedError("Invalid credentials.");
2761
- }
2762
- if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
2763
- logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
2764
- throw new UnauthorizedError("Email address is not verified.");
2802
+ size() {
2803
+ this.pruneExpired();
2804
+ return this.cache.size;
2805
+ }
2806
+ detachKeyFromTags(key) {
2807
+ const tags = this.keyTags.get(key);
2808
+ if (!tags) {
2809
+ return;
2765
2810
  }
2766
- if (isFeatureEnabled("mfa") && user.mfa_enabled) {
2767
- const mfaSecret = revealMfaSecret(user.mfa_secret);
2768
- if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
2769
- logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
2770
- throw new UnauthorizedError("Invalid MFA code.");
2811
+ for (const tag of tags) {
2812
+ const keys = this.tagIndex.get(tag);
2813
+ if (!keys) {
2814
+ continue;
2815
+ }
2816
+ keys.delete(key);
2817
+ if (keys.size === 0) {
2818
+ this.tagIndex.delete(tag);
2771
2819
  }
2772
2820
  }
2773
- logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
2774
- return await this.tokens.createToken(user.id, {
2775
- name: "password-login",
2776
- abilities: resolveAbilitiesForRole(user.role),
2777
- expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
2778
- });
2821
+ this.keyTags.delete(key);
2779
2822
  }
2780
- async loginWithOAuth(providerName, code) {
2781
- const provider = this.oauthProviders.get(providerName);
2782
- if (!provider) {
2783
- throw new UnauthorizedError("Unsupported OAuth provider.");
2823
+ getFreshEntry(key) {
2824
+ const entry = this.cache.get(key);
2825
+ if (!entry) {
2826
+ return;
2784
2827
  }
2785
- const profile = await provider.exchangeCode(code);
2786
- const user = await this.findOrCreateOAuthUser(providerName, profile);
2787
- logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
2788
- return await this.tokens.createToken(user.id, {
2789
- name: `${providerName}-oauth`,
2790
- abilities: resolveAbilitiesForRole(user.role),
2791
- expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
2792
- });
2828
+ if (entry.expiresAt <= Date.now()) {
2829
+ this.invalidate(key);
2830
+ return;
2831
+ }
2832
+ entry.lastAccessedAt = Date.now();
2833
+ return entry;
2793
2834
  }
2794
- buildOAuthAuthorizationUrl(providerName, state) {
2795
- const provider = this.oauthProviders.get(providerName);
2796
- if (!provider) {
2797
- throw new UnauthorizedError("Unsupported OAuth provider.");
2835
+ pruneExpired() {
2836
+ const now = Date.now();
2837
+ for (const [key, entry] of this.cache.entries()) {
2838
+ if (entry.expiresAt <= now) {
2839
+ this.invalidate(key);
2840
+ }
2798
2841
  }
2799
- return provider.getAuthorizationUrl(state);
2800
2842
  }
2801
- async findOrCreateOAuthUser(providerName, profile) {
2802
- const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
2803
- if (existingIdentity) {
2804
- return await this.users.findByIdOrThrow(existingIdentity.user_id);
2843
+ evictOverflow() {
2844
+ while (this.cache.size > this.maxEntries) {
2845
+ let oldestKey;
2846
+ let oldestAccessTime = Number.POSITIVE_INFINITY;
2847
+ for (const [key, entry] of this.cache.entries()) {
2848
+ if (entry.lastAccessedAt < oldestAccessTime) {
2849
+ oldestAccessTime = entry.lastAccessedAt;
2850
+ oldestKey = key;
2851
+ }
2852
+ }
2853
+ if (!oldestKey) {
2854
+ return;
2855
+ }
2856
+ this.invalidate(oldestKey);
2805
2857
  }
2806
- const existingUser = await this.users.findByEmail(profile.email);
2807
- const user = existingUser ?? await this.users.create({
2808
- name: profile.name,
2809
- email: profile.email,
2810
- role: "member",
2811
- tenant_id: currentTenantId(),
2812
- email_verified_at: new Date,
2813
- created_at: new Date,
2814
- updated_at: new Date
2815
- });
2816
- await this.oauthIdentities.create({
2817
- user_id: user.id,
2818
- provider: providerName,
2819
- provider_user_id: profile.providerUserId,
2820
- email: profile.email,
2821
- created_at: new Date
2822
- });
2823
- return user;
2824
2858
  }
2825
2859
  }
2860
+ var simpleCache_default = SimpleCache;
2826
2861
 
2827
- // ../../src/modules/user/notificationTable.ts
2828
- var notificationTable = defineTable({
2829
- name: "notification",
2830
- primaryKey: "id",
2831
- columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
2832
- defaultOrderBy: { column: "created_at", direction: "DESC" }
2833
- });
2834
-
2835
- // ../../src/modules/user/oauthIdentityRepository.ts
2836
- var oauthIdentityTable = defineTable({
2837
- name: "oauth_identity",
2838
- primaryKey: "id",
2839
- columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
2840
- });
2841
-
2842
- // ../../src/modules/user/table.ts
2843
- var userTable = defineTable({
2844
- name: "users",
2845
- primaryKey: "id",
2846
- columns: [
2847
- "id",
2848
- "name",
2849
- "email",
2850
- "email_lookup",
2851
- "role",
2852
- "tenant_id",
2853
- "password_hash",
2854
- "email_verified_at",
2855
- "mfa_secret",
2856
- "mfa_enabled",
2857
- "created_at",
2858
- "updated_at"
2859
- ],
2860
- defaultOrderBy: { column: "id", direction: "ASC" }
2861
- });
2862
-
2863
- // ../../src/modules/user/provider.ts
2864
- var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
2865
-
2866
- // ../../src/core/auth/guard.ts
2867
- function devHeaderAbilities(role) {
2868
- if (role === "admin") {
2869
- return [...ADMIN_ABILITIES];
2862
+ // ../../src/core/cache/simpleCacheStore.ts
2863
+ class SimpleCacheStore {
2864
+ cache;
2865
+ constructor(cache) {
2866
+ this.cache = cache;
2867
+ }
2868
+ get(key) {
2869
+ return Promise.resolve(this.cache.get(key));
2870
+ }
2871
+ set(key, value, ttlMs) {
2872
+ this.cache.set(key, value, ttlMs);
2873
+ return Promise.resolve();
2874
+ }
2875
+ getOrSet(key, loader, ttlMs) {
2876
+ return this.cache.getOrSet(key, loader, ttlMs);
2877
+ }
2878
+ attachTags(key, tags) {
2879
+ this.cache.attachTags(key, tags);
2880
+ return Promise.resolve();
2881
+ }
2882
+ flushTags(tags) {
2883
+ return Promise.resolve(this.cache.flushTags(tags));
2884
+ }
2885
+ invalidate(key) {
2886
+ return Promise.resolve(this.cache.invalidate(key));
2887
+ }
2888
+ invalidateByPrefix(prefix) {
2889
+ return Promise.resolve(this.cache.invalidateByPrefix(prefix));
2890
+ }
2891
+ clear() {
2892
+ this.cache.clear();
2893
+ return Promise.resolve();
2894
+ }
2895
+ size() {
2896
+ return Promise.resolve(this.cache.size());
2870
2897
  }
2871
- return [...MEMBER_ABILITIES];
2872
2898
  }
2899
+ var simpleCacheStore_default = SimpleCacheStore;
2873
2900
 
2874
- class GuestGuard {
2875
- resolve(request) {
2876
- const userId = request.headers.get("x-authenticated-user-id");
2877
- if (!userId) {
2878
- return null;
2901
+ // ../../src/core/cache/createCacheStore.ts
2902
+ function createCacheStore(options) {
2903
+ if (options.driver === "redis") {
2904
+ if (!options.redisUrl) {
2905
+ throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
2879
2906
  }
2880
- const role = request.headers.get("x-authenticated-user-role");
2881
- return {
2882
- id: userId,
2883
- abilities: devHeaderAbilities(role),
2884
- ...role ? { role } : {}
2885
- };
2907
+ return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
2908
+ }
2909
+ return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
2910
+ }
2911
+ // ../../src/core/cache/taggedCache.ts
2912
+ class TaggedCache {
2913
+ store;
2914
+ tags;
2915
+ constructor(store, tags) {
2916
+ this.store = store;
2917
+ this.tags = tags;
2918
+ }
2919
+ async remember(key, callback, ttlMs) {
2920
+ const value = await this.store.getOrSet(key, callback, ttlMs);
2921
+ await this.store.attachTags(key, this.tags);
2922
+ return value;
2923
+ }
2924
+ async flush() {
2925
+ return this.store.flushTags(this.tags);
2886
2926
  }
2887
2927
  }
2928
+ var taggedCache_default = TaggedCache;
2888
2929
 
2889
- class ApiTokenGuard {
2890
- options;
2891
- constructor(options) {
2892
- this.options = options;
2930
+ // ../../src/core/cache/repository.ts
2931
+ class CacheRepository {
2932
+ store;
2933
+ constructor(store) {
2934
+ this.store = store;
2893
2935
  }
2894
- resolve(request) {
2895
- const authorization = request.headers.get("authorization");
2896
- if (!authorization?.startsWith("Bearer ")) {
2897
- return null;
2898
- }
2899
- const token = authorization.slice("Bearer ".length).trim();
2900
- if (token !== this.options.token) {
2901
- return null;
2902
- }
2903
- return this.options.user;
2936
+ async get(key) {
2937
+ return this.store.get(key);
2938
+ }
2939
+ async remember(key, callback, ttlMs) {
2940
+ return this.store.getOrSet(key, callback, ttlMs);
2941
+ }
2942
+ async forget(key) {
2943
+ return this.store.invalidate(key);
2944
+ }
2945
+ async flush() {
2946
+ await this.store.clear();
2947
+ }
2948
+ tags(...names) {
2949
+ return new taggedCache_default(this.store, names);
2950
+ }
2951
+ async getOrSet(key, loader, ttlMs) {
2952
+ return this.remember(key, loader, ttlMs);
2953
+ }
2954
+ async invalidate(key) {
2955
+ return this.forget(key);
2956
+ }
2957
+ async invalidateByPrefix(prefix) {
2958
+ return this.store.invalidateByPrefix(prefix);
2959
+ }
2960
+ async clear() {
2961
+ await this.flush();
2962
+ }
2963
+ async size() {
2964
+ return this.store.size();
2904
2965
  }
2905
2966
  }
2906
-
2907
- class DatabaseTokenGuard {
2908
- container;
2909
- constructor(container) {
2910
- this.container = container;
2967
+ var repository_default2 = CacheRepository;
2968
+ // ../../src/core/cache/tags.ts
2969
+ var CACHE_TAGS = {
2970
+ organizations: "organizations",
2971
+ projects: "projects",
2972
+ tasks: "tasks",
2973
+ comments: "comments",
2974
+ attachments: "attachments",
2975
+ reports: "reports"
2976
+ };
2977
+ // ../../src/core/contracts/container.ts
2978
+ class ServiceContainer {
2979
+ services = new Map;
2980
+ singletonFactories = new Map;
2981
+ bindings = new Map;
2982
+ set(key, value) {
2983
+ this.singletonFactories.delete(key);
2984
+ this.bindings.delete(key);
2985
+ this.services.set(key, value);
2986
+ return value;
2911
2987
  }
2912
- async resolve(request) {
2913
- const authorization = request.headers.get("authorization");
2914
- if (!authorization?.startsWith("Bearer ")) {
2915
- return null;
2988
+ singleton(key, factory) {
2989
+ this.bindings.delete(key);
2990
+ this.services.delete(key);
2991
+ this.singletonFactories.set(key, factory);
2992
+ }
2993
+ bind(key, factory) {
2994
+ this.singletonFactories.delete(key);
2995
+ this.services.delete(key);
2996
+ this.bindings.set(key, factory);
2997
+ }
2998
+ get(key) {
2999
+ if (this.services.has(key)) {
3000
+ return this.services.get(key);
2916
3001
  }
2917
- const token = authorization.slice("Bearer ".length).trim();
2918
- if (!token) {
2919
- return null;
3002
+ const singletonFactory = this.singletonFactories.get(key);
3003
+ if (singletonFactory) {
3004
+ const value = singletonFactory(this);
3005
+ this.services.set(key, value);
3006
+ return value;
2920
3007
  }
2921
- if (!this.container.has(tokenServiceToken)) {
2922
- return null;
3008
+ const binding = this.bindings.get(key);
3009
+ if (binding) {
3010
+ return binding(this);
2923
3011
  }
2924
- const tokenService = this.container.resolve(tokenServiceToken);
2925
- return await tokenService.resolveUserFromToken(token);
3012
+ throw new Error(`Service "${key}" is not registered.`);
2926
3013
  }
2927
- }
2928
-
2929
- class CompositeGuard {
2930
- guards;
2931
- constructor(guards) {
2932
- this.guards = guards;
3014
+ resolve(key) {
3015
+ return this.get(key);
2933
3016
  }
2934
- async resolve(request) {
2935
- for (const guard of this.guards) {
2936
- const user = await Promise.resolve(guard.resolve(request));
2937
- if (user) {
2938
- return user;
2939
- }
2940
- }
2941
- return null;
3017
+ has(key) {
3018
+ return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
2942
3019
  }
2943
3020
  }
2944
3021
 
2945
- class AuthManager {
2946
- guard;
2947
- constructor(guard) {
2948
- this.guard = guard;
3022
+ class ConfigStore {
3023
+ values = new Map;
3024
+ set(key, value) {
3025
+ this.values.set(key, value);
3026
+ return value;
2949
3027
  }
2950
- async resolve(request) {
2951
- if (request) {
2952
- return await Promise.resolve(this.guard.resolve(request));
2953
- }
2954
- return currentAuthUser();
3028
+ get(key) {
3029
+ return this.values.get(key);
2955
3030
  }
2956
- user(request) {
2957
- return this.resolve(request);
3031
+ require(key) {
3032
+ if (!this.values.has(key)) {
3033
+ throw new Error(`Config key "${key}" is not defined.`);
3034
+ }
3035
+ return this.values.get(key);
2958
3036
  }
2959
- async check(request) {
2960
- return await this.user(request) !== null;
3037
+ has(key) {
3038
+ return this.values.has(key);
2961
3039
  }
2962
- async requireUser(request) {
2963
- const user = await this.user(request);
2964
- if (!user) {
2965
- throw new UnauthorizedError;
3040
+ }
3041
+ // ../../src/core/database/connection.ts
3042
+ function createDatabaseConnection2(source) {
3043
+ return {
3044
+ async unsafe(query, params = []) {
3045
+ return await source.unsafe(query, params);
2966
3046
  }
2967
- return user;
2968
- }
3047
+ };
2969
3048
  }
2970
- // ../../src/modules/organization/memberRepository.ts
2971
- class OrganizationMemberRepository {
2972
- constructor() {}
2973
- async findMembership(userId, organizationId) {
2974
- const rows = await connection_default`
2975
- SELECT id, organization_id, user_id, role, created_at
2976
- FROM organization_member
2977
- WHERE user_id = ${userId} AND organization_id = ${organizationId}
2978
- LIMIT 1
2979
- `;
2980
- return rows[0] ?? null;
2981
- }
2982
- async listForUser(userId) {
2983
- return await connection_default`
2984
- SELECT id, organization_id, user_id, role, created_at
2985
- FROM organization_member
2986
- WHERE user_id = ${userId}
2987
- ORDER BY organization_id
2988
- `;
2989
- }
2990
- async listForOrganization(organizationId) {
2991
- return await connection_default`
2992
- SELECT id, organization_id, user_id, role, created_at
2993
- FROM organization_member
2994
- WHERE organization_id = ${organizationId}
2995
- ORDER BY id
2996
- `;
3049
+ // ../../src/core/database/migrations/advisoryLock.ts
3050
+ var MIGRATION_LOCK_KEY = 42424242;
3051
+ async function withMigrationLock(db2, callback, lockKey = MIGRATION_LOCK_KEY) {
3052
+ await db2.unsafe("SELECT pg_advisory_lock($1)", [lockKey]);
3053
+ try {
3054
+ return await callback();
3055
+ } finally {
3056
+ await db2.unsafe("SELECT pg_advisory_unlock($1)", [lockKey]);
2997
3057
  }
2998
- async addMember(input) {
2999
- const rows = await connection_default`
3000
- INSERT INTO organization_member (organization_id, user_id, role)
3001
- VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
3002
- RETURNING id, organization_id, user_id, role, created_at
3003
- `;
3004
- const row = rows[0];
3005
- if (!row) {
3006
- throw new Error("Organization member insert did not return a row.");
3058
+ }
3059
+ // ../../src/core/database/migrations/runner.ts
3060
+ import { readdir } from "fs/promises";
3061
+ import { join } from "path";
3062
+ import { pathToFileURL } from "url";
3063
+ var MIGRATIONS_TABLE = "framework_migrations";
3064
+ async function ensureMigrationsTable(db2) {
3065
+ await db2.unsafe(`
3066
+ CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
3067
+ name TEXT PRIMARY KEY,
3068
+ batch INTEGER NOT NULL,
3069
+ run_on TIMESTAMPTZ NOT NULL DEFAULT NOW()
3070
+ )
3071
+ `);
3072
+ }
3073
+ async function getAppliedMigrations(db2) {
3074
+ await ensureMigrationsTable(db2);
3075
+ return await db2.unsafe(`
3076
+ SELECT name, batch
3077
+ FROM ${MIGRATIONS_TABLE}
3078
+ ORDER BY batch ASC, name ASC
3079
+ `);
3080
+ }
3081
+ async function loadMigrationsFromDirectory(directory) {
3082
+ const entries = await readdir(directory);
3083
+ const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
3084
+ const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
3085
+ const moduleUrl = pathToFileURL(join(directory, fileName)).href;
3086
+ const module = await import(moduleUrl);
3087
+ return module.default;
3088
+ }));
3089
+ return loadedMigrations.filter((migration) => migration?.name !== undefined);
3090
+ }
3091
+ async function getMigrationStatus(db2, migrations) {
3092
+ const applied = await getAppliedMigrations(db2);
3093
+ const appliedByName = new Map(applied.map(({ name, batch }) => [name, Number(batch)]));
3094
+ return migrations.map(({ name }) => ({
3095
+ name,
3096
+ status: appliedByName.has(name) ? "up" : "pending",
3097
+ batch: appliedByName.get(name) ?? null
3098
+ }));
3099
+ }
3100
+ async function runPendingMigrations(db2, migrations, options = {}) {
3101
+ const applied = await getAppliedMigrations(db2);
3102
+ const appliedNames = new Set(applied.map(({ name }) => name));
3103
+ const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
3104
+ const pendingMigrations = migrations.filter(({ name }) => !appliedNames.has(name));
3105
+ for (const migration of pendingMigrations) {
3106
+ options.onMigration?.(migration.name);
3107
+ await migration.up(db2);
3108
+ const inserted = await db2.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING RETURNING name`, [migration.name, nextBatch]);
3109
+ if (inserted.length === 0) {
3110
+ throw new Error(`Migration ${migration.name} was applied but not recorded.`);
3007
3111
  }
3008
- return row;
3009
- }
3010
- async removeMember(organizationId, userId) {
3011
- const rows = await connection_default`
3012
- DELETE FROM organization_member
3013
- WHERE organization_id = ${organizationId} AND user_id = ${userId}
3014
- RETURNING id
3015
- `;
3016
- return rows.length > 0;
3017
3112
  }
3113
+ return pendingMigrations.length;
3018
3114
  }
3019
- var memberRepository_default = OrganizationMemberRepository;
3020
-
3021
- // ../../src/core/auth/membershipContext.ts
3022
- var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
3023
- var membershipRepository = new memberRepository_default;
3024
- async function runWithMembershipContext(callback) {
3025
- const user = currentAuthUser();
3026
- if (!user || isGlobalAdmin(user)) {
3027
- return await callback();
3115
+ async function migrateDatabase(db2, migrations, options = {}) {
3116
+ const { advisoryLock = false, onMigration } = options;
3117
+ if (advisoryLock) {
3118
+ return withMigrationLock(db2, () => runPendingMigrations(db2, migrations, { onMigration }));
3028
3119
  }
3029
- const memberships = await membershipRepository.listForUser(resolveUserId(user));
3030
- const context = {
3031
- organizationIds: memberships.map((membership) => membership.organization_id),
3032
- rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
3033
- };
3034
- return await membershipContext.run(context, callback);
3035
- }
3036
- function currentOrgRole(organizationId) {
3037
- return membershipContext.getStore()?.rolesByOrganizationId.get(organizationId) ?? null;
3038
- }
3039
- function hasOrgMembership(organizationId) {
3040
- return currentOrgRole(organizationId) !== null;
3041
- }
3042
- function currentOrganizationIds() {
3043
- return membershipContext.getStore()?.organizationIds ?? [];
3120
+ return runPendingMigrations(db2, migrations, { onMigration });
3044
3121
  }
3045
- function hasMinimumOrgRole2(organizationId, minimum) {
3046
- const role = currentOrgRole(organizationId);
3047
- if (!role) {
3048
- return false;
3122
+ async function rollbackDatabase(db2, migrations, options = {}) {
3123
+ const applied = await getAppliedMigrations(db2);
3124
+ if (applied.length === 0) {
3125
+ return 0;
3049
3126
  }
3050
- const ranks = {
3051
- member: 1,
3052
- admin: 2,
3053
- owner: 3
3054
- };
3055
- return ranks[role] >= ranks[minimum];
3127
+ const lastBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0);
3128
+ const migrationsToRollback = new Set(applied.filter(({ batch }) => Number(batch) === lastBatch).map(({ name }) => name));
3129
+ let rolledBack = 0;
3130
+ for (const migration of [...migrations].reverse()) {
3131
+ if (!migrationsToRollback.has(migration.name)) {
3132
+ continue;
3133
+ }
3134
+ options.onMigration?.(migration.name);
3135
+ await migration.down(db2);
3136
+ await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = $1`, [migration.name]);
3137
+ rolledBack += 1;
3138
+ }
3139
+ return rolledBack;
3056
3140
  }
3057
- // ../../src/core/auth/membershipContextMiddleware.ts
3058
- function createMembershipContextMiddleware() {
3059
- return async (_request, next) => {
3060
- return await runWithMembershipContext(async () => await next());
3141
+ async function freshDatabase(db2, migrations, options = {}) {
3142
+ const runFresh = async () => {
3143
+ const applied = await getAppliedMigrations(db2);
3144
+ const appliedNames = new Set(applied.map(({ name }) => name));
3145
+ const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
3146
+ for (const migration of [...appliedMigrations].reverse()) {
3147
+ options.onMigration?.(migration.name);
3148
+ await migration.down(db2);
3149
+ }
3150
+ if (appliedMigrations.length > 0) {
3151
+ await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
3152
+ }
3153
+ await runPendingMigrations(db2, migrations, options);
3061
3154
  };
3155
+ if (options.advisoryLock) {
3156
+ await withMigrationLock(db2, runFresh);
3157
+ return;
3158
+ }
3159
+ await runFresh();
3062
3160
  }
3063
-
3064
- // ../../src/core/auth/membershipMiddleware.ts
3065
- function createMembershipMiddleware() {
3066
- return createMembershipContextMiddleware();
3161
+ // ../../src/core/database/model.ts
3162
+ var modelRepositories = new WeakMap;
3163
+ var modelGlobalScopes = new WeakMap;
3164
+ var modelBooted = new WeakSet;
3165
+ function resolveModelRepository(model) {
3166
+ const repository = modelRepositories.get(model);
3167
+ if (!repository) {
3168
+ throw new Error(`${model.name}.repository() is not implemented.`);
3169
+ }
3170
+ return repository;
3067
3171
  }
3068
- // ../../src/core/auth/membershipScope.ts
3069
- function resolveOrganizationScope() {
3070
- const user = currentAuthUser();
3071
- if (!user) {
3072
- return null;
3172
+ function modelStatics(model) {
3173
+ return model;
3174
+ }
3175
+ function ensureBooted(model) {
3176
+ if (modelBooted.has(model)) {
3177
+ return;
3073
3178
  }
3074
- if (isGlobalAdmin(user)) {
3075
- return null;
3179
+ modelBooted.add(model);
3180
+ const boot = model.boot;
3181
+ if (typeof boot === "function") {
3182
+ boot.call(model);
3076
3183
  }
3077
- return currentOrganizationIds();
3078
3184
  }
3079
- function scopedOrganizationIds(requestedOrganizationId) {
3080
- const scope = resolveOrganizationScope();
3081
- if (scope === null) {
3082
- return requestedOrganizationId === undefined ? null : [requestedOrganizationId];
3185
+ function getGlobalScopes(model) {
3186
+ return modelGlobalScopes.get(model) ?? [];
3187
+ }
3188
+ function hydrateValue(value, cast) {
3189
+ if (value === null || value === undefined) {
3190
+ return value;
3083
3191
  }
3084
- if (requestedOrganizationId !== undefined) {
3085
- return scope.includes(requestedOrganizationId) ? [requestedOrganizationId] : [];
3192
+ switch (cast) {
3193
+ case "date":
3194
+ case "datetime":
3195
+ return value instanceof Date ? value : new Date(String(value));
3196
+ case "json":
3197
+ return typeof value === "string" ? JSON.parse(value) : value;
3198
+ case "bool":
3199
+ case "boolean":
3200
+ return value === true || value === 1 || value === "1" || value === "true";
3201
+ default:
3202
+ return value;
3086
3203
  }
3087
- return scope;
3088
3204
  }
3089
- function appendOrganizationScope(where, requestedOrganizationId) {
3090
- const organizationIds = scopedOrganizationIds(requestedOrganizationId);
3091
- if (organizationIds === null) {
3092
- return where;
3205
+ function dehydrateValue(value, cast) {
3206
+ if (value === null || value === undefined) {
3207
+ return value;
3093
3208
  }
3094
- if (organizationIds.length === 0) {
3095
- return {
3096
- ...where,
3097
- organization_id: [-1]
3098
- };
3209
+ switch (cast) {
3210
+ case "date":
3211
+ case "datetime":
3212
+ return value instanceof Date ? value : new Date(String(value));
3213
+ case "json":
3214
+ return typeof value === "string" ? value : JSON.stringify(value);
3215
+ case "bool":
3216
+ case "boolean":
3217
+ return Boolean(value);
3218
+ default:
3219
+ return value;
3099
3220
  }
3100
- return {
3101
- ...where,
3102
- organization_id: organizationIds.length === 1 ? organizationIds[0] : organizationIds
3103
- };
3104
3221
  }
3105
- function appendProjectScope(where, accessibleProjectIds, requestedProjectId) {
3106
- if (accessibleProjectIds === null) {
3107
- if (requestedProjectId === undefined) {
3108
- return where;
3109
- }
3110
- return {
3111
- ...where,
3112
- project_id: requestedProjectId
3113
- };
3114
- }
3115
- if (accessibleProjectIds.length === 0) {
3116
- return {
3117
- ...where,
3118
- project_id: [-1]
3119
- };
3222
+ function filterMassAssignable(fillable, guarded, input) {
3223
+ const resolvedGuarded = guarded ?? true;
3224
+ if (fillable && fillable.length > 0) {
3225
+ const allowed = new Set(fillable);
3226
+ return Object.fromEntries(Object.entries(input).filter(([key]) => allowed.has(key)));
3120
3227
  }
3121
- if (requestedProjectId !== undefined) {
3122
- return {
3123
- ...where,
3124
- project_id: accessibleProjectIds.includes(requestedProjectId) ? requestedProjectId : -1
3125
- };
3228
+ if (resolvedGuarded === true || resolvedGuarded.includes("*")) {
3229
+ return {};
3126
3230
  }
3127
- return {
3128
- ...where,
3129
- project_id: accessibleProjectIds
3130
- };
3231
+ const blocked = new Set(resolvedGuarded);
3232
+ return Object.fromEntries(Object.entries(input).filter(([key]) => !blocked.has(key)));
3131
3233
  }
3132
- function emptyPaginateResult(page, perPage) {
3133
- return {
3134
- data: [],
3135
- meta: {
3136
- page,
3137
- per_page: perPage,
3138
- total: 0,
3139
- last_page: 1
3234
+ function applyCasts(values, casts, direction) {
3235
+ if (Object.keys(casts).length === 0) {
3236
+ return values;
3237
+ }
3238
+ const result = { ...values };
3239
+ const castFn = direction === "hydrate" ? hydrateValue : dehydrateValue;
3240
+ for (const [key, cast] of Object.entries(casts)) {
3241
+ if (key in result && cast) {
3242
+ result[key] = castFn(result[key], cast);
3140
3243
  }
3141
- };
3142
- }
3143
- function assertResourceInCurrentTenant(resourceTenantId, resourceLabel, resourceId) {
3144
- if (resourceTenantId !== currentTenantId()) {
3145
- throw new NotFoundError(`${resourceLabel} ${resourceId} not found.`);
3146
3244
  }
3245
+ return result;
3147
3246
  }
3148
- function assertOrganizationReadable(organizationId) {
3149
- const user = currentAuthUser();
3150
- if (!user || isGlobalAdmin(user)) {
3151
- return;
3247
+ function applyTimestampsOnCreate(columns, values, enabled) {
3248
+ if (!enabled) {
3249
+ return values;
3152
3250
  }
3153
- const organizationIds = scopedOrganizationIds();
3154
- if (organizationIds !== null && !organizationIds.includes(organizationId)) {
3155
- throw new NotFoundError(`Organization ${organizationId} not found.`);
3251
+ const now = new Date;
3252
+ const result = { ...values };
3253
+ if (columns.includes("created_at")) {
3254
+ result.created_at = now;
3255
+ }
3256
+ if (columns.includes("updated_at")) {
3257
+ result.updated_at = now;
3156
3258
  }
3259
+ return result;
3157
3260
  }
3158
- // ../../src/core/contracts/di.ts
3159
- function getRequiredDependency(dependencies, key) {
3160
- const dependency = dependencies[key];
3161
- if (dependency === undefined) {
3162
- throw new Error(`Required dependency "${key}" is not registered.`);
3261
+ function applyTimestampsOnUpdate(columns, values, enabled) {
3262
+ if (!enabled) {
3263
+ return values;
3163
3264
  }
3164
- return dependency;
3165
- }
3166
- function resolveService(dependencies, token) {
3167
- return dependencies.container.resolve(token);
3265
+ const result = { ...values };
3266
+ if (columns.includes("updated_at")) {
3267
+ result.updated_at = new Date;
3268
+ }
3269
+ return result;
3168
3270
  }
3169
3271
 
3170
- // ../../src/core/logging/logger.ts
3171
- class Logger {
3172
- channel;
3173
- constructor(channel = "app") {
3174
- this.channel = channel;
3175
- }
3176
- write(level, message, context = {}) {
3177
- const entry = {
3178
- level,
3179
- channel: this.channel,
3180
- message,
3181
- timestamp: new Date().toISOString(),
3182
- ...context
3183
- };
3184
- const line = JSON.stringify(entry);
3185
- if (level === "error") {
3186
- console.error(line);
3187
- return;
3188
- }
3189
- console.log(line);
3272
+ class Model {
3273
+ attributes;
3274
+ repository;
3275
+ static $fillable;
3276
+ static $guarded;
3277
+ static $casts = {};
3278
+ static $timestamps = true;
3279
+ _exists;
3280
+ constructor(attributes, repository, exists = true) {
3281
+ this.attributes = attributes;
3282
+ this.repository = repository;
3283
+ this._exists = exists;
3190
3284
  }
3191
- debug(message, context) {
3192
- this.write("debug", message, context);
3285
+ get $exists() {
3286
+ return this._exists;
3193
3287
  }
3194
- info(message, context) {
3195
- this.write("info", message, context);
3288
+ get(key) {
3289
+ return this.attributes[key];
3196
3290
  }
3197
- warn(message, context) {
3198
- this.write("warn", message, context);
3291
+ get id() {
3292
+ return this.attributes[this.primaryKey()];
3199
3293
  }
3200
- error(message, context) {
3201
- this.write("error", message, context);
3294
+ toObject() {
3295
+ return { ...this.attributes };
3202
3296
  }
3203
- }
3204
- var appLogger = new Logger("app");
3205
-
3206
- // ../../src/core/runtime/applicationRegistry.ts
3207
- var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
3208
- var activeContext;
3209
- function readStoredApplicationContext() {
3210
- if (activeContext) {
3211
- return activeContext;
3297
+ primaryKey() {
3298
+ throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
3212
3299
  }
3213
- const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
3214
- if (globalContext) {
3215
- activeContext = globalContext;
3300
+ static primaryKeyField() {
3301
+ return resolveModelRepository(this).getTable().primaryKey;
3216
3302
  }
3217
- return activeContext;
3218
- }
3219
- function setActiveApplicationContext(context) {
3220
- activeContext = context;
3221
- globalThis[APPLICATION_CONTEXT_KEY] = context;
3222
- }
3223
- function requireActiveApplicationContext() {
3224
- const context = readStoredApplicationContext();
3225
- if (!context) {
3226
- throw new Error("The application context has not been bootstrapped.");
3303
+ static hydrateAttributes(attributes) {
3304
+ const casts = modelStatics(this).$casts ?? {};
3305
+ return applyCasts(attributes, casts, "hydrate");
3227
3306
  }
3228
- return context;
3229
- }
3230
- function resolveApplicationCache() {
3231
- return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
3232
- }
3233
- function resolveApplicationQueue() {
3234
- return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
3235
- }
3236
- function resolveApplicationEventBus() {
3237
- return requireActiveApplicationContext().container.resolve(CORE_EVENT_BUS_TOKEN);
3238
- }
3239
- function resolveApplicationAuth() {
3240
- return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
3241
- }
3242
- function resolveApplicationPolicyGate() {
3243
- return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
3244
- }
3245
- function resolveApplicationConfig() {
3246
- return requireActiveApplicationContext().config;
3247
- }
3248
- function resolveApplicationLogger() {
3249
- return appLogger;
3250
- }
3251
- function resolveApplicationDependencies() {
3252
- return requireActiveApplicationContext().dependencies;
3253
- }
3254
-
3255
- // ../../src/core/auth/resolveMembershipService.ts
3256
- function resolveMembershipService() {
3257
- const dependencies = resolveApplicationDependencies();
3258
- if (dependencies.container.has("core.membership")) {
3259
- return dependencies.container.resolve("core.membership");
3307
+ static dehydrateAttributes(attributes) {
3308
+ const casts = modelStatics(this).$casts ?? {};
3309
+ return applyCasts(attributes, casts, "dehydrate");
3260
3310
  }
3261
- return new membershipService_default;
3262
- }
3263
-
3264
- // ../../src/core/auth/membershipService.ts
3265
- class MembershipService {
3266
- members;
3267
- constructor(members = membershipRepository) {
3268
- this.members = members;
3311
+ static fromRecord(record, repository, exists = true) {
3312
+ const statics = modelStatics(this);
3313
+ const hydrated = statics.hydrateAttributes(record);
3314
+ return new statics(hydrated, repository, exists);
3269
3315
  }
3270
- async listOrganizationIdsForUser(userId) {
3271
- const memberships = await this.members.listForUser(userId);
3272
- return memberships.map((membership) => membership.organization_id);
3316
+ static boot() {}
3317
+ static addGlobalScope(_name, scope) {
3318
+ ensureBooted(this);
3319
+ const existing = modelGlobalScopes.get(this) ?? [];
3320
+ modelGlobalScopes.set(this, [
3321
+ ...existing,
3322
+ scope
3323
+ ]);
3273
3324
  }
3274
- async getOrgRole(userId, organizationId) {
3275
- const membership = await this.members.findMembership(userId, organizationId);
3276
- return membership?.role ?? null;
3325
+ static repository() {
3326
+ return resolveModelRepository(this);
3277
3327
  }
3278
- async requireOrgAccess(organizationId, minimumRole = "member", user = currentAuthUser()) {
3279
- if (!user) {
3280
- throw new ForbiddenError("Authentication required.");
3281
- }
3282
- if (isGlobalAdmin(user)) {
3283
- return "owner";
3328
+ static query() {
3329
+ ensureBooted(this);
3330
+ const repository = resolveModelRepository(this);
3331
+ let query = repository.query();
3332
+ for (const scope of getGlobalScopes(this)) {
3333
+ query = scope(query);
3284
3334
  }
3285
- const role = await this.getOrgRole(resolveUserId(user), organizationId);
3286
- if (!role || !hasMinimumOrgRole(role, minimumRole)) {
3287
- throw new ForbiddenError("Organization membership required.");
3335
+ return query;
3336
+ }
3337
+ static async create(attributes) {
3338
+ const statics = modelStatics(this);
3339
+ ensureBooted(this);
3340
+ const repository = resolveModelRepository(this);
3341
+ const table = repository.getTable();
3342
+ const timestamps = statics.$timestamps ?? true;
3343
+ const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
3344
+ const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
3345
+ const payload = statics.dehydrateAttributes(withTimestamps);
3346
+ const record = await repository.create(payload);
3347
+ return statics.fromRecord(record, repository, true);
3348
+ }
3349
+ static async find(id) {
3350
+ const statics = modelStatics(this);
3351
+ const repository = resolveModelRepository(this);
3352
+ const primaryKey = repository.getTable().primaryKey;
3353
+ const record = await Model.query.call(this).where({ [primaryKey]: id }).first();
3354
+ return record ? statics.fromRecord(record, repository, true) : null;
3355
+ }
3356
+ static async findOrFail(id, errorFactory) {
3357
+ const model = await Model.find.call(this, id);
3358
+ if (model) {
3359
+ return model;
3288
3360
  }
3289
- return role;
3361
+ throw errorFactory?.(id) ?? new NotFoundError(`${this.name} ${String(id)} not found.`);
3290
3362
  }
3291
- async filterAccessibleOrganizationIds(organizationIds, user = currentAuthUser()) {
3292
- if (!user) {
3293
- return [];
3363
+ static async all(options = {}) {
3364
+ const statics = modelStatics(this);
3365
+ const repository = resolveModelRepository(this);
3366
+ let query = Model.query.call(this);
3367
+ if (options.orderBy) {
3368
+ query = query.orderBy(options.orderBy);
3294
3369
  }
3295
- if (isGlobalAdmin(user)) {
3296
- return organizationIds;
3370
+ if (options.limit !== undefined) {
3371
+ query = query.limit(options.limit);
3297
3372
  }
3298
- const allowed = new Set(await this.listOrganizationIdsForUser(resolveUserId(user)));
3299
- return organizationIds.filter((organizationId) => allowed.has(organizationId));
3373
+ const rows = await query.get();
3374
+ return rows.map((row) => statics.fromRecord(row, repository, true));
3300
3375
  }
3301
- async addOwnerOnOrganizationCreate(organizationId, userId) {
3302
- await this.members.addMember({
3303
- organizationId,
3304
- userId,
3305
- role: "owner"
3306
- });
3376
+ static async firstWhere(where, options = {}) {
3377
+ const statics = modelStatics(this);
3378
+ const repository = resolveModelRepository(this);
3379
+ let query = Model.query.call(this).where(where);
3380
+ if (options.orderBy) {
3381
+ query = query.orderBy(options.orderBy);
3382
+ }
3383
+ const record = await query.first();
3384
+ return record ? statics.fromRecord(record, repository, true) : null;
3307
3385
  }
3308
- listMembersForOrganization(organizationId) {
3309
- return this.members.listForOrganization(organizationId);
3386
+ async save() {
3387
+ const ModelClass = modelStatics(this.constructor);
3388
+ const timestamps = ModelClass.$timestamps ?? true;
3389
+ const casts = ModelClass.$casts ?? {};
3390
+ const table = this.repository.getTable();
3391
+ if (this.$exists) {
3392
+ const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
3393
+ const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
3394
+ this.attributes = ModelClass.hydrateAttributes(record2);
3395
+ return this;
3396
+ }
3397
+ const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
3398
+ const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
3399
+ const payload = ModelClass.dehydrateAttributes(withTimestamps);
3400
+ const record = await this.repository.create(payload);
3401
+ this.attributes = ModelClass.hydrateAttributes(record);
3402
+ this._exists = true;
3403
+ return this;
3310
3404
  }
3311
- addMember(input) {
3312
- return this.members.addMember(input);
3405
+ async update(changes) {
3406
+ const ModelClass = modelStatics(this.constructor);
3407
+ const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, changes);
3408
+ Object.assign(this.attributes, assignable);
3409
+ return await this.save();
3313
3410
  }
3314
- removeMember(organizationId, userId) {
3315
- return this.members.removeMember(organizationId, userId);
3411
+ async delete() {
3412
+ if (resolveSoftDeleteColumn(this.repository.getTable())) {
3413
+ return await this.repository.deleteById(this.id);
3414
+ }
3415
+ return await this.repository.forceDeleteById(this.id);
3316
3416
  }
3317
- }
3318
- var membershipService_default = MembershipService;
3319
- // ../../src/core/auth/policy.ts
3320
- class Policy {
3321
- constructor() {}
3322
- view(_user, _resource) {
3323
- return false;
3417
+ async forceDelete() {
3418
+ return await this.repository.forceDeleteById(this.id);
3324
3419
  }
3325
- create(_user) {
3326
- return false;
3420
+ async restore() {
3421
+ const ModelClass = modelStatics(this.constructor);
3422
+ const record = await this.repository.restoreById(this.id);
3423
+ if (!record) {
3424
+ return null;
3425
+ }
3426
+ this.attributes = ModelClass.hydrateAttributes(record);
3427
+ return this;
3327
3428
  }
3328
- update(_user, _resource) {
3329
- return false;
3429
+ async loadHasMany(as, relation, childRepository, options = {}) {
3430
+ const grouped = await childRepository.withConnection(this.repository.getConnection()).loadHasManyForParents([this.attributes], relation, options);
3431
+ const loaded = grouped.get(this.attributes[relation.localKey]) ?? [];
3432
+ return Object.assign(this, { [as]: loaded });
3330
3433
  }
3331
- delete(_user, _resource) {
3332
- return false;
3434
+ async loadHasOne(as, relation, childRepository, options = {}) {
3435
+ const loaded = await this.loadHasMany(as, relation, childRepository, { ...options, limit: 1 });
3436
+ const value = loaded[as]?.[0];
3437
+ return Object.assign(this, { [as]: value });
3333
3438
  }
3334
- }
3335
- var BLOCKED_POLICY_ACTIONS = new Set([
3336
- "constructor",
3337
- "toString",
3338
- "valueOf",
3339
- "hasOwnProperty",
3340
- "isPrototypeOf",
3341
- "propertyIsEnumerable",
3342
- "__proto__"
3343
- ]);
3344
-
3345
- class PolicyGate {
3346
- constructor() {}
3347
- policies = new Map;
3348
- register(resource, policy) {
3349
- this.policies.set(resource, policy);
3439
+ async loadBelongsTo(as, relation, parentRepository, options = {}) {
3440
+ const grouped = await this.repository.loadBelongsToForParents([this.attributes], relation, parentRepository, options);
3441
+ const loaded = grouped.get(this.attributes[relation.foreignKey]);
3442
+ return Object.assign(this, { [as]: loaded });
3350
3443
  }
3351
- allows(resource, action, user, model) {
3352
- const policy = this.policies.get(resource);
3353
- if (!policy) {
3354
- return false;
3355
- }
3356
- if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
3357
- return false;
3358
- }
3359
- const handler = policy[action];
3360
- if (typeof handler !== "function") {
3361
- return false;
3444
+ async loadBelongsToMany(as, relation, relatedRepository, options = {}) {
3445
+ const connection = this.repository.getConnection();
3446
+ const parentId = this.attributes[relation.parentKey];
3447
+ const pivotRows = await connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = $1`, [parentId]);
3448
+ if (pivotRows.length === 0) {
3449
+ return Object.assign(this, { [as]: [] });
3362
3450
  }
3363
- const resolvedUser = user === undefined ? currentAuthUser() : user;
3364
- return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
3451
+ const relatedIds = [
3452
+ ...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
3453
+ ];
3454
+ const relatedRows = await relatedRepository.withConnection(connection).findAll({
3455
+ ...options,
3456
+ where: {
3457
+ [relation.relatedKey]: relatedIds
3458
+ }
3459
+ });
3460
+ const grouped = indexBelongsToManyRelation([this.attributes], pivotRows, relatedRows, relation);
3461
+ const loaded = grouped.get(parentId) ?? [];
3462
+ return Object.assign(this, { [as]: loaded });
3365
3463
  }
3366
- authorize(resource, action, user, model) {
3367
- if (!this.allows(resource, action, user, model)) {
3368
- throw new ForbiddenError;
3369
- }
3464
+ mergeAttributes(patch) {
3465
+ Object.assign(this.attributes, patch);
3466
+ return this;
3370
3467
  }
3371
3468
  }
3372
- // ../../src/core/database/bindConnection.ts
3373
- function bindDatabaseConnection2(connection) {
3374
- bindDatabaseConnection(connection);
3469
+ function registerModelRepository(model, repository) {
3470
+ modelRepositories.set(model, repository);
3471
+ ensureBooted(model);
3472
+ return model;
3375
3473
  }
3376
-
3377
- // ../../src/domain/scim.ts
3378
- var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
3379
-
3380
- // ../../src/core/security/timingSafeCompare.ts
3381
- import { timingSafeEqual } from "crypto";
3382
- function timingSafeCompareString(left, right) {
3383
- const leftBuffer = Buffer.from(left);
3384
- const rightBuffer = Buffer.from(right);
3385
- if (leftBuffer.length !== rightBuffer.length) {
3386
- return false;
3474
+ // ../../src/core/database/schema/columnDefinition.ts
3475
+ class ColumnDefinition {
3476
+ name;
3477
+ kind;
3478
+ length;
3479
+ isNullable = false;
3480
+ isPrimary = false;
3481
+ isUnique = false;
3482
+ autoIncrement = false;
3483
+ defaultValue;
3484
+ checkExpression;
3485
+ foreignKey;
3486
+ constructor(name, kind) {
3487
+ this.name = name;
3488
+ this.kind = kind;
3387
3489
  }
3388
- return timingSafeEqual(leftBuffer, rightBuffer);
3389
- }
3390
-
3391
- // ../../src/core/security/scimTenantTokens.ts
3392
- function parseScimTenantTokens(raw) {
3393
- const tokens = new Map;
3394
- if (!raw?.trim()) {
3395
- return tokens;
3490
+ nullable() {
3491
+ this.isNullable = true;
3492
+ return this;
3396
3493
  }
3397
- for (const entry of raw.split(",")) {
3398
- const [tenantPart, tokenPart] = entry.split(":");
3399
- if (!tenantPart || !tokenPart) {
3400
- continue;
3494
+ notNullable() {
3495
+ this.isNullable = false;
3496
+ return this;
3497
+ }
3498
+ default(value) {
3499
+ if (typeof value === "boolean") {
3500
+ this.defaultValue = value ? "TRUE" : "FALSE";
3501
+ return this;
3401
3502
  }
3402
- const tenantId = Number.parseInt(tenantPart.trim(), 10);
3403
- const token = tokenPart.trim();
3404
- if (Number.isInteger(tenantId) && tenantId > 0 && token.length > 0) {
3405
- tokens.set(tenantId, token);
3503
+ if (typeof value === "number") {
3504
+ this.defaultValue = String(value);
3505
+ return this;
3406
3506
  }
3507
+ this.defaultValue = `'${value.replace(/'/g, "''")}'`;
3508
+ return this;
3407
3509
  }
3408
- return tokens;
3409
- }
3410
- function resolveScimTenantFromToken(token) {
3411
- const tenantTokens = parseScimTenantTokens(process.env.SCIM_TENANT_TOKENS);
3412
- for (const [tenantId, expectedToken] of tenantTokens) {
3413
- if (timingSafeCompareString(token, expectedToken)) {
3414
- return tenantId;
3415
- }
3510
+ defaultRaw(expression) {
3511
+ this.defaultValue = expression;
3512
+ return this;
3416
3513
  }
3417
- const fallbackToken = process.env.SCIM_BEARER_TOKEN ?? TEST_SCIM_BEARER_TOKEN;
3418
- if (timingSafeCompareString(token, fallbackToken)) {
3419
- return 1;
3514
+ unique() {
3515
+ this.isUnique = true;
3516
+ return this;
3420
3517
  }
3421
- return null;
3422
- }
3423
-
3424
- // ../../src/core/tenant/resolveTenant.ts
3425
- async function resolveTenant(tenantId) {
3426
- const rows = await repositoryConnection`
3427
- SELECT id, slug, plan, region
3428
- FROM tenant
3429
- WHERE id = ${tenantId}
3430
- LIMIT 1
3431
- `;
3432
- const row = rows[0];
3433
- return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
3434
- }
3435
-
3436
- // ../../src/core/tenant/tenantDatabaseScope.ts
3437
- async function applyTenantContextToTransaction(transaction, tenantId) {
3438
- await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
3439
- await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
3440
- }
3441
- async function runWithTenantDatabase(tenant, callback) {
3442
- if (hasActiveDatabaseConnection()) {
3443
- const activeConnection2 = getActiveDatabaseConnection(getDefaultDatabasePool());
3444
- await applyTenantContextToTransaction(activeConnection2, tenant.id);
3445
- return await runWithTenant(tenant, callback);
3518
+ primary() {
3519
+ this.isPrimary = true;
3520
+ return this;
3521
+ }
3522
+ check(expression) {
3523
+ this.checkExpression = expression;
3524
+ return this;
3446
3525
  }
3447
- return await getDefaultDatabasePool().begin(async (transaction) => {
3448
- await applyTenantContextToTransaction(transaction, tenant.id);
3449
- return await runWithDatabaseConnection(transaction, async () => {
3450
- return await runWithTenant(tenant, callback);
3451
- });
3452
- });
3453
- }
3454
- function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
3455
- return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
3456
3526
  }
3457
3527
 
3458
- // ../../src/core/auth/scimAuthMiddleware.ts
3459
- function createScimAuthMiddleware() {
3460
- return async (request, next) => {
3461
- const authorization = request.headers.get("authorization");
3462
- if (!authorization?.startsWith("Bearer ")) {
3463
- return jsonScimError("SCIM bearer token required.", 401);
3464
- }
3465
- const token = authorization.slice("Bearer ".length).trim();
3466
- const tenantId = resolveScimTenantFromToken(token);
3467
- if (tenantId === null) {
3468
- return jsonScimError("Invalid SCIM bearer token.", 401);
3528
+ class ForeignIdColumnDefinition extends ColumnDefinition {
3529
+ constructor(name) {
3530
+ super(name, "foreignId");
3531
+ this.notNullable();
3532
+ }
3533
+ references(table, column = "id") {
3534
+ this.foreignKey = {
3535
+ referencesTable: table,
3536
+ referencesColumn: column
3537
+ };
3538
+ return this;
3539
+ }
3540
+ constrained(table) {
3541
+ const referencesTable = table ?? inferReferencedTable(this.name);
3542
+ return this.references(referencesTable);
3543
+ }
3544
+ cascadeOnDelete() {
3545
+ if (!this.foreignKey) {
3546
+ throw new Error(`Foreign key is not defined for column ${this.name}`);
3469
3547
  }
3470
- const tenant = await resolveTenant(tenantId);
3471
- if (!tenant) {
3472
- return jsonScimError("SCIM tenant not found.", 401);
3548
+ this.foreignKey.onDelete = "cascade";
3549
+ return this;
3550
+ }
3551
+ nullOnDelete() {
3552
+ if (!this.foreignKey) {
3553
+ throw new Error(`Foreign key is not defined for column ${this.name}`);
3473
3554
  }
3474
- return await runWithTenantDatabase(tenant, async () => {
3475
- bindDatabaseConnection2(getActiveDatabaseConnection(getDefaultDatabasePool()));
3476
- try {
3477
- return await next();
3478
- } finally {
3479
- resetBoundDatabaseConnection();
3480
- }
3481
- });
3482
- };
3555
+ this.foreignKey.onDelete = "set null";
3556
+ return this;
3557
+ }
3483
3558
  }
3484
- function jsonScimError(detail, status) {
3485
- return Response.json({
3486
- schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
3487
- detail,
3488
- status: String(status)
3489
- }, {
3490
- status,
3491
- headers: { "content-type": "application/scim+json" }
3492
- });
3559
+ function inferReferencedTable(columnName) {
3560
+ if (!columnName.endsWith("_id")) {
3561
+ throw new Error(`Cannot infer referenced table from column ${columnName}`);
3562
+ }
3563
+ return columnName.slice(0, -3);
3493
3564
  }
3494
- // ../../src/core/cache/redisCacheStore.ts
3495
- var {RedisClient } = globalThis.Bun;
3496
- var KEY_PREFIX = "workhub:cache:";
3497
- var TAG_PREFIX = "workhub:cache:tag:";
3498
3565
 
3499
- class RedisCacheStore {
3500
- ttlMs;
3501
- maxEntries;
3502
- client;
3503
- inflight = new Map;
3504
- keyTags = new Map;
3505
- constructor(redisUrl, ttlMs, maxEntries) {
3506
- this.ttlMs = ttlMs;
3507
- this.maxEntries = maxEntries;
3508
- this.client = new RedisClient(redisUrl);
3566
+ // ../../src/core/database/schema/blueprint.ts
3567
+ class Blueprint {
3568
+ table;
3569
+ action;
3570
+ columns = [];
3571
+ indexes = [];
3572
+ droppedColumns = [];
3573
+ droppedIndexes = [];
3574
+ constructor(table, action) {
3575
+ this.table = table;
3576
+ this.action = action;
3509
3577
  }
3510
- async get(key) {
3511
- const raw = await this.client.get(this.storageKey(key));
3512
- if (raw === null) {
3513
- return;
3514
- }
3515
- return JSON.parse(raw);
3578
+ id(name = "id") {
3579
+ const column = new ColumnDefinition(name, "id");
3580
+ column.primary();
3581
+ column.autoIncrement = true;
3582
+ this.columns.push(column);
3583
+ return column;
3516
3584
  }
3517
- async set(key, value, ttlMs) {
3518
- const resolvedTtlMs = ttlMs ?? this.ttlMs;
3519
- const payload = JSON.stringify(value);
3520
- if (resolvedTtlMs > 0) {
3521
- await this.client.psetex(this.storageKey(key), resolvedTtlMs, payload);
3522
- } else {
3523
- await this.client.set(this.storageKey(key), payload);
3524
- }
3525
- await this.enforceMaxEntries();
3585
+ string(name, length) {
3586
+ const column = new ColumnDefinition(name, "string");
3587
+ column.length = length;
3588
+ column.notNullable();
3589
+ this.columns.push(column);
3590
+ return column;
3526
3591
  }
3527
- async getOrSet(key, loader, ttlMs) {
3528
- const cached = await this.get(key);
3529
- if (cached !== undefined) {
3530
- return cached;
3531
- }
3532
- const inflightRequest = this.inflight.get(key);
3533
- if (inflightRequest) {
3534
- return inflightRequest;
3535
- }
3536
- const pendingRequest = loader().then(async (value) => {
3537
- await this.set(key, value, ttlMs);
3538
- return value;
3539
- }).finally(() => {
3540
- this.inflight.delete(key);
3541
- });
3542
- this.inflight.set(key, pendingRequest);
3543
- return pendingRequest;
3592
+ text(name) {
3593
+ const column = new ColumnDefinition(name, "text");
3594
+ column.notNullable();
3595
+ this.columns.push(column);
3596
+ return column;
3544
3597
  }
3545
- async attachTags(key, tags) {
3546
- if (tags.length === 0) {
3547
- return;
3548
- }
3549
- let tagsForKey = this.keyTags.get(key);
3550
- if (!tagsForKey) {
3551
- tagsForKey = new Set;
3552
- this.keyTags.set(key, tagsForKey);
3553
- }
3554
- for (const tag of tags) {
3555
- tagsForKey.add(tag);
3556
- await this.client.sadd(this.tagKey(tag), key);
3557
- }
3598
+ boolean(name) {
3599
+ const column = new ColumnDefinition(name, "boolean");
3600
+ column.notNullable();
3601
+ this.columns.push(column);
3602
+ return column;
3558
3603
  }
3559
- async flushTags(tags) {
3560
- const keysToRemove = new Set;
3561
- for (const tag of tags) {
3562
- const members = await this.client.smembers(this.tagKey(tag));
3563
- for (const member of members) {
3564
- keysToRemove.add(member);
3565
- }
3566
- }
3567
- let removed = 0;
3568
- for (const key of keysToRemove) {
3569
- if (await this.invalidate(key)) {
3570
- removed += 1;
3571
- }
3572
- }
3573
- for (const tag of tags) {
3574
- await this.client.del(this.tagKey(tag));
3575
- }
3576
- return removed;
3604
+ integer(name) {
3605
+ const column = new ColumnDefinition(name, "integer");
3606
+ column.notNullable();
3607
+ this.columns.push(column);
3608
+ return column;
3577
3609
  }
3578
- async invalidate(key) {
3579
- const deleted = await this.client.del(this.storageKey(key));
3580
- await this.detachKeyFromTags(key);
3581
- return deleted > 0;
3610
+ bigInteger(name) {
3611
+ const column = new ColumnDefinition(name, "bigInteger");
3612
+ column.notNullable();
3613
+ this.columns.push(column);
3614
+ return column;
3582
3615
  }
3583
- async invalidateByPrefix(prefix) {
3584
- const keys = await this.client.keys(`${KEY_PREFIX}*`);
3585
- let removed = 0;
3586
- for (const storageKey of keys) {
3587
- const key = storageKey.slice(KEY_PREFIX.length);
3588
- if (key === prefix || key.startsWith(`${prefix}?`)) {
3589
- if (await this.invalidate(key)) {
3590
- removed += 1;
3591
- }
3592
- }
3593
- }
3594
- return removed;
3616
+ timestamp(name) {
3617
+ const column = new ColumnDefinition(name, "timestamp");
3618
+ column.notNullable();
3619
+ this.columns.push(column);
3620
+ return column;
3595
3621
  }
3596
- async clear() {
3597
- const keys = await this.client.keys(`${KEY_PREFIX}*`);
3598
- if (keys.length > 0) {
3599
- await this.client.del(...keys);
3600
- }
3601
- const tagKeys = await this.client.keys(`${TAG_PREFIX}*`);
3602
- if (tagKeys.length > 0) {
3603
- await this.client.del(...tagKeys);
3604
- }
3605
- this.inflight.clear();
3606
- this.keyTags.clear();
3622
+ json(name) {
3623
+ const column = new ColumnDefinition(name, "json");
3624
+ column.notNullable();
3625
+ this.columns.push(column);
3626
+ return column;
3607
3627
  }
3608
- async size() {
3609
- const keys = await this.client.keys(`${KEY_PREFIX}*`);
3610
- return keys.length;
3628
+ jsonb(name) {
3629
+ const column = new ColumnDefinition(name, "jsonb");
3630
+ column.notNullable();
3631
+ this.columns.push(column);
3632
+ return column;
3611
3633
  }
3612
- storageKey(key) {
3613
- return `${KEY_PREFIX}${key}`;
3634
+ foreignId(name) {
3635
+ const column = new ForeignIdColumnDefinition(name);
3636
+ this.columns.push(column);
3637
+ return column;
3614
3638
  }
3615
- tagKey(tag) {
3616
- return `${TAG_PREFIX}${tag}`;
3639
+ timestamps() {
3640
+ this.timestamp("created_at").defaultRaw("NOW()");
3641
+ this.timestamp("updated_at").defaultRaw("NOW()");
3617
3642
  }
3618
- async detachKeyFromTags(key) {
3619
- const tags = this.keyTags.get(key);
3620
- if (!tags) {
3621
- return;
3622
- }
3623
- for (const tag of tags) {
3624
- await this.client.srem(this.tagKey(tag), key);
3625
- }
3626
- this.keyTags.delete(key);
3643
+ softDeletes() {
3644
+ this.timestamp("deleted_at").nullable();
3627
3645
  }
3628
- async enforceMaxEntries() {
3629
- const keys = await this.client.keys(`${KEY_PREFIX}*`);
3630
- if (keys.length <= this.maxEntries) {
3631
- return;
3632
- }
3633
- const overflow = keys.length - this.maxEntries;
3634
- const keysToRemove = keys.slice(0, overflow);
3635
- if (keysToRemove.length > 0) {
3636
- await this.client.del(...keysToRemove);
3637
- }
3646
+ dropColumn(name) {
3647
+ this.droppedColumns.push(name);
3638
3648
  }
3639
- }
3640
- var redisCacheStore_default = RedisCacheStore;
3641
-
3642
- // ../../src/core/cache/simpleCache.ts
3643
- class SimpleCache {
3644
- ttlMs;
3645
- maxEntries;
3646
- cache = new Map;
3647
- inflight = new Map;
3648
- tagIndex = new Map;
3649
- keyTags = new Map;
3650
- constructor(ttlMs = 3600000, maxEntries = 100) {
3651
- this.ttlMs = ttlMs;
3652
- this.maxEntries = maxEntries;
3653
- if (!Number.isFinite(ttlMs) || ttlMs < 0) {
3654
- throw new RangeError("ttlMs must be a non-negative number.");
3655
- }
3656
- if (!Number.isInteger(maxEntries) || maxEntries < 1) {
3657
- throw new RangeError("maxEntries must be a positive integer.");
3658
- }
3649
+ dropSoftDeletes() {
3650
+ this.dropColumn("deleted_at");
3651
+ this.dropIndex(`idx_${this.table}_deleted_at`);
3659
3652
  }
3660
- get(key) {
3661
- return this.getFreshEntry(key)?.value;
3653
+ dropIndex(name) {
3654
+ this.droppedIndexes.push(name);
3662
3655
  }
3663
- set(key, value, ttlMs) {
3664
- const now = Date.now();
3665
- const resolvedTtlMs = ttlMs ?? this.ttlMs;
3666
- this.cache.set(key, {
3667
- value,
3668
- expiresAt: now + resolvedTtlMs,
3669
- lastAccessedAt: now
3656
+ unique(columns, name) {
3657
+ this.indexes.push({
3658
+ name,
3659
+ columns: Array.isArray(columns) ? columns : [columns],
3660
+ kind: "unique"
3670
3661
  });
3671
- this.evictOverflow();
3672
3662
  }
3673
- async getOrSet(key, loader, ttlMs) {
3674
- this.pruneExpired();
3675
- const cachedEntry = this.getFreshEntry(key);
3676
- if (cachedEntry) {
3677
- return cachedEntry.value;
3678
- }
3679
- const inflightRequest = this.inflight.get(key);
3680
- if (inflightRequest) {
3681
- return inflightRequest;
3682
- }
3683
- const pendingRequest = loader().then((value) => {
3684
- this.set(key, value, ttlMs);
3685
- return value;
3686
- }).finally(() => {
3687
- this.inflight.delete(key);
3663
+ index(columns, options = {}) {
3664
+ this.indexes.push({
3665
+ name: options.name,
3666
+ columns: Array.isArray(columns) ? columns : [columns],
3667
+ kind: "index",
3668
+ order: options.order
3688
3669
  });
3689
- this.inflight.set(key, pendingRequest);
3690
- return pendingRequest;
3691
- }
3692
- attachTags(key, tags) {
3693
- if (tags.length === 0) {
3694
- return;
3695
- }
3696
- let tagsForKey = this.keyTags.get(key);
3697
- if (!tagsForKey) {
3698
- tagsForKey = new Set;
3699
- this.keyTags.set(key, tagsForKey);
3700
- }
3701
- for (const tag of tags) {
3702
- tagsForKey.add(tag);
3703
- let keysForTag = this.tagIndex.get(tag);
3704
- if (!keysForTag) {
3705
- keysForTag = new Set;
3706
- this.tagIndex.set(tag, keysForTag);
3707
- }
3708
- keysForTag.add(key);
3709
- }
3710
3670
  }
3711
- flushTags(tags) {
3712
- const keysToRemove = new Set;
3713
- for (const tag of tags) {
3714
- const keys = this.tagIndex.get(tag);
3715
- if (!keys) {
3716
- continue;
3717
- }
3718
- for (const key of keys) {
3719
- keysToRemove.add(key);
3720
- }
3721
- }
3722
- let removed = 0;
3723
- for (const key of keysToRemove) {
3724
- if (this.invalidate(key)) {
3725
- removed += 1;
3726
- }
3727
- }
3728
- for (const tag of tags) {
3729
- this.tagIndex.delete(tag);
3730
- }
3731
- return removed;
3671
+ partialIndex(columns, where, nameOrOptions) {
3672
+ const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
3673
+ this.indexes.push({
3674
+ name: options.name,
3675
+ columns: Array.isArray(columns) ? columns : [columns],
3676
+ kind: options.unique ? "uniquePartial" : "partial",
3677
+ where
3678
+ });
3732
3679
  }
3733
- invalidate(key) {
3734
- const removed = this.cache.delete(key);
3735
- if (removed) {
3736
- this.detachKeyFromTags(key);
3737
- }
3738
- return removed;
3680
+ fullText(columns, name) {
3681
+ this.indexes.push({
3682
+ name,
3683
+ columns: Array.isArray(columns) ? columns : [columns],
3684
+ kind: "fullText"
3685
+ });
3739
3686
  }
3740
- invalidateByPrefix(prefix) {
3741
- let removed = 0;
3742
- for (const key of [...this.cache.keys()]) {
3743
- if (key === prefix || key.startsWith(`${prefix}?`)) {
3744
- if (this.invalidate(key)) {
3745
- removed += 1;
3746
- }
3747
- }
3748
- }
3749
- return removed;
3687
+ ginIndex(column, name) {
3688
+ this.indexes.push({
3689
+ name,
3690
+ columns: [column],
3691
+ kind: "gin"
3692
+ });
3750
3693
  }
3751
- clear() {
3752
- this.cache.clear();
3753
- this.inflight.clear();
3754
- this.tagIndex.clear();
3755
- this.keyTags.clear();
3694
+ }
3695
+ // ../../src/core/database/schema/driver.ts
3696
+ function normalizeConnectionName(connection) {
3697
+ const normalized = connection.trim().toLowerCase();
3698
+ if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
3699
+ return "pgsql";
3756
3700
  }
3757
- size() {
3758
- this.pruneExpired();
3759
- return this.cache.size;
3701
+ if (normalized === "mysql" || normalized === "mariadb") {
3702
+ return "mysql";
3760
3703
  }
3761
- detachKeyFromTags(key) {
3762
- const tags = this.keyTags.get(key);
3763
- if (!tags) {
3764
- return;
3765
- }
3766
- for (const tag of tags) {
3767
- const keys = this.tagIndex.get(tag);
3768
- if (!keys) {
3769
- continue;
3770
- }
3771
- keys.delete(key);
3772
- if (keys.size === 0) {
3773
- this.tagIndex.delete(tag);
3774
- }
3775
- }
3776
- this.keyTags.delete(key);
3704
+ if (normalized === "sqlite") {
3705
+ return "sqlite";
3777
3706
  }
3778
- getFreshEntry(key) {
3779
- const entry = this.cache.get(key);
3780
- if (!entry) {
3781
- return;
3782
- }
3783
- if (entry.expiresAt <= Date.now()) {
3784
- this.invalidate(key);
3785
- return;
3786
- }
3787
- entry.lastAccessedAt = Date.now();
3788
- return entry;
3707
+ throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
3708
+ }
3709
+ function resolveDriverFromUrl(url) {
3710
+ const normalized = url.trim().toLowerCase();
3711
+ if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
3712
+ return "pgsql";
3789
3713
  }
3790
- pruneExpired() {
3791
- const now = Date.now();
3792
- for (const [key, entry] of this.cache.entries()) {
3793
- if (entry.expiresAt <= now) {
3794
- this.invalidate(key);
3795
- }
3796
- }
3714
+ if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
3715
+ return "mysql";
3797
3716
  }
3798
- evictOverflow() {
3799
- while (this.cache.size > this.maxEntries) {
3800
- let oldestKey;
3801
- let oldestAccessTime = Number.POSITIVE_INFINITY;
3802
- for (const [key, entry] of this.cache.entries()) {
3803
- if (entry.lastAccessedAt < oldestAccessTime) {
3804
- oldestAccessTime = entry.lastAccessedAt;
3805
- oldestKey = key;
3806
- }
3807
- }
3808
- if (!oldestKey) {
3809
- return;
3810
- }
3811
- this.invalidate(oldestKey);
3812
- }
3717
+ if (normalized.startsWith("sqlite:")) {
3718
+ return "sqlite";
3813
3719
  }
3720
+ return null;
3814
3721
  }
3815
- var simpleCache_default = SimpleCache;
3816
-
3817
- // ../../src/core/cache/simpleCacheStore.ts
3818
- class SimpleCacheStore {
3819
- cache;
3820
- constructor(cache) {
3821
- this.cache = cache;
3822
- }
3823
- get(key) {
3824
- return Promise.resolve(this.cache.get(key));
3722
+ function resolveDatabaseDriver(options = {}) {
3723
+ const connection = options.connection ?? process.env.DB_CONNECTION;
3724
+ if (connection) {
3725
+ return normalizeConnectionName(connection);
3825
3726
  }
3826
- set(key, value, ttlMs) {
3827
- this.cache.set(key, value, ttlMs);
3828
- return Promise.resolve();
3727
+ const url = options.url ?? process.env.DATABASE_URL ?? "";
3728
+ const fromUrl = resolveDriverFromUrl(url);
3729
+ if (fromUrl) {
3730
+ return fromUrl;
3829
3731
  }
3830
- getOrSet(key, loader, ttlMs) {
3831
- return this.cache.getOrSet(key, loader, ttlMs);
3732
+ return "pgsql";
3733
+ }
3734
+ // ../../src/core/database/schema/errors.ts
3735
+ class UnsupportedSchemaFeatureError extends Error {
3736
+ constructor(feature, driver) {
3737
+ super(`${feature} is not supported for the ${driver} driver`);
3738
+ this.name = "UnsupportedSchemaFeatureError";
3832
3739
  }
3833
- attachTags(key, tags) {
3834
- this.cache.attachTags(key, tags);
3835
- return Promise.resolve();
3740
+ }
3741
+ // ../../src/core/database/schema/grammars/grammar.ts
3742
+ function compileColumnType(driver, column) {
3743
+ switch (column.kind) {
3744
+ case "id":
3745
+ return compileIdType(driver);
3746
+ case "string":
3747
+ return compileStringType(driver, column.length);
3748
+ case "text":
3749
+ return compileTextType(driver);
3750
+ case "boolean":
3751
+ return compileBooleanType(driver);
3752
+ case "integer":
3753
+ case "foreignId":
3754
+ return compileIntegerType(driver);
3755
+ case "bigInteger":
3756
+ return compileBigIntegerType(driver);
3757
+ case "timestamp":
3758
+ return compileTimestampType(driver);
3759
+ case "json":
3760
+ return compileJsonType(driver);
3761
+ case "jsonb":
3762
+ return compileJsonbType(driver);
3763
+ default:
3764
+ throw new Error(`Unsupported column kind: ${column.kind}`);
3836
3765
  }
3837
- flushTags(tags) {
3838
- return Promise.resolve(this.cache.flushTags(tags));
3766
+ }
3767
+ function compileIdType(driver) {
3768
+ switch (driver) {
3769
+ case "pgsql":
3770
+ return "SERIAL";
3771
+ case "mysql":
3772
+ return "BIGINT UNSIGNED";
3773
+ case "sqlite":
3774
+ return "INTEGER";
3839
3775
  }
3840
- invalidate(key) {
3841
- return Promise.resolve(this.cache.invalidate(key));
3776
+ }
3777
+ function compileStringType(driver, length) {
3778
+ switch (driver) {
3779
+ case "pgsql":
3780
+ return "TEXT";
3781
+ case "mysql":
3782
+ return length ? `VARCHAR(${length})` : "VARCHAR(255)";
3783
+ case "sqlite":
3784
+ return "TEXT";
3842
3785
  }
3843
- invalidateByPrefix(prefix) {
3844
- return Promise.resolve(this.cache.invalidateByPrefix(prefix));
3786
+ }
3787
+ function compileTextType(driver) {
3788
+ switch (driver) {
3789
+ case "pgsql":
3790
+ case "sqlite":
3791
+ return "TEXT";
3792
+ case "mysql":
3793
+ return "TEXT";
3845
3794
  }
3846
- clear() {
3847
- this.cache.clear();
3848
- return Promise.resolve();
3795
+ }
3796
+ function compileBooleanType(driver) {
3797
+ switch (driver) {
3798
+ case "pgsql":
3799
+ return "BOOLEAN";
3800
+ case "mysql":
3801
+ return "BOOLEAN";
3802
+ case "sqlite":
3803
+ return "INTEGER";
3849
3804
  }
3850
- size() {
3851
- return Promise.resolve(this.cache.size());
3805
+ }
3806
+ function compileIntegerType(driver) {
3807
+ switch (driver) {
3808
+ case "pgsql":
3809
+ return "INTEGER";
3810
+ case "mysql":
3811
+ return "INT";
3812
+ case "sqlite":
3813
+ return "INTEGER";
3852
3814
  }
3853
3815
  }
3854
- var simpleCacheStore_default = SimpleCacheStore;
3855
-
3856
- // ../../src/core/cache/createCacheStore.ts
3857
- function createCacheStore(options) {
3858
- if (options.driver === "redis") {
3859
- if (!options.redisUrl) {
3860
- throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
3861
- }
3862
- return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
3816
+ function compileBigIntegerType(driver) {
3817
+ switch (driver) {
3818
+ case "pgsql":
3819
+ return "BIGINT";
3820
+ case "mysql":
3821
+ return "BIGINT";
3822
+ case "sqlite":
3823
+ return "INTEGER";
3863
3824
  }
3864
- return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
3865
3825
  }
3866
- // ../../src/core/cache/taggedCache.ts
3867
- class TaggedCache {
3868
- store;
3869
- tags;
3870
- constructor(store, tags) {
3871
- this.store = store;
3872
- this.tags = tags;
3826
+ function compileTimestampType(driver) {
3827
+ switch (driver) {
3828
+ case "pgsql":
3829
+ return "TIMESTAMPTZ";
3830
+ case "mysql":
3831
+ return "TIMESTAMP";
3832
+ case "sqlite":
3833
+ return "TEXT";
3873
3834
  }
3874
- async remember(key, callback, ttlMs) {
3875
- const value = await this.store.getOrSet(key, callback, ttlMs);
3876
- await this.store.attachTags(key, this.tags);
3877
- return value;
3835
+ }
3836
+ function compileJsonType(driver) {
3837
+ switch (driver) {
3838
+ case "pgsql":
3839
+ return "JSONB";
3840
+ case "mysql":
3841
+ return "JSON";
3842
+ case "sqlite":
3843
+ return "TEXT";
3878
3844
  }
3879
- async flush() {
3880
- return this.store.flushTags(this.tags);
3845
+ }
3846
+ function compileJsonbType(driver) {
3847
+ switch (driver) {
3848
+ case "pgsql":
3849
+ return "JSONB";
3850
+ case "mysql":
3851
+ return "JSON";
3852
+ case "sqlite":
3853
+ return "TEXT";
3881
3854
  }
3882
3855
  }
3883
- var taggedCache_default = TaggedCache;
3884
3856
 
3885
- // ../../src/core/cache/repository.ts
3886
- class CacheRepository {
3887
- store;
3888
- constructor(store) {
3889
- this.store = store;
3890
- }
3891
- async get(key) {
3892
- return this.store.get(key);
3893
- }
3894
- async remember(key, callback, ttlMs) {
3895
- return this.store.getOrSet(key, callback, ttlMs);
3857
+ // ../../src/core/database/schema/grammars/compileStatements.ts
3858
+ function compileCreateTable(driver, blueprint) {
3859
+ const table = quoteIdentifier(blueprint.table);
3860
+ const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
3861
+ for (const index of blueprint.indexes) {
3862
+ if (index.kind === "unique" && index.columns.length > 1) {
3863
+ const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
3864
+ parts.push(`UNIQUE (${columns})`);
3865
+ }
3896
3866
  }
3897
- async forget(key) {
3898
- return this.store.invalidate(key);
3867
+ const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
3868
+ ${parts.join(`,
3869
+ `)}
3870
+ )`];
3871
+ for (const index of blueprint.indexes) {
3872
+ if (index.kind === "unique" && index.columns.length === 1) {
3873
+ continue;
3874
+ }
3875
+ if (index.kind === "index") {
3876
+ statements.push(compileIndex(driver, blueprint.table, index));
3877
+ } else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
3878
+ statements.push(...compileSpecialIndex(driver, blueprint.table, index));
3879
+ }
3899
3880
  }
3900
- async flush() {
3901
- await this.store.clear();
3881
+ return statements;
3882
+ }
3883
+ function compileAlterTable(driver, blueprint) {
3884
+ const statements = [];
3885
+ const table = quoteIdentifier(blueprint.table);
3886
+ for (const column of blueprint.columns) {
3887
+ const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
3888
+ statements.push(`ALTER TABLE ${table}
3889
+ ${addPrefix} ${compileColumn(driver, column, "alter")}`);
3902
3890
  }
3903
- tags(...names) {
3904
- return new taggedCache_default(this.store, names);
3891
+ for (const columnName of blueprint.droppedColumns) {
3892
+ const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
3893
+ statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
3905
3894
  }
3906
- async getOrSet(key, loader, ttlMs) {
3907
- return this.remember(key, loader, ttlMs);
3895
+ for (const indexName of blueprint.droppedIndexes) {
3896
+ statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
3908
3897
  }
3909
- async invalidate(key) {
3910
- return this.forget(key);
3898
+ for (const index of blueprint.indexes) {
3899
+ if (index.kind === "index" || index.kind === "unique") {
3900
+ statements.push(compileIndex(driver, blueprint.table, index));
3901
+ } else {
3902
+ statements.push(...compileSpecialIndex(driver, blueprint.table, index));
3903
+ }
3911
3904
  }
3912
- async invalidateByPrefix(prefix) {
3913
- return this.store.invalidateByPrefix(prefix);
3905
+ return statements;
3906
+ }
3907
+ function compileDropTable(driver, tableName) {
3908
+ const cascade = driver === "pgsql" ? " CASCADE" : "";
3909
+ return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
3910
+ }
3911
+ function compileColumn(driver, column, mode) {
3912
+ const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
3913
+ if (column.autoIncrement && driver === "mysql") {
3914
+ parts[1] = `${parts[1]} AUTO_INCREMENT`;
3914
3915
  }
3915
- async clear() {
3916
- await this.flush();
3916
+ if (column.isPrimary && mode === "create") {
3917
+ if (driver === "sqlite") {
3918
+ parts.push("PRIMARY KEY AUTOINCREMENT");
3919
+ } else {
3920
+ parts.push("PRIMARY KEY");
3921
+ }
3922
+ } else if (!column.isNullable) {
3923
+ parts.push("NOT NULL");
3924
+ } else if (column.isNullable) {
3925
+ parts.push("NULL");
3917
3926
  }
3918
- async size() {
3919
- return this.store.size();
3927
+ if (column.defaultValue !== undefined) {
3928
+ parts.push(`DEFAULT ${column.defaultValue}`);
3920
3929
  }
3921
- }
3922
- var repository_default2 = CacheRepository;
3923
- // ../../src/core/cache/tags.ts
3924
- var CACHE_TAGS = {
3925
- organizations: "organizations",
3926
- projects: "projects",
3927
- tasks: "tasks",
3928
- comments: "comments",
3929
- attachments: "attachments",
3930
- reports: "reports"
3931
- };
3932
- // ../../src/core/contracts/container.ts
3933
- class ServiceContainer {
3934
- services = new Map;
3935
- singletonFactories = new Map;
3936
- bindings = new Map;
3937
- set(key, value) {
3938
- this.singletonFactories.delete(key);
3939
- this.bindings.delete(key);
3940
- this.services.set(key, value);
3941
- return value;
3930
+ if (column.isUnique) {
3931
+ parts.push("UNIQUE");
3942
3932
  }
3943
- singleton(key, factory) {
3944
- this.bindings.delete(key);
3945
- this.services.delete(key);
3946
- this.singletonFactories.set(key, factory);
3933
+ if (column.checkExpression) {
3934
+ parts.push(`CHECK (${column.checkExpression})`);
3947
3935
  }
3948
- bind(key, factory) {
3949
- this.singletonFactories.delete(key);
3950
- this.services.delete(key);
3951
- this.bindings.set(key, factory);
3936
+ if (column.foreignKey) {
3937
+ const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
3938
+ const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
3939
+ let clause = `REFERENCES ${reference}`;
3940
+ if (onDelete === "cascade") {
3941
+ clause += " ON DELETE CASCADE";
3942
+ } else if (onDelete === "set null") {
3943
+ clause += " ON DELETE SET NULL";
3944
+ }
3945
+ parts.push(clause);
3952
3946
  }
3953
- get(key) {
3954
- if (this.services.has(key)) {
3955
- return this.services.get(key);
3947
+ return parts.join(" ");
3948
+ }
3949
+ function compileIndex(_driver, tableName, index) {
3950
+ const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
3951
+ const columns = index.columns.map((column) => {
3952
+ const quoted = quoteIdentifier(column);
3953
+ if (index.order === "desc") {
3954
+ return `${quoted} DESC`;
3955
+ }
3956
+ return quoted;
3957
+ }).join(", ");
3958
+ const unique = index.kind === "unique" ? "UNIQUE " : "";
3959
+ return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
3960
+ }
3961
+ function compileSpecialIndex(driver, tableName, index) {
3962
+ const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
3963
+ const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
3964
+ switch (index.kind) {
3965
+ case "partial":
3966
+ case "uniquePartial": {
3967
+ if (driver !== "pgsql") {
3968
+ throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
3969
+ }
3970
+ const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
3971
+ return [
3972
+ `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
3973
+ ];
3956
3974
  }
3957
- const singletonFactory = this.singletonFactories.get(key);
3958
- if (singletonFactory) {
3959
- const value = singletonFactory(this);
3960
- this.services.set(key, value);
3961
- return value;
3975
+ case "gin": {
3976
+ if (driver !== "pgsql") {
3977
+ throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
3978
+ }
3979
+ return [
3980
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
3981
+ ];
3962
3982
  }
3963
- const binding = this.bindings.get(key);
3964
- if (binding) {
3965
- return binding(this);
3983
+ case "fullText": {
3984
+ if (driver === "mysql") {
3985
+ return [
3986
+ `CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
3987
+ ];
3988
+ }
3989
+ if (driver === "pgsql") {
3990
+ throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
3991
+ }
3992
+ throw new UnsupportedSchemaFeatureError("fullText()", driver);
3966
3993
  }
3967
- throw new Error(`Service "${key}" is not registered.`);
3968
- }
3969
- resolve(key) {
3970
- return this.get(key);
3994
+ default:
3995
+ return [];
3971
3996
  }
3972
- has(key) {
3973
- return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
3997
+ }
3998
+ function defaultIndexName(tableName, columns, kind) {
3999
+ return `idx_${tableName}_${columns.join("_")}_${kind}`;
4000
+ }
4001
+ function compileBlueprint(driver, blueprint) {
4002
+ switch (blueprint.action) {
4003
+ case "create":
4004
+ return compileCreateTable(driver, blueprint);
4005
+ case "alter":
4006
+ return compileAlterTable(driver, blueprint);
4007
+ case "drop":
4008
+ return compileDropTable(driver, blueprint.table);
4009
+ default:
4010
+ throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
3974
4011
  }
3975
4012
  }
4013
+ // ../../src/core/database/schema/grammars/createGrammar.ts
4014
+ function createGrammar(driver) {
4015
+ return {
4016
+ driver,
4017
+ compile(blueprint) {
4018
+ return compileBlueprint(driver, blueprint);
4019
+ }
4020
+ };
4021
+ }
3976
4022
 
3977
- class ConfigStore {
3978
- values = new Map;
3979
- set(key, value) {
3980
- this.values.set(key, value);
3981
- return value;
4023
+ // ../../src/core/database/schema/grammars/mysqlGrammar.ts
4024
+ var MySqlGrammar = createGrammar("mysql");
4025
+
4026
+ // ../../src/core/database/schema/grammars/postgresGrammar.ts
4027
+ var PostgresGrammar = createGrammar("pgsql");
4028
+
4029
+ // ../../src/core/database/schema/grammars/sqliteGrammar.ts
4030
+ var SqliteGrammar = createGrammar("sqlite");
4031
+
4032
+ // ../../src/core/database/schema/grammars/index.ts
4033
+ function grammarForDriver(driver) {
4034
+ switch (driver) {
4035
+ case "pgsql":
4036
+ return PostgresGrammar;
4037
+ case "mysql":
4038
+ return MySqlGrammar;
4039
+ case "sqlite":
4040
+ return SqliteGrammar;
4041
+ default:
4042
+ throw new Error(`Unsupported database driver: ${driver}`);
3982
4043
  }
3983
- get(key) {
3984
- return this.values.get(key);
4044
+ }
4045
+ // ../../src/core/database/schema/schema.ts
4046
+ class SchemaBuilder {
4047
+ #driver;
4048
+ #statements = [];
4049
+ constructor(driver) {
4050
+ this.#driver = driver;
3985
4051
  }
3986
- require(key) {
3987
- if (!this.values.has(key)) {
3988
- throw new Error(`Config key "${key}" is not defined.`);
3989
- }
3990
- return this.values.get(key);
4052
+ create(table, callback) {
4053
+ const blueprint = new Blueprint(table, "create");
4054
+ callback(blueprint);
4055
+ this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
4056
+ return this;
3991
4057
  }
3992
- has(key) {
3993
- return this.values.has(key);
4058
+ table(table, callback) {
4059
+ const blueprint = new Blueprint(table, "alter");
4060
+ callback(blueprint);
4061
+ this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
4062
+ return this;
3994
4063
  }
3995
- }
3996
- // ../../src/core/database/migrations/advisoryLock.ts
3997
- var MIGRATION_LOCK_KEY = 42424242;
3998
- async function withMigrationLock(db2, callback, lockKey = MIGRATION_LOCK_KEY) {
3999
- await db2.unsafe("SELECT pg_advisory_lock($1)", [lockKey]);
4000
- try {
4001
- return await callback();
4002
- } finally {
4003
- await db2.unsafe("SELECT pg_advisory_unlock($1)", [lockKey]);
4064
+ drop(table) {
4065
+ const blueprint = new Blueprint(table, "drop");
4066
+ this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
4067
+ return this;
4004
4068
  }
4005
- }
4006
- // ../../src/core/database/migrations/runner.ts
4007
- import { readdir } from "fs/promises";
4008
- import { join } from "path";
4009
- import { pathToFileURL } from "url";
4010
- var MIGRATIONS_TABLE = "framework_migrations";
4011
- async function ensureMigrationsTable(db2) {
4012
- await db2.unsafe(`
4013
- CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
4014
- name TEXT PRIMARY KEY,
4015
- batch INTEGER NOT NULL,
4016
- run_on TIMESTAMPTZ NOT NULL DEFAULT NOW()
4017
- )
4018
- `);
4019
- }
4020
- async function getAppliedMigrations(db2) {
4021
- await ensureMigrationsTable(db2);
4022
- return await db2.unsafe(`
4023
- SELECT name, batch
4024
- FROM ${MIGRATIONS_TABLE}
4025
- ORDER BY batch ASC, name ASC
4026
- `);
4027
- }
4028
- async function loadMigrationsFromDirectory(directory) {
4029
- const entries = await readdir(directory);
4030
- const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
4031
- const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
4032
- const moduleUrl = pathToFileURL(join(directory, fileName)).href;
4033
- const module = await import(moduleUrl);
4034
- return module.default;
4035
- }));
4036
- return loadedMigrations.filter((migration) => migration?.name !== undefined);
4037
- }
4038
- async function getMigrationStatus(db2, migrations) {
4039
- const applied = await getAppliedMigrations(db2);
4040
- const appliedByName = new Map(applied.map(({ name, batch }) => [name, Number(batch)]));
4041
- return migrations.map(({ name }) => ({
4042
- name,
4043
- status: appliedByName.has(name) ? "up" : "pending",
4044
- batch: appliedByName.get(name) ?? null
4045
- }));
4046
- }
4047
- async function runPendingMigrations(db2, migrations, options = {}) {
4048
- const applied = await getAppliedMigrations(db2);
4049
- const appliedNames = new Set(applied.map(({ name }) => name));
4050
- const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
4051
- const pendingMigrations = migrations.filter(({ name }) => !appliedNames.has(name));
4052
- for (const migration of pendingMigrations) {
4053
- options.onMigration?.(migration.name);
4054
- await migration.up(db2);
4055
- const inserted = await db2.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING RETURNING name`, [migration.name, nextBatch]);
4056
- if (inserted.length === 0) {
4057
- throw new Error(`Migration ${migration.name} was applied but not recorded.`);
4058
- }
4069
+ toSql() {
4070
+ return [...this.#statements];
4059
4071
  }
4060
- return pendingMigrations.length;
4061
- }
4062
- async function migrateDatabase(db2, migrations, options = {}) {
4063
- const { advisoryLock = false, onMigration } = options;
4064
- if (advisoryLock) {
4065
- return withMigrationLock(db2, () => runPendingMigrations(db2, migrations, { onMigration }));
4072
+ async execute(db2) {
4073
+ for (const statement of this.#statements) {
4074
+ await db2.unsafe(statement);
4075
+ }
4066
4076
  }
4067
- return runPendingMigrations(db2, migrations, { onMigration });
4068
4077
  }
4069
- async function rollbackDatabase(db2, migrations, options = {}) {
4070
- const applied = await getAppliedMigrations(db2);
4071
- if (applied.length === 0) {
4072
- return 0;
4078
+
4079
+ class Schema {
4080
+ static builder(driver) {
4081
+ return new SchemaBuilder(driver ?? resolveDatabaseDriver());
4073
4082
  }
4074
- const lastBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0);
4075
- const migrationsToRollback = new Set(applied.filter(({ batch }) => Number(batch) === lastBatch).map(({ name }) => name));
4076
- let rolledBack = 0;
4077
- for (const migration of [...migrations].reverse()) {
4078
- if (!migrationsToRollback.has(migration.name)) {
4079
- continue;
4080
- }
4081
- options.onMigration?.(migration.name);
4082
- await migration.down(db2);
4083
- await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = $1`, [migration.name]);
4084
- rolledBack += 1;
4083
+ static async run(db2, driver, callback) {
4084
+ const schema = Schema.builder(driver);
4085
+ await callback(schema);
4086
+ await schema.execute(db2);
4085
4087
  }
4086
- return rolledBack;
4087
4088
  }
4088
- async function freshDatabase(db2, migrations, options = {}) {
4089
- const runFresh = async () => {
4090
- const applied = await getAppliedMigrations(db2);
4091
- const appliedNames = new Set(applied.map(({ name }) => name));
4092
- const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
4093
- for (const migration of [...appliedMigrations].reverse()) {
4094
- options.onMigration?.(migration.name);
4095
- await migration.down(db2);
4096
- }
4097
- if (appliedMigrations.length > 0) {
4098
- await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
4089
+ function createSchemaBuilder(db2, driver) {
4090
+ const builder = Schema.builder(driver);
4091
+ return Object.assign(builder, {
4092
+ async commit() {
4093
+ await builder.execute(db2);
4099
4094
  }
4100
- await runPendingMigrations(db2, migrations, options);
4101
- };
4102
- if (options.advisoryLock) {
4103
- await withMigrationLock(db2, runFresh);
4104
- return;
4105
- }
4106
- await runFresh();
4095
+ });
4107
4096
  }
4108
4097
  // ../../src/core/database/seeders/runner.ts
4109
4098
  import { readdir as readdir2 } from "fs/promises";
@@ -4130,6 +4119,19 @@ async function runSeedersFromDirectory(directory, db2, options) {
4130
4119
  }
4131
4120
  return seeders.length;
4132
4121
  }
4122
+ // ../../src/core/database/transaction.ts
4123
+ function supportsTransactions(connection) {
4124
+ return typeof connection.begin === "function";
4125
+ }
4126
+ async function runInTransaction(operation) {
4127
+ const pool = resolveRepositoryConnection();
4128
+ if (!supportsTransactions(pool)) {
4129
+ throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
4130
+ }
4131
+ return await pool.begin(async (transaction) => {
4132
+ return await operation(createDatabaseConnection2(transaction));
4133
+ });
4134
+ }
4133
4135
  // ../../src/core/mail/mailer.ts
4134
4136
  function resolveSmtpConfig() {
4135
4137
  const host = process.env.MAIL_HOST?.trim();
@@ -6060,7 +6062,7 @@ var failedJobTable = defineTable({
6060
6062
  });
6061
6063
 
6062
6064
  // ../../src/core/queue/failedJobRepository.ts
6063
- class FailedJobRepository extends baseRepository_default {
6065
+ class FailedJobRepository extends BaseRepository {
6064
6066
  constructor() {
6065
6067
  super(failedJobTable);
6066
6068
  }
@@ -6853,7 +6855,7 @@ export {
6853
6855
  createFlashMiddleware,
6854
6856
  createFailedJobService,
6855
6857
  createDatabaseQueryProxy,
6856
- createDatabaseConnection,
6858
+ createDatabaseConnection2 as createDatabaseConnection,
6857
6859
  createCsrfTokenCookie,
6858
6860
  createCsrfProtection,
6859
6861
  createCsrfMiddleware,