@continuous-excellence/ze-great-dashboard-aws 0.2.0 → 0.3.1

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
@@ -743,7 +743,7 @@ import { z as z2 } from "zod";
743
743
 
744
744
  // packages/aws/src/internal-board.ts
745
745
  import { z } from "zod";
746
- var durationSchema = z.string().regex(/^\d+(?:ms|s|m|h)$/).refine((value) => Number.parseInt(value, 10) > 0).brand();
746
+ var durationSchema = z.string().regex(/^\d+(?:ms|s|m|h)$/).refine((value2) => Number.parseInt(value2, 10) > 0).brand();
747
747
  var positionSchema = z.object({
748
748
  x: z.number().int().min(0),
749
749
  y: z.number().int().min(0),
@@ -834,8 +834,8 @@ async function assembleRelease(input) {
834
834
  );
835
835
  return { metadata, files };
836
836
  }
837
- function sha256(value) {
838
- return createHash("sha256").update(value).digest("hex");
837
+ function sha256(value2) {
838
+ return createHash("sha256").update(value2).digest("hex");
839
839
  }
840
840
 
841
841
  // packages/aws/src/bootstrap.ts
@@ -910,8 +910,8 @@ function requiredBootstrapParameters(kind, values) {
910
910
  ];
911
911
  const missing = keys.filter((key) => !values[key]);
912
912
  if (missing.length) throw new Error(`Missing required bootstrap values: ${missing.join(", ")}`);
913
- const optional = kind === "core" ? ["RuntimeSecretArn", "ArtifactKmsKeyArn"] : [];
914
- return [...keys, ...optional].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
913
+ const optional2 = kind === "core" ? ["RuntimeSecretArn", "ArtifactKmsKeyArn"] : [];
914
+ return [...keys, ...optional2].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
915
915
  }
916
916
 
917
917
  // packages/aws/src/handoff.ts
@@ -945,9 +945,9 @@ function capturedStackIdentity(input) {
945
945
  ...typeof StackId === "string" ? { StackId } : {}
946
946
  };
947
947
  }
948
- function required(value, name) {
949
- if (!value) throw new Error(`Bootstrap manifest is missing ${name}`);
950
- return value;
948
+ function required(value2, name) {
949
+ if (!value2) throw new Error(`Bootstrap manifest is missing ${name}`);
950
+ return value2;
951
951
  }
952
952
  function region(config) {
953
953
  return required(config.region, "region");
@@ -1100,6 +1100,12 @@ async function bootstrapHandoff(input) {
1100
1100
  expectedContracts,
1101
1101
  templatePath,
1102
1102
  parameterPath: "core-bootstrap.json",
1103
+ expectedOutputs: [
1104
+ "BootstrapContractVersion",
1105
+ "ArtifactBucketName",
1106
+ "ApplicationStackName",
1107
+ "CloudFormationExecutionRoleArn"
1108
+ ],
1103
1109
  requiredCapturedFiles: [],
1104
1110
  reviewCheckpoints: [
1105
1111
  "Review every IAM action and CAPABILITY_NAMED_IAM acknowledgement.",
@@ -1130,6 +1136,7 @@ async function bootstrapHandoff(input) {
1130
1136
  expectedContracts,
1131
1137
  templatePath,
1132
1138
  parameterPath: "github-oidc-bootstrap.json",
1139
+ expectedOutputs: ["BootstrapContractVersion", "GitHubDeployRoleArn"],
1133
1140
  requiredCapturedFiles: [coreCapturePath],
1134
1141
  reviewCheckpoints: [
1135
1142
  "Review the exact immutable GitHub OIDC subject, audience, bucket lambda/* prefix, application stack, and execution role.",
@@ -1157,6 +1164,7 @@ async function bootstrapHandoff(input) {
1157
1164
  return {
1158
1165
  phase: "github-environment",
1159
1166
  expectedContracts,
1167
+ expectedOutputs: [],
1160
1168
  requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1161
1169
  reviewCheckpoints: [
1162
1170
  "A GitHub administrator must complete and verify the immutable-subject migration before deployments."
@@ -1167,6 +1175,7 @@ async function bootstrapHandoff(input) {
1167
1175
  return {
1168
1176
  phase: "application-gateway",
1169
1177
  expectedContracts,
1178
+ expectedOutputs: [],
1170
1179
  requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1171
1180
  reviewCheckpoints: [
1172
1181
  "Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
@@ -1227,7 +1236,7 @@ async function verifyBootstrap(input) {
1227
1236
  coreOutputs.CloudFormationExecutionRoleArn,
1228
1237
  "core CloudFormationExecutionRoleArn output"
1229
1238
  );
1230
- for (const [key, value] of Object.entries({
1239
+ for (const [key, value2] of Object.entries({
1231
1240
  GitHubOidcProviderArn: github?.providerArn,
1232
1241
  GitHubRepository: github?.repository,
1233
1242
  GitHubOwnerId: github?.ownerId,
@@ -1237,7 +1246,7 @@ async function verifyBootstrap(input) {
1237
1246
  ArtifactBucketName: coreOutputs.ArtifactBucketName,
1238
1247
  CloudFormationExecutionRoleArn: executionRole
1239
1248
  }))
1240
- assertEqual(oidcParameters[key], value, `OIDC ${key}`);
1249
+ assertEqual(oidcParameters[key], value2, `OIDC ${key}`);
1241
1250
  if (!/^\d+$/.test(github?.ownerId ?? "") || !/^\d+$/.test(github?.repositoryId ?? ""))
1242
1251
  throw new Error("GitHub immutable owner and repository IDs must be numeric");
1243
1252
  const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
@@ -1291,6 +1300,268 @@ async function verifyBootstrap(input) {
1291
1300
  };
1292
1301
  }
1293
1302
 
1303
+ // packages/aws/src/guided.ts
1304
+ function value(input, label) {
1305
+ if (typeof input !== "string" || !input.trim()) throw new Error(`${label} is required`);
1306
+ return input.trim();
1307
+ }
1308
+ function repositoryParts(repository) {
1309
+ const [owner, name, extra] = repository.split("/");
1310
+ if (!owner || !name || extra) throw new Error("repository must be owner/repository");
1311
+ return [owner, name];
1312
+ }
1313
+ function accountFromArn(arn) {
1314
+ return arn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
1315
+ }
1316
+ async function optional(runner, command, args2) {
1317
+ if (!runner) return void 0;
1318
+ try {
1319
+ return await runner.execute(command, args2);
1320
+ } catch {
1321
+ return void 0;
1322
+ }
1323
+ }
1324
+ async function discovered(input) {
1325
+ const identity = await optional(input.runner, "aws", [
1326
+ "sts",
1327
+ "get-caller-identity",
1328
+ "--output",
1329
+ "json"
1330
+ ]);
1331
+ const aws = parseOrUnavailable(identity) ?? {};
1332
+ const accountId = typeof aws.Account === "string" ? aws.Account : void 0;
1333
+ const region2 = await optional(input.runner, "aws", ["configure", "get", "region"]);
1334
+ const repo = await optional(input.runner, "gh", ["api", `repos/${input.repository}`]);
1335
+ const github = parseOrUnavailable(repo) ?? {};
1336
+ return {
1337
+ accountId,
1338
+ region: region2?.trim() || void 0,
1339
+ ownerId: typeof github.owner?.id === "number" || typeof github.owner?.id === "string" ? String(github.owner.id) : void 0,
1340
+ repositoryId: typeof github.id === "number" || typeof github.id === "string" ? String(github.id) : void 0
1341
+ };
1342
+ }
1343
+ async function scaffoldBootstrapManifest(input) {
1344
+ const slug = value(input.slug, "slug");
1345
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(slug))
1346
+ throw new Error("slug must use lowercase letters, digits, and hyphens");
1347
+ const repository = value(input.repository, "repository");
1348
+ repositoryParts(repository);
1349
+ const environment = value(input.environment, "environment");
1350
+ const providerArn = value(input.providerArn, "github OIDC provider ARN");
1351
+ if (!accountFromArn(providerArn)) throw new Error("github OIDC provider ARN is invalid");
1352
+ const found = await discovered(input);
1353
+ const accountId = input.accountId ?? found.accountId;
1354
+ const region2 = input.region ?? found.region;
1355
+ const ownerId = input.ownerId ?? found.ownerId;
1356
+ const repositoryId = input.repositoryId ?? found.repositoryId;
1357
+ if (!accountId) throw new Error("--account-id is required when AWS identity is unavailable");
1358
+ if (!region2) throw new Error("--region is required when AWS Region is unavailable");
1359
+ if (!ownerId) throw new Error("--github-owner-id is required when GitHub is unavailable");
1360
+ if (!repositoryId)
1361
+ throw new Error("--github-repository-id is required when GitHub is unavailable");
1362
+ return {
1363
+ region: region2,
1364
+ core: {
1365
+ stackName: `${slug}-bootstrap`,
1366
+ artifactBucketName: `${slug}-lambda-artifacts-${accountId}`,
1367
+ applicationStackName: slug,
1368
+ dashboardFunctionName: slug
1369
+ },
1370
+ githubOidc: {
1371
+ stackName: `${slug}-github-bootstrap`,
1372
+ providerArn,
1373
+ repository,
1374
+ ownerId,
1375
+ repositoryId,
1376
+ environment
1377
+ }
1378
+ };
1379
+ }
1380
+ function incomplete(config) {
1381
+ const fields = [
1382
+ ["region", config.region],
1383
+ ["core.stackName", config.core?.stackName],
1384
+ ["core.artifactBucketName", config.core?.artifactBucketName],
1385
+ ["core.applicationStackName", config.core?.applicationStackName],
1386
+ ["core.dashboardFunctionName", config.core?.dashboardFunctionName],
1387
+ ["githubOidc.stackName", config.githubOidc?.stackName],
1388
+ ["githubOidc.providerArn", config.githubOidc?.providerArn],
1389
+ ["githubOidc.repository", config.githubOidc?.repository],
1390
+ ["githubOidc.ownerId", config.githubOidc?.ownerId],
1391
+ ["githubOidc.repositoryId", config.githubOidc?.repositoryId],
1392
+ ["githubOidc.environment", config.githubOidc?.environment]
1393
+ ];
1394
+ return fields.filter(([, v]) => typeof v !== "string" || !v).map(([name]) => name);
1395
+ }
1396
+ function parseOrUnavailable(raw) {
1397
+ if (!raw) return void 0;
1398
+ try {
1399
+ return JSON.parse(raw);
1400
+ } catch {
1401
+ return void 0;
1402
+ }
1403
+ }
1404
+ function absent(raw) {
1405
+ return /not found|nosuchentity|404/i.test(raw ?? "");
1406
+ }
1407
+ async function bootstrapPreflight(input) {
1408
+ const checks = [];
1409
+ const missing = incomplete(input.config);
1410
+ checks.push(
1411
+ missing.length ? { name: "manifest", status: "missing", detail: `Missing: ${missing.join(", ")}` } : { name: "manifest", status: "ready", detail: "Manifest has all bootstrap fields." }
1412
+ );
1413
+ if (missing.length) return { ready: false, checks };
1414
+ const runner = input.runner;
1415
+ const identityRaw = await optional(runner, "aws", [
1416
+ "sts",
1417
+ "get-caller-identity",
1418
+ "--output",
1419
+ "json"
1420
+ ]);
1421
+ const identity = parseOrUnavailable(identityRaw);
1422
+ const providerAccount = accountFromArn(input.config.githubOidc?.providerArn ?? "");
1423
+ if (!identity || typeof identity.Account !== "string")
1424
+ checks.push({
1425
+ name: "aws-identity",
1426
+ status: "unverified",
1427
+ detail: "AWS identity could not be read."
1428
+ });
1429
+ else
1430
+ checks.push(
1431
+ identity.Account === providerAccount ? {
1432
+ name: "aws-identity",
1433
+ status: "ready",
1434
+ detail: `AWS account ${identity.Account} matches the provider ARN.`
1435
+ } : {
1436
+ name: "aws-identity",
1437
+ status: "mismatch",
1438
+ detail: `AWS account ${identity.Account} does not match provider account ${providerAccount}.`
1439
+ }
1440
+ );
1441
+ const regionRaw = await optional(runner, "aws", ["configure", "get", "region"]);
1442
+ if (!regionRaw?.trim())
1443
+ checks.push({
1444
+ name: "aws-region",
1445
+ status: "unverified",
1446
+ detail: "AWS Region could not be read."
1447
+ });
1448
+ else
1449
+ checks.push(
1450
+ regionRaw.trim() === input.config.region ? { name: "aws-region", status: "ready", detail: `AWS Region is ${input.config.region}.` } : {
1451
+ name: "aws-region",
1452
+ status: "mismatch",
1453
+ detail: `AWS Region ${regionRaw.trim()} does not match manifest ${input.config.region}.`
1454
+ }
1455
+ );
1456
+ const providerRaw = await optional(runner, "aws", [
1457
+ "iam",
1458
+ "get-open-id-connect-provider",
1459
+ "--open-id-connect-provider-arn",
1460
+ input.config.githubOidc?.providerArn ?? ""
1461
+ ]);
1462
+ checks.push(
1463
+ !providerRaw ? {
1464
+ name: "oidc-provider",
1465
+ status: "unverified",
1466
+ detail: "OIDC provider could not be queried."
1467
+ } : absent(providerRaw) ? { name: "oidc-provider", status: "missing", detail: "OIDC provider does not exist." } : { name: "oidc-provider", status: "ready", detail: "OIDC provider exists." }
1468
+ );
1469
+ const repository = input.config.githubOidc?.repository ?? "";
1470
+ const repoRaw = await optional(runner, "gh", ["api", `repos/${repository}`]);
1471
+ const repo = parseOrUnavailable(repoRaw);
1472
+ if (!repoRaw)
1473
+ checks.push({
1474
+ name: "github-repository",
1475
+ status: "unverified",
1476
+ detail: "GitHub repository could not be queried."
1477
+ });
1478
+ else if (absent(repoRaw))
1479
+ checks.push({
1480
+ name: "github-repository",
1481
+ status: "missing",
1482
+ detail: "GitHub repository does not exist or is unavailable."
1483
+ });
1484
+ else if (String(repo?.id) !== input.config.githubOidc?.repositoryId || String(repo?.owner?.id) !== input.config.githubOidc?.ownerId)
1485
+ checks.push({
1486
+ name: "github-repository",
1487
+ status: "mismatch",
1488
+ detail: "GitHub numeric repository identity does not match the manifest."
1489
+ });
1490
+ else
1491
+ checks.push({
1492
+ name: "github-repository",
1493
+ status: "ready",
1494
+ detail: "GitHub repository identity matches the manifest."
1495
+ });
1496
+ const environmentRaw = await optional(runner, "gh", [
1497
+ "api",
1498
+ `repos/${repository}/environments/${input.config.githubOidc?.environment}`
1499
+ ]);
1500
+ checks.push(
1501
+ !environmentRaw ? {
1502
+ name: "github-environment",
1503
+ status: "unverified",
1504
+ detail: "GitHub Environment could not be queried."
1505
+ } : absent(environmentRaw) ? {
1506
+ name: "github-environment",
1507
+ status: "missing",
1508
+ detail: "GitHub Environment does not exist."
1509
+ } : {
1510
+ name: "github-environment",
1511
+ status: "ready",
1512
+ detail: "GitHub Environment exists; its policy is administrator-owned context."
1513
+ }
1514
+ );
1515
+ const subject = await (input.provider ?? githubOidcProvider).prerequisite(input.config, runner);
1516
+ checks.push({
1517
+ name: "immutable-subject",
1518
+ status: subject.status === "immutable-subject-required" ? "mismatch" : subject.status,
1519
+ detail: subject.detail
1520
+ });
1521
+ return {
1522
+ ready: !checks.some(({ status }) => status === "missing" || status === "mismatch"),
1523
+ checks
1524
+ };
1525
+ }
1526
+ function quote(args2) {
1527
+ return args2.map((arg) => `'${arg.replaceAll("'", `'\\"'\\"'`)}'`).join(" ");
1528
+ }
1529
+ async function bootstrapGuide(input) {
1530
+ const handoff = await bootstrapHandoff(input);
1531
+ const lines = [
1532
+ `Phase: ${handoff.phase}`,
1533
+ `Required captures: ${handoff.requiredCapturedFiles.join(", ") || "none"}`
1534
+ ];
1535
+ if (handoff.expectedOutputs.length)
1536
+ lines.push(`Expected outputs: ${handoff.expectedOutputs.join(", ")}`);
1537
+ if (handoff.prerequisite)
1538
+ lines.push(`Prerequisite: ${handoff.prerequisite.status} \u2014 ${handoff.prerequisite.detail}`);
1539
+ lines.push("", "Commands (copy and run each AWS command yourself):");
1540
+ for (const command of handoff.commands) {
1541
+ if (command.name === "review-change-set")
1542
+ lines.push("PAUSE: review the change set before approval.");
1543
+ if (command.name === "execute-reviewed-change-set")
1544
+ lines.push("PAUSE: execute only after explicit approval.");
1545
+ lines.push(`${command.captureFile ? `${command.captureFile}: ` : ""}${quote(command.args)}`);
1546
+ }
1547
+ if (handoff.reviewCheckpoints.length)
1548
+ lines.push("", "Review pauses:", ...handoff.reviewCheckpoints.map((item) => `- ${item}`));
1549
+ if (input.coreStack !== void 0 && input.githubOidcStack !== void 0) {
1550
+ const verified = await verifyBootstrap({
1551
+ config: input.config,
1552
+ coreStack: input.coreStack,
1553
+ githubOidcStack: input.githubOidcStack
1554
+ });
1555
+ lines.push(
1556
+ "",
1557
+ "Optional GitHub administrator action (after the successful verification above):"
1558
+ );
1559
+ for (const command of verified.githubEnvironmentInstructions) lines.push(quote(command));
1560
+ }
1561
+ return `${lines.join("\n")}
1562
+ `;
1563
+ }
1564
+
1294
1565
  // packages/aws/src/index.ts
1295
1566
  var run = promisify(execFile);
1296
1567
  function deploymentTemplate(template, values) {
@@ -1442,9 +1713,9 @@ var actualDependencies = {
1442
1713
  nodeVersion: process.versions.node,
1443
1714
  packageVersion: ""
1444
1715
  };
1445
- function readParameterValues(value) {
1446
- if (!Array.isArray(value)) throw new Error("must contain a JSON parameter array");
1447
- const parameters = value.map((entry) => {
1716
+ function readParameterValues(value2) {
1717
+ if (!Array.isArray(value2)) throw new Error("must contain a JSON parameter array");
1718
+ const parameters = value2.map((entry) => {
1448
1719
  if (!entry || typeof entry !== "object" || typeof entry.ParameterKey !== "string" || typeof entry.ParameterValue !== "string")
1449
1720
  throw new Error("entries must contain string ParameterKey and ParameterValue fields");
1450
1721
  return entry;
@@ -1577,23 +1848,23 @@ async function installedPackageVersion() {
1577
1848
  return typeof packageManifest.version === "string" ? packageManifest.version : "";
1578
1849
  }
1579
1850
  var requiredOption = (name, fallback) => {
1580
- const value = option(name, fallback);
1581
- if (!value) throw new Error(`${name} is required`);
1582
- return value;
1851
+ const value2 = option(name, fallback);
1852
+ if (!value2) throw new Error(`${name} is required`);
1853
+ return value2;
1583
1854
  };
1584
- function parameter(key, value) {
1585
- return { ParameterKey: key, ParameterValue: value };
1855
+ function parameter(key, value2) {
1856
+ return { ParameterKey: key, ParameterValue: value2 };
1586
1857
  }
1587
1858
  async function existingParameters(path) {
1588
1859
  try {
1589
1860
  const parsed = JSON.parse(await readFile5(path, "utf8"));
1590
1861
  if (!Array.isArray(parsed)) throw new Error(`${path} must contain a JSON parameter array`);
1591
- const values = parsed.map((value) => {
1592
- if (!value || typeof value !== "object" || typeof value.ParameterKey !== "string" || typeof value.ParameterValue !== "string")
1862
+ const values = parsed.map((value2) => {
1863
+ if (!value2 || typeof value2 !== "object" || typeof value2.ParameterKey !== "string" || typeof value2.ParameterValue !== "string")
1593
1864
  throw new Error(
1594
1865
  `${path} entries must contain string ParameterKey and ParameterValue fields`
1595
1866
  );
1596
- return value;
1867
+ return value2;
1597
1868
  });
1598
1869
  const keys = values.map(({ ParameterKey }) => ParameterKey);
1599
1870
  if (new Set(keys).size !== keys.length) throw new Error(`${path} contains duplicate parameters`);
@@ -1748,20 +2019,24 @@ try {
1748
2019
  (key) => !packageManaged.has(key) && (includeDefaults || !hasDefault(key) || Object.hasOwn(existingValues, key) || key === "Name" && Boolean(functionName))
1749
2020
  ).map((key) => {
1750
2021
  const definition = definitions[key];
1751
- const value = key === "LambdaArtifactBucket" ? artifactBucket : key === "Name" && functionName ? functionName : existingValues[key] ?? definition?.Default;
1752
- if (value === void 0) throw new Error(`No value for CloudFormation parameter ${key}`);
1753
- return parameter(key, String(value));
2022
+ const value2 = key === "LambdaArtifactBucket" ? artifactBucket : key === "Name" && functionName ? functionName : existingValues[key] ?? definition?.Default;
2023
+ if (value2 === void 0) throw new Error(`No value for CloudFormation parameter ${key}`);
2024
+ return parameter(key, String(value2));
1754
2025
  });
1755
2026
  await writeFile3(output, `${JSON.stringify(parameters, null, 2)}
1756
2027
  `);
1757
2028
  console.log(JSON.stringify({ output }));
1758
2029
  } else if (args[0] === "bootstrap") {
1759
2030
  const action = args[1];
1760
- const config = await bootstrapConfig();
1761
- if (action === "handoff") {
1762
- const configPath = requiredOption("--config");
1763
- const coreStackPath = option("--core-stack-json");
1764
- const githubStackPath = option("--github-oidc-stack-json");
2031
+ if (action === "init") {
2032
+ const output = requiredOption("--output");
2033
+ try {
2034
+ await readFile5(output, "utf8");
2035
+ throw new Error(`Refusing to overwrite existing manifest: ${output}`);
2036
+ } catch (error) {
2037
+ if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT"))
2038
+ throw error;
2039
+ }
1765
2040
  const runner = {
1766
2041
  async execute(command, commandArgs) {
1767
2042
  const { execFile: execFile3 } = await import("node:child_process");
@@ -1769,9 +2044,59 @@ try {
1769
2044
  return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
1770
2045
  }
1771
2046
  };
1772
- console.log(
1773
- JSON.stringify(
1774
- await bootstrapHandoff({
2047
+ const manifest = await scaffoldBootstrapManifest({
2048
+ slug: requiredOption("--slug"),
2049
+ repository: requiredOption("--repository"),
2050
+ environment: requiredOption("--environment"),
2051
+ providerArn: requiredOption("--github-oidc-provider-arn"),
2052
+ region: option("--region"),
2053
+ accountId: option("--account-id"),
2054
+ ownerId: option("--github-owner-id"),
2055
+ repositoryId: option("--github-repository-id"),
2056
+ runner
2057
+ });
2058
+ await writeFile3(output, `${JSON.stringify(manifest, null, 2)}
2059
+ `, { flag: "wx" });
2060
+ console.log(JSON.stringify({ output, manifest }));
2061
+ } else {
2062
+ const config = await bootstrapConfig();
2063
+ if (action === "preflight") {
2064
+ requiredOption("--config");
2065
+ const runner = {
2066
+ async execute(command, commandArgs) {
2067
+ const { execFile: execFile3 } = await import("node:child_process");
2068
+ const { promisify: promisify3 } = await import("node:util");
2069
+ try {
2070
+ return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2071
+ } catch (error) {
2072
+ if (error && typeof error === "object" && "stderr" in error) {
2073
+ const stderr = error.stderr;
2074
+ if (typeof stderr === "string" && /not found|404|nosuchentity/i.test(stderr))
2075
+ return stderr;
2076
+ }
2077
+ throw error;
2078
+ }
2079
+ }
2080
+ };
2081
+ const result = await bootstrapPreflight({ config, runner });
2082
+ if (option("--format") === "text")
2083
+ for (const check of result.checks)
2084
+ console.log(`${check.status.toUpperCase()} ${check.name}: ${check.detail}`);
2085
+ else console.log(JSON.stringify(result));
2086
+ if (!result.ready) process.exitCode = 1;
2087
+ } else if (action === "guide") {
2088
+ const configPath = requiredOption("--config");
2089
+ const coreStackPath = option("--core-stack-json");
2090
+ const githubStackPath = option("--github-oidc-stack-json");
2091
+ const runner = {
2092
+ async execute(command, commandArgs) {
2093
+ const { execFile: execFile3 } = await import("node:child_process");
2094
+ const { promisify: promisify3 } = await import("node:util");
2095
+ return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2096
+ }
2097
+ };
2098
+ process.stdout.write(
2099
+ await bootstrapGuide({
1775
2100
  config,
1776
2101
  configPath,
1777
2102
  coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
@@ -1779,117 +2104,140 @@ try {
1779
2104
  githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
1780
2105
  runner
1781
2106
  })
1782
- )
1783
- );
1784
- } else if (action === "verify") {
1785
- requiredOption("--config");
1786
- const coreStackPath = requiredOption("--core-stack-json");
1787
- const githubStackPath = requiredOption("--github-oidc-stack-json");
1788
- console.log(
1789
- JSON.stringify(
1790
- await verifyBootstrap({
1791
- config,
1792
- coreStack: JSON.parse(await readFile5(coreStackPath, "utf8")),
1793
- githubOidcStack: JSON.parse(await readFile5(githubStackPath, "utf8"))
1794
- })
1795
- )
1796
- );
1797
- } else if (action === "template") {
1798
- const kind = bootstrapKind();
1799
- console.log(
1800
- JSON.stringify({
1801
- kind,
1802
- template: await bootstrapTemplatePath(kind),
1803
- contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind))
1804
- })
1805
- );
1806
- } else if (action === "parameters") {
1807
- const kind = bootstrapKind();
1808
- const output = option("--output", `aws-dashboard-bootstrap-${kind}.json`) ?? `aws-dashboard-bootstrap-${kind}.json`;
1809
- let coreOutputs = {};
1810
- const coreStackPath = option("--core-stack-json");
1811
- if (kind === "github-oidc" && coreStackPath) {
1812
- coreOutputs = coreBootstrapOutputs(
1813
- deployedBootstrapStack(
1814
- JSON.parse(await readFile5(coreStackPath, "utf8")),
1815
- bootstrapContractVersion(await bootstrapTemplate("core"))
2107
+ );
2108
+ } else if (action === "handoff") {
2109
+ const configPath = requiredOption("--config");
2110
+ const coreStackPath = option("--core-stack-json");
2111
+ const githubStackPath = option("--github-oidc-stack-json");
2112
+ const runner = {
2113
+ async execute(command, commandArgs) {
2114
+ const { execFile: execFile3 } = await import("node:child_process");
2115
+ const { promisify: promisify3 } = await import("node:util");
2116
+ return (await promisify3(execFile3)(command, commandArgs)).stdout.trim();
2117
+ }
2118
+ };
2119
+ console.log(
2120
+ JSON.stringify(
2121
+ await bootstrapHandoff({
2122
+ config,
2123
+ configPath,
2124
+ coreStack: coreStackPath ? JSON.parse(await readFile5(coreStackPath, "utf8")) : void 0,
2125
+ coreStackPath,
2126
+ githubOidcStack: githubStackPath ? JSON.parse(await readFile5(githubStackPath, "utf8")) : void 0,
2127
+ runner
2128
+ })
1816
2129
  )
1817
2130
  );
1818
- }
1819
- const supplied = requiredBootstrapParameters(kind, bootstrapValues(config, coreOutputs));
1820
- let parameters = supplied;
1821
- const deployedStackPath = option("--deployed-stack-json");
1822
- if (deployedStackPath) {
1823
- const stack = deployedBootstrapStack(
1824
- JSON.parse(await readFile5(deployedStackPath, "utf8")),
1825
- bootstrapContractVersion(await bootstrapTemplate(kind))
2131
+ } else if (action === "verify") {
2132
+ requiredOption("--config");
2133
+ const coreStackPath = requiredOption("--core-stack-json");
2134
+ const githubStackPath = requiredOption("--github-oidc-stack-json");
2135
+ console.log(
2136
+ JSON.stringify(
2137
+ await verifyBootstrap({
2138
+ config,
2139
+ coreStack: JSON.parse(await readFile5(coreStackPath, "utf8")),
2140
+ githubOidcStack: JSON.parse(await readFile5(githubStackPath, "utf8"))
2141
+ })
2142
+ )
1826
2143
  );
1827
- parameters = mergeBootstrapParameters(supplied, stack.Parameters ?? []);
1828
- }
1829
- await writeFile3(output, `${JSON.stringify(parameters, null, 2)}
2144
+ } else if (action === "template") {
2145
+ const kind = bootstrapKind();
2146
+ console.log(
2147
+ JSON.stringify({
2148
+ kind,
2149
+ template: await bootstrapTemplatePath(kind),
2150
+ contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind))
2151
+ })
2152
+ );
2153
+ } else if (action === "parameters") {
2154
+ const kind = bootstrapKind();
2155
+ const output = option("--output", `aws-dashboard-bootstrap-${kind}.json`) ?? `aws-dashboard-bootstrap-${kind}.json`;
2156
+ let coreOutputs = {};
2157
+ const coreStackPath = option("--core-stack-json");
2158
+ if (kind === "github-oidc" && coreStackPath) {
2159
+ coreOutputs = coreBootstrapOutputs(
2160
+ deployedBootstrapStack(
2161
+ JSON.parse(await readFile5(coreStackPath, "utf8")),
2162
+ bootstrapContractVersion(await bootstrapTemplate("core"))
2163
+ )
2164
+ );
2165
+ }
2166
+ const supplied = requiredBootstrapParameters(kind, bootstrapValues(config, coreOutputs));
2167
+ let parameters = supplied;
2168
+ const deployedStackPath = option("--deployed-stack-json");
2169
+ if (deployedStackPath) {
2170
+ const stack = deployedBootstrapStack(
2171
+ JSON.parse(await readFile5(deployedStackPath, "utf8")),
2172
+ bootstrapContractVersion(await bootstrapTemplate(kind))
2173
+ );
2174
+ parameters = mergeBootstrapParameters(supplied, stack.Parameters ?? []);
2175
+ }
2176
+ await writeFile3(output, `${JSON.stringify(parameters, null, 2)}
1830
2177
  `);
1831
- console.log(
1832
- JSON.stringify({ output, kind, preservedDeployedValues: Boolean(deployedStackPath) })
1833
- );
1834
- } else if (action === "status") {
1835
- const kind = bootstrapKind();
1836
- const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
1837
- const region2 = option("--region", config.region);
1838
- const awsCommand = [
1839
- "aws",
1840
- "cloudformation",
1841
- "describe-stacks",
1842
- "--stack-name",
1843
- stackName2,
1844
- ...region2 ? ["--region", region2] : [],
1845
- "--no-cli-pager"
1846
- ];
1847
- console.log(
1848
- JSON.stringify({
1849
- kind,
1850
- contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind)),
1851
- awsCommand,
1852
- shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
1853
- })
1854
- );
1855
- } else if (action === "change-set") {
1856
- const kind = bootstrapKind();
1857
- const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
1858
- const changeSetName = requiredOption("--change-set-name");
1859
- const parametersPath = requiredOption("--parameters");
1860
- const region2 = option("--region", config.region);
1861
- await existingParameters(parametersPath);
1862
- const awsCommand = [
1863
- "aws",
1864
- "cloudformation",
1865
- "create-change-set",
1866
- "--stack-name",
1867
- stackName2,
1868
- "--change-set-name",
1869
- changeSetName,
1870
- "--change-set-type",
1871
- option("--change-set-type", "UPDATE") ?? "UPDATE",
1872
- "--template-body",
1873
- `file://${await bootstrapTemplatePath(kind)}`,
1874
- "--parameters",
1875
- `file://${parametersPath}`,
1876
- "--capabilities",
1877
- "CAPABILITY_NAMED_IAM",
1878
- ...region2 ? ["--region", region2] : [],
1879
- "--no-cli-pager"
1880
- ];
1881
- console.log(
1882
- JSON.stringify({
1883
- kind,
1884
- reviewRequired: true,
1885
- awsCommand,
1886
- shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
1887
- })
1888
- );
1889
- } else {
1890
- throw new Error(
1891
- "Usage: ze-great-dashboard-aws bootstrap handoff|verify --config manifest.json [options], or bootstrap template|parameters|status|change-set --kind core|github-oidc [options]"
1892
- );
2178
+ console.log(
2179
+ JSON.stringify({ output, kind, preservedDeployedValues: Boolean(deployedStackPath) })
2180
+ );
2181
+ } else if (action === "status") {
2182
+ const kind = bootstrapKind();
2183
+ const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
2184
+ const region2 = option("--region", config.region);
2185
+ const awsCommand = [
2186
+ "aws",
2187
+ "cloudformation",
2188
+ "describe-stacks",
2189
+ "--stack-name",
2190
+ stackName2,
2191
+ ...region2 ? ["--region", region2] : [],
2192
+ "--no-cli-pager"
2193
+ ];
2194
+ console.log(
2195
+ JSON.stringify({
2196
+ kind,
2197
+ contractVersion: bootstrapContractVersion(await bootstrapTemplate(kind)),
2198
+ awsCommand,
2199
+ shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
2200
+ })
2201
+ );
2202
+ } else if (action === "change-set") {
2203
+ const kind = bootstrapKind();
2204
+ const stackName2 = requiredOption("--stack-name", bootstrapStackName(kind, config));
2205
+ const changeSetName = requiredOption("--change-set-name");
2206
+ const parametersPath = requiredOption("--parameters");
2207
+ const region2 = option("--region", config.region);
2208
+ await existingParameters(parametersPath);
2209
+ const awsCommand = [
2210
+ "aws",
2211
+ "cloudformation",
2212
+ "create-change-set",
2213
+ "--stack-name",
2214
+ stackName2,
2215
+ "--change-set-name",
2216
+ changeSetName,
2217
+ "--change-set-type",
2218
+ option("--change-set-type", "UPDATE") ?? "UPDATE",
2219
+ "--template-body",
2220
+ `file://${await bootstrapTemplatePath(kind)}`,
2221
+ "--parameters",
2222
+ `file://${parametersPath}`,
2223
+ "--capabilities",
2224
+ "CAPABILITY_NAMED_IAM",
2225
+ ...region2 ? ["--region", region2] : [],
2226
+ "--no-cli-pager"
2227
+ ];
2228
+ console.log(
2229
+ JSON.stringify({
2230
+ kind,
2231
+ reviewRequired: true,
2232
+ awsCommand,
2233
+ shellCommand: args.includes("--format-shell") ? shellCommand(awsCommand) : void 0
2234
+ })
2235
+ );
2236
+ } else {
2237
+ throw new Error(
2238
+ "Usage: ze-great-dashboard-aws bootstrap init|preflight|guide|handoff|verify --config manifest.json [options], or bootstrap template|parameters|status|change-set --kind core|github-oidc [options]"
2239
+ );
2240
+ }
1893
2241
  }
1894
2242
  } else if (args[0] !== "package")
1895
2243
  throw new Error(
@@ -0,0 +1,32 @@
1
+ import type { BootstrapConfig } from './bootstrap.js';
2
+ import { type BootstrapProvider, bootstrapHandoff, type CommandRunner } from './handoff.js';
3
+ export type BootstrapCheckStatus = 'ready' | 'missing' | 'mismatch' | 'unverified';
4
+ export type BootstrapPreflightCheck = {
5
+ name: string;
6
+ status: BootstrapCheckStatus;
7
+ detail: string;
8
+ };
9
+ export type BootstrapPreflight = {
10
+ ready: boolean;
11
+ checks: BootstrapPreflightCheck[];
12
+ };
13
+ export type BootstrapInitInput = {
14
+ slug: string;
15
+ repository: string;
16
+ environment: string;
17
+ providerArn: string;
18
+ region?: string;
19
+ accountId?: string;
20
+ ownerId?: string;
21
+ repositoryId?: string;
22
+ runner?: CommandRunner;
23
+ };
24
+ /** Creates the non-secret manifest data; callers control where (and whether) it is written. */
25
+ export declare function scaffoldBootstrapManifest(input: BootstrapInitInput): Promise<BootstrapConfig>;
26
+ /** Runs only named read-only discovery checks. Network/auth failures remain usable offline. */
27
+ export declare function bootstrapPreflight(input: {
28
+ config: BootstrapConfig;
29
+ runner?: CommandRunner;
30
+ provider?: BootstrapProvider;
31
+ }): Promise<BootstrapPreflight>;
32
+ export declare function bootstrapGuide(input: Parameters<typeof bootstrapHandoff>[0]): Promise<string>;
package/dist/handoff.d.ts CHANGED
@@ -25,6 +25,7 @@ export type BootstrapHandoff = {
25
25
  };
26
26
  templatePath?: string;
27
27
  parameterPath?: string;
28
+ expectedOutputs: string[];
28
29
  requiredCapturedFiles: string[];
29
30
  reviewCheckpoints: string[];
30
31
  commands: HandoffCommand[];
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { type BootstrapConfig, type BootstrapKind, bootstrapContractVersion, bootstrapTemplate, bootstrapTemplatePath, type CloudFormationParameterValue, coreBootstrapOutputs, type DeployedBootstrapStack, deployedBootstrapStack, mergeBootstrapParameters, requiredBootstrapParameters, } from './bootstrap.js';
2
+ export { type BootstrapCheckStatus, type BootstrapInitInput, type BootstrapPreflight, type BootstrapPreflightCheck, bootstrapGuide, bootstrapPreflight, scaffoldBootstrapManifest, } from './guided.js';
2
3
  export { type BootstrapHandoff, type BootstrapPhase, type BootstrapProvider, type BootstrapVerification, bootstrapHandoff, type CommandRunner, githubOidcProvider, verifyBootstrap, } from './handoff.js';
3
4
  export type ReleaseMetadata = {
4
5
  dashboardVersion: string;
package/dist/index.js CHANGED
@@ -730,7 +730,7 @@ import { z as z2 } from "zod";
730
730
 
731
731
  // packages/aws/src/internal-board.ts
732
732
  import { z } from "zod";
733
- var durationSchema = z.string().regex(/^\d+(?:ms|s|m|h)$/).refine((value) => Number.parseInt(value, 10) > 0).brand();
733
+ var durationSchema = z.string().regex(/^\d+(?:ms|s|m|h)$/).refine((value2) => Number.parseInt(value2, 10) > 0).brand();
734
734
  var positionSchema = z.object({
735
735
  x: z.number().int().min(0),
736
736
  y: z.number().int().min(0),
@@ -821,8 +821,8 @@ async function assembleRelease(input) {
821
821
  );
822
822
  return { metadata, files };
823
823
  }
824
- function sha256(value) {
825
- return createHash("sha256").update(value).digest("hex");
824
+ function sha256(value2) {
825
+ return createHash("sha256").update(value2).digest("hex");
826
826
  }
827
827
 
828
828
  // packages/aws/src/bootstrap.ts
@@ -897,8 +897,8 @@ function requiredBootstrapParameters(kind, values) {
897
897
  ];
898
898
  const missing = keys.filter((key) => !values[key]);
899
899
  if (missing.length) throw new Error(`Missing required bootstrap values: ${missing.join(", ")}`);
900
- const optional = kind === "core" ? ["RuntimeSecretArn", "ArtifactKmsKeyArn"] : [];
901
- return [...keys, ...optional].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
900
+ const optional2 = kind === "core" ? ["RuntimeSecretArn", "ArtifactKmsKeyArn"] : [];
901
+ return [...keys, ...optional2].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
902
902
  }
903
903
 
904
904
  // packages/aws/src/handoff.ts
@@ -932,9 +932,9 @@ function capturedStackIdentity(input) {
932
932
  ...typeof StackId === "string" ? { StackId } : {}
933
933
  };
934
934
  }
935
- function required(value, name) {
936
- if (!value) throw new Error(`Bootstrap manifest is missing ${name}`);
937
- return value;
935
+ function required(value2, name) {
936
+ if (!value2) throw new Error(`Bootstrap manifest is missing ${name}`);
937
+ return value2;
938
938
  }
939
939
  function region(config) {
940
940
  return required(config.region, "region");
@@ -1087,6 +1087,12 @@ async function bootstrapHandoff(input) {
1087
1087
  expectedContracts,
1088
1088
  templatePath,
1089
1089
  parameterPath: "core-bootstrap.json",
1090
+ expectedOutputs: [
1091
+ "BootstrapContractVersion",
1092
+ "ArtifactBucketName",
1093
+ "ApplicationStackName",
1094
+ "CloudFormationExecutionRoleArn"
1095
+ ],
1090
1096
  requiredCapturedFiles: [],
1091
1097
  reviewCheckpoints: [
1092
1098
  "Review every IAM action and CAPABILITY_NAMED_IAM acknowledgement.",
@@ -1117,6 +1123,7 @@ async function bootstrapHandoff(input) {
1117
1123
  expectedContracts,
1118
1124
  templatePath,
1119
1125
  parameterPath: "github-oidc-bootstrap.json",
1126
+ expectedOutputs: ["BootstrapContractVersion", "GitHubDeployRoleArn"],
1120
1127
  requiredCapturedFiles: [coreCapturePath],
1121
1128
  reviewCheckpoints: [
1122
1129
  "Review the exact immutable GitHub OIDC subject, audience, bucket lambda/* prefix, application stack, and execution role.",
@@ -1144,6 +1151,7 @@ async function bootstrapHandoff(input) {
1144
1151
  return {
1145
1152
  phase: "github-environment",
1146
1153
  expectedContracts,
1154
+ expectedOutputs: [],
1147
1155
  requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1148
1156
  reviewCheckpoints: [
1149
1157
  "A GitHub administrator must complete and verify the immutable-subject migration before deployments."
@@ -1154,6 +1162,7 @@ async function bootstrapHandoff(input) {
1154
1162
  return {
1155
1163
  phase: "application-gateway",
1156
1164
  expectedContracts,
1165
+ expectedOutputs: [],
1157
1166
  requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1158
1167
  reviewCheckpoints: [
1159
1168
  "Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
@@ -1214,7 +1223,7 @@ async function verifyBootstrap(input) {
1214
1223
  coreOutputs.CloudFormationExecutionRoleArn,
1215
1224
  "core CloudFormationExecutionRoleArn output"
1216
1225
  );
1217
- for (const [key, value] of Object.entries({
1226
+ for (const [key, value2] of Object.entries({
1218
1227
  GitHubOidcProviderArn: github?.providerArn,
1219
1228
  GitHubRepository: github?.repository,
1220
1229
  GitHubOwnerId: github?.ownerId,
@@ -1224,7 +1233,7 @@ async function verifyBootstrap(input) {
1224
1233
  ArtifactBucketName: coreOutputs.ArtifactBucketName,
1225
1234
  CloudFormationExecutionRoleArn: executionRole
1226
1235
  }))
1227
- assertEqual(oidcParameters[key], value, `OIDC ${key}`);
1236
+ assertEqual(oidcParameters[key], value2, `OIDC ${key}`);
1228
1237
  if (!/^\d+$/.test(github?.ownerId ?? "") || !/^\d+$/.test(github?.repositoryId ?? ""))
1229
1238
  throw new Error("GitHub immutable owner and repository IDs must be numeric");
1230
1239
  const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
@@ -1278,6 +1287,268 @@ async function verifyBootstrap(input) {
1278
1287
  };
1279
1288
  }
1280
1289
 
1290
+ // packages/aws/src/guided.ts
1291
+ function value(input, label) {
1292
+ if (typeof input !== "string" || !input.trim()) throw new Error(`${label} is required`);
1293
+ return input.trim();
1294
+ }
1295
+ function repositoryParts(repository) {
1296
+ const [owner, name, extra] = repository.split("/");
1297
+ if (!owner || !name || extra) throw new Error("repository must be owner/repository");
1298
+ return [owner, name];
1299
+ }
1300
+ function accountFromArn(arn) {
1301
+ return arn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
1302
+ }
1303
+ async function optional(runner, command, args) {
1304
+ if (!runner) return void 0;
1305
+ try {
1306
+ return await runner.execute(command, args);
1307
+ } catch {
1308
+ return void 0;
1309
+ }
1310
+ }
1311
+ async function discovered(input) {
1312
+ const identity = await optional(input.runner, "aws", [
1313
+ "sts",
1314
+ "get-caller-identity",
1315
+ "--output",
1316
+ "json"
1317
+ ]);
1318
+ const aws = parseOrUnavailable(identity) ?? {};
1319
+ const accountId = typeof aws.Account === "string" ? aws.Account : void 0;
1320
+ const region2 = await optional(input.runner, "aws", ["configure", "get", "region"]);
1321
+ const repo = await optional(input.runner, "gh", ["api", `repos/${input.repository}`]);
1322
+ const github = parseOrUnavailable(repo) ?? {};
1323
+ return {
1324
+ accountId,
1325
+ region: region2?.trim() || void 0,
1326
+ ownerId: typeof github.owner?.id === "number" || typeof github.owner?.id === "string" ? String(github.owner.id) : void 0,
1327
+ repositoryId: typeof github.id === "number" || typeof github.id === "string" ? String(github.id) : void 0
1328
+ };
1329
+ }
1330
+ async function scaffoldBootstrapManifest(input) {
1331
+ const slug = value(input.slug, "slug");
1332
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(slug))
1333
+ throw new Error("slug must use lowercase letters, digits, and hyphens");
1334
+ const repository = value(input.repository, "repository");
1335
+ repositoryParts(repository);
1336
+ const environment = value(input.environment, "environment");
1337
+ const providerArn = value(input.providerArn, "github OIDC provider ARN");
1338
+ if (!accountFromArn(providerArn)) throw new Error("github OIDC provider ARN is invalid");
1339
+ const found = await discovered(input);
1340
+ const accountId = input.accountId ?? found.accountId;
1341
+ const region2 = input.region ?? found.region;
1342
+ const ownerId = input.ownerId ?? found.ownerId;
1343
+ const repositoryId = input.repositoryId ?? found.repositoryId;
1344
+ if (!accountId) throw new Error("--account-id is required when AWS identity is unavailable");
1345
+ if (!region2) throw new Error("--region is required when AWS Region is unavailable");
1346
+ if (!ownerId) throw new Error("--github-owner-id is required when GitHub is unavailable");
1347
+ if (!repositoryId)
1348
+ throw new Error("--github-repository-id is required when GitHub is unavailable");
1349
+ return {
1350
+ region: region2,
1351
+ core: {
1352
+ stackName: `${slug}-bootstrap`,
1353
+ artifactBucketName: `${slug}-lambda-artifacts-${accountId}`,
1354
+ applicationStackName: slug,
1355
+ dashboardFunctionName: slug
1356
+ },
1357
+ githubOidc: {
1358
+ stackName: `${slug}-github-bootstrap`,
1359
+ providerArn,
1360
+ repository,
1361
+ ownerId,
1362
+ repositoryId,
1363
+ environment
1364
+ }
1365
+ };
1366
+ }
1367
+ function incomplete(config) {
1368
+ const fields = [
1369
+ ["region", config.region],
1370
+ ["core.stackName", config.core?.stackName],
1371
+ ["core.artifactBucketName", config.core?.artifactBucketName],
1372
+ ["core.applicationStackName", config.core?.applicationStackName],
1373
+ ["core.dashboardFunctionName", config.core?.dashboardFunctionName],
1374
+ ["githubOidc.stackName", config.githubOidc?.stackName],
1375
+ ["githubOidc.providerArn", config.githubOidc?.providerArn],
1376
+ ["githubOidc.repository", config.githubOidc?.repository],
1377
+ ["githubOidc.ownerId", config.githubOidc?.ownerId],
1378
+ ["githubOidc.repositoryId", config.githubOidc?.repositoryId],
1379
+ ["githubOidc.environment", config.githubOidc?.environment]
1380
+ ];
1381
+ return fields.filter(([, v]) => typeof v !== "string" || !v).map(([name]) => name);
1382
+ }
1383
+ function parseOrUnavailable(raw) {
1384
+ if (!raw) return void 0;
1385
+ try {
1386
+ return JSON.parse(raw);
1387
+ } catch {
1388
+ return void 0;
1389
+ }
1390
+ }
1391
+ function absent(raw) {
1392
+ return /not found|nosuchentity|404/i.test(raw ?? "");
1393
+ }
1394
+ async function bootstrapPreflight(input) {
1395
+ const checks = [];
1396
+ const missing = incomplete(input.config);
1397
+ checks.push(
1398
+ missing.length ? { name: "manifest", status: "missing", detail: `Missing: ${missing.join(", ")}` } : { name: "manifest", status: "ready", detail: "Manifest has all bootstrap fields." }
1399
+ );
1400
+ if (missing.length) return { ready: false, checks };
1401
+ const runner = input.runner;
1402
+ const identityRaw = await optional(runner, "aws", [
1403
+ "sts",
1404
+ "get-caller-identity",
1405
+ "--output",
1406
+ "json"
1407
+ ]);
1408
+ const identity = parseOrUnavailable(identityRaw);
1409
+ const providerAccount = accountFromArn(input.config.githubOidc?.providerArn ?? "");
1410
+ if (!identity || typeof identity.Account !== "string")
1411
+ checks.push({
1412
+ name: "aws-identity",
1413
+ status: "unverified",
1414
+ detail: "AWS identity could not be read."
1415
+ });
1416
+ else
1417
+ checks.push(
1418
+ identity.Account === providerAccount ? {
1419
+ name: "aws-identity",
1420
+ status: "ready",
1421
+ detail: `AWS account ${identity.Account} matches the provider ARN.`
1422
+ } : {
1423
+ name: "aws-identity",
1424
+ status: "mismatch",
1425
+ detail: `AWS account ${identity.Account} does not match provider account ${providerAccount}.`
1426
+ }
1427
+ );
1428
+ const regionRaw = await optional(runner, "aws", ["configure", "get", "region"]);
1429
+ if (!regionRaw?.trim())
1430
+ checks.push({
1431
+ name: "aws-region",
1432
+ status: "unverified",
1433
+ detail: "AWS Region could not be read."
1434
+ });
1435
+ else
1436
+ checks.push(
1437
+ regionRaw.trim() === input.config.region ? { name: "aws-region", status: "ready", detail: `AWS Region is ${input.config.region}.` } : {
1438
+ name: "aws-region",
1439
+ status: "mismatch",
1440
+ detail: `AWS Region ${regionRaw.trim()} does not match manifest ${input.config.region}.`
1441
+ }
1442
+ );
1443
+ const providerRaw = await optional(runner, "aws", [
1444
+ "iam",
1445
+ "get-open-id-connect-provider",
1446
+ "--open-id-connect-provider-arn",
1447
+ input.config.githubOidc?.providerArn ?? ""
1448
+ ]);
1449
+ checks.push(
1450
+ !providerRaw ? {
1451
+ name: "oidc-provider",
1452
+ status: "unverified",
1453
+ detail: "OIDC provider could not be queried."
1454
+ } : absent(providerRaw) ? { name: "oidc-provider", status: "missing", detail: "OIDC provider does not exist." } : { name: "oidc-provider", status: "ready", detail: "OIDC provider exists." }
1455
+ );
1456
+ const repository = input.config.githubOidc?.repository ?? "";
1457
+ const repoRaw = await optional(runner, "gh", ["api", `repos/${repository}`]);
1458
+ const repo = parseOrUnavailable(repoRaw);
1459
+ if (!repoRaw)
1460
+ checks.push({
1461
+ name: "github-repository",
1462
+ status: "unverified",
1463
+ detail: "GitHub repository could not be queried."
1464
+ });
1465
+ else if (absent(repoRaw))
1466
+ checks.push({
1467
+ name: "github-repository",
1468
+ status: "missing",
1469
+ detail: "GitHub repository does not exist or is unavailable."
1470
+ });
1471
+ else if (String(repo?.id) !== input.config.githubOidc?.repositoryId || String(repo?.owner?.id) !== input.config.githubOidc?.ownerId)
1472
+ checks.push({
1473
+ name: "github-repository",
1474
+ status: "mismatch",
1475
+ detail: "GitHub numeric repository identity does not match the manifest."
1476
+ });
1477
+ else
1478
+ checks.push({
1479
+ name: "github-repository",
1480
+ status: "ready",
1481
+ detail: "GitHub repository identity matches the manifest."
1482
+ });
1483
+ const environmentRaw = await optional(runner, "gh", [
1484
+ "api",
1485
+ `repos/${repository}/environments/${input.config.githubOidc?.environment}`
1486
+ ]);
1487
+ checks.push(
1488
+ !environmentRaw ? {
1489
+ name: "github-environment",
1490
+ status: "unverified",
1491
+ detail: "GitHub Environment could not be queried."
1492
+ } : absent(environmentRaw) ? {
1493
+ name: "github-environment",
1494
+ status: "missing",
1495
+ detail: "GitHub Environment does not exist."
1496
+ } : {
1497
+ name: "github-environment",
1498
+ status: "ready",
1499
+ detail: "GitHub Environment exists; its policy is administrator-owned context."
1500
+ }
1501
+ );
1502
+ const subject = await (input.provider ?? githubOidcProvider).prerequisite(input.config, runner);
1503
+ checks.push({
1504
+ name: "immutable-subject",
1505
+ status: subject.status === "immutable-subject-required" ? "mismatch" : subject.status,
1506
+ detail: subject.detail
1507
+ });
1508
+ return {
1509
+ ready: !checks.some(({ status }) => status === "missing" || status === "mismatch"),
1510
+ checks
1511
+ };
1512
+ }
1513
+ function quote(args) {
1514
+ return args.map((arg) => `'${arg.replaceAll("'", `'\\"'\\"'`)}'`).join(" ");
1515
+ }
1516
+ async function bootstrapGuide(input) {
1517
+ const handoff = await bootstrapHandoff(input);
1518
+ const lines = [
1519
+ `Phase: ${handoff.phase}`,
1520
+ `Required captures: ${handoff.requiredCapturedFiles.join(", ") || "none"}`
1521
+ ];
1522
+ if (handoff.expectedOutputs.length)
1523
+ lines.push(`Expected outputs: ${handoff.expectedOutputs.join(", ")}`);
1524
+ if (handoff.prerequisite)
1525
+ lines.push(`Prerequisite: ${handoff.prerequisite.status} \u2014 ${handoff.prerequisite.detail}`);
1526
+ lines.push("", "Commands (copy and run each AWS command yourself):");
1527
+ for (const command of handoff.commands) {
1528
+ if (command.name === "review-change-set")
1529
+ lines.push("PAUSE: review the change set before approval.");
1530
+ if (command.name === "execute-reviewed-change-set")
1531
+ lines.push("PAUSE: execute only after explicit approval.");
1532
+ lines.push(`${command.captureFile ? `${command.captureFile}: ` : ""}${quote(command.args)}`);
1533
+ }
1534
+ if (handoff.reviewCheckpoints.length)
1535
+ lines.push("", "Review pauses:", ...handoff.reviewCheckpoints.map((item) => `- ${item}`));
1536
+ if (input.coreStack !== void 0 && input.githubOidcStack !== void 0) {
1537
+ const verified = await verifyBootstrap({
1538
+ config: input.config,
1539
+ coreStack: input.coreStack,
1540
+ githubOidcStack: input.githubOidcStack
1541
+ });
1542
+ lines.push(
1543
+ "",
1544
+ "Optional GitHub administrator action (after the successful verification above):"
1545
+ );
1546
+ for (const command of verified.githubEnvironmentInstructions) lines.push(quote(command));
1547
+ }
1548
+ return `${lines.join("\n")}
1549
+ `;
1550
+ }
1551
+
1281
1552
  // packages/aws/src/index.ts
1282
1553
  var run = promisify(execFile);
1283
1554
  function deploymentTemplate(template, values) {
@@ -1418,7 +1689,9 @@ async function cloudFormationTemplate() {
1418
1689
  }
1419
1690
  export {
1420
1691
  bootstrapContractVersion,
1692
+ bootstrapGuide,
1421
1693
  bootstrapHandoff,
1694
+ bootstrapPreflight,
1422
1695
  bootstrapTemplate,
1423
1696
  bootstrapTemplatePath,
1424
1697
  cloudFormationTemplate,
@@ -1430,5 +1703,6 @@ export {
1430
1703
  packageLambda,
1431
1704
  publishClientAssets,
1432
1705
  requiredBootstrapParameters,
1706
+ scaffoldBootstrapManifest,
1433
1707
  verifyBootstrap
1434
1708
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@continuous-excellence/ze-great-dashboard-aws",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "AWS Lambda and CloudFormation adapter for Ze Great Dashboard.",
6
6
  "keywords": [