@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.cjs
CHANGED
|
@@ -395,13 +395,15 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user, db) {
|
|
|
395
395
|
let updatedValue = value;
|
|
396
396
|
if (field.hooks?.beforeChange) {
|
|
397
397
|
for (const hook of field.hooks.beforeChange) {
|
|
398
|
-
|
|
398
|
+
const typedHook = hook;
|
|
399
|
+
const hookArgs = {
|
|
399
400
|
value: updatedValue,
|
|
400
401
|
originalDoc: originalDoc ?? void 0,
|
|
401
402
|
data: result,
|
|
402
403
|
user,
|
|
403
404
|
db
|
|
404
|
-
}
|
|
405
|
+
};
|
|
406
|
+
updatedValue = await typedHook(hookArgs);
|
|
405
407
|
}
|
|
406
408
|
result[field.name] = updatedValue;
|
|
407
409
|
}
|
|
@@ -461,12 +463,14 @@ async function executeFieldAfterRead(fields, doc, user, db) {
|
|
|
461
463
|
let updatedValue = value;
|
|
462
464
|
if (field.hooks?.afterRead) {
|
|
463
465
|
for (const hook of field.hooks.afterRead) {
|
|
464
|
-
|
|
466
|
+
const typedHook = hook;
|
|
467
|
+
const hookArgs = {
|
|
465
468
|
value: updatedValue,
|
|
466
469
|
doc: result,
|
|
467
470
|
user,
|
|
468
471
|
db
|
|
469
|
-
}
|
|
472
|
+
};
|
|
473
|
+
updatedValue = await typedHook(hookArgs);
|
|
470
474
|
}
|
|
471
475
|
result[field.name] = updatedValue;
|
|
472
476
|
}
|
|
@@ -1363,7 +1367,7 @@ var GlobalController = class {
|
|
|
1363
1367
|
query = beforeReadResult;
|
|
1364
1368
|
}
|
|
1365
1369
|
let data = await db.getGlobal({ slug: this.global.slug });
|
|
1366
|
-
const isEmpty = !data ||
|
|
1370
|
+
const isEmpty = !data || isFunctionallyEmpty(data);
|
|
1367
1371
|
if (isEmpty && this.global.initialData) {
|
|
1368
1372
|
console.log(`[dyrected/core] Auto-seeding global "${this.global.slug}" from config.initialData`);
|
|
1369
1373
|
await db.updateGlobal({ slug: this.global.slug, data: this.global.initialData });
|
|
@@ -1434,7 +1438,7 @@ var GlobalController = class {
|
|
|
1434
1438
|
return c.json({ message: "Invalid initial data" }, 400);
|
|
1435
1439
|
}
|
|
1436
1440
|
const existing = await db.getGlobal({ slug: this.global.slug });
|
|
1437
|
-
if (existing &&
|
|
1441
|
+
if (existing && !isFunctionallyEmpty(existing)) {
|
|
1438
1442
|
return c.json({ message: "Global is not empty, skipping seed" });
|
|
1439
1443
|
}
|
|
1440
1444
|
console.log(`[dyrected/core] Auto-seeding global: ${this.global.slug}`);
|
|
@@ -1442,6 +1446,19 @@ var GlobalController = class {
|
|
|
1442
1446
|
return c.json({ message: "Seed successful", data: initialData }, 201);
|
|
1443
1447
|
}
|
|
1444
1448
|
};
|
|
1449
|
+
function isFunctionallyEmpty(obj) {
|
|
1450
|
+
if (obj === null || obj === void 0 || obj === "") return true;
|
|
1451
|
+
if (Array.isArray(obj)) {
|
|
1452
|
+
if (obj.length === 0) return true;
|
|
1453
|
+
return obj.every(isFunctionallyEmpty);
|
|
1454
|
+
}
|
|
1455
|
+
if (typeof obj === "object") {
|
|
1456
|
+
const keys = Object.keys(obj);
|
|
1457
|
+
if (keys.length === 0) return true;
|
|
1458
|
+
return keys.every((key) => isFunctionallyEmpty(obj[key]));
|
|
1459
|
+
}
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1445
1462
|
|
|
1446
1463
|
// src/controllers/media.controller.ts
|
|
1447
1464
|
var MediaController = class {
|
|
@@ -2077,13 +2094,396 @@ var AuthController = class {
|
|
|
2077
2094
|
}
|
|
2078
2095
|
};
|
|
2079
2096
|
|
|
2080
|
-
// src/controllers/
|
|
2081
|
-
var
|
|
2097
|
+
// src/controllers/admin-auth.controller.ts
|
|
2098
|
+
var import_node_crypto2 = require("crypto");
|
|
2082
2099
|
var import_node_util3 = require("util");
|
|
2100
|
+
var import_jose2 = require("jose");
|
|
2101
|
+
|
|
2102
|
+
// src/utils/admin-auth.ts
|
|
2103
|
+
function getAdminAuthCollection(config) {
|
|
2104
|
+
const requestedSlug = config.adminAuth?.collectionSlug;
|
|
2105
|
+
if (requestedSlug) {
|
|
2106
|
+
return config.collections.find((collection) => collection.slug === requestedSlug) ?? null;
|
|
2107
|
+
}
|
|
2108
|
+
return config.collections.find((collection) => collection.slug === "__admins") ?? config.collections.find((collection) => collection.auth) ?? null;
|
|
2109
|
+
}
|
|
2110
|
+
function getPublicAdminAuthConfig(adminAuth) {
|
|
2111
|
+
const providers = (adminAuth?.providers ?? []).map((provider) => ({
|
|
2112
|
+
id: provider.id,
|
|
2113
|
+
type: provider.type,
|
|
2114
|
+
displayName: provider.displayName || humanizeProviderName(provider.id, provider.type),
|
|
2115
|
+
autoRedirect: provider.autoRedirect
|
|
2116
|
+
}));
|
|
2117
|
+
return {
|
|
2118
|
+
mode: adminAuth?.mode ?? "local",
|
|
2119
|
+
collectionSlug: adminAuth?.collectionSlug,
|
|
2120
|
+
provisioningMode: adminAuth?.provisioningMode,
|
|
2121
|
+
providers
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
function humanizeProviderName(id, type) {
|
|
2125
|
+
const cleaned = id.replace(/[-_]+/g, " ").trim();
|
|
2126
|
+
if (!cleaned) return type.toUpperCase();
|
|
2127
|
+
return cleaned.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// src/controllers/admin-auth.controller.ts
|
|
2131
|
+
var AdminAuthController = class {
|
|
2132
|
+
constructor(config) {
|
|
2133
|
+
this.config = config;
|
|
2134
|
+
}
|
|
2135
|
+
config;
|
|
2136
|
+
providers(c) {
|
|
2137
|
+
return c.json(getPublicAdminAuthConfig(this.config.adminAuth));
|
|
2138
|
+
}
|
|
2139
|
+
async start(c) {
|
|
2140
|
+
const provider = this.getProvider(c.req.param("provider"));
|
|
2141
|
+
if (!provider) {
|
|
2142
|
+
return c.json({ error: true, message: "Admin auth provider not found." }, 404);
|
|
2143
|
+
}
|
|
2144
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
2145
|
+
const returnTo = this.normalizeReturnTo(c.req.query("returnTo"), c);
|
|
2146
|
+
if (provider.type === "oidc") {
|
|
2147
|
+
const redirectUrl = await this.buildOidcStartUrl(
|
|
2148
|
+
provider,
|
|
2149
|
+
returnTo,
|
|
2150
|
+
siteId,
|
|
2151
|
+
new URL(c.req.url).origin
|
|
2152
|
+
);
|
|
2153
|
+
return c.redirect(redirectUrl, 302);
|
|
2154
|
+
}
|
|
2155
|
+
if (!provider.startUrl) {
|
|
2156
|
+
return c.json({ error: true, message: "This provider does not expose a start URL." }, 400);
|
|
2157
|
+
}
|
|
2158
|
+
const url = new URL(provider.startUrl);
|
|
2159
|
+
url.searchParams.set("returnTo", returnTo);
|
|
2160
|
+
if (siteId) url.searchParams.set("siteId", siteId);
|
|
2161
|
+
url.searchParams.set("provider", provider.id);
|
|
2162
|
+
return c.redirect(url.toString(), 302);
|
|
2163
|
+
}
|
|
2164
|
+
async callback(c) {
|
|
2165
|
+
const provider = this.getProvider(c.req.param("provider"));
|
|
2166
|
+
if (!provider) {
|
|
2167
|
+
return c.json({ error: true, message: "Admin auth provider not found." }, 404);
|
|
2168
|
+
}
|
|
2169
|
+
try {
|
|
2170
|
+
const exchange = await this.completeProviderAuth(provider, c);
|
|
2171
|
+
const redirectUrl = new URL(exchange.returnTo);
|
|
2172
|
+
redirectUrl.searchParams.set("dyrectedExternalToken", exchange.token);
|
|
2173
|
+
redirectUrl.searchParams.set("dyrectedAdminCollection", exchange.collectionSlug);
|
|
2174
|
+
return c.redirect(redirectUrl.toString(), 302);
|
|
2175
|
+
} catch (error) {
|
|
2176
|
+
const message = error instanceof Error ? error.message : "External authentication failed.";
|
|
2177
|
+
const fallback = this.normalizeReturnTo(c.req.query("returnTo"), c);
|
|
2178
|
+
const redirectUrl = new URL(fallback);
|
|
2179
|
+
redirectUrl.searchParams.set("dyrectedExternalError", message);
|
|
2180
|
+
return c.redirect(redirectUrl.toString(), 302);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
async exchange(c) {
|
|
2184
|
+
const provider = this.getProvider(c.req.param("provider"));
|
|
2185
|
+
if (!provider) {
|
|
2186
|
+
return c.json({ error: true, message: "Admin auth provider not found." }, 404);
|
|
2187
|
+
}
|
|
2188
|
+
try {
|
|
2189
|
+
const exchange = await this.completeProviderAuth(provider, c);
|
|
2190
|
+
return c.json({
|
|
2191
|
+
token: exchange.token,
|
|
2192
|
+
collectionSlug: exchange.collectionSlug,
|
|
2193
|
+
providerId: provider.id
|
|
2194
|
+
});
|
|
2195
|
+
} catch (error) {
|
|
2196
|
+
const message = error instanceof Error ? error.message : "External authentication failed.";
|
|
2197
|
+
const status = message === "Access denied for this site." ? 403 : 401;
|
|
2198
|
+
return c.json({ error: true, message }, status);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
async logout(c) {
|
|
2202
|
+
return c.json({ success: true, message: "Logged out. Discard your token." });
|
|
2203
|
+
}
|
|
2204
|
+
getProvider(id) {
|
|
2205
|
+
if (this.config.adminAuth?.mode !== "external") return void 0;
|
|
2206
|
+
return this.config.adminAuth.providers.find((provider) => provider.id === id);
|
|
2207
|
+
}
|
|
2208
|
+
async completeProviderAuth(provider, c) {
|
|
2209
|
+
const resolved = provider.type === "oidc" ? await this.resolveOidcIdentity(provider, c) : await this.resolveCustomIdentity(provider, c);
|
|
2210
|
+
const identity = resolved.identity;
|
|
2211
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
2212
|
+
const adminCollection = getAdminAuthCollection(this.config);
|
|
2213
|
+
if (!adminCollection?.auth) {
|
|
2214
|
+
throw new Error("Admin auth collection is not configured.");
|
|
2215
|
+
}
|
|
2216
|
+
const db = c.get("config").db;
|
|
2217
|
+
if (!db) throw new Error("Database not configured.");
|
|
2218
|
+
let user = await this.findUserByExternalIdentity(db, adminCollection.slug, provider.id, identity.sub) ?? (identity.email ? (await db.find({
|
|
2219
|
+
collection: adminCollection.slug,
|
|
2220
|
+
where: { email: identity.email },
|
|
2221
|
+
limit: 1
|
|
2222
|
+
})).docs[0] : null);
|
|
2223
|
+
const provisioningMode = this.config.adminAuth?.provisioningMode ?? "jit_plus_membership_management";
|
|
2224
|
+
const allowJitProvisioning = provisioningMode !== "preprovisioned_only" && provider.allowJitProvisioning !== false;
|
|
2225
|
+
if (!user && !allowJitProvisioning) {
|
|
2226
|
+
throw new Error("This account has not been provisioned for admin access.");
|
|
2227
|
+
}
|
|
2228
|
+
const access = await this.resolveAccess(identity, provider.id, siteId, c, user);
|
|
2229
|
+
if (!access.allowed) {
|
|
2230
|
+
throw new Error("Access denied for this site.");
|
|
2231
|
+
}
|
|
2232
|
+
if (!user && allowJitProvisioning) {
|
|
2233
|
+
user = await db.create({
|
|
2234
|
+
collection: adminCollection.slug,
|
|
2235
|
+
data: {
|
|
2236
|
+
...await this.buildUserData(identity, provider.id, access.roles, access.data),
|
|
2237
|
+
password: await hashPassword((0, import_node_crypto2.randomBytes)(32).toString("hex"))
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
} else if (user) {
|
|
2241
|
+
user = await db.update({
|
|
2242
|
+
collection: adminCollection.slug,
|
|
2243
|
+
id: user.id,
|
|
2244
|
+
data: await this.buildUserData(identity, provider.id, access.roles, access.data)
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
if (!user?.email) {
|
|
2248
|
+
throw new Error("Authenticated admin user is missing an email address.");
|
|
2249
|
+
}
|
|
2250
|
+
const token = await signCollectionToken({
|
|
2251
|
+
sub: user.id,
|
|
2252
|
+
email: user.email,
|
|
2253
|
+
collection: adminCollection.slug,
|
|
2254
|
+
providerId: provider.id,
|
|
2255
|
+
authSource: "external"
|
|
2256
|
+
});
|
|
2257
|
+
return {
|
|
2258
|
+
token,
|
|
2259
|
+
collectionSlug: adminCollection.slug,
|
|
2260
|
+
returnTo: resolved.returnTo
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
async resolveAccess(identity, providerId, siteId, c, user) {
|
|
2264
|
+
const resolved = await this.config.adminAuth?.resolveAccess?.({
|
|
2265
|
+
identity,
|
|
2266
|
+
providerId,
|
|
2267
|
+
siteId,
|
|
2268
|
+
workspaceId: c.get("workspaceId"),
|
|
2269
|
+
req: {
|
|
2270
|
+
method: c.req.method,
|
|
2271
|
+
path: c.req.path,
|
|
2272
|
+
url: c.req.url,
|
|
2273
|
+
headers: Object.fromEntries(c.req.raw.headers.entries())
|
|
2274
|
+
},
|
|
2275
|
+
user
|
|
2276
|
+
});
|
|
2277
|
+
if (resolved) return resolved;
|
|
2278
|
+
return { allowed: true, roles: identity.roles, data: {} };
|
|
2279
|
+
}
|
|
2280
|
+
async buildUserData(identity, providerId, roles, extraData) {
|
|
2281
|
+
return {
|
|
2282
|
+
...identity.email ? { email: identity.email } : {},
|
|
2283
|
+
...identity.name ? { name: identity.name } : {},
|
|
2284
|
+
...roles ? { roles } : {},
|
|
2285
|
+
...extraData ?? {},
|
|
2286
|
+
authProvider: providerId,
|
|
2287
|
+
externalSubject: identity.sub,
|
|
2288
|
+
authSource: "external",
|
|
2289
|
+
lastLoginAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
async findUserByExternalIdentity(db, collection, providerId, externalSubject) {
|
|
2293
|
+
const result = await db.find({
|
|
2294
|
+
collection,
|
|
2295
|
+
where: {
|
|
2296
|
+
authProvider: providerId,
|
|
2297
|
+
externalSubject
|
|
2298
|
+
},
|
|
2299
|
+
limit: 1
|
|
2300
|
+
});
|
|
2301
|
+
return result.docs[0] ?? null;
|
|
2302
|
+
}
|
|
2303
|
+
async buildOidcStartUrl(provider, returnTo, siteId, origin) {
|
|
2304
|
+
const discovery = await this.getOidcDiscovery(provider);
|
|
2305
|
+
const nonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
2306
|
+
const state = await this.signState({
|
|
2307
|
+
providerId: provider.id,
|
|
2308
|
+
returnTo,
|
|
2309
|
+
siteId,
|
|
2310
|
+
nonce
|
|
2311
|
+
});
|
|
2312
|
+
const url = new URL(provider.authorizationEndpoint || discovery.authorization_endpoint);
|
|
2313
|
+
url.searchParams.set("response_type", "code");
|
|
2314
|
+
url.searchParams.set("client_id", provider.clientId);
|
|
2315
|
+
url.searchParams.set("redirect_uri", provider.redirectUri || this.defaultCallbackPath(provider.id, origin));
|
|
2316
|
+
url.searchParams.set("scope", (provider.scopes ?? ["openid", "profile", "email"]).join(" "));
|
|
2317
|
+
url.searchParams.set("state", state);
|
|
2318
|
+
url.searchParams.set("nonce", nonce);
|
|
2319
|
+
return url.toString();
|
|
2320
|
+
}
|
|
2321
|
+
async resolveOidcIdentity(provider, c) {
|
|
2322
|
+
const code = c.req.query("code");
|
|
2323
|
+
const state = c.req.query("state");
|
|
2324
|
+
if (!code || !state) throw new Error("Missing OIDC callback parameters.");
|
|
2325
|
+
const parsedState = await this.verifyState(state, provider.id);
|
|
2326
|
+
const discovery = await this.getOidcDiscovery(provider);
|
|
2327
|
+
const redirectUri = provider.redirectUri || this.defaultCallbackPath(provider.id, new URL(c.req.url).origin);
|
|
2328
|
+
const tokenResponse = await fetch(provider.tokenEndpoint || discovery.token_endpoint, {
|
|
2329
|
+
method: "POST",
|
|
2330
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2331
|
+
body: new URLSearchParams({
|
|
2332
|
+
grant_type: "authorization_code",
|
|
2333
|
+
code,
|
|
2334
|
+
redirect_uri: redirectUri,
|
|
2335
|
+
client_id: provider.clientId,
|
|
2336
|
+
client_secret: provider.clientSecret
|
|
2337
|
+
})
|
|
2338
|
+
});
|
|
2339
|
+
if (!tokenResponse.ok) {
|
|
2340
|
+
throw new Error("OIDC token exchange failed.");
|
|
2341
|
+
}
|
|
2342
|
+
const tokenBody = await tokenResponse.json();
|
|
2343
|
+
const idToken = tokenBody.id_token;
|
|
2344
|
+
let claims = {};
|
|
2345
|
+
if (idToken) {
|
|
2346
|
+
const jwks = (0, import_jose2.createRemoteJWKSet)(new URL(discovery.jwks_uri));
|
|
2347
|
+
const { payload } = await (0, import_jose2.jwtVerify)(idToken, jwks, {
|
|
2348
|
+
issuer: provider.issuer,
|
|
2349
|
+
audience: provider.clientId
|
|
2350
|
+
});
|
|
2351
|
+
if (parsedState.nonce && payload.nonce !== parsedState.nonce) {
|
|
2352
|
+
throw new Error("OIDC nonce verification failed.");
|
|
2353
|
+
}
|
|
2354
|
+
claims = payload;
|
|
2355
|
+
} else if (tokenBody.access_token && (provider.userInfoEndpoint || discovery.userinfo_endpoint)) {
|
|
2356
|
+
const userInfo = await fetch(provider.userInfoEndpoint || discovery.userinfo_endpoint, {
|
|
2357
|
+
headers: { Authorization: `Bearer ${tokenBody.access_token}` }
|
|
2358
|
+
});
|
|
2359
|
+
if (!userInfo.ok) throw new Error("OIDC userinfo request failed.");
|
|
2360
|
+
claims = await userInfo.json();
|
|
2361
|
+
} else {
|
|
2362
|
+
throw new Error("OIDC provider did not return usable identity claims.");
|
|
2363
|
+
}
|
|
2364
|
+
return {
|
|
2365
|
+
identity: this.mapClaims(claims, provider.claimMapping),
|
|
2366
|
+
returnTo: parsedState.returnTo
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
async resolveCustomIdentity(provider, c) {
|
|
2370
|
+
const body = c.req.method === "POST" ? await c.req.json().catch(() => ({})) : {};
|
|
2371
|
+
const tokenParam = provider.tokenParam || "token";
|
|
2372
|
+
const token = body?.[tokenParam] || c.req.query(tokenParam);
|
|
2373
|
+
if (!token) {
|
|
2374
|
+
throw new Error(`Missing ${tokenParam} for external auth exchange.`);
|
|
2375
|
+
}
|
|
2376
|
+
let claims;
|
|
2377
|
+
if (provider.secret) {
|
|
2378
|
+
const secret = new import_node_util3.TextEncoder().encode(provider.secret);
|
|
2379
|
+
const verified = await (0, import_jose2.jwtVerify)(token, secret, {
|
|
2380
|
+
issuer: provider.issuer,
|
|
2381
|
+
audience: provider.audience
|
|
2382
|
+
});
|
|
2383
|
+
claims = verified.payload;
|
|
2384
|
+
} else if (provider.jwksUri) {
|
|
2385
|
+
const jwks = (0, import_jose2.createRemoteJWKSet)(new URL(provider.jwksUri));
|
|
2386
|
+
const verified = await (0, import_jose2.jwtVerify)(token, jwks, {
|
|
2387
|
+
issuer: provider.issuer,
|
|
2388
|
+
audience: provider.audience
|
|
2389
|
+
});
|
|
2390
|
+
claims = verified.payload;
|
|
2391
|
+
} else {
|
|
2392
|
+
claims = (0, import_jose2.decodeJwt)(token);
|
|
2393
|
+
if (provider.issuer && claims.iss !== provider.issuer) {
|
|
2394
|
+
throw new Error("External auth issuer mismatch.");
|
|
2395
|
+
}
|
|
2396
|
+
if (provider.audience) {
|
|
2397
|
+
const aud = claims.aud;
|
|
2398
|
+
const matches = Array.isArray(aud) ? aud.includes(provider.audience) : aud === provider.audience;
|
|
2399
|
+
if (!matches) throw new Error("External auth audience mismatch.");
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
return {
|
|
2403
|
+
identity: this.mapClaims(claims, provider.claimMapping),
|
|
2404
|
+
returnTo: this.normalizeReturnTo(
|
|
2405
|
+
body?.returnTo || c.req.query("returnTo"),
|
|
2406
|
+
c
|
|
2407
|
+
)
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
mapClaims(claims, mapping) {
|
|
2411
|
+
const subKey = mapping?.sub || "sub";
|
|
2412
|
+
const emailKey = mapping?.email || "email";
|
|
2413
|
+
const nameKey = mapping?.name || "name";
|
|
2414
|
+
const rolesKey = mapping?.roles;
|
|
2415
|
+
const groupsKey = mapping?.groups;
|
|
2416
|
+
const siteIdsKey = mapping?.siteIds;
|
|
2417
|
+
const workspaceIdsKey = mapping?.workspaceIds;
|
|
2418
|
+
const sub = claims[subKey];
|
|
2419
|
+
if (typeof sub !== "string" || !sub) {
|
|
2420
|
+
throw new Error("External identity is missing a subject.");
|
|
2421
|
+
}
|
|
2422
|
+
return {
|
|
2423
|
+
sub,
|
|
2424
|
+
email: this.readStringClaim(claims[emailKey]),
|
|
2425
|
+
name: this.readStringClaim(claims[nameKey]),
|
|
2426
|
+
roles: this.readStringArrayClaim(rolesKey ? claims[rolesKey] : void 0),
|
|
2427
|
+
groups: this.readStringArrayClaim(groupsKey ? claims[groupsKey] : void 0),
|
|
2428
|
+
siteIds: this.readStringArrayClaim(siteIdsKey ? claims[siteIdsKey] : void 0),
|
|
2429
|
+
workspaceIds: this.readStringArrayClaim(workspaceIdsKey ? claims[workspaceIdsKey] : void 0),
|
|
2430
|
+
rawClaims: claims
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
readStringClaim(value) {
|
|
2434
|
+
return typeof value === "string" ? value : void 0;
|
|
2435
|
+
}
|
|
2436
|
+
readStringArrayClaim(value) {
|
|
2437
|
+
if (!value) return void 0;
|
|
2438
|
+
if (Array.isArray(value)) {
|
|
2439
|
+
return value.filter((entry) => typeof entry === "string");
|
|
2440
|
+
}
|
|
2441
|
+
if (typeof value === "string") return [value];
|
|
2442
|
+
return void 0;
|
|
2443
|
+
}
|
|
2444
|
+
async getOidcDiscovery(provider) {
|
|
2445
|
+
const discoveryUrl = new URL(
|
|
2446
|
+
provider.issuer.replace(/\/$/, "") + "/.well-known/openid-configuration"
|
|
2447
|
+
);
|
|
2448
|
+
const response = await fetch(discoveryUrl.toString());
|
|
2449
|
+
if (!response.ok) throw new Error("Failed to load OIDC discovery document.");
|
|
2450
|
+
return response.json();
|
|
2451
|
+
}
|
|
2452
|
+
defaultCallbackPath(providerId, origin = "") {
|
|
2453
|
+
return `${origin}/api/admin/auth/${providerId}/callback`;
|
|
2454
|
+
}
|
|
2455
|
+
async signState(payload) {
|
|
2456
|
+
return new import_jose2.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("15m").sign(this.getStateSecret());
|
|
2457
|
+
}
|
|
2458
|
+
async verifyState(token, providerId) {
|
|
2459
|
+
const { payload } = await (0, import_jose2.jwtVerify)(token, this.getStateSecret());
|
|
2460
|
+
const state = payload;
|
|
2461
|
+
if (state.providerId !== providerId) {
|
|
2462
|
+
throw new Error("OIDC state verification failed.");
|
|
2463
|
+
}
|
|
2464
|
+
return state;
|
|
2465
|
+
}
|
|
2466
|
+
getStateSecret() {
|
|
2467
|
+
const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET;
|
|
2468
|
+
if (!secret) {
|
|
2469
|
+
throw new Error("[dyrected/core] DYRECTED_JWT_SECRET is not set.");
|
|
2470
|
+
}
|
|
2471
|
+
return new import_node_util3.TextEncoder().encode(secret);
|
|
2472
|
+
}
|
|
2473
|
+
normalizeReturnTo(returnTo, c) {
|
|
2474
|
+
if (returnTo) return returnTo;
|
|
2475
|
+
const url = new URL(c.req.url);
|
|
2476
|
+
return `${url.origin}/admin`;
|
|
2477
|
+
}
|
|
2478
|
+
};
|
|
2479
|
+
|
|
2480
|
+
// src/controllers/preview.controller.ts
|
|
2481
|
+
var import_jose3 = require("jose");
|
|
2482
|
+
var import_node_util4 = require("util");
|
|
2083
2483
|
var PreviewController = class {
|
|
2084
2484
|
getSecret() {
|
|
2085
2485
|
const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET || "dyrected-preview-secret-change-me";
|
|
2086
|
-
return new
|
|
2486
|
+
return new import_node_util4.TextEncoder().encode(secret);
|
|
2087
2487
|
}
|
|
2088
2488
|
/**
|
|
2089
2489
|
* POST /api/preview-token
|
|
@@ -2094,7 +2494,7 @@ var PreviewController = class {
|
|
|
2094
2494
|
if (!body?.collectionSlug || !body?.data) {
|
|
2095
2495
|
return c.json({ error: true, message: "collectionSlug and data are required." }, 400);
|
|
2096
2496
|
}
|
|
2097
|
-
const token = await new
|
|
2497
|
+
const token = await new import_jose3.SignJWT({
|
|
2098
2498
|
collectionSlug: body.collectionSlug,
|
|
2099
2499
|
documentId: body.documentId,
|
|
2100
2500
|
data: body.data
|
|
@@ -2112,7 +2512,7 @@ var PreviewController = class {
|
|
|
2112
2512
|
return c.json({ error: true, message: "token query parameter is required." }, 400);
|
|
2113
2513
|
}
|
|
2114
2514
|
try {
|
|
2115
|
-
const { payload } = await (0,
|
|
2515
|
+
const { payload } = await (0, import_jose3.jwtVerify)(token, this.getSecret());
|
|
2116
2516
|
return c.json(payload);
|
|
2117
2517
|
} catch (err) {
|
|
2118
2518
|
return c.json({ error: true, message: "Invalid or expired preview token." }, 401);
|
|
@@ -3097,6 +3497,9 @@ function registerRoutes(app, config) {
|
|
|
3097
3497
|
if (dynamic.admin) {
|
|
3098
3498
|
config.admin = { ...config.admin, ...dynamic.admin };
|
|
3099
3499
|
}
|
|
3500
|
+
if (dynamic.adminAuth) {
|
|
3501
|
+
config.adminAuth = { ...config.adminAuth, ...dynamic.adminAuth };
|
|
3502
|
+
}
|
|
3100
3503
|
}
|
|
3101
3504
|
const user = c.get("user");
|
|
3102
3505
|
const accessArgs = { user, req: c.req, doc: null };
|
|
@@ -3185,7 +3588,8 @@ function registerRoutes(app, config) {
|
|
|
3185
3588
|
return c.json({
|
|
3186
3589
|
collections: filteredCollections,
|
|
3187
3590
|
globals: filteredGlobals,
|
|
3188
|
-
admin: config.admin || {}
|
|
3591
|
+
admin: config.admin || {},
|
|
3592
|
+
adminAuth: getPublicAdminAuthConfig(config.adminAuth)
|
|
3189
3593
|
});
|
|
3190
3594
|
});
|
|
3191
3595
|
app.get("/api/dyrected/options/:collection/:field", optionalAuth(), async (c) => {
|
|
@@ -3377,6 +3781,12 @@ function registerRoutes(app, config) {
|
|
|
3377
3781
|
app.delete(`${prefix}/media/:id`, accessGate(col, "delete"), (c) => mediaController.delete(c));
|
|
3378
3782
|
}
|
|
3379
3783
|
}
|
|
3784
|
+
const adminAuthController = new AdminAuthController(config);
|
|
3785
|
+
app.get("/api/admin/auth/providers", (c) => adminAuthController.providers(c));
|
|
3786
|
+
app.get("/api/admin/auth/:provider/start", (c) => adminAuthController.start(c));
|
|
3787
|
+
app.get("/api/admin/auth/:provider/callback", (c) => adminAuthController.callback(c));
|
|
3788
|
+
app.post("/api/admin/auth/:provider/exchange", (c) => adminAuthController.exchange(c));
|
|
3789
|
+
app.post("/api/admin/logout", (c) => adminAuthController.logout(c));
|
|
3380
3790
|
for (const collection of config.collections) {
|
|
3381
3791
|
if (!collection.auth) continue;
|
|
3382
3792
|
const path = `/api/collections/${collection.slug}`;
|
|
@@ -3421,9 +3831,8 @@ function registerRoutes(app, config) {
|
|
|
3421
3831
|
const previewController = new PreviewController();
|
|
3422
3832
|
app.post("/api/preview-token", requireAuth(), (c) => previewController.createToken(c));
|
|
3423
3833
|
app.get("/api/preview-data", (c) => previewController.getData(c));
|
|
3424
|
-
app.
|
|
3834
|
+
app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(), async (c) => {
|
|
3425
3835
|
const slug = c.req.param("slug");
|
|
3426
|
-
const id = c.req.param("id");
|
|
3427
3836
|
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
3428
3837
|
const config2 = c.get("config");
|
|
3429
3838
|
if (config2.collections.some((col) => col.slug === slug)) {
|
|
@@ -3438,23 +3847,25 @@ function registerRoutes(app, config) {
|
|
|
3438
3847
|
return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
|
|
3439
3848
|
}
|
|
3440
3849
|
const controller = new CollectionController(collection);
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
if (key === "id") return id;
|
|
3450
|
-
return origParam(key);
|
|
3451
|
-
};
|
|
3452
|
-
return controller.transition(c);
|
|
3850
|
+
return controller.transition(c);
|
|
3851
|
+
});
|
|
3852
|
+
app.get("/api/collections/:slug/:id/workflow-history", requireAuth(), async (c) => {
|
|
3853
|
+
const slug = c.req.param("slug");
|
|
3854
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
3855
|
+
const config2 = c.get("config");
|
|
3856
|
+
if (config2.collections.some((col) => col.slug === slug)) {
|
|
3857
|
+
return c.json({ message: "Not Found" }, 404);
|
|
3453
3858
|
}
|
|
3454
|
-
if (
|
|
3455
|
-
return
|
|
3859
|
+
if (!config2.onSchemaFetch || !siteId) {
|
|
3860
|
+
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
3456
3861
|
}
|
|
3457
|
-
|
|
3862
|
+
const dynamic = await config2.onSchemaFetch(siteId);
|
|
3863
|
+
const collection = dynamic.collections?.find((col) => col.slug === slug);
|
|
3864
|
+
if (!collection?.workflow) {
|
|
3865
|
+
return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
|
|
3866
|
+
}
|
|
3867
|
+
const controller = new CollectionController(collection);
|
|
3868
|
+
return controller.workflowHistory(c);
|
|
3458
3869
|
});
|
|
3459
3870
|
app.all("/api/collections/:slug/:id?", async (c) => {
|
|
3460
3871
|
const slug = c.req.param("slug");
|
|
@@ -3503,8 +3914,9 @@ function registerRoutes(app, config) {
|
|
|
3503
3914
|
}
|
|
3504
3915
|
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
3505
3916
|
});
|
|
3506
|
-
app.all("/api/globals/:slug", async (c) => {
|
|
3917
|
+
app.all("/api/globals/:slug/:id?", async (c) => {
|
|
3507
3918
|
const slug = c.req.param("slug");
|
|
3919
|
+
const id = c.req.param("id");
|
|
3508
3920
|
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
3509
3921
|
const config2 = c.get("config");
|
|
3510
3922
|
if (config2.globals.some((glb) => glb.slug === slug)) {
|
|
@@ -3515,8 +3927,13 @@ function registerRoutes(app, config) {
|
|
|
3515
3927
|
const global = dynamic.globals?.find((glb) => glb.slug === slug);
|
|
3516
3928
|
if (global) {
|
|
3517
3929
|
const controller = new GlobalController(global);
|
|
3518
|
-
|
|
3519
|
-
if (
|
|
3930
|
+
const method = c.req.method;
|
|
3931
|
+
if (id) {
|
|
3932
|
+
if (method === "POST" && id === "seed") return controller.seed(c);
|
|
3933
|
+
} else {
|
|
3934
|
+
if (method === "GET") return controller.get(c);
|
|
3935
|
+
if (method === "PATCH") return controller.update(c);
|
|
3936
|
+
}
|
|
3520
3937
|
}
|
|
3521
3938
|
}
|
|
3522
3939
|
return c.json({ message: `Global "${slug}" not found` }, 404);
|
|
@@ -3605,6 +4022,10 @@ function normalizeConfig(config) {
|
|
|
3605
4022
|
const globals = config?.globals || [];
|
|
3606
4023
|
const needsAudit = collections.some((col) => col.audit);
|
|
3607
4024
|
const needsWorkflow = collections.some((col) => col.workflow);
|
|
4025
|
+
const adminAuthCollectionSlug = getAdminAuthCollection({
|
|
4026
|
+
collections,
|
|
4027
|
+
adminAuth: config.adminAuth
|
|
4028
|
+
})?.slug;
|
|
3608
4029
|
const normalizedCollections = collections.map((col) => {
|
|
3609
4030
|
let fields = col.fields || [];
|
|
3610
4031
|
const existingFieldNames = new Set(fields.map((f) => f.name));
|
|
@@ -3691,6 +4112,39 @@ function normalizeConfig(config) {
|
|
|
3691
4112
|
return field;
|
|
3692
4113
|
});
|
|
3693
4114
|
}
|
|
4115
|
+
if (adminAuthCollectionSlug && col.slug === adminAuthCollectionSlug && config.adminAuth?.mode === "external") {
|
|
4116
|
+
const externalAdminFields = [
|
|
4117
|
+
{
|
|
4118
|
+
name: "authProvider",
|
|
4119
|
+
type: "text",
|
|
4120
|
+
label: "Auth Provider",
|
|
4121
|
+
admin: { readOnly: true, hidden: true }
|
|
4122
|
+
},
|
|
4123
|
+
{
|
|
4124
|
+
name: "externalSubject",
|
|
4125
|
+
type: "text",
|
|
4126
|
+
label: "External Subject",
|
|
4127
|
+
admin: { readOnly: true, hidden: true }
|
|
4128
|
+
},
|
|
4129
|
+
{
|
|
4130
|
+
name: "authSource",
|
|
4131
|
+
type: "text",
|
|
4132
|
+
label: "Auth Source",
|
|
4133
|
+
admin: { readOnly: true, hidden: true }
|
|
4134
|
+
},
|
|
4135
|
+
{
|
|
4136
|
+
name: "lastLoginAt",
|
|
4137
|
+
type: "date",
|
|
4138
|
+
label: "Last Login At",
|
|
4139
|
+
admin: { readOnly: true, hidden: true }
|
|
4140
|
+
}
|
|
4141
|
+
];
|
|
4142
|
+
for (const field of externalAdminFields) {
|
|
4143
|
+
if (!existingFieldNames.has(field.name)) {
|
|
4144
|
+
fields = [...fields, field];
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
3694
4148
|
const updatedFieldNames = new Set(fields.map((f) => f.name));
|
|
3695
4149
|
const fieldsToInject = SYSTEM_FIELDS.filter((f) => !updatedFieldNames.has(f.name));
|
|
3696
4150
|
return {
|
package/dist/server.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as hono_types from 'hono/types';
|
|
2
2
|
import * as hono from 'hono';
|
|
3
3
|
import { Hono, Context } from 'hono';
|
|
4
|
-
import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './index-
|
|
4
|
+
import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './index-DfkPT4NR.cjs';
|
|
5
5
|
import * as hono_utils_http_status from 'hono/utils/http-status';
|
|
6
6
|
import * as hono_utils_types from 'hono/utils/types';
|
|
7
7
|
import 'lucide-react';
|
package/dist/server.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as hono_types from 'hono/types';
|
|
2
2
|
import * as hono from 'hono';
|
|
3
3
|
import { Hono, Context } from 'hono';
|
|
4
|
-
import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './index-
|
|
4
|
+
import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './index-DfkPT4NR.js';
|
|
5
5
|
import * as hono_utils_http_status from 'hono/utils/http-status';
|
|
6
6
|
import * as hono_utils_types from 'hono/utils/types';
|
|
7
7
|
import 'lucide-react';
|