@dyrected/core 2.5.39 → 2.5.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-4C4L6SD2.js +1524 -0
- package/dist/chunk-FCXE5CVC.js +1528 -0
- package/dist/index-BKYugiYp.d.cts +1988 -0
- package/dist/index-BKYugiYp.d.ts +1988 -0
- package/dist/index-BSWCiAen.d.cts +1983 -0
- package/dist/index-BSWCiAen.d.ts +1983 -0
- package/dist/index-BfLDb3b0.d.cts +1467 -0
- package/dist/index-BfLDb3b0.d.ts +1467 -0
- package/dist/index-Bi4sX_eC.d.cts +1491 -0
- package/dist/index-Bi4sX_eC.d.ts +1491 -0
- package/dist/index-C9Vp1DCT.d.cts +1563 -0
- package/dist/index-C9Vp1DCT.d.ts +1563 -0
- package/dist/index-CFezd8p1.d.cts +1986 -0
- package/dist/index-CFezd8p1.d.ts +1986 -0
- package/dist/index-DR2z1rrf.d.cts +1503 -0
- package/dist/index-DR2z1rrf.d.ts +1503 -0
- package/dist/index-DfkPT4NR.d.cts +1584 -0
- package/dist/index-DfkPT4NR.d.ts +1584 -0
- package/dist/index-xB1lMg79.d.cts +1456 -0
- package/dist/index-xB1lMg79.d.ts +1456 -0
- package/dist/index.cjs +78 -5
- package/dist/index.d.cts +77 -3
- package/dist/index.d.ts +77 -3
- package/dist/index.js +6 -2
- package/dist/server.cjs +486 -32
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +419 -29
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -5,13 +5,15 @@ import {
|
|
|
5
5
|
executeFieldAfterRead,
|
|
6
6
|
executeFieldBeforeChange,
|
|
7
7
|
generateOpenApi,
|
|
8
|
+
getAdminAuthCollection,
|
|
9
|
+
getPublicAdminAuthConfig,
|
|
8
10
|
initializeWorkflowDocument,
|
|
9
11
|
materializeWorkflowDocument,
|
|
10
12
|
normalizeConfig,
|
|
11
13
|
runCollectionHooks,
|
|
12
14
|
saveWorkflowDraft,
|
|
13
15
|
transitionWorkflow
|
|
14
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-FCXE5CVC.js";
|
|
15
17
|
|
|
16
18
|
// src/app.ts
|
|
17
19
|
import { Hono } from "hono";
|
|
@@ -889,7 +891,7 @@ var GlobalController = class {
|
|
|
889
891
|
query = beforeReadResult;
|
|
890
892
|
}
|
|
891
893
|
let data = await db.getGlobal({ slug: this.global.slug });
|
|
892
|
-
const isEmpty = !data ||
|
|
894
|
+
const isEmpty = !data || isFunctionallyEmpty(data);
|
|
893
895
|
if (isEmpty && this.global.initialData) {
|
|
894
896
|
console.log(`[dyrected/core] Auto-seeding global "${this.global.slug}" from config.initialData`);
|
|
895
897
|
await db.updateGlobal({ slug: this.global.slug, data: this.global.initialData });
|
|
@@ -960,7 +962,7 @@ var GlobalController = class {
|
|
|
960
962
|
return c.json({ message: "Invalid initial data" }, 400);
|
|
961
963
|
}
|
|
962
964
|
const existing = await db.getGlobal({ slug: this.global.slug });
|
|
963
|
-
if (existing &&
|
|
965
|
+
if (existing && !isFunctionallyEmpty(existing)) {
|
|
964
966
|
return c.json({ message: "Global is not empty, skipping seed" });
|
|
965
967
|
}
|
|
966
968
|
console.log(`[dyrected/core] Auto-seeding global: ${this.global.slug}`);
|
|
@@ -968,6 +970,19 @@ var GlobalController = class {
|
|
|
968
970
|
return c.json({ message: "Seed successful", data: initialData }, 201);
|
|
969
971
|
}
|
|
970
972
|
};
|
|
973
|
+
function isFunctionallyEmpty(obj) {
|
|
974
|
+
if (obj === null || obj === void 0 || obj === "") return true;
|
|
975
|
+
if (Array.isArray(obj)) {
|
|
976
|
+
if (obj.length === 0) return true;
|
|
977
|
+
return obj.every(isFunctionallyEmpty);
|
|
978
|
+
}
|
|
979
|
+
if (typeof obj === "object") {
|
|
980
|
+
const keys = Object.keys(obj);
|
|
981
|
+
if (keys.length === 0) return true;
|
|
982
|
+
return keys.every((key) => isFunctionallyEmpty(obj[key]));
|
|
983
|
+
}
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
971
986
|
|
|
972
987
|
// src/controllers/media.controller.ts
|
|
973
988
|
var MediaController = class {
|
|
@@ -1603,13 +1618,371 @@ var AuthController = class {
|
|
|
1603
1618
|
}
|
|
1604
1619
|
};
|
|
1605
1620
|
|
|
1606
|
-
// src/controllers/
|
|
1607
|
-
import {
|
|
1621
|
+
// src/controllers/admin-auth.controller.ts
|
|
1622
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1608
1623
|
import { TextEncoder as TextEncoder2 } from "util";
|
|
1624
|
+
import {
|
|
1625
|
+
SignJWT as SignJWT2,
|
|
1626
|
+
createRemoteJWKSet,
|
|
1627
|
+
jwtVerify as jwtVerify2,
|
|
1628
|
+
decodeJwt as decodeJwt2
|
|
1629
|
+
} from "jose";
|
|
1630
|
+
var AdminAuthController = class {
|
|
1631
|
+
constructor(config) {
|
|
1632
|
+
this.config = config;
|
|
1633
|
+
}
|
|
1634
|
+
config;
|
|
1635
|
+
providers(c) {
|
|
1636
|
+
return c.json(getPublicAdminAuthConfig(this.config.adminAuth));
|
|
1637
|
+
}
|
|
1638
|
+
async start(c) {
|
|
1639
|
+
const provider = this.getProvider(c.req.param("provider"));
|
|
1640
|
+
if (!provider) {
|
|
1641
|
+
return c.json({ error: true, message: "Admin auth provider not found." }, 404);
|
|
1642
|
+
}
|
|
1643
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
1644
|
+
const returnTo = this.normalizeReturnTo(c.req.query("returnTo"), c);
|
|
1645
|
+
if (provider.type === "oidc") {
|
|
1646
|
+
const redirectUrl = await this.buildOidcStartUrl(
|
|
1647
|
+
provider,
|
|
1648
|
+
returnTo,
|
|
1649
|
+
siteId,
|
|
1650
|
+
new URL(c.req.url).origin
|
|
1651
|
+
);
|
|
1652
|
+
return c.redirect(redirectUrl, 302);
|
|
1653
|
+
}
|
|
1654
|
+
if (!provider.startUrl) {
|
|
1655
|
+
return c.json({ error: true, message: "This provider does not expose a start URL." }, 400);
|
|
1656
|
+
}
|
|
1657
|
+
const url = new URL(provider.startUrl);
|
|
1658
|
+
url.searchParams.set("returnTo", returnTo);
|
|
1659
|
+
if (siteId) url.searchParams.set("siteId", siteId);
|
|
1660
|
+
url.searchParams.set("provider", provider.id);
|
|
1661
|
+
return c.redirect(url.toString(), 302);
|
|
1662
|
+
}
|
|
1663
|
+
async callback(c) {
|
|
1664
|
+
const provider = this.getProvider(c.req.param("provider"));
|
|
1665
|
+
if (!provider) {
|
|
1666
|
+
return c.json({ error: true, message: "Admin auth provider not found." }, 404);
|
|
1667
|
+
}
|
|
1668
|
+
try {
|
|
1669
|
+
const exchange = await this.completeProviderAuth(provider, c);
|
|
1670
|
+
const redirectUrl = new URL(exchange.returnTo);
|
|
1671
|
+
redirectUrl.searchParams.set("dyrectedExternalToken", exchange.token);
|
|
1672
|
+
redirectUrl.searchParams.set("dyrectedAdminCollection", exchange.collectionSlug);
|
|
1673
|
+
return c.redirect(redirectUrl.toString(), 302);
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
const message = error instanceof Error ? error.message : "External authentication failed.";
|
|
1676
|
+
const fallback = this.normalizeReturnTo(c.req.query("returnTo"), c);
|
|
1677
|
+
const redirectUrl = new URL(fallback);
|
|
1678
|
+
redirectUrl.searchParams.set("dyrectedExternalError", message);
|
|
1679
|
+
return c.redirect(redirectUrl.toString(), 302);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
async exchange(c) {
|
|
1683
|
+
const provider = this.getProvider(c.req.param("provider"));
|
|
1684
|
+
if (!provider) {
|
|
1685
|
+
return c.json({ error: true, message: "Admin auth provider not found." }, 404);
|
|
1686
|
+
}
|
|
1687
|
+
try {
|
|
1688
|
+
const exchange = await this.completeProviderAuth(provider, c);
|
|
1689
|
+
return c.json({
|
|
1690
|
+
token: exchange.token,
|
|
1691
|
+
collectionSlug: exchange.collectionSlug,
|
|
1692
|
+
providerId: provider.id
|
|
1693
|
+
});
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
const message = error instanceof Error ? error.message : "External authentication failed.";
|
|
1696
|
+
const status = message === "Access denied for this site." ? 403 : 401;
|
|
1697
|
+
return c.json({ error: true, message }, status);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
async logout(c) {
|
|
1701
|
+
return c.json({ success: true, message: "Logged out. Discard your token." });
|
|
1702
|
+
}
|
|
1703
|
+
getProvider(id) {
|
|
1704
|
+
if (this.config.adminAuth?.mode !== "external") return void 0;
|
|
1705
|
+
return this.config.adminAuth.providers.find((provider) => provider.id === id);
|
|
1706
|
+
}
|
|
1707
|
+
async completeProviderAuth(provider, c) {
|
|
1708
|
+
const resolved = provider.type === "oidc" ? await this.resolveOidcIdentity(provider, c) : await this.resolveCustomIdentity(provider, c);
|
|
1709
|
+
const identity = resolved.identity;
|
|
1710
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
1711
|
+
const adminCollection = getAdminAuthCollection(this.config);
|
|
1712
|
+
if (!adminCollection?.auth) {
|
|
1713
|
+
throw new Error("Admin auth collection is not configured.");
|
|
1714
|
+
}
|
|
1715
|
+
const db = c.get("config").db;
|
|
1716
|
+
if (!db) throw new Error("Database not configured.");
|
|
1717
|
+
let user = await this.findUserByExternalIdentity(db, adminCollection.slug, provider.id, identity.sub) ?? (identity.email ? (await db.find({
|
|
1718
|
+
collection: adminCollection.slug,
|
|
1719
|
+
where: { email: identity.email },
|
|
1720
|
+
limit: 1
|
|
1721
|
+
})).docs[0] : null);
|
|
1722
|
+
const provisioningMode = this.config.adminAuth?.provisioningMode ?? "jit_plus_membership_management";
|
|
1723
|
+
const allowJitProvisioning = provisioningMode !== "preprovisioned_only" && provider.allowJitProvisioning !== false;
|
|
1724
|
+
if (!user && !allowJitProvisioning) {
|
|
1725
|
+
throw new Error("This account has not been provisioned for admin access.");
|
|
1726
|
+
}
|
|
1727
|
+
const access = await this.resolveAccess(identity, provider.id, siteId, c, user);
|
|
1728
|
+
if (!access.allowed) {
|
|
1729
|
+
throw new Error("Access denied for this site.");
|
|
1730
|
+
}
|
|
1731
|
+
if (!user && allowJitProvisioning) {
|
|
1732
|
+
user = await db.create({
|
|
1733
|
+
collection: adminCollection.slug,
|
|
1734
|
+
data: {
|
|
1735
|
+
...await this.buildUserData(identity, provider.id, access.roles, access.data),
|
|
1736
|
+
password: await hashPassword(randomBytes2(32).toString("hex"))
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
} else if (user) {
|
|
1740
|
+
user = await db.update({
|
|
1741
|
+
collection: adminCollection.slug,
|
|
1742
|
+
id: user.id,
|
|
1743
|
+
data: await this.buildUserData(identity, provider.id, access.roles, access.data)
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
if (!user?.email) {
|
|
1747
|
+
throw new Error("Authenticated admin user is missing an email address.");
|
|
1748
|
+
}
|
|
1749
|
+
const token = await signCollectionToken({
|
|
1750
|
+
sub: user.id,
|
|
1751
|
+
email: user.email,
|
|
1752
|
+
collection: adminCollection.slug,
|
|
1753
|
+
providerId: provider.id,
|
|
1754
|
+
authSource: "external"
|
|
1755
|
+
});
|
|
1756
|
+
return {
|
|
1757
|
+
token,
|
|
1758
|
+
collectionSlug: adminCollection.slug,
|
|
1759
|
+
returnTo: resolved.returnTo
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
async resolveAccess(identity, providerId, siteId, c, user) {
|
|
1763
|
+
const resolved = await this.config.adminAuth?.resolveAccess?.({
|
|
1764
|
+
identity,
|
|
1765
|
+
providerId,
|
|
1766
|
+
siteId,
|
|
1767
|
+
workspaceId: c.get("workspaceId"),
|
|
1768
|
+
req: {
|
|
1769
|
+
method: c.req.method,
|
|
1770
|
+
path: c.req.path,
|
|
1771
|
+
url: c.req.url,
|
|
1772
|
+
headers: Object.fromEntries(c.req.raw.headers.entries())
|
|
1773
|
+
},
|
|
1774
|
+
user
|
|
1775
|
+
});
|
|
1776
|
+
if (resolved) return resolved;
|
|
1777
|
+
return { allowed: true, roles: identity.roles, data: {} };
|
|
1778
|
+
}
|
|
1779
|
+
async buildUserData(identity, providerId, roles, extraData) {
|
|
1780
|
+
return {
|
|
1781
|
+
...identity.email ? { email: identity.email } : {},
|
|
1782
|
+
...identity.name ? { name: identity.name } : {},
|
|
1783
|
+
...roles ? { roles } : {},
|
|
1784
|
+
...extraData ?? {},
|
|
1785
|
+
authProvider: providerId,
|
|
1786
|
+
externalSubject: identity.sub,
|
|
1787
|
+
authSource: "external",
|
|
1788
|
+
lastLoginAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
async findUserByExternalIdentity(db, collection, providerId, externalSubject) {
|
|
1792
|
+
const result = await db.find({
|
|
1793
|
+
collection,
|
|
1794
|
+
where: {
|
|
1795
|
+
authProvider: providerId,
|
|
1796
|
+
externalSubject
|
|
1797
|
+
},
|
|
1798
|
+
limit: 1
|
|
1799
|
+
});
|
|
1800
|
+
return result.docs[0] ?? null;
|
|
1801
|
+
}
|
|
1802
|
+
async buildOidcStartUrl(provider, returnTo, siteId, origin) {
|
|
1803
|
+
const discovery = await this.getOidcDiscovery(provider);
|
|
1804
|
+
const nonce = randomBytes2(16).toString("hex");
|
|
1805
|
+
const state = await this.signState({
|
|
1806
|
+
providerId: provider.id,
|
|
1807
|
+
returnTo,
|
|
1808
|
+
siteId,
|
|
1809
|
+
nonce
|
|
1810
|
+
});
|
|
1811
|
+
const url = new URL(provider.authorizationEndpoint || discovery.authorization_endpoint);
|
|
1812
|
+
url.searchParams.set("response_type", "code");
|
|
1813
|
+
url.searchParams.set("client_id", provider.clientId);
|
|
1814
|
+
url.searchParams.set("redirect_uri", provider.redirectUri || this.defaultCallbackPath(provider.id, origin));
|
|
1815
|
+
url.searchParams.set("scope", (provider.scopes ?? ["openid", "profile", "email"]).join(" "));
|
|
1816
|
+
url.searchParams.set("state", state);
|
|
1817
|
+
url.searchParams.set("nonce", nonce);
|
|
1818
|
+
return url.toString();
|
|
1819
|
+
}
|
|
1820
|
+
async resolveOidcIdentity(provider, c) {
|
|
1821
|
+
const code = c.req.query("code");
|
|
1822
|
+
const state = c.req.query("state");
|
|
1823
|
+
if (!code || !state) throw new Error("Missing OIDC callback parameters.");
|
|
1824
|
+
const parsedState = await this.verifyState(state, provider.id);
|
|
1825
|
+
const discovery = await this.getOidcDiscovery(provider);
|
|
1826
|
+
const redirectUri = provider.redirectUri || this.defaultCallbackPath(provider.id, new URL(c.req.url).origin);
|
|
1827
|
+
const tokenResponse = await fetch(provider.tokenEndpoint || discovery.token_endpoint, {
|
|
1828
|
+
method: "POST",
|
|
1829
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1830
|
+
body: new URLSearchParams({
|
|
1831
|
+
grant_type: "authorization_code",
|
|
1832
|
+
code,
|
|
1833
|
+
redirect_uri: redirectUri,
|
|
1834
|
+
client_id: provider.clientId,
|
|
1835
|
+
client_secret: provider.clientSecret
|
|
1836
|
+
})
|
|
1837
|
+
});
|
|
1838
|
+
if (!tokenResponse.ok) {
|
|
1839
|
+
throw new Error("OIDC token exchange failed.");
|
|
1840
|
+
}
|
|
1841
|
+
const tokenBody = await tokenResponse.json();
|
|
1842
|
+
const idToken = tokenBody.id_token;
|
|
1843
|
+
let claims = {};
|
|
1844
|
+
if (idToken) {
|
|
1845
|
+
const jwks = createRemoteJWKSet(new URL(discovery.jwks_uri));
|
|
1846
|
+
const { payload } = await jwtVerify2(idToken, jwks, {
|
|
1847
|
+
issuer: provider.issuer,
|
|
1848
|
+
audience: provider.clientId
|
|
1849
|
+
});
|
|
1850
|
+
if (parsedState.nonce && payload.nonce !== parsedState.nonce) {
|
|
1851
|
+
throw new Error("OIDC nonce verification failed.");
|
|
1852
|
+
}
|
|
1853
|
+
claims = payload;
|
|
1854
|
+
} else if (tokenBody.access_token && (provider.userInfoEndpoint || discovery.userinfo_endpoint)) {
|
|
1855
|
+
const userInfo = await fetch(provider.userInfoEndpoint || discovery.userinfo_endpoint, {
|
|
1856
|
+
headers: { Authorization: `Bearer ${tokenBody.access_token}` }
|
|
1857
|
+
});
|
|
1858
|
+
if (!userInfo.ok) throw new Error("OIDC userinfo request failed.");
|
|
1859
|
+
claims = await userInfo.json();
|
|
1860
|
+
} else {
|
|
1861
|
+
throw new Error("OIDC provider did not return usable identity claims.");
|
|
1862
|
+
}
|
|
1863
|
+
return {
|
|
1864
|
+
identity: this.mapClaims(claims, provider.claimMapping),
|
|
1865
|
+
returnTo: parsedState.returnTo
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
async resolveCustomIdentity(provider, c) {
|
|
1869
|
+
const body = c.req.method === "POST" ? await c.req.json().catch(() => ({})) : {};
|
|
1870
|
+
const tokenParam = provider.tokenParam || "token";
|
|
1871
|
+
const token = body?.[tokenParam] || c.req.query(tokenParam);
|
|
1872
|
+
if (!token) {
|
|
1873
|
+
throw new Error(`Missing ${tokenParam} for external auth exchange.`);
|
|
1874
|
+
}
|
|
1875
|
+
let claims;
|
|
1876
|
+
if (provider.secret) {
|
|
1877
|
+
const secret = new TextEncoder2().encode(provider.secret);
|
|
1878
|
+
const verified = await jwtVerify2(token, secret, {
|
|
1879
|
+
issuer: provider.issuer,
|
|
1880
|
+
audience: provider.audience
|
|
1881
|
+
});
|
|
1882
|
+
claims = verified.payload;
|
|
1883
|
+
} else if (provider.jwksUri) {
|
|
1884
|
+
const jwks = createRemoteJWKSet(new URL(provider.jwksUri));
|
|
1885
|
+
const verified = await jwtVerify2(token, jwks, {
|
|
1886
|
+
issuer: provider.issuer,
|
|
1887
|
+
audience: provider.audience
|
|
1888
|
+
});
|
|
1889
|
+
claims = verified.payload;
|
|
1890
|
+
} else {
|
|
1891
|
+
claims = decodeJwt2(token);
|
|
1892
|
+
if (provider.issuer && claims.iss !== provider.issuer) {
|
|
1893
|
+
throw new Error("External auth issuer mismatch.");
|
|
1894
|
+
}
|
|
1895
|
+
if (provider.audience) {
|
|
1896
|
+
const aud = claims.aud;
|
|
1897
|
+
const matches = Array.isArray(aud) ? aud.includes(provider.audience) : aud === provider.audience;
|
|
1898
|
+
if (!matches) throw new Error("External auth audience mismatch.");
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
return {
|
|
1902
|
+
identity: this.mapClaims(claims, provider.claimMapping),
|
|
1903
|
+
returnTo: this.normalizeReturnTo(
|
|
1904
|
+
body?.returnTo || c.req.query("returnTo"),
|
|
1905
|
+
c
|
|
1906
|
+
)
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
mapClaims(claims, mapping) {
|
|
1910
|
+
const subKey = mapping?.sub || "sub";
|
|
1911
|
+
const emailKey = mapping?.email || "email";
|
|
1912
|
+
const nameKey = mapping?.name || "name";
|
|
1913
|
+
const rolesKey = mapping?.roles;
|
|
1914
|
+
const groupsKey = mapping?.groups;
|
|
1915
|
+
const siteIdsKey = mapping?.siteIds;
|
|
1916
|
+
const workspaceIdsKey = mapping?.workspaceIds;
|
|
1917
|
+
const sub = claims[subKey];
|
|
1918
|
+
if (typeof sub !== "string" || !sub) {
|
|
1919
|
+
throw new Error("External identity is missing a subject.");
|
|
1920
|
+
}
|
|
1921
|
+
return {
|
|
1922
|
+
sub,
|
|
1923
|
+
email: this.readStringClaim(claims[emailKey]),
|
|
1924
|
+
name: this.readStringClaim(claims[nameKey]),
|
|
1925
|
+
roles: this.readStringArrayClaim(rolesKey ? claims[rolesKey] : void 0),
|
|
1926
|
+
groups: this.readStringArrayClaim(groupsKey ? claims[groupsKey] : void 0),
|
|
1927
|
+
siteIds: this.readStringArrayClaim(siteIdsKey ? claims[siteIdsKey] : void 0),
|
|
1928
|
+
workspaceIds: this.readStringArrayClaim(workspaceIdsKey ? claims[workspaceIdsKey] : void 0),
|
|
1929
|
+
rawClaims: claims
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
readStringClaim(value) {
|
|
1933
|
+
return typeof value === "string" ? value : void 0;
|
|
1934
|
+
}
|
|
1935
|
+
readStringArrayClaim(value) {
|
|
1936
|
+
if (!value) return void 0;
|
|
1937
|
+
if (Array.isArray(value)) {
|
|
1938
|
+
return value.filter((entry) => typeof entry === "string");
|
|
1939
|
+
}
|
|
1940
|
+
if (typeof value === "string") return [value];
|
|
1941
|
+
return void 0;
|
|
1942
|
+
}
|
|
1943
|
+
async getOidcDiscovery(provider) {
|
|
1944
|
+
const discoveryUrl = new URL(
|
|
1945
|
+
provider.issuer.replace(/\/$/, "") + "/.well-known/openid-configuration"
|
|
1946
|
+
);
|
|
1947
|
+
const response = await fetch(discoveryUrl.toString());
|
|
1948
|
+
if (!response.ok) throw new Error("Failed to load OIDC discovery document.");
|
|
1949
|
+
return response.json();
|
|
1950
|
+
}
|
|
1951
|
+
defaultCallbackPath(providerId, origin = "") {
|
|
1952
|
+
return `${origin}/api/admin/auth/${providerId}/callback`;
|
|
1953
|
+
}
|
|
1954
|
+
async signState(payload) {
|
|
1955
|
+
return new SignJWT2(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("15m").sign(this.getStateSecret());
|
|
1956
|
+
}
|
|
1957
|
+
async verifyState(token, providerId) {
|
|
1958
|
+
const { payload } = await jwtVerify2(token, this.getStateSecret());
|
|
1959
|
+
const state = payload;
|
|
1960
|
+
if (state.providerId !== providerId) {
|
|
1961
|
+
throw new Error("OIDC state verification failed.");
|
|
1962
|
+
}
|
|
1963
|
+
return state;
|
|
1964
|
+
}
|
|
1965
|
+
getStateSecret() {
|
|
1966
|
+
const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET;
|
|
1967
|
+
if (!secret) {
|
|
1968
|
+
throw new Error("[dyrected/core] DYRECTED_JWT_SECRET is not set.");
|
|
1969
|
+
}
|
|
1970
|
+
return new TextEncoder2().encode(secret);
|
|
1971
|
+
}
|
|
1972
|
+
normalizeReturnTo(returnTo, c) {
|
|
1973
|
+
if (returnTo) return returnTo;
|
|
1974
|
+
const url = new URL(c.req.url);
|
|
1975
|
+
return `${url.origin}/admin`;
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
|
|
1979
|
+
// src/controllers/preview.controller.ts
|
|
1980
|
+
import { SignJWT as SignJWT3, jwtVerify as jwtVerify3 } from "jose";
|
|
1981
|
+
import { TextEncoder as TextEncoder3 } from "util";
|
|
1609
1982
|
var PreviewController = class {
|
|
1610
1983
|
getSecret() {
|
|
1611
1984
|
const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET || "dyrected-preview-secret-change-me";
|
|
1612
|
-
return new
|
|
1985
|
+
return new TextEncoder3().encode(secret);
|
|
1613
1986
|
}
|
|
1614
1987
|
/**
|
|
1615
1988
|
* POST /api/preview-token
|
|
@@ -1620,7 +1993,7 @@ var PreviewController = class {
|
|
|
1620
1993
|
if (!body?.collectionSlug || !body?.data) {
|
|
1621
1994
|
return c.json({ error: true, message: "collectionSlug and data are required." }, 400);
|
|
1622
1995
|
}
|
|
1623
|
-
const token = await new
|
|
1996
|
+
const token = await new SignJWT3({
|
|
1624
1997
|
collectionSlug: body.collectionSlug,
|
|
1625
1998
|
documentId: body.documentId,
|
|
1626
1999
|
data: body.data
|
|
@@ -1638,7 +2011,7 @@ var PreviewController = class {
|
|
|
1638
2011
|
return c.json({ error: true, message: "token query parameter is required." }, 400);
|
|
1639
2012
|
}
|
|
1640
2013
|
try {
|
|
1641
|
-
const { payload } = await
|
|
2014
|
+
const { payload } = await jwtVerify3(token, this.getSecret());
|
|
1642
2015
|
return c.json(payload);
|
|
1643
2016
|
} catch (err) {
|
|
1644
2017
|
return c.json({ error: true, message: "Invalid or expired preview token." }, 401);
|
|
@@ -1798,6 +2171,9 @@ function registerRoutes(app, config) {
|
|
|
1798
2171
|
if (dynamic.admin) {
|
|
1799
2172
|
config.admin = { ...config.admin, ...dynamic.admin };
|
|
1800
2173
|
}
|
|
2174
|
+
if (dynamic.adminAuth) {
|
|
2175
|
+
config.adminAuth = { ...config.adminAuth, ...dynamic.adminAuth };
|
|
2176
|
+
}
|
|
1801
2177
|
}
|
|
1802
2178
|
const user = c.get("user");
|
|
1803
2179
|
const accessArgs = { user, req: c.req, doc: null };
|
|
@@ -1886,7 +2262,8 @@ function registerRoutes(app, config) {
|
|
|
1886
2262
|
return c.json({
|
|
1887
2263
|
collections: filteredCollections,
|
|
1888
2264
|
globals: filteredGlobals,
|
|
1889
|
-
admin: config.admin || {}
|
|
2265
|
+
admin: config.admin || {},
|
|
2266
|
+
adminAuth: getPublicAdminAuthConfig(config.adminAuth)
|
|
1890
2267
|
});
|
|
1891
2268
|
});
|
|
1892
2269
|
app.get("/api/dyrected/options/:collection/:field", optionalAuth(), async (c) => {
|
|
@@ -2078,6 +2455,12 @@ function registerRoutes(app, config) {
|
|
|
2078
2455
|
app.delete(`${prefix}/media/:id`, accessGate(col, "delete"), (c) => mediaController.delete(c));
|
|
2079
2456
|
}
|
|
2080
2457
|
}
|
|
2458
|
+
const adminAuthController = new AdminAuthController(config);
|
|
2459
|
+
app.get("/api/admin/auth/providers", (c) => adminAuthController.providers(c));
|
|
2460
|
+
app.get("/api/admin/auth/:provider/start", (c) => adminAuthController.start(c));
|
|
2461
|
+
app.get("/api/admin/auth/:provider/callback", (c) => adminAuthController.callback(c));
|
|
2462
|
+
app.post("/api/admin/auth/:provider/exchange", (c) => adminAuthController.exchange(c));
|
|
2463
|
+
app.post("/api/admin/logout", (c) => adminAuthController.logout(c));
|
|
2081
2464
|
for (const collection of config.collections) {
|
|
2082
2465
|
if (!collection.auth) continue;
|
|
2083
2466
|
const path = `/api/collections/${collection.slug}`;
|
|
@@ -2122,9 +2505,8 @@ function registerRoutes(app, config) {
|
|
|
2122
2505
|
const previewController = new PreviewController();
|
|
2123
2506
|
app.post("/api/preview-token", requireAuth(), (c) => previewController.createToken(c));
|
|
2124
2507
|
app.get("/api/preview-data", (c) => previewController.getData(c));
|
|
2125
|
-
app.
|
|
2508
|
+
app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(), async (c) => {
|
|
2126
2509
|
const slug = c.req.param("slug");
|
|
2127
|
-
const id = c.req.param("id");
|
|
2128
2510
|
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
2129
2511
|
const config2 = c.get("config");
|
|
2130
2512
|
if (config2.collections.some((col) => col.slug === slug)) {
|
|
@@ -2139,23 +2521,25 @@ function registerRoutes(app, config) {
|
|
|
2139
2521
|
return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
|
|
2140
2522
|
}
|
|
2141
2523
|
const controller = new CollectionController(collection);
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
if (key === "id") return id;
|
|
2151
|
-
return origParam(key);
|
|
2152
|
-
};
|
|
2153
|
-
return controller.transition(c);
|
|
2524
|
+
return controller.transition(c);
|
|
2525
|
+
});
|
|
2526
|
+
app.get("/api/collections/:slug/:id/workflow-history", requireAuth(), async (c) => {
|
|
2527
|
+
const slug = c.req.param("slug");
|
|
2528
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
2529
|
+
const config2 = c.get("config");
|
|
2530
|
+
if (config2.collections.some((col) => col.slug === slug)) {
|
|
2531
|
+
return c.json({ message: "Not Found" }, 404);
|
|
2154
2532
|
}
|
|
2155
|
-
if (
|
|
2156
|
-
return
|
|
2533
|
+
if (!config2.onSchemaFetch || !siteId) {
|
|
2534
|
+
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
2535
|
+
}
|
|
2536
|
+
const dynamic = await config2.onSchemaFetch(siteId);
|
|
2537
|
+
const collection = dynamic.collections?.find((col) => col.slug === slug);
|
|
2538
|
+
if (!collection?.workflow) {
|
|
2539
|
+
return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
|
|
2157
2540
|
}
|
|
2158
|
-
|
|
2541
|
+
const controller = new CollectionController(collection);
|
|
2542
|
+
return controller.workflowHistory(c);
|
|
2159
2543
|
});
|
|
2160
2544
|
app.all("/api/collections/:slug/:id?", async (c) => {
|
|
2161
2545
|
const slug = c.req.param("slug");
|
|
@@ -2204,8 +2588,9 @@ function registerRoutes(app, config) {
|
|
|
2204
2588
|
}
|
|
2205
2589
|
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
2206
2590
|
});
|
|
2207
|
-
app.all("/api/globals/:slug", async (c) => {
|
|
2591
|
+
app.all("/api/globals/:slug/:id?", async (c) => {
|
|
2208
2592
|
const slug = c.req.param("slug");
|
|
2593
|
+
const id = c.req.param("id");
|
|
2209
2594
|
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
2210
2595
|
const config2 = c.get("config");
|
|
2211
2596
|
if (config2.globals.some((glb) => glb.slug === slug)) {
|
|
@@ -2216,8 +2601,13 @@ function registerRoutes(app, config) {
|
|
|
2216
2601
|
const global = dynamic.globals?.find((glb) => glb.slug === slug);
|
|
2217
2602
|
if (global) {
|
|
2218
2603
|
const controller = new GlobalController(global);
|
|
2219
|
-
|
|
2220
|
-
if (
|
|
2604
|
+
const method = c.req.method;
|
|
2605
|
+
if (id) {
|
|
2606
|
+
if (method === "POST" && id === "seed") return controller.seed(c);
|
|
2607
|
+
} else {
|
|
2608
|
+
if (method === "GET") return controller.get(c);
|
|
2609
|
+
if (method === "PATCH") return controller.update(c);
|
|
2610
|
+
}
|
|
2221
2611
|
}
|
|
2222
2612
|
}
|
|
2223
2613
|
return c.json({ message: `Global "${slug}" not found` }, 404);
|