@continuous-excellence/ze-great-dashboard-aws 0.16.0 → 0.17.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/index.js CHANGED
@@ -728,33 +728,87 @@ import { parseDocument, Scalar } from "yaml";
728
728
  import { createHash } from "node:crypto";
729
729
  import { mkdir, readFile, writeFile } from "node:fs/promises";
730
730
  import { join, resolve } from "node:path";
731
- import { parse, stringify } from "yaml";
731
+
732
+ // packages/shared/src/board-config.ts
732
733
  import { z as z2 } from "zod";
733
734
 
734
- // packages/aws/src/internal-board.ts
735
+ // packages/shared/src/duration.ts
735
736
  import { z } from "zod";
736
- var durationSchema = z.string().regex(/^\d+(?:ms|s|m|h)$/).refine((value2) => Number.parseInt(value2, 10) > 0).brand();
737
- var positionSchema = z.union([
738
- z.object({
739
- x: z.number().int().min(0),
740
- y: z.number().int().min(0),
741
- w: z.number().int().min(1).max(12),
742
- h: z.number().int().min(1)
737
+ var DURATION_PATTERN = /^(\d+)(ms|s|m|h)$/;
738
+ var UNIT_MILLIS = {
739
+ ms: 1,
740
+ s: 1e3,
741
+ m: 6e4,
742
+ h: 36e5
743
+ };
744
+ function parseDuration(value2) {
745
+ const match = DURATION_PATTERN.exec(value2);
746
+ if (!match) return null;
747
+ const [, rawAmount, rawUnit] = match;
748
+ if (rawAmount === void 0 || rawUnit === void 0) return null;
749
+ const amount = Number(rawAmount);
750
+ if (amount <= 0) return null;
751
+ return amount * UNIT_MILLIS[rawUnit];
752
+ }
753
+ var durationSchema = z.string().refine((value2) => parseDuration(value2) !== null, {
754
+ message: 'must be a positive duration like "30s", "5m", or "1h"'
755
+ }).brand();
756
+
757
+ // packages/shared/src/board-config.ts
758
+ var positionSchema = z2.union([
759
+ z2.object({
760
+ x: z2.number().int().min(0),
761
+ y: z2.number().int().min(0),
762
+ w: z2.number().int().min(1).max(12),
763
+ h: z2.number().int().min(1)
743
764
  }),
744
- z.object({ x: z.literal(0), y: z.literal(0), w: z.literal(0), h: z.literal(0) })
765
+ z2.object({ x: z2.literal(0), y: z2.literal(0), w: z2.literal(0), h: z2.literal(0) })
745
766
  ]);
746
- var panelSchema = z.looseObject({
747
- // Presentation-only; ids still address proxy calls and the generated allowlist.
748
- label: z.string().min(1).optional(),
749
- id: z.string().min(1),
750
- type: z.string().min(1),
751
- source: z.string().min(1).optional(),
752
- density: z.enum(["auto", "comfortable", "compact"]).optional(),
767
+ var panelDensities = ["auto", "comfortable", "compact"];
768
+ var panelDensitySchema = z2.enum(panelDensities);
769
+ var visibleRunningAnimations = [
770
+ "radial",
771
+ "runway",
772
+ "orbit",
773
+ "signal-field",
774
+ "telemetry-bloom",
775
+ "release-transit",
776
+ "status-weather",
777
+ "falling-shapes"
778
+ ];
779
+ var runningAnimationSchema = z2.enum([...visibleRunningAnimations, "off"]);
780
+ var panelSchema = z2.looseObject({
781
+ /** Presentation-only wall label. `id` remains the stable proxy and allowlist address. */
782
+ label: z2.string().min(1).optional(),
783
+ id: z2.string().min(1),
784
+ type: z2.string().min(1),
785
+ source: z2.string().min(1).optional(),
786
+ pipeline: z2.union([z2.string(), z2.number()]).optional(),
787
+ /** Resolved public source branch, supplied by the board endpoint for client memory scoping. */
788
+ branch: z2.string().min(1).optional(),
789
+ /** Content-density bias; position still controls the panel's explicit grid placement. */
790
+ density: panelDensitySchema.optional(),
791
+ /** Advisory in v1 — a panel without a position renders in config order rather than not at all. */
753
792
  position: positionSchema.optional(),
754
793
  refresh: durationSchema.optional(),
755
- link: z.url().optional(),
756
- url: z.url().optional(),
757
- json_path: z.string().regex(/^\$(?:\.[A-Za-z_][A-Za-z0-9_]*|\[\d+\])*$/).optional()
794
+ /** Active pipeline runs are checked more often than normal panels. */
795
+ running_refresh: durationSchema.optional(),
796
+ /** Polling cadence around the estimated completion boundary. */
797
+ running_completion_refresh: durationSchema.optional(),
798
+ /** Maximum length of the tighter completion polling burst. */
799
+ running_completion_window: durationSchema.optional(),
800
+ /** Optional active-run treatment. Omitted selects a visible treatment at random. */
801
+ running_animation: runningAnimationSchema.optional(),
802
+ /** Local animation-demo cycle duration; ignored by other panel types. */
803
+ demo_run_duration: durationSchema.optional(),
804
+ /** Local animation-demo focused-review duration; ignored by other panel types. */
805
+ demo_review_duration: durationSchema.optional(),
806
+ /** A deliberate override only. Adapters derive links; hand-written ones drift. */
807
+ link: z2.url().optional(),
808
+ /** Source-agnostic endpoint used by the http-value signal. */
809
+ url: z2.url().optional(),
810
+ /** Small, deliberate JSON path subset: $.version, $.deployment.version, or $.response.docs[0].latestVersion. */
811
+ json_path: z2.string().regex(/^\$(?:\.[A-Za-z_][A-Za-z0-9_]*|\[\d+\])*$/, "must be a simple JSON path").optional()
758
812
  }).superRefine((panel, ctx) => {
759
813
  if ("display" in panel) {
760
814
  ctx.addIssue({
@@ -764,13 +818,218 @@ var panelSchema = z.looseObject({
764
818
  });
765
819
  }
766
820
  });
767
- var sourceSchema = z.looseObject({
768
- type: z.string().min(1),
769
- token_env: z.string().min(1).optional()
821
+ var sourceSchema = z2.looseObject({
822
+ type: z2.string().min(1),
823
+ /** Credentials are never in the config — only the name of the env var holding one. */
824
+ token_env: z2.string().min(1).optional()
770
825
  });
771
- var boardSchema = z.object({
826
+ var boardSchema = z2.object({
772
827
  refresh: durationSchema.optional(),
773
- panels: z.array(panelSchema).min(1).superRefine((panels, ctx) => {
828
+ running_refresh: durationSchema.optional(),
829
+ running_completion_refresh: durationSchema.optional(),
830
+ running_completion_window: durationSchema.optional(),
831
+ panels: z2.array(panelSchema).min(1).superRefine((panels, ctx) => {
832
+ const seen = /* @__PURE__ */ new Map();
833
+ panels.forEach((panel, index) => {
834
+ const firstIndex = seen.get(panel.id);
835
+ if (firstIndex === void 0) {
836
+ seen.set(panel.id, index);
837
+ return;
838
+ }
839
+ ctx.addIssue({
840
+ code: "custom",
841
+ path: [index, "id"],
842
+ message: `duplicate panel id "${panel.id}" (already used by panel at index ${firstIndex})`
843
+ });
844
+ });
845
+ })
846
+ });
847
+ var authSchema = z2.object({
848
+ issuer: z2.url(),
849
+ /** Absent in gateway mode — the gateway does the flow, the proxy still validates. */
850
+ client_id: z2.string().min(1).optional(),
851
+ allow: z2.object({
852
+ groups: z2.array(z2.string().min(1)).optional(),
853
+ subjects: z2.array(z2.string().min(1)).optional()
854
+ }).optional()
855
+ });
856
+ var boardConfigSchema = z2.object({
857
+ sources: z2.record(z2.string().min(1), sourceSchema).default({}),
858
+ boards: z2.record(z2.string().min(1), boardSchema),
859
+ auth: authSchema.optional()
860
+ });
861
+
862
+ // packages/shared/src/board-config-modeline.ts
863
+ var MODEL_LINE = /^# yaml-language-server:\s*\$schema=(\S+)\s*$/;
864
+ var SCHEMA_FILE = "board-config.schema.json";
865
+ function readBoardSchemaModeline(source) {
866
+ const line = source.split(/\r?\n/).find((candidate) => candidate.includes("yaml-language-server"));
867
+ if (!line) throw new Error(`Board configuration must include a ${SCHEMA_FILE} modeline`);
868
+ const match = MODEL_LINE.exec(line);
869
+ if (!match?.[1]) {
870
+ throw new Error(
871
+ `Malformed board schema modeline; expected "# yaml-language-server: $schema=<URL>/${SCHEMA_FILE}"`
872
+ );
873
+ }
874
+ let url;
875
+ try {
876
+ url = new URL(match[1]);
877
+ } catch {
878
+ throw new Error(`Malformed board schema modeline; schema URL is not a valid URL: ${match[1]}`);
879
+ }
880
+ if (!["http:", "https:"].includes(url.protocol) || !url.pathname.endsWith(`/${SCHEMA_FILE}`)) {
881
+ throw new Error(`Malformed board schema modeline; URL must end in /${SCHEMA_FILE}: ${match[1]}`);
882
+ }
883
+ return match[1];
884
+ }
885
+ function boardSchemaModeline(url) {
886
+ return `# yaml-language-server: $schema=${url}`;
887
+ }
888
+ var boardSchemaFileName = SCHEMA_FILE;
889
+
890
+ // packages/shared/src/client-env.ts
891
+ import { z as z3 } from "zod";
892
+ var clientEnvSchema = z3.object({
893
+ /** Where the immutable client assets live. The one variable that repoints the whole client. */
894
+ assetPath: z3.string().min(1),
895
+ /** Same-origin path the proxy is mounted at, so there is no CORS and no cookie problem. */
896
+ proxyPath: z3.string().min(1),
897
+ /** Which board this is, resolved server-side so the client parses no URLs. */
898
+ board: z3.string().min(1),
899
+ /** The client version being served, so two published versions are visibly distinguishable. */
900
+ clientVersion: z3.string().min(1),
901
+ /** Omitted entirely when unauthenticated — presence is what turns auth on. */
902
+ auth: z3.object({
903
+ issuer: z3.string().min(1),
904
+ clientId: z3.string().min(1).optional()
905
+ }).optional()
906
+ });
907
+ var clientIdentitySchema = clientEnvSchema.pick({ assetPath: true, clientVersion: true });
908
+
909
+ // packages/shared/src/envelope.ts
910
+ import { z as z4 } from "zod";
911
+ var errorKindSchema = z4.enum([
912
+ "unreachable",
913
+ "unauthorized",
914
+ "not-found",
915
+ "no-runs",
916
+ "upstream-error"
917
+ ]);
918
+ var envelopeErrorSchema = z4.object({
919
+ kind: errorKindSchema,
920
+ message: z4.string()
921
+ });
922
+ var envelopeSchema = z4.discriminatedUnion("state", [
923
+ z4.object({
924
+ panelId: z4.string().min(1),
925
+ state: z4.literal("ok"),
926
+ /** From the upstream response `Date`. A cached value must never be shown without its age. */
927
+ observedAt: z4.iso.datetime(),
928
+ /** Adapter-derived. The authority for this panel; the dashboard itself asserts nothing. */
929
+ link: z4.url().nullable(),
930
+ signal: z4.unknown()
931
+ }),
932
+ z4.object({
933
+ panelId: z4.string().min(1),
934
+ state: z4.literal("error"),
935
+ /**
936
+ * Populated even on error: knowing *when* a failure was observed is the difference between
937
+ * "broken just now" and "broken all morning", and a panel that can't reach its source is
938
+ * exactly when a viewer most wants to click through.
939
+ */
940
+ observedAt: z4.iso.datetime(),
941
+ link: z4.url().nullable(),
942
+ error: envelopeErrorSchema
943
+ })
944
+ ]);
945
+ var pipelineActivitySchema = z4.object({
946
+ kind: z4.enum(["job", "stage", "step"]),
947
+ name: z4.string().min(1),
948
+ parent: z4.string().min(1).optional()
949
+ });
950
+ var pipelineStatusSchema = z4.object({
951
+ type: z4.literal("pipeline-status"),
952
+ status: z4.enum(["passed", "failed", "running", "cancelled", "unknown"]),
953
+ /** The source's unmodified status/result vocabulary, for an honest display. */
954
+ rawStatus: z4.string(),
955
+ name: z4.string().min(1),
956
+ /** The branch represented by this status, when the upstream supplies or filters by one. */
957
+ branch: z4.string().min(1).optional(),
958
+ /** Elapsed execution time, supplied only after the upstream run has completed. */
959
+ durationMs: z4.number().int().nonnegative().optional(),
960
+ /** When an active run began, so the client can advance its elapsed display between polls. */
961
+ runStartedAt: z4.iso.datetime().optional(),
962
+ /** Stable source-run identity, when the adapter provides one. */
963
+ sourceRunId: z4.string().min(1).optional(),
964
+ /** Best-effort live work detail; adapters may describe a job, stage, or step. */
965
+ activity: pipelineActivitySchema.optional(),
966
+ /** Advisory duration from recent completed runs remembered by this browser. */
967
+ estimatedDurationMs: z4.number().int().positive().optional(),
968
+ /** When this workflow run was last updated by its source, distinct from our observation time. */
969
+ sourceUpdatedAt: z4.iso.datetime().optional()
970
+ });
971
+ var pullRequestHealthItemSchema = z4.object({
972
+ label: z4.string().min(1),
973
+ status: pipelineStatusSchema.shape.status,
974
+ detail: z4.string().min(1),
975
+ link: z4.url().nullable()
976
+ });
977
+ var pullRequestHealthSchema = z4.object({
978
+ type: z4.literal("pull-request-health"),
979
+ status: pipelineStatusSchema.shape.status,
980
+ summary: z4.string().min(1),
981
+ workflows: z4.array(pullRequestHealthItemSchema),
982
+ pullRequests: z4.array(pullRequestHealthItemSchema)
983
+ });
984
+ var httpValueSchema = z4.object({
985
+ type: z4.literal("http-value"),
986
+ value: z4.union([z4.string(), z4.number(), z4.boolean()])
987
+ });
988
+
989
+ // packages/aws/src/release.ts
990
+ import { parse, stringify } from "yaml";
991
+ import { z as z6 } from "zod";
992
+
993
+ // packages/aws/src/internal-board.ts
994
+ import { z as z5 } from "zod";
995
+ var durationSchema2 = z5.string().regex(/^\d+(?:ms|s|m|h)$/).refine((value2) => Number.parseInt(value2, 10) > 0).brand();
996
+ var positionSchema2 = z5.union([
997
+ z5.object({
998
+ x: z5.number().int().min(0),
999
+ y: z5.number().int().min(0),
1000
+ w: z5.number().int().min(1).max(12),
1001
+ h: z5.number().int().min(1)
1002
+ }),
1003
+ z5.object({ x: z5.literal(0), y: z5.literal(0), w: z5.literal(0), h: z5.literal(0) })
1004
+ ]);
1005
+ var panelSchema2 = z5.looseObject({
1006
+ // Presentation-only; ids still address proxy calls and the generated allowlist.
1007
+ label: z5.string().min(1).optional(),
1008
+ id: z5.string().min(1),
1009
+ type: z5.string().min(1),
1010
+ source: z5.string().min(1).optional(),
1011
+ density: z5.enum(["auto", "comfortable", "compact"]).optional(),
1012
+ position: positionSchema2.optional(),
1013
+ refresh: durationSchema2.optional(),
1014
+ link: z5.url().optional(),
1015
+ url: z5.url().optional(),
1016
+ json_path: z5.string().regex(/^\$(?:\.[A-Za-z_][A-Za-z0-9_]*|\[\d+\])*$/).optional()
1017
+ }).superRefine((panel, ctx) => {
1018
+ if ("display" in panel) {
1019
+ ctx.addIssue({
1020
+ code: "custom",
1021
+ path: ["display"],
1022
+ message: "display was removed; use density"
1023
+ });
1024
+ }
1025
+ });
1026
+ var sourceSchema2 = z5.looseObject({
1027
+ type: z5.string().min(1),
1028
+ token_env: z5.string().min(1).optional()
1029
+ });
1030
+ var boardSchema2 = z5.object({
1031
+ refresh: durationSchema2.optional(),
1032
+ panels: z5.array(panelSchema2).min(1).superRefine((panels, ctx) => {
774
1033
  const seen = /* @__PURE__ */ new Map();
775
1034
  panels.forEach((panel, index) => {
776
1035
  const firstIndex = seen.get(panel.id);
@@ -784,15 +1043,15 @@ var boardSchema = z.object({
784
1043
  });
785
1044
  })
786
1045
  });
787
- var boardConfigSchema = z.object({
788
- sources: z.record(z.string().min(1), sourceSchema).default({}),
789
- boards: z.record(z.string().min(1), boardSchema),
790
- auth: z.object({
791
- issuer: z.url(),
792
- client_id: z.string().min(1).optional(),
793
- allow: z.object({
794
- groups: z.array(z.string().min(1)).optional(),
795
- subjects: z.array(z.string().min(1)).optional()
1046
+ var boardConfigSchema2 = z5.object({
1047
+ sources: z5.record(z5.string().min(1), sourceSchema2).default({}),
1048
+ boards: z5.record(z5.string().min(1), boardSchema2),
1049
+ auth: z5.object({
1050
+ issuer: z5.url(),
1051
+ client_id: z5.string().min(1).optional(),
1052
+ allow: z5.object({
1053
+ groups: z5.array(z5.string().min(1)).optional(),
1054
+ subjects: z5.array(z5.string().min(1)).optional()
796
1055
  }).optional()
797
1056
  }).optional()
798
1057
  });
@@ -805,13 +1064,18 @@ function clientAssetUrl(version, domain = CANONICAL_ASSET_DOMAIN) {
805
1064
  throw new Error(`Invalid dashboard version: ${version}`);
806
1065
  return `${domain.replace(/\/+$/, "")}/dashboard/${version}`;
807
1066
  }
808
- async function validateBoardConfig(path) {
1067
+ async function validateBoardConfig(path, expectedSchemaUrl) {
809
1068
  const source = await readFile(resolve(path), "utf8");
810
- const result = boardConfigSchema.safeParse(parse(source));
811
- if (!result.success)
1069
+ const authoredSchemaUrl = readBoardSchemaModeline(source);
1070
+ const result = boardConfigSchema2.safeParse(parse(source));
1071
+ if (!result.success) {
1072
+ const stale = authoredSchemaUrl !== expectedSchemaUrl ? `
1073
+ Stale schema modeline: expected ${expectedSchemaUrl}` : "";
812
1074
  throw new Error(`Invalid board configuration:
813
- ${z2.prettifyError(result.error)}`);
814
- const yaml = stringify(result.data, { sortMapEntries: true });
1075
+ ${z6.prettifyError(result.error)}${stale}`);
1076
+ }
1077
+ const yaml = `${boardSchemaModeline(expectedSchemaUrl)}
1078
+ ${stringify(result.data, { sortMapEntries: true })}`;
815
1079
  return {
816
1080
  yaml,
817
1081
  sha256: sha256(yaml),
@@ -819,7 +1083,11 @@ ${z2.prettifyError(result.error)}`);
819
1083
  };
820
1084
  }
821
1085
  async function assembleRelease(input) {
822
- const board = await validateBoardConfig(input.boardConfigPath);
1086
+ const assetUrl = clientAssetUrl(input.version, input.assetDomain);
1087
+ const board = await validateBoardConfig(
1088
+ input.boardConfigPath,
1089
+ `${assetUrl}/${boardSchemaFileName}`
1090
+ );
823
1091
  if (board.usesCredentials && !input.secretReference)
824
1092
  throw new Error(
825
1093
  "Board config uses token_env; SecretReference must name a Secrets Manager credential-map or Parameter Store SecureString ARN"
@@ -829,7 +1097,7 @@ async function assembleRelease(input) {
829
1097
  await writeFile(join(outputDir, "board.yaml"), board.yaml);
830
1098
  const metadata = {
831
1099
  dashboardVersion: input.version,
832
- clientAssetUrl: clientAssetUrl(input.version, input.assetDomain),
1100
+ clientAssetUrl: assetUrl,
833
1101
  serverRuntimeVersion: CORE_RUNTIME_VERSION,
834
1102
  supportedProviders: input.providers ?? ["aws-lambda"],
835
1103
  artifactChecksums: { "board.yaml": board.sha256 },
package/dist/lambda.mjs CHANGED
@@ -64060,6 +64060,36 @@ var boardConfigSchema = external_exports.object({
64060
64060
  auth: authSchema.optional()
64061
64061
  });
64062
64062
 
64063
+ // packages/shared/src/board-config-modeline.ts
64064
+ var MODEL_LINE = /^# yaml-language-server:\s*\$schema=(\S+)\s*$/;
64065
+ var SCHEMA_FILE = "board-config.schema.json";
64066
+ function schemaUrlForAssetPath(assetPath) {
64067
+ return `${assetPath.replace(/\/+$/, "")}/${SCHEMA_FILE}`;
64068
+ }
64069
+ function readBoardSchemaModeline(source) {
64070
+ const line = source.split(/\r?\n/).find((candidate) => candidate.includes("yaml-language-server"));
64071
+ if (!line) throw new Error(`Board configuration must include a ${SCHEMA_FILE} modeline`);
64072
+ const match2 = MODEL_LINE.exec(line);
64073
+ if (!match2?.[1]) {
64074
+ throw new Error(
64075
+ `Malformed board schema modeline; expected "# yaml-language-server: $schema=<URL>/${SCHEMA_FILE}"`
64076
+ );
64077
+ }
64078
+ let url2;
64079
+ try {
64080
+ url2 = new URL(match2[1]);
64081
+ } catch {
64082
+ throw new Error(`Malformed board schema modeline; schema URL is not a valid URL: ${match2[1]}`);
64083
+ }
64084
+ if (!["http:", "https:"].includes(url2.protocol) || !url2.pathname.endsWith(`/${SCHEMA_FILE}`)) {
64085
+ throw new Error(`Malformed board schema modeline; URL must end in /${SCHEMA_FILE}: ${match2[1]}`);
64086
+ }
64087
+ return match2[1];
64088
+ }
64089
+ function boardSchemaModeline(url2) {
64090
+ return `# yaml-language-server: $schema=${url2}`;
64091
+ }
64092
+
64063
64093
  // packages/shared/src/client-env.ts
64064
64094
  var clientEnvSchema = external_exports.object({
64065
64095
  /** Where the immutable client assets live. The one variable that repoints the whole client. */
@@ -67039,12 +67069,16 @@ function createApp(deps) {
67039
67069
  ...deps.boardConfig.auth ? { auth: deps.boardConfig.auth } : {}
67040
67070
  };
67041
67071
  const filenameBoard = safeDownloadFilename(boardName);
67042
- return new Response((0, import_yaml.stringify)(outputConfig, { sortMapEntries: true }), {
67043
- headers: {
67044
- "content-type": "text/yaml; charset=utf-8",
67045
- "content-disposition": `attachment; filename="${filenameBoard}-layout-${mode}.yaml"`
67072
+ return new Response(
67073
+ `${boardSchemaModeline(schemaUrlForAssetPath(config2.assetPath))}
67074
+ ${(0, import_yaml.stringify)(outputConfig, { sortMapEntries: true })}`,
67075
+ {
67076
+ headers: {
67077
+ "content-type": "text/yaml; charset=utf-8",
67078
+ "content-disposition": `attachment; filename="${filenameBoard}-layout-${mode}.yaml"`
67079
+ }
67046
67080
  }
67047
- });
67081
+ );
67048
67082
  };
67049
67083
  app.get("/api/boards/:board/rendered", (c5) => layoutDownload(c5, "rendered"));
67050
67084
  app.get("/api/boards/:board/authored", (c5) => layoutDownload(c5, "authored"));
@@ -67138,11 +67172,16 @@ function deriveVersionLabel(assetPath) {
67138
67172
  // packages/server/src/board-config.ts
67139
67173
  import { readFile as readFile3 } from "node:fs/promises";
67140
67174
  var import_yaml2 = __toESM(require_dist(), 1);
67141
- async function loadBoardConfig(location, fetcher = globalThis.fetch) {
67175
+ async function loadBoardConfig(location, fetcher = globalThis.fetch, expectedSchemaUrl) {
67142
67176
  const text = isUrl(location) ? await fetchBoardConfig(location, fetcher) : await readFile3(location, "utf-8");
67177
+ const authoredSchemaUrl = readBoardSchemaModeline(text);
67143
67178
  const result = boardConfigSchema.safeParse((0, import_yaml2.parse)(text));
67144
- if (!result.success) throw new Error(`Invalid board configuration:
67145
- ${result.error.message}`);
67179
+ if (!result.success) {
67180
+ const stale = expectedSchemaUrl && authoredSchemaUrl !== expectedSchemaUrl ? `
67181
+ Stale schema modeline: expected ${expectedSchemaUrl}` : "";
67182
+ throw new Error(`Invalid board configuration:
67183
+ ${result.error.message}${stale}`);
67184
+ }
67146
67185
  return result.data;
67147
67186
  }
67148
67187
  function isUrl(value) {
@@ -67218,7 +67257,11 @@ async function startup(options = {}) {
67218
67257
  const fetcher = options.fetcher ?? globalThis.fetch;
67219
67258
  const [, boardConfig] = await Promise.all([
67220
67259
  waitForTemplate(config2, fetcher),
67221
- loadBoardConfig(config2.boardConfigUrl, fetcher)
67260
+ loadBoardConfig(
67261
+ config2.boardConfigUrl,
67262
+ fetcher,
67263
+ config2.assetPath.includes("__ASSET_PATH__") ? void 0 : schemaUrlForAssetPath(config2.assetPath)
67264
+ )
67222
67265
  ]);
67223
67266
  const board = selectBoard(config2.board, boardConfig);
67224
67267
  const resolvedConfig = { ...config2, board };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@continuous-excellence/ze-great-dashboard-aws",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "description": "AWS Lambda and CloudFormation adapter for Ze Great Dashboard.",
6
6
  "keywords": [
@@ -29,6 +29,7 @@
29
29
  "files": [
30
30
  "dist",
31
31
  "client",
32
+ "board-config.schema.json",
32
33
  "bootstrap",
33
34
  "template.yml",
34
35
  "template-ecs.yml",