@continuous-excellence/ze-great-dashboard-aws 0.24.2 → 0.25.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
@@ -303,6 +303,8 @@ import { z as z3 } from "zod";
303
303
  var clientEnvSchema = z3.object({
304
304
  /** Where the immutable client assets live. The one variable that repoints the whole client. */
305
305
  assetPath: z3.string().min(1),
306
+ /** Opaque SHA-256 fingerprint of the canonical asset path, for diagnostic correlation only. */
307
+ assetPathId: z3.string().regex(/^sha256:[a-f0-9]{64}$/),
306
308
  /** Same-origin path the proxy is mounted at, so there is no CORS and no cookie problem. */
307
309
  proxyPath: z3.string().min(1),
308
310
  /** Which board this is, resolved server-side so the client parses no URLs. */
@@ -313,7 +315,7 @@ var clientEnvSchema = z3.object({
313
315
  clientId: z3.string().min(1).optional()
314
316
  }).optional()
315
317
  });
316
- var clientIdentitySchema = clientEnvSchema.pick({ assetPath: true });
318
+ var clientIdentityResponseSchema = clientEnvSchema.pick({ assetPath: true, assetPathId: true }).extend({ serverVersion: z3.string().min(1) });
317
319
 
318
320
  // packages/shared/src/envelope.ts
319
321
  import { z as z4 } from "zod";
package/dist/index.js CHANGED
@@ -984,6 +984,8 @@ import { z as z3 } from "zod";
984
984
  var clientEnvSchema = z3.object({
985
985
  /** Where the immutable client assets live. The one variable that repoints the whole client. */
986
986
  assetPath: z3.string().min(1),
987
+ /** Opaque SHA-256 fingerprint of the canonical asset path, for diagnostic correlation only. */
988
+ assetPathId: z3.string().regex(/^sha256:[a-f0-9]{64}$/),
987
989
  /** Same-origin path the proxy is mounted at, so there is no CORS and no cookie problem. */
988
990
  proxyPath: z3.string().min(1),
989
991
  /** Which board this is, resolved server-side so the client parses no URLs. */
@@ -994,7 +996,7 @@ var clientEnvSchema = z3.object({
994
996
  clientId: z3.string().min(1).optional()
995
997
  }).optional()
996
998
  });
997
- var clientIdentitySchema = clientEnvSchema.pick({ assetPath: true });
999
+ var clientIdentityResponseSchema = clientEnvSchema.pick({ assetPath: true, assetPathId: true }).extend({ serverVersion: z3.string().min(1) });
998
1000
 
999
1001
  // packages/shared/src/envelope.ts
1000
1002
  import { z as z4 } from "zod";
package/dist/lambda.mjs CHANGED
@@ -25202,7 +25202,7 @@ var require_dist_cjs11 = __commonJS({
25202
25202
  var { setCredentialFeature: setCredentialFeature2 } = (init_client3(), __toCommonJS(client_exports2));
25203
25203
  var { CredentialsProviderError: CredentialsProviderError2, parseKnownFiles: parseKnownFiles2, getProfileName: getProfileName2 } = (init_config2(), __toCommonJS(config_exports));
25204
25204
  var { HttpRequest: HttpRequest2 } = (init_protocols(), __toCommonJS(protocols_exports));
25205
- var { createHash: createHash5, createPrivateKey, createPublicKey, sign: sign2 } = __require("node:crypto");
25205
+ var { createHash: createHash6, createPrivateKey, createPublicKey, sign: sign2 } = __require("node:crypto");
25206
25206
  var { promises } = __require("node:fs");
25207
25207
  var { homedir: homedir2 } = __require("node:os");
25208
25208
  var { dirname, join: join5 } = __require("node:path");
@@ -25371,7 +25371,7 @@ var require_dist_cjs11 = __commonJS({
25371
25371
  getTokenFilePath() {
25372
25372
  const directory = process.env.AWS_LOGIN_CACHE_DIRECTORY ?? join5(homedir2(), ".aws", "login", "cache");
25373
25373
  const loginSessionBytes = Buffer.from(this.loginSession, "utf8");
25374
- const loginSessionSha256 = createHash5("sha256").update(loginSessionBytes).digest("hex");
25374
+ const loginSessionSha256 = createHash6("sha256").update(loginSessionBytes).digest("hex");
25375
25375
  return join5(directory, `${loginSessionSha256}.json`);
25376
25376
  }
25377
25377
  derToRawSignature(derSignature) {
@@ -68552,6 +68552,8 @@ function boardSchemaModeline(url2) {
68552
68552
  var clientEnvSchema = external_exports.object({
68553
68553
  /** Where the immutable client assets live. The one variable that repoints the whole client. */
68554
68554
  assetPath: external_exports.string().min(1),
68555
+ /** Opaque SHA-256 fingerprint of the canonical asset path, for diagnostic correlation only. */
68556
+ assetPathId: external_exports.string().regex(/^sha256:[a-f0-9]{64}$/),
68555
68557
  /** Same-origin path the proxy is mounted at, so there is no CORS and no cookie problem. */
68556
68558
  proxyPath: external_exports.string().min(1),
68557
68559
  /** Which board this is, resolved server-side so the client parses no URLs. */
@@ -68562,7 +68564,7 @@ var clientEnvSchema = external_exports.object({
68562
68564
  clientId: external_exports.string().min(1).optional()
68563
68565
  }).optional()
68564
68566
  });
68565
- var clientIdentitySchema = clientEnvSchema.pick({ assetPath: true });
68567
+ var clientIdentityResponseSchema = clientEnvSchema.pick({ assetPath: true, assetPathId: true }).extend({ serverVersion: external_exports.string().min(1) });
68566
68568
 
68567
68569
  // packages/shared/src/envelope.ts
68568
68570
  var errorKindSchema = external_exports.enum([
@@ -72032,6 +72034,68 @@ var Hono2 = class extends Hono {
72032
72034
  // packages/server/src/app.ts
72033
72035
  var import_yaml = __toESM(require_dist(), 1);
72034
72036
 
72037
+ // packages/server/src/config.ts
72038
+ import { createHash as createHash5 } from "node:crypto";
72039
+ import { fileURLToPath } from "node:url";
72040
+ var DEFAULT_BOARD_CONFIG_URL = fileURLToPath(
72041
+ new URL("../../../boards/example.yaml", import.meta.url)
72042
+ );
72043
+ var configSchema = external_exports.object({
72044
+ /**
72045
+ * Where the built client lives. Points at a versioned CDN path in a deployment
72046
+ * (`https://assets.../dashboard/1.0.7`) or at the Vite dev server locally. Trailing slashes are
72047
+ * trimmed so `${assetPath}/index.html` never doubles up.
72048
+ */
72049
+ assetPath: external_exports.string({
72050
+ error: "ASSET_PATH is required \u2014 the server has no client to serve without it."
72051
+ }).min(1, "ASSET_PATH is required \u2014 the server has no client to serve without it.").transform((value) => value.replace(/\/+$/, "")),
72052
+ proxyPath: external_exports.string().min(1).default("/api"),
72053
+ /** A URL or a local file path. Local development uses a path and needs no credential. */
72054
+ boardConfigUrl: external_exports.string().min(1).default(DEFAULT_BOARD_CONFIG_URL),
72055
+ /** Optional for single-board files; startup selects the only board automatically. */
72056
+ board: external_exports.string().min(1).optional(),
72057
+ /** Optional ARN of the one Secrets Manager JSON credential map for this Lambda. */
72058
+ secretReference: external_exports.string().min(1).optional(),
72059
+ /** Immutable server image identifier, supplied by the image build and emitted only in startup logs. */
72060
+ serverRelease: external_exports.string().min(1).default("development"),
72061
+ port: external_exports.coerce.number().int().min(1).max(65535).default(3e3),
72062
+ host: external_exports.string().min(1).default("localhost"),
72063
+ /**
72064
+ * How long to keep retrying an unreachable template before giving up, in milliseconds.
72065
+ *
72066
+ * Zero — the default, and what every deployment uses — means a bad ASSET_PATH fails on the first
72067
+ * attempt, which is the behavior a misconfiguration deserves. `npm run dev` sets a few seconds
72068
+ * because it starts the server and the Vite dev server at the same moment, and without this the
72069
+ * local loop is a race the developer loses about half the time.
72070
+ */
72071
+ templateWaitMillis: external_exports.coerce.number().int().min(0).default(0)
72072
+ });
72073
+ function loadConfig2(env2 = process.env) {
72074
+ const result = configSchema.safeParse({
72075
+ assetPath: env2.ASSET_PATH,
72076
+ proxyPath: env2.PROXY_PATH,
72077
+ boardConfigUrl: env2.BOARD_CONFIG_URL,
72078
+ board: env2.BOARD,
72079
+ secretReference: env2.SECRET_REFERENCE,
72080
+ serverRelease: env2.SERVER_RELEASE,
72081
+ port: env2.PORT,
72082
+ host: env2.HOST,
72083
+ templateWaitMillis: env2.TEMPLATE_WAIT_MS
72084
+ });
72085
+ if (!result.success) {
72086
+ throw new Error(`Invalid server configuration:
72087
+ ${external_exports.prettifyError(result.error)}`);
72088
+ }
72089
+ return result.data;
72090
+ }
72091
+ function assetPathId(assetPath) {
72092
+ return `sha256:${createHash5("sha256").update(assetPath).digest("hex")}`;
72093
+ }
72094
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
72095
+ function isLocalHost(host) {
72096
+ return LOCAL_HOSTS.has(host);
72097
+ }
72098
+
72035
72099
  // packages/server/src/logger.ts
72036
72100
  var consoleLogger = {
72037
72101
  log(event) {
@@ -72049,6 +72113,29 @@ function destinationOrigin(value) {
72049
72113
  return void 0;
72050
72114
  }
72051
72115
  }
72116
+ function serverDiagnosticContext(serverVersion, configuredAssetPathId) {
72117
+ return { serverVersion, configuredAssetPathId };
72118
+ }
72119
+ function clientDiagnosticClaims(headers, context) {
72120
+ const version2 = headers.get("x-dashboard-client-version");
72121
+ const origin = headers.get("x-dashboard-client-origin");
72122
+ const assetPathId2 = headers.get("x-dashboard-client-asset-id");
72123
+ const clientOrigin = safeOrigin(origin);
72124
+ return {
72125
+ ...version2 && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(version2) ? { clientVersion: version2 } : {},
72126
+ ...clientOrigin ? { clientOrigin } : {},
72127
+ ...assetPathId2 && /^sha256:[a-f0-9]{64}$/.test(assetPathId2) ? { clientAssetPathMatchesConfigured: assetPathId2 === context.configuredAssetPathId } : {}
72128
+ };
72129
+ }
72130
+ function safeOrigin(value) {
72131
+ if (!value) return void 0;
72132
+ try {
72133
+ const origin = new URL(value).origin;
72134
+ return origin === "null" ? void 0 : origin;
72135
+ } catch {
72136
+ return void 0;
72137
+ }
72138
+ }
72052
72139
 
72053
72140
  // packages/server/src/template.ts
72054
72141
  var ASSET_PATH_SENTINEL = "/__ASSET_PATH__";
@@ -72134,6 +72221,10 @@ function createApp(deps) {
72134
72221
  const credentials = deps.credentials ?? environmentCredentials();
72135
72222
  const githubClient = createGithubClient(credentials);
72136
72223
  const logger2 = deps.logger ?? consoleLogger;
72224
+ const diagnosticContext = serverDiagnosticContext(
72225
+ config2.serverRelease,
72226
+ assetPathId(config2.assetPath)
72227
+ );
72137
72228
  const app = new Hono2();
72138
72229
  app.use("/api/*", async (c5, next) => {
72139
72230
  const id = requestId();
@@ -72145,8 +72236,10 @@ function createApp(deps) {
72145
72236
  const id = c5.get("dashboardRequestId");
72146
72237
  logger2.log({
72147
72238
  event: "server.unhandled_exception",
72239
+ serverVersion: diagnosticContext.serverVersion,
72148
72240
  ...id ? { requestId: id } : {},
72149
- operation: "route-handler"
72241
+ operation: "route-handler",
72242
+ ...clientDiagnosticClaims(c5.req.raw.headers, diagnosticContext)
72150
72243
  });
72151
72244
  return c5.json(
72152
72245
  { error: "The dashboard could not complete this request." },
@@ -72156,7 +72249,11 @@ function createApp(deps) {
72156
72249
  });
72157
72250
  app.get("/health", (c5) => c5.json({ status: "ok" }));
72158
72251
  app.get(`${config2.proxyPath}/client`, (c5) => {
72159
- const identity = { assetPath: config2.assetPath };
72252
+ const identity = {
72253
+ assetPath: config2.assetPath,
72254
+ assetPathId: diagnosticContext.configuredAssetPathId,
72255
+ serverVersion: diagnosticContext.serverVersion
72256
+ };
72160
72257
  return c5.json(identity, 200, { "cache-control": "no-store" });
72161
72258
  });
72162
72259
  app.get("/", (c5) => renderEntrypoint(c5.req.raw, selectedBoard));
@@ -72386,10 +72483,12 @@ ${(0, import_yaml.stringify)(outputConfig, { sortMapEntries: true })}`,
72386
72483
  function rejected(c5, context) {
72387
72484
  logger2.log({
72388
72485
  event: "api.operation_rejected",
72486
+ serverVersion: diagnosticContext.serverVersion,
72389
72487
  requestId: c5.get("dashboardRequestId"),
72390
72488
  boardId: context.boardId,
72391
72489
  panelId: context.panelId,
72392
- operation: context.operation
72490
+ operation: context.operation,
72491
+ ...clientDiagnosticClaims(c5.req.raw.headers, diagnosticContext)
72393
72492
  });
72394
72493
  return c5.notFound();
72395
72494
  }
@@ -72400,10 +72499,12 @@ ${(0, import_yaml.stringify)(outputConfig, { sortMapEntries: true })}`,
72400
72499
  if (adapted.envelope?.state === "error") {
72401
72500
  logger2.log({
72402
72501
  event: "panel.observation_failed",
72502
+ serverVersion: diagnosticContext.serverVersion,
72403
72503
  requestId: c5.get("dashboardRequestId"),
72404
72504
  boardId: context.boardId,
72405
72505
  panelId: context.panelId,
72406
72506
  operation: context.operation,
72507
+ ...clientDiagnosticClaims(c5.req.raw.headers, diagnosticContext),
72407
72508
  errorKind: adapted.envelope.error.kind,
72408
72509
  elapsedMs: Math.round(performance.now() - started),
72409
72510
  ...context.sourceName ? { sourceName: context.sourceName } : {},
@@ -72419,6 +72520,7 @@ ${(0, import_yaml.stringify)(outputConfig, { sortMapEntries: true })}`,
72419
72520
  const template = await templates.get(config2.assetPath);
72420
72521
  const env2 = {
72421
72522
  assetPath: config2.assetPath,
72523
+ assetPathId: diagnosticContext.configuredAssetPathId,
72422
72524
  proxyPath: config2.proxyPath,
72423
72525
  board
72424
72526
  };
@@ -72465,68 +72567,13 @@ async function fetchBoardConfig(url2, fetcher) {
72465
72567
  return response.text();
72466
72568
  }
72467
72569
 
72468
- // packages/server/src/config.ts
72469
- import { fileURLToPath } from "node:url";
72470
- var DEFAULT_BOARD_CONFIG_URL = fileURLToPath(
72471
- new URL("../../../boards/example.yaml", import.meta.url)
72472
- );
72473
- var configSchema = external_exports.object({
72474
- /**
72475
- * Where the built client lives. Points at a versioned CDN path in a deployment
72476
- * (`https://assets.../dashboard/1.0.7`) or at the Vite dev server locally. Trailing slashes are
72477
- * trimmed so `${assetPath}/index.html` never doubles up.
72478
- */
72479
- assetPath: external_exports.string({
72480
- error: "ASSET_PATH is required \u2014 the server has no client to serve without it."
72481
- }).min(1, "ASSET_PATH is required \u2014 the server has no client to serve without it.").transform((value) => value.replace(/\/+$/, "")),
72482
- proxyPath: external_exports.string().min(1).default("/api"),
72483
- /** A URL or a local file path. Local development uses a path and needs no credential. */
72484
- boardConfigUrl: external_exports.string().min(1).default(DEFAULT_BOARD_CONFIG_URL),
72485
- /** Optional for single-board files; startup selects the only board automatically. */
72486
- board: external_exports.string().min(1).optional(),
72487
- /** Optional ARN of the one Secrets Manager JSON credential map for this Lambda. */
72488
- secretReference: external_exports.string().min(1).optional(),
72489
- /** Immutable server image identifier, supplied by the image build and emitted only in startup logs. */
72490
- serverRelease: external_exports.string().min(1).default("development"),
72491
- port: external_exports.coerce.number().int().min(1).max(65535).default(3e3),
72492
- host: external_exports.string().min(1).default("localhost"),
72493
- /**
72494
- * How long to keep retrying an unreachable template before giving up, in milliseconds.
72495
- *
72496
- * Zero — the default, and what every deployment uses — means a bad ASSET_PATH fails on the first
72497
- * attempt, which is the behavior a misconfiguration deserves. `npm run dev` sets a few seconds
72498
- * because it starts the server and the Vite dev server at the same moment, and without this the
72499
- * local loop is a race the developer loses about half the time.
72500
- */
72501
- templateWaitMillis: external_exports.coerce.number().int().min(0).default(0)
72502
- });
72503
- function loadConfig2(env2 = process.env) {
72504
- const result = configSchema.safeParse({
72505
- assetPath: env2.ASSET_PATH,
72506
- proxyPath: env2.PROXY_PATH,
72507
- boardConfigUrl: env2.BOARD_CONFIG_URL,
72508
- board: env2.BOARD,
72509
- secretReference: env2.SECRET_REFERENCE,
72510
- serverRelease: env2.SERVER_RELEASE,
72511
- port: env2.PORT,
72512
- host: env2.HOST,
72513
- templateWaitMillis: env2.TEMPLATE_WAIT_MS
72514
- });
72515
- if (!result.success) {
72516
- throw new Error(`Invalid server configuration:
72517
- ${external_exports.prettifyError(result.error)}`);
72518
- }
72519
- return result.data;
72520
- }
72521
- var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
72522
- function isLocalHost(host) {
72523
- return LOCAL_HOSTS.has(host);
72524
- }
72525
-
72526
72570
  // packages/server/src/startup.ts
72527
72571
  async function startup(options = {}) {
72528
72572
  const logger2 = options.logger ?? consoleLogger;
72529
- logger2.log({ event: "server.starting" });
72573
+ logger2.log({
72574
+ event: "server.starting",
72575
+ serverVersion: process.env.SERVER_RELEASE ?? "development"
72576
+ });
72530
72577
  try {
72531
72578
  const config2 = loadConfig2();
72532
72579
  const fetcher = options.fetcher ?? globalThis.fetch;
@@ -72559,7 +72606,11 @@ async function startup(options = {}) {
72559
72606
  config: resolvedConfig
72560
72607
  };
72561
72608
  } catch (error62) {
72562
- logger2.log({ event: "server.startup_failed", category: startupFailureCategory(error62) });
72609
+ logger2.log({
72610
+ event: "server.startup_failed",
72611
+ serverVersion: process.env.SERVER_RELEASE ?? "development",
72612
+ category: startupFailureCategory(error62)
72613
+ });
72563
72614
  throw error62;
72564
72615
  }
72565
72616
  }
@@ -72598,7 +72649,12 @@ async function waitForTemplate(config2, fetcher) {
72598
72649
  var RETRY_INTERVAL_MILLIS = 250;
72599
72650
  function warnAboutMissingAuth(config2, logger2) {
72600
72651
  if (isLocalHost(config2.host)) return;
72601
- logger2.log({ event: "server.no_auth_warning", host: config2.host, port: config2.port });
72652
+ logger2.log({
72653
+ event: "server.no_auth_warning",
72654
+ serverVersion: config2.serverRelease,
72655
+ host: config2.host,
72656
+ port: config2.port
72657
+ });
72602
72658
  }
72603
72659
  function startupFailureCategory(error62) {
72604
72660
  const message = error62 instanceof Error ? error62.message : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@continuous-excellence/ze-great-dashboard-aws",
3
- "version": "0.24.2",
3
+ "version": "0.25.0",
4
4
  "type": "module",
5
5
  "description": "AWS server runtime and CloudFormation deployment tooling for Ze Great Dashboard.",
6
6
  "keywords": [