@continuous-excellence/ze-great-dashboard-aws 0.29.0 → 0.30.0
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/cli.js +17 -2
- package/dist/index.js +17 -2
- package/dist/lambda.mjs +89 -20
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -327,11 +327,22 @@ var authSchema = z2.strictObject({
|
|
|
327
327
|
/** A deliberately portable, explicit identity boundary for the first direct-OIDC release. */
|
|
328
328
|
allow: z2.strictObject({ subjects: z2.array(z2.string().min(1)).min(1) })
|
|
329
329
|
});
|
|
330
|
+
var securityPolicies = ["warn", "required", "unsecured"];
|
|
331
|
+
var securityPolicySchema = z2.enum(securityPolicies);
|
|
330
332
|
var boardConfigSchema = z2.object({
|
|
331
333
|
sources: z2.record(z2.string().min(1), sourceSchema).default({}),
|
|
332
334
|
boards: z2.record(z2.string().min(1), boardSchema),
|
|
333
|
-
auth: authSchema.optional()
|
|
335
|
+
auth: authSchema.optional(),
|
|
336
|
+
security: securityPolicySchema.optional()
|
|
334
337
|
}).superRefine((config, ctx) => {
|
|
338
|
+
if (config.security === "unsecured" && config.auth) {
|
|
339
|
+
ctx.addIssue({
|
|
340
|
+
code: "custom",
|
|
341
|
+
path: ["security"],
|
|
342
|
+
message: "security: unsecured cannot be combined with auth",
|
|
343
|
+
params: { constraint: "Remove auth or select warn or required." }
|
|
344
|
+
});
|
|
345
|
+
}
|
|
335
346
|
for (const [sourceName, source] of Object.entries(config.sources)) {
|
|
336
347
|
const sourceSchema2 = source.type === "azure-devops" ? azureDevOpsSourceSchema : source.type === "gitlab-ci" ? gitlabCiSourceSchema : void 0;
|
|
337
348
|
if (!sourceSchema2) continue;
|
|
@@ -397,6 +408,8 @@ var boardSchemaFileName = SCHEMA_FILE;
|
|
|
397
408
|
|
|
398
409
|
// packages/shared/src/client-env.ts
|
|
399
410
|
import { z as z3 } from "zod";
|
|
411
|
+
var clientSecurityStates = ["warning", "blocked"];
|
|
412
|
+
var clientSecurityStateSchema = z3.enum(clientSecurityStates);
|
|
400
413
|
var clientEnvSchema = z3.object({
|
|
401
414
|
/** Where the immutable client assets live. The one variable that repoints the whole client. */
|
|
402
415
|
assetPath: z3.string().min(1),
|
|
@@ -411,7 +424,9 @@ var clientEnvSchema = z3.object({
|
|
|
411
424
|
issuer: z3.string().min(1),
|
|
412
425
|
clientId: z3.string().min(1),
|
|
413
426
|
audience: z3.string().min(1)
|
|
414
|
-
}).optional()
|
|
427
|
+
}).optional(),
|
|
428
|
+
/** Public deployment posture; it contains no board, source, or credential details. */
|
|
429
|
+
security: clientSecurityStateSchema.optional()
|
|
415
430
|
});
|
|
416
431
|
var clientIdentityResponseSchema = clientEnvSchema.pick({ assetPath: true, assetPathId: true }).extend({ serverVersion: z3.string().min(1) });
|
|
417
432
|
|
package/dist/index.js
CHANGED
|
@@ -1008,11 +1008,22 @@ var authSchema = z2.strictObject({
|
|
|
1008
1008
|
/** A deliberately portable, explicit identity boundary for the first direct-OIDC release. */
|
|
1009
1009
|
allow: z2.strictObject({ subjects: z2.array(z2.string().min(1)).min(1) })
|
|
1010
1010
|
});
|
|
1011
|
+
var securityPolicies = ["warn", "required", "unsecured"];
|
|
1012
|
+
var securityPolicySchema = z2.enum(securityPolicies);
|
|
1011
1013
|
var boardConfigSchema = z2.object({
|
|
1012
1014
|
sources: z2.record(z2.string().min(1), sourceSchema).default({}),
|
|
1013
1015
|
boards: z2.record(z2.string().min(1), boardSchema),
|
|
1014
|
-
auth: authSchema.optional()
|
|
1016
|
+
auth: authSchema.optional(),
|
|
1017
|
+
security: securityPolicySchema.optional()
|
|
1015
1018
|
}).superRefine((config, ctx) => {
|
|
1019
|
+
if (config.security === "unsecured" && config.auth) {
|
|
1020
|
+
ctx.addIssue({
|
|
1021
|
+
code: "custom",
|
|
1022
|
+
path: ["security"],
|
|
1023
|
+
message: "security: unsecured cannot be combined with auth",
|
|
1024
|
+
params: { constraint: "Remove auth or select warn or required." }
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1016
1027
|
for (const [sourceName, source] of Object.entries(config.sources)) {
|
|
1017
1028
|
const sourceSchema2 = source.type === "azure-devops" ? azureDevOpsSourceSchema : source.type === "gitlab-ci" ? gitlabCiSourceSchema : void 0;
|
|
1018
1029
|
if (!sourceSchema2) continue;
|
|
@@ -1078,6 +1089,8 @@ var boardSchemaFileName = SCHEMA_FILE;
|
|
|
1078
1089
|
|
|
1079
1090
|
// packages/shared/src/client-env.ts
|
|
1080
1091
|
import { z as z3 } from "zod";
|
|
1092
|
+
var clientSecurityStates = ["warning", "blocked"];
|
|
1093
|
+
var clientSecurityStateSchema = z3.enum(clientSecurityStates);
|
|
1081
1094
|
var clientEnvSchema = z3.object({
|
|
1082
1095
|
/** Where the immutable client assets live. The one variable that repoints the whole client. */
|
|
1083
1096
|
assetPath: z3.string().min(1),
|
|
@@ -1092,7 +1105,9 @@ var clientEnvSchema = z3.object({
|
|
|
1092
1105
|
issuer: z3.string().min(1),
|
|
1093
1106
|
clientId: z3.string().min(1),
|
|
1094
1107
|
audience: z3.string().min(1)
|
|
1095
|
-
}).optional()
|
|
1108
|
+
}).optional(),
|
|
1109
|
+
/** Public deployment posture; it contains no board, source, or credential details. */
|
|
1110
|
+
security: clientSecurityStateSchema.optional()
|
|
1096
1111
|
});
|
|
1097
1112
|
var clientIdentityResponseSchema = clientEnvSchema.pick({ assetPath: true, assetPathId: true }).extend({ serverVersion: z3.string().min(1) });
|
|
1098
1113
|
|
package/dist/lambda.mjs
CHANGED
|
@@ -69364,11 +69364,22 @@ var authSchema = external_exports.strictObject({
|
|
|
69364
69364
|
/** A deliberately portable, explicit identity boundary for the first direct-OIDC release. */
|
|
69365
69365
|
allow: external_exports.strictObject({ subjects: external_exports.array(external_exports.string().min(1)).min(1) })
|
|
69366
69366
|
});
|
|
69367
|
+
var securityPolicies = ["warn", "required", "unsecured"];
|
|
69368
|
+
var securityPolicySchema = external_exports.enum(securityPolicies);
|
|
69367
69369
|
var boardConfigSchema = external_exports.object({
|
|
69368
69370
|
sources: external_exports.record(external_exports.string().min(1), sourceSchema).default({}),
|
|
69369
69371
|
boards: external_exports.record(external_exports.string().min(1), boardSchema),
|
|
69370
|
-
auth: authSchema.optional()
|
|
69372
|
+
auth: authSchema.optional(),
|
|
69373
|
+
security: securityPolicySchema.optional()
|
|
69371
69374
|
}).superRefine((config2, ctx) => {
|
|
69375
|
+
if (config2.security === "unsecured" && config2.auth) {
|
|
69376
|
+
ctx.addIssue({
|
|
69377
|
+
code: "custom",
|
|
69378
|
+
path: ["security"],
|
|
69379
|
+
message: "security: unsecured cannot be combined with auth",
|
|
69380
|
+
params: { constraint: "Remove auth or select warn or required." }
|
|
69381
|
+
});
|
|
69382
|
+
}
|
|
69372
69383
|
for (const [sourceName, source] of Object.entries(config2.sources)) {
|
|
69373
69384
|
const sourceSchema2 = source.type === "azure-devops" ? azureDevOpsSourceSchema : source.type === "gitlab-ci" ? gitlabCiSourceSchema : void 0;
|
|
69374
69385
|
if (!sourceSchema2) continue;
|
|
@@ -69435,6 +69446,8 @@ function boardSchemaModeline(url2) {
|
|
|
69435
69446
|
}
|
|
69436
69447
|
|
|
69437
69448
|
// packages/shared/src/client-env.ts
|
|
69449
|
+
var clientSecurityStates = ["warning", "blocked"];
|
|
69450
|
+
var clientSecurityStateSchema = external_exports.enum(clientSecurityStates);
|
|
69438
69451
|
var clientEnvSchema = external_exports.object({
|
|
69439
69452
|
/** Where the immutable client assets live. The one variable that repoints the whole client. */
|
|
69440
69453
|
assetPath: external_exports.string().min(1),
|
|
@@ -69449,7 +69462,9 @@ var clientEnvSchema = external_exports.object({
|
|
|
69449
69462
|
issuer: external_exports.string().min(1),
|
|
69450
69463
|
clientId: external_exports.string().min(1),
|
|
69451
69464
|
audience: external_exports.string().min(1)
|
|
69452
|
-
}).optional()
|
|
69465
|
+
}).optional(),
|
|
69466
|
+
/** Public deployment posture; it contains no board, source, or credential details. */
|
|
69467
|
+
security: clientSecurityStateSchema.optional()
|
|
69453
69468
|
});
|
|
69454
69469
|
var clientIdentityResponseSchema = clientEnvSchema.pick({ assetPath: true, assetPathId: true }).extend({ serverVersion: external_exports.string().min(1) });
|
|
69455
69470
|
|
|
@@ -73042,7 +73057,9 @@ var configSchema = external_exports.object({
|
|
|
73042
73057
|
* because it starts the server and the Vite dev server at the same moment, and without this the
|
|
73043
73058
|
* local loop is a race the developer loses about half the time.
|
|
73044
73059
|
*/
|
|
73045
|
-
templateWaitMillis: external_exports.coerce.number().int().min(0).default(0)
|
|
73060
|
+
templateWaitMillis: external_exports.coerce.number().int().min(0).default(0),
|
|
73061
|
+
/** Explicitly suppresses only the deployed authless warning, never required mode. */
|
|
73062
|
+
allowUnprotectedDashboard: external_exports.enum(["true"]).optional().transform((value) => value === "true")
|
|
73046
73063
|
});
|
|
73047
73064
|
function loadConfig2(env2 = process.env) {
|
|
73048
73065
|
const result = configSchema.safeParse({
|
|
@@ -73054,7 +73071,8 @@ function loadConfig2(env2 = process.env) {
|
|
|
73054
73071
|
serverRelease: env2.SERVER_RELEASE,
|
|
73055
73072
|
port: env2.PORT,
|
|
73056
73073
|
host: env2.HOST,
|
|
73057
|
-
templateWaitMillis: env2.TEMPLATE_WAIT_MS
|
|
73074
|
+
templateWaitMillis: env2.TEMPLATE_WAIT_MS,
|
|
73075
|
+
allowUnprotectedDashboard: env2.ALLOW_UNPROTECTED_DASHBOARD
|
|
73058
73076
|
});
|
|
73059
73077
|
if (!result.success) {
|
|
73060
73078
|
throw new Error(`Invalid server configuration:
|
|
@@ -73945,6 +73963,7 @@ function serializeForScriptTag(value) {
|
|
|
73945
73963
|
// packages/server/src/app.ts
|
|
73946
73964
|
function createApp(deps) {
|
|
73947
73965
|
const { config: config2 } = deps;
|
|
73966
|
+
const app = new Hono2();
|
|
73948
73967
|
const templates = new TemplateCache(deps.fetcher ?? globalThis.fetch);
|
|
73949
73968
|
const selectedBoard = config2.board ?? Object.keys(deps.boardConfig?.boards ?? {})[0] ?? "ze-great-team";
|
|
73950
73969
|
const allowlist = deps.allowlist ?? (deps.boardConfig ? deriveAllowlist(deps.boardConfig) : /* @__PURE__ */ new Map());
|
|
@@ -73955,7 +73974,6 @@ function createApp(deps) {
|
|
|
73955
73974
|
config2.serverRelease,
|
|
73956
73975
|
assetPathId(config2.assetPath)
|
|
73957
73976
|
);
|
|
73958
|
-
const app = new Hono2();
|
|
73959
73977
|
app.use("/api/*", async (c5, next) => {
|
|
73960
73978
|
const id = requestId();
|
|
73961
73979
|
c5.set("dashboardRequestId", id);
|
|
@@ -74036,6 +74054,7 @@ function createApp(deps) {
|
|
|
74036
74054
|
const outputConfig = {
|
|
74037
74055
|
sources,
|
|
74038
74056
|
boards: { [boardName]: outputBoard },
|
|
74057
|
+
...deps.boardConfig.security ? { security: deps.boardConfig.security } : {},
|
|
74039
74058
|
...deps.boardConfig.auth ? { auth: deps.boardConfig.auth } : {}
|
|
74040
74059
|
};
|
|
74041
74060
|
const filenameBoard = safeDownloadFilename(boardName);
|
|
@@ -74298,7 +74317,8 @@ ${(0, import_yaml.stringify)(outputConfig, { sortMapEntries: true })}`,
|
|
|
74298
74317
|
clientId: deps.boardConfig.auth.client_id,
|
|
74299
74318
|
audience: deps.boardConfig.auth.audience
|
|
74300
74319
|
}
|
|
74301
|
-
} : {}
|
|
74320
|
+
} : {},
|
|
74321
|
+
...deps.securityState === "warning" ? { security: "warning" } : {}
|
|
74302
74322
|
};
|
|
74303
74323
|
return new Response(renderIndexHtml(template, env2), {
|
|
74304
74324
|
headers: {
|
|
@@ -74316,6 +74336,28 @@ function safeDownloadFilename(value) {
|
|
|
74316
74336
|
return sanitized || "board";
|
|
74317
74337
|
}
|
|
74318
74338
|
|
|
74339
|
+
// packages/server/src/blocked-app.ts
|
|
74340
|
+
function createBlockedApp() {
|
|
74341
|
+
const app = new Hono2();
|
|
74342
|
+
app.get("/health", (c5) => c5.json({ status: "ok" }));
|
|
74343
|
+
app.all(
|
|
74344
|
+
"/api/*",
|
|
74345
|
+
(c5) => c5.json({ error: "Dashboard authentication is required but is not configured." }, 503, {
|
|
74346
|
+
"cache-control": "no-store"
|
|
74347
|
+
})
|
|
74348
|
+
);
|
|
74349
|
+
const blocked = () => new Response(blockedEntrypoint(), {
|
|
74350
|
+
status: 503,
|
|
74351
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }
|
|
74352
|
+
});
|
|
74353
|
+
app.get("/", blocked);
|
|
74354
|
+
app.get("/boards/:board", blocked);
|
|
74355
|
+
return app;
|
|
74356
|
+
}
|
|
74357
|
+
function blockedEntrypoint() {
|
|
74358
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Dashboard unavailable</title><style>body{margin:0;background:#12140f;color:#f4f2e8;font:clamp(18px,2vw,28px)/1.45 system-ui,sans-serif}main{max-width:42rem;margin:12vh auto;padding:2rem;border:4px solid #e05252;background:#1c1f18}h1{margin-top:0;color:#ff9b8e}a{color:#f4f2e8}</style></head><body><main role="alert"><h1>\u26A0 Authentication configuration required</h1><p>This dashboard is blocked because authentication is required but has not been configured.</p><p>No dashboard data was loaded.</p><p>Configure the board\u2019s <code>auth</code> block, then restart the dashboard. <a href="https://github.com/robertfmurdock/ze-great-dashboard/blob/main/docs/oidc-authentication.md">Read the authentication setup guide</a>.</p></main></body></html>`;
|
|
74359
|
+
}
|
|
74360
|
+
|
|
74319
74361
|
// packages/server/src/board-config.ts
|
|
74320
74362
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
74321
74363
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
@@ -74374,6 +74416,14 @@ async function fetchBoardConfig(url2, fetcher) {
|
|
|
74374
74416
|
return response.text();
|
|
74375
74417
|
}
|
|
74376
74418
|
|
|
74419
|
+
// packages/server/src/security-posture.ts
|
|
74420
|
+
function deploymentSecurityState(config2, boardConfig) {
|
|
74421
|
+
if (boardConfig.auth || boardConfig.security === "unsecured" || isLocalHost(config2.host))
|
|
74422
|
+
return "normal";
|
|
74423
|
+
if (boardConfig.security === "required") return "blocked";
|
|
74424
|
+
return config2.allowUnprotectedDashboard ? "normal" : "warning";
|
|
74425
|
+
}
|
|
74426
|
+
|
|
74377
74427
|
// packages/server/src/startup-failure.ts
|
|
74378
74428
|
var StartupFailure = class extends Error {
|
|
74379
74429
|
diagnostic;
|
|
@@ -74399,21 +74449,41 @@ async function startup(options = {}) {
|
|
|
74399
74449
|
try {
|
|
74400
74450
|
const config2 = loadConfig2();
|
|
74401
74451
|
const fetcher = options.fetcher ?? globalThis.fetch;
|
|
74402
|
-
const
|
|
74403
|
-
|
|
74404
|
-
|
|
74405
|
-
|
|
74406
|
-
|
|
74407
|
-
|
|
74408
|
-
|
|
74409
|
-
|
|
74410
|
-
|
|
74411
|
-
|
|
74412
|
-
|
|
74413
|
-
]);
|
|
74452
|
+
const localTemplate = isLocalHost(config2.host) ? waitForTemplate(config2, fetcher).then(
|
|
74453
|
+
() => void 0,
|
|
74454
|
+
(error63) => new StartupFailure("template", error63)
|
|
74455
|
+
) : void 0;
|
|
74456
|
+
const boardConfig = await loadBoardConfig(
|
|
74457
|
+
config2.boardConfigUrl,
|
|
74458
|
+
fetcher,
|
|
74459
|
+
config2.assetPath.includes("__ASSET_PATH__") ? void 0 : schemaUrlForAssetPath(config2.assetPath)
|
|
74460
|
+
).catch((error63) => {
|
|
74461
|
+
throw new StartupFailure("board-config", error63);
|
|
74462
|
+
});
|
|
74414
74463
|
category = "board-config";
|
|
74415
74464
|
const board = selectBoard(config2.board, boardConfig);
|
|
74416
74465
|
const resolvedConfig = { ...config2, board };
|
|
74466
|
+
const securityState = deploymentSecurityState(resolvedConfig, boardConfig);
|
|
74467
|
+
if (securityState === "blocked") {
|
|
74468
|
+
logger2.log({
|
|
74469
|
+
event: "server.required_auth_missing",
|
|
74470
|
+
serverVersion: config2.serverRelease,
|
|
74471
|
+
host: config2.host,
|
|
74472
|
+
port: config2.port
|
|
74473
|
+
});
|
|
74474
|
+
return {
|
|
74475
|
+
app: createBlockedApp(),
|
|
74476
|
+
config: resolvedConfig
|
|
74477
|
+
};
|
|
74478
|
+
}
|
|
74479
|
+
if (localTemplate) {
|
|
74480
|
+
const failure3 = await localTemplate;
|
|
74481
|
+
if (failure3) throw failure3;
|
|
74482
|
+
} else {
|
|
74483
|
+
await waitForTemplate(config2, fetcher).catch((error63) => {
|
|
74484
|
+
throw new StartupFailure("template", error63);
|
|
74485
|
+
});
|
|
74486
|
+
}
|
|
74417
74487
|
const allowlist = deriveValidatedAllowlist(boardConfig);
|
|
74418
74488
|
const credentialNames = Object.values(boardConfig.sources).flatMap(credentialEnvironmentNames);
|
|
74419
74489
|
category = "credentials";
|
|
@@ -74424,7 +74494,7 @@ async function startup(options = {}) {
|
|
|
74424
74494
|
category = "authentication";
|
|
74425
74495
|
const accessTokenVerifier = boardConfig.auth ? await createAccessTokenVerifier(boardConfig.auth, fetcher) : void 0;
|
|
74426
74496
|
category = "unknown";
|
|
74427
|
-
warnAboutMissingAuth(resolvedConfig, logger2);
|
|
74497
|
+
if (securityState === "warning") warnAboutMissingAuth(resolvedConfig, logger2);
|
|
74428
74498
|
return {
|
|
74429
74499
|
app: createApp({
|
|
74430
74500
|
config: resolvedConfig,
|
|
@@ -74497,7 +74567,6 @@ async function waitForTemplate(config2, fetcher) {
|
|
|
74497
74567
|
}
|
|
74498
74568
|
var RETRY_INTERVAL_MILLIS = 250;
|
|
74499
74569
|
function warnAboutMissingAuth(config2, logger2) {
|
|
74500
|
-
if (isLocalHost(config2.host)) return;
|
|
74501
74570
|
logger2.log({
|
|
74502
74571
|
event: "server.no_auth_warning",
|
|
74503
74572
|
serverVersion: config2.serverRelease,
|
package/package.json
CHANGED