@continuous-excellence/ze-great-dashboard-aws 0.1.33 → 0.3.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
@@ -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
@@ -879,8 +879,8 @@ function coreBootstrapOutputs(stack) {
879
879
  (output) => Boolean(output.OutputValue)
880
880
  ).map((output) => [output.OutputKey, output.OutputValue])
881
881
  );
882
- const required = ["ArtifactBucketName", "ApplicationStackName", "CloudFormationExecutionRoleArn"];
883
- const missing = required.filter((key) => !values[key]);
882
+ const required2 = ["ArtifactBucketName", "ApplicationStackName", "CloudFormationExecutionRoleArn"];
883
+ const missing = required2.filter((key) => !values[key]);
884
884
  if (missing.length) throw new Error(`Core stack JSON is missing outputs: ${missing.join(", ")}`);
885
885
  return values;
886
886
  }
@@ -897,8 +897,656 @@ 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
+ }
903
+
904
+ // packages/aws/src/handoff.ts
905
+ var githubSubjectKeys = [
906
+ "repository_owner",
907
+ "repository_owner_id",
908
+ "repository",
909
+ "repository_id",
910
+ "context"
911
+ ];
912
+ function outputValues(stack) {
913
+ return Object.fromEntries(
914
+ (stack.Outputs ?? []).filter(
915
+ (entry) => typeof entry.OutputKey === "string" && typeof entry.OutputValue === "string"
916
+ ).map(({ OutputKey, OutputValue }) => [OutputKey, OutputValue])
917
+ );
918
+ }
919
+ function parameterValues(stack) {
920
+ return Object.fromEntries(
921
+ (stack.Parameters ?? []).filter(
922
+ (entry) => typeof entry.ParameterKey === "string" && typeof entry.ParameterValue === "string"
923
+ ).map(({ ParameterKey, ParameterValue }) => [ParameterKey, ParameterValue])
924
+ );
925
+ }
926
+ function capturedStackIdentity(input) {
927
+ const candidate = input && typeof input === "object" && "Stacks" in input ? input.Stacks?.[0] : input;
928
+ if (!candidate || typeof candidate !== "object") return {};
929
+ const { StackName, StackId } = candidate;
930
+ return {
931
+ ...typeof StackName === "string" ? { StackName } : {},
932
+ ...typeof StackId === "string" ? { StackId } : {}
933
+ };
934
+ }
935
+ function required(value2, name) {
936
+ if (!value2) throw new Error(`Bootstrap manifest is missing ${name}`);
937
+ return value2;
938
+ }
939
+ function region(config) {
940
+ return required(config.region, "region");
941
+ }
942
+ function stackName(config, kind) {
943
+ return required(
944
+ kind === "core" ? config.core?.stackName : config.githubOidc?.stackName,
945
+ `${kind}.stackName`
946
+ );
947
+ }
948
+ function changeSetCommands(input) {
949
+ const name = `${input.kind}-initial-review`;
950
+ const stack = stackName(input.config, input.kind);
951
+ const common = ["--stack-name", stack, "--region", region(input.config)];
952
+ const parameters = [
953
+ "ze-great-dashboard-aws",
954
+ "bootstrap",
955
+ "parameters",
956
+ "--kind",
957
+ input.kind,
958
+ "--config",
959
+ input.configPath,
960
+ "--output",
961
+ input.parameterPath
962
+ ];
963
+ if (input.kind === "github-oidc")
964
+ parameters.push("--core-stack-json", input.coreCapturePath ?? "core-deployed-stack.json");
965
+ return [
966
+ { name: "generate-parameters", args: parameters },
967
+ {
968
+ name: "create-change-set",
969
+ args: [
970
+ "aws",
971
+ "cloudformation",
972
+ "create-change-set",
973
+ ...common,
974
+ "--change-set-name",
975
+ name,
976
+ "--change-set-type",
977
+ "CREATE",
978
+ "--template-body",
979
+ `file://${input.templatePath}`,
980
+ "--parameters",
981
+ `file://${input.parameterPath}`,
982
+ "--capabilities",
983
+ "CAPABILITY_NAMED_IAM",
984
+ "--no-cli-pager"
985
+ ]
986
+ },
987
+ {
988
+ name: "wait-for-change-set",
989
+ args: [
990
+ "aws",
991
+ "cloudformation",
992
+ "wait",
993
+ "change-set-create-complete",
994
+ ...common,
995
+ "--change-set-name",
996
+ name
997
+ ]
998
+ },
999
+ {
1000
+ name: "review-change-set",
1001
+ args: [
1002
+ "aws",
1003
+ "cloudformation",
1004
+ "describe-change-set",
1005
+ ...common,
1006
+ "--change-set-name",
1007
+ name,
1008
+ "--no-cli-pager"
1009
+ ]
1010
+ },
1011
+ {
1012
+ name: "execute-reviewed-change-set",
1013
+ args: ["aws", "cloudformation", "execute-change-set", ...common, "--change-set-name", name]
1014
+ },
1015
+ {
1016
+ name: "wait-for-stack",
1017
+ args: ["aws", "cloudformation", "wait", "stack-create-complete", ...common]
1018
+ },
1019
+ {
1020
+ name: "capture-stack",
1021
+ captureFile: `${input.kind}-deployed-stack.json`,
1022
+ args: ["aws", "cloudformation", "describe-stacks", ...common, "--no-cli-pager"]
1023
+ }
1024
+ ];
1025
+ }
1026
+ var githubOidcProvider = {
1027
+ name: "github-oidc",
1028
+ async prerequisite(config, runner) {
1029
+ const repository = config.githubOidc?.repository;
1030
+ if (!repository)
1031
+ return {
1032
+ status: "unverified",
1033
+ blocking: false,
1034
+ detail: "GitHub repository is absent from the manifest."
1035
+ };
1036
+ if (!runner)
1037
+ return {
1038
+ status: "unverified",
1039
+ blocking: false,
1040
+ detail: "GitHub OIDC subject setting was not queried."
1041
+ };
1042
+ try {
1043
+ const response = JSON.parse(
1044
+ await runner.execute("gh", ["api", `repos/${repository}/actions/oidc/customization/sub`])
1045
+ );
1046
+ const keys = response.include_claim_keys;
1047
+ if (response.use_default === false && Array.isArray(keys) && githubSubjectKeys.every((key) => keys.includes(key)))
1048
+ return {
1049
+ status: "ready",
1050
+ blocking: false,
1051
+ detail: "GitHub emits the immutable owner/repository-ID OIDC subject required by this adapter."
1052
+ };
1053
+ return {
1054
+ status: "immutable-subject-required",
1055
+ blocking: true,
1056
+ detail: "Coordinate migration: inventory and temporarily make existing name-based trust policies compatible, enable immutable GitHub OIDC subjects as a separate GitHub-admin action, verify existing deployments, then retire legacy trust."
1057
+ };
1058
+ } catch {
1059
+ return {
1060
+ status: "unverified",
1061
+ blocking: false,
1062
+ detail: "GitHub OIDC subject setting could not be verified (gh CLI, authentication, permission, or network unavailable)."
1063
+ };
1064
+ }
1065
+ }
1066
+ };
1067
+ async function contracts() {
1068
+ return {
1069
+ core: bootstrapContractVersion(await bootstrapTemplate("core")),
1070
+ githubOidc: bootstrapContractVersion(await bootstrapTemplate("github-oidc"))
1071
+ };
1072
+ }
1073
+ async function bootstrapHandoff(input) {
1074
+ const expectedContracts = await contracts();
1075
+ const coreCapturePath = input.coreStackPath ?? "core-deployed-stack.json";
1076
+ const core = input.coreStack === void 0 ? void 0 : deployedBootstrapStack(input.coreStack, expectedContracts.core);
1077
+ const oidc = input.githubOidcStack === void 0 ? void 0 : deployedBootstrapStack(input.githubOidcStack, expectedContracts.githubOidc);
1078
+ if (!core) {
1079
+ requiredBootstrapParameters("core", {
1080
+ ArtifactBucketName: input.config.core?.artifactBucketName,
1081
+ ApplicationStackName: input.config.core?.applicationStackName,
1082
+ DashboardFunctionName: input.config.core?.dashboardFunctionName
1083
+ });
1084
+ const templatePath = await bootstrapTemplatePath("core");
1085
+ return {
1086
+ phase: "core",
1087
+ expectedContracts,
1088
+ templatePath,
1089
+ parameterPath: "core-bootstrap.json",
1090
+ expectedOutputs: [
1091
+ "BootstrapContractVersion",
1092
+ "ArtifactBucketName",
1093
+ "ApplicationStackName",
1094
+ "CloudFormationExecutionRoleArn"
1095
+ ],
1096
+ requiredCapturedFiles: [],
1097
+ reviewCheckpoints: [
1098
+ "Review every IAM action and CAPABILITY_NAMED_IAM acknowledgement.",
1099
+ "Do not approve retained bucket or role replacements."
1100
+ ],
1101
+ commands: changeSetCommands({
1102
+ kind: "core",
1103
+ config: input.config,
1104
+ configPath: input.configPath,
1105
+ templatePath,
1106
+ parameterPath: "core-bootstrap.json"
1107
+ })
1108
+ };
1109
+ }
1110
+ coreBootstrapOutputs(core);
1111
+ if (!oidc) {
1112
+ requiredBootstrapParameters("github-oidc", {
1113
+ GitHubOidcProviderArn: input.config.githubOidc?.providerArn,
1114
+ GitHubRepository: input.config.githubOidc?.repository,
1115
+ GitHubOwnerId: input.config.githubOidc?.ownerId,
1116
+ GitHubRepositoryId: input.config.githubOidc?.repositoryId,
1117
+ GitHubEnvironment: input.config.githubOidc?.environment,
1118
+ ...coreBootstrapOutputs(core)
1119
+ });
1120
+ const templatePath = await bootstrapTemplatePath("github-oidc");
1121
+ return {
1122
+ phase: "github-oidc",
1123
+ expectedContracts,
1124
+ templatePath,
1125
+ parameterPath: "github-oidc-bootstrap.json",
1126
+ expectedOutputs: ["BootstrapContractVersion", "GitHubDeployRoleArn"],
1127
+ requiredCapturedFiles: [coreCapturePath],
1128
+ reviewCheckpoints: [
1129
+ "Review the exact immutable GitHub OIDC subject, audience, bucket lambda/* prefix, application stack, and execution role.",
1130
+ "Do not execute until the GitHub Environment prerequisite is ready."
1131
+ ],
1132
+ commands: changeSetCommands({
1133
+ kind: "github-oidc",
1134
+ config: input.config,
1135
+ configPath: input.configPath,
1136
+ templatePath,
1137
+ parameterPath: "github-oidc-bootstrap.json",
1138
+ coreCapturePath
1139
+ }),
1140
+ prerequisite: await (input.provider ?? githubOidcProvider).prerequisite(
1141
+ input.config,
1142
+ input.runner
1143
+ )
1144
+ };
1145
+ }
1146
+ const prerequisite = await (input.provider ?? githubOidcProvider).prerequisite(
1147
+ input.config,
1148
+ input.runner
1149
+ );
1150
+ if (prerequisite.status !== "ready")
1151
+ return {
1152
+ phase: "github-environment",
1153
+ expectedContracts,
1154
+ expectedOutputs: [],
1155
+ requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1156
+ reviewCheckpoints: [
1157
+ "A GitHub administrator must complete and verify the immutable-subject migration before deployments."
1158
+ ],
1159
+ commands: [],
1160
+ prerequisite
1161
+ };
1162
+ return {
1163
+ phase: "application-gateway",
1164
+ expectedContracts,
1165
+ expectedOutputs: [],
1166
+ requiredCapturedFiles: [coreCapturePath, "github-oidc-deployed-stack.json"],
1167
+ reviewCheckpoints: [
1168
+ "Consumer owns gateway selection, private Lambda permission, authentication, and smoke tests."
1169
+ ],
1170
+ commands: [],
1171
+ prerequisite
1172
+ };
1173
+ }
1174
+ function assertEqual(actual, expected, label) {
1175
+ if (actual !== expected)
1176
+ throw new Error(
1177
+ `${label} mismatch (captured: ${actual ?? "missing"}; manifest: ${expected ?? "missing"})`
1178
+ );
1179
+ }
1180
+ async function verifyBootstrap(input) {
1181
+ const expected = await contracts();
1182
+ const core = deployedBootstrapStack(input.coreStack, expected.core);
1183
+ const oidc = deployedBootstrapStack(input.githubOidcStack, expected.githubOidc);
1184
+ for (const [kind, capture] of [
1185
+ ["Core", capturedStackIdentity(input.coreStack)],
1186
+ ["GitHub OIDC", capturedStackIdentity(input.githubOidcStack)]
1187
+ ]) {
1188
+ const expectedName = kind === "Core" ? input.config.core?.stackName : input.config.githubOidc?.stackName;
1189
+ assertEqual(capture.StackName, expectedName, `${kind} stack name`);
1190
+ const capturedRegion = capture.StackId?.match(/^arn:[^:]+:cloudformation:([^:]+):/)?.[1];
1191
+ assertEqual(capturedRegion, input.config.region, `${kind} stack Region`);
1192
+ }
1193
+ const coreOutputs = coreBootstrapOutputs(core);
1194
+ const coreParameters = parameterValues(core);
1195
+ assertEqual(
1196
+ coreParameters.ArtifactBucketName,
1197
+ input.config.core?.artifactBucketName,
1198
+ "Artifact bucket"
1199
+ );
1200
+ assertEqual(
1201
+ coreParameters.ApplicationStackName,
1202
+ input.config.core?.applicationStackName,
1203
+ "Application stack"
1204
+ );
1205
+ assertEqual(
1206
+ coreParameters.DashboardFunctionName,
1207
+ input.config.core?.dashboardFunctionName,
1208
+ "Dashboard function"
1209
+ );
1210
+ assertEqual(
1211
+ coreOutputs.ArtifactBucketName,
1212
+ input.config.core?.artifactBucketName,
1213
+ "Core output artifact bucket"
1214
+ );
1215
+ assertEqual(
1216
+ coreOutputs.ApplicationStackName,
1217
+ input.config.core?.applicationStackName,
1218
+ "Core output application stack"
1219
+ );
1220
+ const oidcParameters = parameterValues(oidc);
1221
+ const github = input.config.githubOidc;
1222
+ const executionRole = required(
1223
+ coreOutputs.CloudFormationExecutionRoleArn,
1224
+ "core CloudFormationExecutionRoleArn output"
1225
+ );
1226
+ for (const [key, value2] of Object.entries({
1227
+ GitHubOidcProviderArn: github?.providerArn,
1228
+ GitHubRepository: github?.repository,
1229
+ GitHubOwnerId: github?.ownerId,
1230
+ GitHubRepositoryId: github?.repositoryId,
1231
+ GitHubEnvironment: github?.environment,
1232
+ ApplicationStackName: coreOutputs.ApplicationStackName,
1233
+ ArtifactBucketName: coreOutputs.ArtifactBucketName,
1234
+ CloudFormationExecutionRoleArn: executionRole
1235
+ }))
1236
+ assertEqual(oidcParameters[key], value2, `OIDC ${key}`);
1237
+ if (!/^\d+$/.test(github?.ownerId ?? "") || !/^\d+$/.test(github?.repositoryId ?? ""))
1238
+ throw new Error("GitHub immutable owner and repository IDs must be numeric");
1239
+ const [owner, repository, ...extra] = (github?.repository ?? "").split("/");
1240
+ if (!owner || !repository || extra.length)
1241
+ throw new Error("GitHub repository must be owner/repository");
1242
+ const deployRole = outputValues(oidc).GitHubDeployRoleArn;
1243
+ if (!deployRole) throw new Error("GitHub OIDC stack JSON is missing outputs: GitHubDeployRoleArn");
1244
+ const providerArn = github?.providerArn ?? "";
1245
+ const account = providerArn.match(/^arn:[^:]+:iam::(\d+):oidc-provider\//)?.[1];
1246
+ const deployAccount = deployRole.match(/^arn:[^:]+:iam::(\d+):role\//)?.[1];
1247
+ const executionAccount = executionRole.match(/^arn:[^:]+:iam::(\d+):role\//)?.[1];
1248
+ if (!account || account !== deployAccount || account !== executionAccount)
1249
+ throw new Error("Provider and reviewed role ARNs must belong to the same AWS account");
1250
+ return {
1251
+ verified: true,
1252
+ immutableSubject: `repo:${owner}@${github?.ownerId}/${repository}@${github?.repositoryId}:environment:${github?.environment}`,
1253
+ environmentVariables: {
1254
+ AWS_DEPLOY_ROLE_ARN: deployRole,
1255
+ AWS_CLOUDFORMATION_EXECUTION_ROLE_ARN: executionRole
1256
+ },
1257
+ reviewedArns: {
1258
+ githubDeployRoleArn: deployRole,
1259
+ cloudFormationExecutionRoleArn: executionRole
1260
+ },
1261
+ githubEnvironmentInstructions: [
1262
+ [
1263
+ "gh",
1264
+ "variable",
1265
+ "set",
1266
+ "AWS_DEPLOY_ROLE_ARN",
1267
+ "--repo",
1268
+ `${owner}/${repository}`,
1269
+ "--env",
1270
+ github?.environment ?? "",
1271
+ "--body",
1272
+ deployRole
1273
+ ],
1274
+ [
1275
+ "gh",
1276
+ "variable",
1277
+ "set",
1278
+ "AWS_CLOUDFORMATION_EXECUTION_ROLE_ARN",
1279
+ "--repo",
1280
+ `${owner}/${repository}`,
1281
+ "--env",
1282
+ github?.environment ?? "",
1283
+ "--body",
1284
+ executionRole
1285
+ ]
1286
+ ]
1287
+ };
1288
+ }
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
+ `;
902
1550
  }
903
1551
 
904
1552
  // packages/aws/src/index.ts
@@ -1041,14 +1689,20 @@ async function cloudFormationTemplate() {
1041
1689
  }
1042
1690
  export {
1043
1691
  bootstrapContractVersion,
1692
+ bootstrapGuide,
1693
+ bootstrapHandoff,
1694
+ bootstrapPreflight,
1044
1695
  bootstrapTemplate,
1045
1696
  bootstrapTemplatePath,
1046
1697
  cloudFormationTemplate,
1047
1698
  coreBootstrapOutputs,
1048
1699
  deployLambda,
1049
1700
  deployedBootstrapStack,
1701
+ githubOidcProvider,
1050
1702
  mergeBootstrapParameters,
1051
1703
  packageLambda,
1052
1704
  publishClientAssets,
1053
- requiredBootstrapParameters
1705
+ requiredBootstrapParameters,
1706
+ scaffoldBootstrapManifest,
1707
+ verifyBootstrap
1054
1708
  };