@continuous-excellence/ze-great-dashboard-aws 0.29.1 → 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 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 [, boardConfig] = await Promise.all([
74403
- waitForTemplate(config2, fetcher).catch((error63) => {
74404
- throw new StartupFailure("template", error63);
74405
- }),
74406
- loadBoardConfig(
74407
- config2.boardConfigUrl,
74408
- fetcher,
74409
- config2.assetPath.includes("__ASSET_PATH__") ? void 0 : schemaUrlForAssetPath(config2.assetPath)
74410
- ).catch((error63) => {
74411
- throw new StartupFailure("board-config", error63);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@continuous-excellence/ze-great-dashboard-aws",
3
- "version": "0.29.1",
3
+ "version": "0.30.0",
4
4
  "type": "module",
5
5
  "description": "AWS server runtime and CloudFormation deployment tooling for Ze Great Dashboard.",
6
6
  "keywords": [