@geekmidas/cli 10.0.0-alpha.78 → 10.0.0-alpha.80

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.
Files changed (129) hide show
  1. package/dist/{add-D2XKszYP.mjs → add-CIxT-Iq6.mjs} +2 -2
  2. package/dist/{add-D2XKszYP.mjs.map → add-CIxT-Iq6.mjs.map} +1 -1
  3. package/dist/{add-BvOICikp.cjs → add-DJ0VE-y-.cjs} +7 -7
  4. package/dist/{add-BvOICikp.cjs.map → add-DJ0VE-y-.cjs.map} +1 -1
  5. package/dist/{aws-Bja4zGSs.mjs → aws-B-6uEhgP.mjs} +8 -2
  6. package/dist/{aws-Bja4zGSs.mjs.map → aws-B-6uEhgP.mjs.map} +1 -1
  7. package/dist/{aws-CQAuluiu.cjs → aws-BTawF0F-.cjs} +19 -2
  8. package/dist/{aws-CQAuluiu.cjs.map → aws-BTawF0F-.cjs.map} +1 -1
  9. package/dist/{bundler-C6T-1OLT.cjs → bundler-BlbSLFLn.cjs} +3 -3
  10. package/dist/{bundler-C6T-1OLT.cjs.map → bundler-BlbSLFLn.cjs.map} +1 -1
  11. package/dist/{bundler-CtHzKqnZ.mjs → bundler-D4pl87A5.mjs} +3 -3
  12. package/dist/{bundler-CtHzKqnZ.mjs.map → bundler-D4pl87A5.mjs.map} +1 -1
  13. package/dist/{cli-BgPyhx1i.mjs → cli-7aLfgIGt.mjs} +3 -3
  14. package/dist/{cli-BgPyhx1i.mjs.map → cli-7aLfgIGt.mjs.map} +1 -1
  15. package/dist/{cli-K6Vj_pPW.cjs → cli-BkNZBe_-.cjs} +4 -4
  16. package/dist/{cli-K6Vj_pPW.cjs.map → cli-BkNZBe_-.cjs.map} +1 -1
  17. package/dist/{compose-B7MC19eC.cjs → compose-Bn4OJONR.cjs} +88 -60
  18. package/dist/{compose-B7MC19eC.cjs.map → compose-Bn4OJONR.cjs.map} +1 -1
  19. package/dist/{compose-DByK353T.mjs → compose-n7K8H5j8.mjs} +36 -8
  20. package/dist/{compose-DByK353T.mjs.map → compose-n7K8H5j8.mjs.map} +1 -1
  21. package/dist/{config-Bql0AmJt.d.mts → config-L_6NivCp.d.mts} +3 -3
  22. package/dist/{config-Bql0AmJt.d.mts.map → config-L_6NivCp.d.mts.map} +1 -1
  23. package/dist/{config-DlNpHK8p.d.cts → config-Mr4rcXFC.d.cts} +3 -3
  24. package/dist/{config-DlNpHK8p.d.cts.map → config-Mr4rcXFC.d.cts.map} +1 -1
  25. package/dist/config.d.cts +3 -3
  26. package/dist/config.d.mts +3 -3
  27. package/dist/{databases-C2p5LGAp.cjs → databases-BY-pEImp.cjs} +185 -7
  28. package/dist/databases-BY-pEImp.cjs.map +1 -0
  29. package/dist/{databases-hUHTuwhx.mjs → databases-CzszQV0N.mjs} +153 -5
  30. package/dist/databases-CzszQV0N.mjs.map +1 -0
  31. package/dist/deploy/public.cjs +12 -11
  32. package/dist/deploy/public.d.cts +19 -3
  33. package/dist/deploy/public.d.cts.map +1 -1
  34. package/dist/deploy/public.d.mts +19 -3
  35. package/dist/deploy/public.d.mts.map +1 -1
  36. package/dist/deploy/public.mjs +6 -6
  37. package/dist/{deploy-BerP7ppB.mjs → deploy-DK4Y6ev4.mjs} +3 -3
  38. package/dist/{deploy-BerP7ppB.mjs.map → deploy-DK4Y6ev4.mjs.map} +1 -1
  39. package/dist/{deploy-Dt42xiQA.cjs → deploy-Ppnh8MHS.cjs} +9 -9
  40. package/dist/{deploy-Dt42xiQA.cjs.map → deploy-Ppnh8MHS.cjs.map} +1 -1
  41. package/dist/{dokploy-ssZQRK2C.mjs → dokploy-Bjq05wkw.mjs} +2 -2
  42. package/dist/{dokploy-ssZQRK2C.mjs.map → dokploy-Bjq05wkw.mjs.map} +1 -1
  43. package/dist/{dokploy-NG9UrVrP.cjs → dokploy-CJvdoNc3.cjs} +2 -2
  44. package/dist/{dokploy-NG9UrVrP.cjs.map → dokploy-CJvdoNc3.cjs.map} +1 -1
  45. package/dist/{encryption-De6MN0O5.mjs → encryption-BrsOJFFz.mjs} +2 -2
  46. package/dist/{encryption-De6MN0O5.mjs.map → encryption-BrsOJFFz.mjs.map} +1 -1
  47. package/dist/{encryption-CbMI24fI.cjs → encryption-CAyR6D7B.cjs} +5 -5
  48. package/dist/{encryption-CbMI24fI.cjs.map → encryption-CAyR6D7B.cjs.map} +1 -1
  49. package/dist/{engine-CoCsNoW_.cjs → engine-DzBZR5D0.cjs} +118 -82
  50. package/dist/engine-DzBZR5D0.cjs.map +1 -0
  51. package/dist/{engine-DOjGIgl4.mjs → engine-ba9CvjTo.mjs} +53 -17
  52. package/dist/engine-ba9CvjTo.mjs.map +1 -0
  53. package/dist/github-BLjU59Ye.cjs +669 -0
  54. package/dist/github-BLjU59Ye.cjs.map +1 -0
  55. package/dist/github-CVdbVlT5.mjs +669 -0
  56. package/dist/github-CVdbVlT5.mjs.map +1 -0
  57. package/dist/{index-BXO0vlme.d.mts → index-C0UmP_lg.d.mts} +2 -2
  58. package/dist/{index-BXO0vlme.d.mts.map → index-C0UmP_lg.d.mts.map} +1 -1
  59. package/dist/{index-Rs0Fua-q.d.cts → index-DTEPwLml.d.cts} +2 -2
  60. package/dist/{index-Rs0Fua-q.d.cts.map → index-DTEPwLml.d.cts.map} +1 -1
  61. package/dist/index.cjs +114 -114
  62. package/dist/index.cjs.map +1 -1
  63. package/dist/index.mjs +41 -41
  64. package/dist/index.mjs.map +1 -1
  65. package/dist/{migrate-Cgoy58Sh.cjs → migrate-DkB833Jj.cjs} +8 -9
  66. package/dist/{migrate-Cgoy58Sh.cjs.map → migrate-DkB833Jj.cjs.map} +1 -1
  67. package/dist/{migrate-CNKaQl3K.mjs → migrate-DlBgATa-.mjs} +3 -4
  68. package/dist/{migrate-CNKaQl3K.mjs.map → migrate-DlBgATa-.mjs.map} +1 -1
  69. package/dist/{migrate-B4GBvCNY.cjs → migrate-iEdyEx4S.cjs} +4 -4
  70. package/dist/{migrate-B4GBvCNY.cjs.map → migrate-iEdyEx4S.cjs.map} +1 -1
  71. package/dist/{migrate-BQ4wVKq_.mjs → migrate-pfu4aYzX.mjs} +2 -2
  72. package/dist/{migrate-BQ4wVKq_.mjs.map → migrate-pfu4aYzX.mjs.map} +1 -1
  73. package/dist/openapi.d.cts +1 -1
  74. package/dist/openapi.d.mts +1 -1
  75. package/dist/reconcile/public.d.cts +1 -1
  76. package/dist/reconcile/public.d.mts +1 -1
  77. package/dist/{resolve-CEaqRBzY.cjs → resolve-4_5N67Fk.cjs} +3 -3
  78. package/dist/{resolve-CEaqRBzY.cjs.map → resolve-4_5N67Fk.cjs.map} +1 -1
  79. package/dist/{resolve-mvaystiE.mjs → resolve-CQPn66jr.mjs} +3 -3
  80. package/dist/{resolve-mvaystiE.mjs.map → resolve-CQPn66jr.mjs.map} +1 -1
  81. package/dist/{rollback-S2C-Y9pB.mjs → rollback-C0cbdsM-.mjs} +3 -3
  82. package/dist/{rollback-S2C-Y9pB.mjs.map → rollback-C0cbdsM-.mjs.map} +1 -1
  83. package/dist/{rollback-F3P-rA-r.cjs → rollback-CgYUPLbq.cjs} +5 -5
  84. package/dist/{rollback-F3P-rA-r.cjs.map → rollback-CgYUPLbq.cjs.map} +1 -1
  85. package/dist/{runtime-D9AxGzrw.d.cts → runtime-D6A-C6zh.d.cts} +2 -2
  86. package/dist/{runtime-D9AxGzrw.d.cts.map → runtime-D6A-C6zh.d.cts.map} +1 -1
  87. package/dist/{runtime-Dflki3Vs.d.mts → runtime-ekxfPNM-.d.mts} +2 -2
  88. package/dist/{runtime-Dflki3Vs.d.mts.map → runtime-ekxfPNM-.d.mts.map} +1 -1
  89. package/dist/sandbox/migrate-worker.cjs +22 -8
  90. package/dist/sandbox/migrate-worker.cjs.map +1 -1
  91. package/dist/sandbox/migrate-worker.mjs +22 -8
  92. package/dist/sandbox/migrate-worker.mjs.map +1 -1
  93. package/dist/{secretsManager-Ti0oZwsS.mjs → secretsManager-Chs9VV2r.mjs} +8 -2
  94. package/dist/{secretsManager-Ti0oZwsS.mjs.map → secretsManager-Chs9VV2r.mjs.map} +1 -1
  95. package/dist/{secretsManager-DUXW3fQt.cjs → secretsManager-DcMlHnRC.cjs} +19 -2
  96. package/dist/{secretsManager-DUXW3fQt.cjs.map → secretsManager-DcMlHnRC.cjs.map} +1 -1
  97. package/dist/{stale-C5FqIVc8.mjs → seeds-BdkFpW0P.mjs} +64 -6
  98. package/dist/seeds-BdkFpW0P.mjs.map +1 -0
  99. package/dist/{stale-BsiPJI7Y.cjs → seeds-ZljwYV_p.cjs} +81 -5
  100. package/dist/seeds-ZljwYV_p.cjs.map +1 -0
  101. package/dist/sniffer-BjG-1mlD.cjs.map +1 -1
  102. package/dist/sniffer-ByWtH28i.mjs.map +1 -1
  103. package/dist/target/public.cjs +1 -1
  104. package/dist/target/public.d.cts +2 -2
  105. package/dist/target/public.d.mts +2 -2
  106. package/dist/target/public.mjs +1 -1
  107. package/dist/{types-DKxkXsKC.d.mts → types-BzCavjGt.d.mts} +29 -2
  108. package/dist/types-BzCavjGt.d.mts.map +1 -0
  109. package/dist/{types-ByY0zi3n.d.cts → types-DZtj9jeI.d.cts} +29 -2
  110. package/dist/types-DZtj9jeI.d.cts.map +1 -0
  111. package/dist/workspace/index.d.cts +2 -2
  112. package/dist/workspace/index.d.mts +2 -2
  113. package/package.json +15 -15
  114. package/dist/databases-C2p5LGAp.cjs.map +0 -1
  115. package/dist/databases-hUHTuwhx.mjs.map +0 -1
  116. package/dist/engine-CoCsNoW_.cjs.map +0 -1
  117. package/dist/engine-DOjGIgl4.mjs.map +0 -1
  118. package/dist/github-CwX9z89E.mjs +0 -213
  119. package/dist/github-CwX9z89E.mjs.map +0 -1
  120. package/dist/github-D9vGqdLk.cjs +0 -213
  121. package/dist/github-D9vGqdLk.cjs.map +0 -1
  122. package/dist/provider-B3Sv_a74.cjs +0 -173
  123. package/dist/provider-B3Sv_a74.cjs.map +0 -1
  124. package/dist/provider-D7rQzzzf.mjs +0 -138
  125. package/dist/provider-D7rQzzzf.mjs.map +0 -1
  126. package/dist/stale-BsiPJI7Y.cjs.map +0 -1
  127. package/dist/stale-C5FqIVc8.mjs.map +0 -1
  128. package/dist/types-ByY0zi3n.d.cts.map +0 -1
  129. package/dist/types-DKxkXsKC.d.mts.map +0 -1
@@ -0,0 +1,669 @@
1
+ import { k as assertDeployedStage } from "./workspace-BBdqmkch.mjs";
2
+ import { loadWorkspaceConfig } from "./config.mjs";
3
+ import { $t as getKeyPath, Jt as isRemoteStore, Yt as secretsStoreFor, en as keystoreProject, nn as readKey } from "./seeds-BdkFpW0P.mjs";
4
+ import { n as secretsParameterName } from "./aws-B-6uEhgP.mjs";
5
+ import { t as secretsManagerSecretName } from "./secretsManager-Chs9VV2r.mjs";
6
+ import { execFileSync } from "node:child_process";
7
+ import { AttachRolePolicyCommand, CreateOpenIDConnectProviderCommand, CreateRoleCommand, DeleteRolePolicyCommand, DetachRolePolicyCommand, GetRoleCommand, IAMClient, ListAttachedRolePoliciesCommand, ListOpenIDConnectProvidersCommand, ListRoleTagsCommand, PutRolePolicyCommand, TagRoleCommand, UntagRoleCommand, UpdateAssumeRolePolicyCommand } from "@aws-sdk/client-iam";
8
+ //#region src/deploy/githubOidc.ts
9
+ /**
10
+ * Template keys whose values are known for a repository's environment job
11
+ * before the job runs. Anything else — `job_workflow_ref`, `ref`, `sha`,
12
+ * `run_id`, `workflow`, `actor`, a custom property — depends on the run.
13
+ */
14
+ const SUPPORTED_CLAIM_KEYS = [
15
+ "repo",
16
+ "context",
17
+ "environment",
18
+ "repository",
19
+ "repository_owner",
20
+ "repository_owner_id",
21
+ "repository_id",
22
+ "repository_visibility"
23
+ ];
24
+ const NEEDS_FACTS = /* @__PURE__ */ new Set([
25
+ "repository_owner_id",
26
+ "repository_id",
27
+ "repository_visibility"
28
+ ]);
29
+ /** A `:` inside a claim value is sent as `%3A`. */
30
+ function claim(value) {
31
+ return value.replaceAll(":", "%3A");
32
+ }
33
+ /** Whether {@link oidcSubject} needs the repository's ids or visibility. */
34
+ function needsRepoFacts(customization) {
35
+ const immutableWithoutPrefix = customization.use_immutable_subject === true && !customization.sub_claim_prefix;
36
+ if (customization.use_default) return immutableWithoutPrefix;
37
+ return (customization.include_claim_keys ?? []).some((key) => NEEDS_FACTS.has(key) || key === "repo" && immutableWithoutPrefix);
38
+ }
39
+ /**
40
+ * The exact `sub` of a job in `repo`'s `stage` environment.
41
+ *
42
+ * @throws OidcSubjectNotSupported when a custom template includes a claim
43
+ * whose value depends on the run, so no one subject can be trusted.
44
+ */
45
+ function oidcSubject(repo, stage, customization, facts) {
46
+ const [owner = "", name = ""] = repo.split("/");
47
+ const environment = `environment:${claim(stage)}`;
48
+ const repoSegment = () => {
49
+ if (customization.sub_claim_prefix) return customization.sub_claim_prefix;
50
+ if (customization.use_immutable_subject) {
51
+ if (!facts) throw new RepoFactsRequired(repo);
52
+ return `repo:${owner}@${facts.ownerId}/${name}@${facts.repoId}`;
53
+ }
54
+ return `repo:${repo}`;
55
+ };
56
+ const immutable = customization.use_immutable_subject === true || customization.sub_claim_prefix !== void 0;
57
+ if (customization.use_default) return {
58
+ subject: `${repoSegment()}:${environment}`,
59
+ kind: immutable ? "immutable" : "default"
60
+ };
61
+ const keys = customization.include_claim_keys ?? [];
62
+ const unsupported = keys.filter((key) => !SUPPORTED_CLAIM_KEYS.includes(key));
63
+ if (keys.length === 0 || unsupported.length > 0) throw new OidcSubjectNotSupported(repo, keys, unsupported);
64
+ if (keys.some((key) => NEEDS_FACTS.has(key)) && !facts) throw new RepoFactsRequired(repo);
65
+ return {
66
+ subject: keys.map((key) => {
67
+ switch (key) {
68
+ case "repo": return repoSegment();
69
+ case "context":
70
+ case "environment": return environment;
71
+ case "repository": return `repository:${claim(repo)}`;
72
+ case "repository_owner": return `repository_owner:${claim(owner)}`;
73
+ case "repository_owner_id": return `repository_owner_id:${facts.ownerId}`;
74
+ case "repository_id": return `repository_id:${facts.repoId}`;
75
+ default: return `repository_visibility:${claim(facts.visibility)}`;
76
+ }
77
+ }).join(":"),
78
+ kind: "custom"
79
+ };
80
+ }
81
+ /** The endpoint a repository's subject format is read from. */
82
+ function customizationEndpoint(repo) {
83
+ return `repos/${repo}/actions/oidc/customization/sub`;
84
+ }
85
+ /**
86
+ * The `sub` GitHub will send for `repo`'s `stage` environment, read from the
87
+ * repository's OIDC settings through `gh`.
88
+ *
89
+ * A repository on a custom template that names no keys of its own uses its
90
+ * organisation's, read from `orgs/{owner}/actions/oidc/customization/sub`.
91
+ * When the settings cannot be read at all — no permission, or a GitHub
92
+ * Enterprise Server without the endpoint — the default format is assumed, with
93
+ * a warning saying where to check it.
94
+ */
95
+ function resolveOidcSubject(gh, repo, stage) {
96
+ const endpoint = customizationEndpoint(repo);
97
+ let customization;
98
+ try {
99
+ customization = JSON.parse(gh(["api", endpoint]));
100
+ } catch {
101
+ return {
102
+ subject: `repo:${repo}:environment:${claim(stage)}`,
103
+ kind: "assumed",
104
+ warning: `Could not read ${endpoint}, so the default subject format is assumed. Check it with: gh api ${endpoint} — a repository on the immutable subject or a custom template sends a different sub, and the role would not be assumable.`
105
+ };
106
+ }
107
+ if (!customization.use_default && (customization.include_claim_keys ?? []).length === 0) {
108
+ const [owner = ""] = repo.split("/");
109
+ const orgEndpoint = `orgs/${owner}/actions/oidc/customization/sub`;
110
+ let org;
111
+ try {
112
+ org = JSON.parse(gh(["api", orgEndpoint]));
113
+ } catch {
114
+ throw new OrgOidcTemplateUnreadable(repo, orgEndpoint);
115
+ }
116
+ customization = {
117
+ ...customization,
118
+ include_claim_keys: org.include_claim_keys ?? [],
119
+ use_immutable_subject: customization.use_immutable_subject ?? org.use_immutable_subject
120
+ };
121
+ }
122
+ const facts = needsRepoFacts(customization) ? readRepoFacts(gh, repo) : void 0;
123
+ return oidcSubject(repo, stage, customization, facts);
124
+ }
125
+ function readRepoFacts(gh, repo) {
126
+ const raw = JSON.parse(gh(["api", `repos/${repo}`]));
127
+ return {
128
+ ownerId: raw.owner.id,
129
+ repoId: raw.id,
130
+ visibility: raw.visibility
131
+ };
132
+ }
133
+ /**
134
+ * The repository's custom subject template includes a claim whose value
135
+ * depends on the run — or names none — so no one subject can be trusted.
136
+ */
137
+ var OidcSubjectNotSupported = class extends Error {
138
+ repo;
139
+ includeClaimKeys;
140
+ unsupportedKeys;
141
+ constructor(repo, includeClaimKeys, unsupportedKeys) {
142
+ super(`${repo} builds its OIDC subject from a custom template, ${JSON.stringify(includeClaimKeys)}, and ${unsupportedKeys.length > 0 ? `${unsupportedKeys.join(", ")} depend${unsupportedKeys.length === 1 ? "s" : ""} on the run` : "it names no claims"}, so the deploy role cannot trust one exact subject. Set the repository back to the default subject — gh api --method PUT ${customizationEndpoint(repo)} -F use_default=true — (or keep the immutable one by also passing -F use_immutable_subject=true), or use a template built only from ${SUPPORTED_CLAIM_KEYS.join(", ")}; then run this again.`);
143
+ this.repo = repo;
144
+ this.includeClaimKeys = includeClaimKeys;
145
+ this.unsupportedKeys = unsupportedKeys;
146
+ this.name = "OidcSubjectNotSupported";
147
+ }
148
+ };
149
+ /** The subject needs the repository's ids, and none were read. */
150
+ var RepoFactsRequired = class extends Error {
151
+ repo;
152
+ constructor(repo) {
153
+ super(`The OIDC subject of ${repo} includes its owner and repository ids; read them with gh api repos/${repo} and pass them in.`);
154
+ this.repo = repo;
155
+ this.name = "RepoFactsRequired";
156
+ }
157
+ };
158
+ /**
159
+ * The repository uses its organisation's subject template, and that template
160
+ * could not be read — so what GitHub sends is unknown, and is not guessed.
161
+ */
162
+ var OrgOidcTemplateUnreadable = class extends Error {
163
+ repo;
164
+ endpoint;
165
+ constructor(repo, endpoint) {
166
+ super(`${repo} uses its organisation's OIDC subject template, and ${endpoint} could not be read. Run this as an organisation owner (gh auth refresh -s admin:org), or set the repository back to the default subject with gh api --method PUT ${customizationEndpoint(repo)} -F use_default=true; then run this again.`);
167
+ this.repo = repo;
168
+ this.endpoint = endpoint;
169
+ this.name = "OrgOidcTemplateUnreadable";
170
+ }
171
+ };
172
+ //#endregion
173
+ //#region src/deploy/githubPolicy.ts
174
+ /**
175
+ * What the deploy role may do in the stage's account.
176
+ *
177
+ * An SST stage creates its own stack, which is most of AWS, so its role gets
178
+ * `AdministratorAccess`. A stage deployed with the `compose` target builds and
179
+ * runs containers on a server; from AWS its deploy job needs only the stage's
180
+ * secrets — read, and written back when a deploy generates a new one — and,
181
+ * when the deploy state is kept in AWS, the stage's state. So a compose stage
182
+ * gets an inline policy naming exactly those resources and nothing else.
183
+ *
184
+ * `--policy-arn` replaces either.
185
+ */
186
+ /**
187
+ * What SST needs to create a stack is most of AWS, so this is the default
188
+ * outside compose — said out loud in the output, and replaceable with
189
+ * `--policy-arn`.
190
+ */
191
+ const DEFAULT_POLICY_ARN = "arn:aws:iam::aws:policy/AdministratorAccess";
192
+ /** The name of the inline policy a compose stage's role is given. */
193
+ const SCOPED_POLICY_NAME = "gkm-deploy";
194
+ /** Every target the stage's apps deploy through. */
195
+ function deployTargets(workspace) {
196
+ const targets = /* @__PURE__ */ new Set([workspace.deploy?.default ?? "dokploy"]);
197
+ for (const app of Object.values(workspace.apps)) targets.add(app.resolvedDeployTarget);
198
+ return [...targets].sort();
199
+ }
200
+ /**
201
+ * The access the stage's deploy role is given: `--policy-arn` when passed,
202
+ * the scoped policy for a stage only the compose target deploys, and
203
+ * `AdministratorAccess` otherwise.
204
+ */
205
+ function deployAccess(workspace, stage, policyArn) {
206
+ if (policyArn) return {
207
+ kind: "managed",
208
+ policyArn,
209
+ reason: "--policy-arn"
210
+ };
211
+ const targets = deployTargets(workspace);
212
+ if (targets.length !== 1 || targets[0] !== "compose") return {
213
+ kind: "managed",
214
+ policyArn: DEFAULT_POLICY_ARN,
215
+ reason: `the ${targets.join(", ")} target${targets.length === 1 ? "" : "s"} deploy${targets.length === 1 ? "s" : ""} infrastructure; --policy-arn to narrow`
216
+ };
217
+ const project = workspace.name;
218
+ const store = workspace.secrets.store ?? "file";
219
+ const state = workspace.state;
220
+ if (typeof store === "object" && typeof store.provider === "object" || state !== void 0 && typeof state.provider === "object") return {
221
+ kind: "managed",
222
+ policyArn: DEFAULT_POLICY_ARN,
223
+ reason: `a custom ${typeof store === "object" && typeof store.provider === "object" ? "secrets store" : "state provider"} cannot be scoped; --policy-arn to narrow`
224
+ };
225
+ const describe = [];
226
+ const build = [];
227
+ if (typeof store === "object" && store.provider === "ssm") {
228
+ const { region } = store;
229
+ const name = secretsParameterName(project, stage);
230
+ describe.push(`read and update ${name} (SSM, ${region})`);
231
+ build.push((account) => [{
232
+ Sid: "StageSecrets",
233
+ Effect: "Allow",
234
+ Action: ["ssm:GetParameter", "ssm:PutParameter"],
235
+ Resource: `arn:aws:ssm:${region}:${account}:parameter${name}`
236
+ }]);
237
+ } else if (typeof store === "object" && store.provider === "secrets-manager") {
238
+ const { region, kmsKeyId } = store;
239
+ const name = secretsManagerSecretName(project, stage);
240
+ describe.push(`read and update ${name} (Secrets Manager, ${region})`);
241
+ build.push((account) => {
242
+ const statements = [{
243
+ Sid: "StageSecrets",
244
+ Effect: "Allow",
245
+ Action: [
246
+ "secretsmanager:GetSecretValue",
247
+ "secretsmanager:PutSecretValue",
248
+ "secretsmanager:CreateSecret"
249
+ ],
250
+ Resource: `arn:aws:secretsmanager:${region}:${account}:secret:${name}-??????`
251
+ }];
252
+ if (kmsKeyId) statements.push({
253
+ Sid: "StageSecretsKey",
254
+ Effect: "Allow",
255
+ Action: ["kms:Decrypt", "kms:GenerateDataKey"],
256
+ Resource: kmsKeyId.startsWith("arn:") ? kmsKeyId : "*",
257
+ Condition: { StringEquals: { "kms:ViaService": `secretsmanager.${region}.amazonaws.com` } }
258
+ });
259
+ return statements;
260
+ });
261
+ }
262
+ if (state?.provider === "ssm") {
263
+ const { region } = state;
264
+ const path = `/gkm/${project}/${stage}/`;
265
+ describe.push(`read and write the deploy state under ${path} (SSM, ${region})`);
266
+ build.push((account) => [{
267
+ Sid: "StageState",
268
+ Effect: "Allow",
269
+ Action: [
270
+ "ssm:GetParameter",
271
+ "ssm:PutParameter",
272
+ "ssm:DeleteParameter"
273
+ ],
274
+ Resource: `arn:aws:ssm:${region}:${account}:parameter${path}*`
275
+ }]);
276
+ } else if (state?.provider === "s3") {
277
+ const prefix = (state.prefix ?? "gkm").replace(/\/+$/, "");
278
+ const path = `${prefix ? `${prefix}/` : ""}${project}/${stage}/`;
279
+ describe.push(`read and write the deploy state under s3://${state.bucket}/${path}`);
280
+ build.push(() => [{
281
+ Sid: "StageState",
282
+ Effect: "Allow",
283
+ Action: [
284
+ "s3:GetObject",
285
+ "s3:PutObject",
286
+ "s3:DeleteObject"
287
+ ],
288
+ Resource: `arn:aws:s3:::${state.bucket}/${path}*`
289
+ }, {
290
+ Sid: "StageStateBucket",
291
+ Effect: "Allow",
292
+ Action: ["s3:ListBucket"],
293
+ Resource: `arn:aws:s3:::${state.bucket}`
294
+ }]);
295
+ }
296
+ return {
297
+ kind: "scoped",
298
+ statements: (account) => build.flatMap((statements) => statements(account)),
299
+ describe
300
+ };
301
+ }
302
+ /** The scoped access as a policy document, or null when it allows nothing. */
303
+ function scopedPolicyDocument(statements) {
304
+ if (statements.length === 0) return null;
305
+ return JSON.stringify({
306
+ Version: "2012-10-17",
307
+ Statement: statements
308
+ });
309
+ }
310
+ /** The account an IAM ARN is in: `arn:aws:iam::<account>:…`. */
311
+ function accountOf(arn) {
312
+ return arn.split(":")[4] ?? "";
313
+ }
314
+ //#endregion
315
+ //#region src/deploy/github.ts
316
+ /**
317
+ * `gkm deploy:github` — let GitHub Actions deploy one stage, with no keys.
318
+ *
319
+ * A stage usually lives in its own AWS account — staging in one, production in
320
+ * another — so this runs once per stage, with that account's profile:
321
+ *
322
+ * gkm deploy:github --stage staging --profile acme-dev
323
+ * gkm deploy:github --stage prod --profile acme-prod
324
+ *
325
+ * In the stage's account it makes sure GitHub's OIDC provider exists, and a
326
+ * role that only this repository's `<stage>` environment can assume. On
327
+ * GitHub it creates that environment and gives it what the generated deploy
328
+ * workflow reads: `AWS_ROLE_ARN`, and — when the stage's secrets are kept in
329
+ * the local file — its key as `GKM_SECRETS_KEY`. With `secrets.store` set to
330
+ * SSM or Secrets Manager the deploy job reads them with the role instead, so
331
+ * no key is handed to GitHub, and this checks the stage's store in the same
332
+ * account, with the same profile. Re-running it converges rather than duplicating.
333
+ *
334
+ * The role trusts the exact `sub` GitHub sends for the environment, read from
335
+ * the repository's OIDC subject settings (`githubOidc.ts`) — the default, the
336
+ * immutable subject with the owner's and repository's ids, or a custom
337
+ * template. A re-run rewrites an existing role's trust to it, so running this
338
+ * again repairs a role that trusted the wrong format. What the role may do is
339
+ * `githubPolicy.ts`: `AdministratorAccess` for SST, and for a compose stage
340
+ * only its own secrets (and deploy state, when that is in AWS).
341
+ */
342
+ const logger = console;
343
+ const GITHUB_OIDC_HOST = "token.actions.githubusercontent.com";
344
+ const AUDIENCE = "sts.amazonaws.com";
345
+ /**
346
+ * The tag recording which managed policy gkm attached to the role — so a
347
+ * re-run detaches only what gkm put there, never a policy someone else did.
348
+ */
349
+ const POLICY_TAG = "gkm:policy-arn";
350
+ const defaultGh = (args, input) => execFileSync("gh", args, {
351
+ encoding: "utf-8",
352
+ stdio: [
353
+ "pipe",
354
+ "pipe",
355
+ "pipe"
356
+ ],
357
+ ...input === void 0 ? {} : { input }
358
+ }).trim();
359
+ /** The role a stage deploys as: `<project>-github-<stage>`, within IAM's 64. */
360
+ function roleName(project, stage) {
361
+ return `${project}-github-${stage}`.toLowerCase().replace(/[^a-z0-9+=,.@_-]/g, "-").slice(0, 64);
362
+ }
363
+ /**
364
+ * Who may assume the role: a workflow job whose OIDC token carries exactly
365
+ * `subject` — the repository's `<stage>` environment, in the format the
366
+ * repository sends (see `githubOidc.ts`), and nothing else. Not another
367
+ * branch's job, not another environment's: that is what keeps a staging
368
+ * deploy from using the production role.
369
+ */
370
+ function trustPolicy(providerArn, subject) {
371
+ return JSON.stringify({
372
+ Version: "2012-10-17",
373
+ Statement: [{
374
+ Effect: "Allow",
375
+ Principal: { Federated: providerArn },
376
+ Action: "sts:AssumeRoleWithWebIdentity",
377
+ Condition: { StringEquals: {
378
+ [`${GITHUB_OIDC_HOST}:aud`]: AUDIENCE,
379
+ [`${GITHUB_OIDC_HOST}:sub`]: subject
380
+ } }
381
+ }]
382
+ });
383
+ }
384
+ /**
385
+ * The subject a trust policy trusts, or undefined when it names none. IAM
386
+ * hands documents back URL-encoded.
387
+ */
388
+ function trustedSubject(document) {
389
+ if (!document) return void 0;
390
+ let parsed;
391
+ try {
392
+ parsed = JSON.parse(safeDecode(document));
393
+ } catch {
394
+ return;
395
+ }
396
+ for (const statement of parsed.Statement ?? []) for (const operator of Object.values(statement.Condition ?? {})) {
397
+ const value = operator[`${GITHUB_OIDC_HOST}:sub`];
398
+ if (typeof value === "string") return value;
399
+ if (Array.isArray(value)) return value.join(", ");
400
+ }
401
+ }
402
+ function safeDecode(value) {
403
+ try {
404
+ return decodeURIComponent(value);
405
+ } catch {
406
+ return value;
407
+ }
408
+ }
409
+ /** GitHub's OIDC provider in this account, created if it is not there. */
410
+ async function ensureOidcProvider(iam) {
411
+ const { OpenIDConnectProviderList = [] } = await iam.send(new ListOpenIDConnectProvidersCommand({}));
412
+ const existing = OpenIDConnectProviderList.find((p) => p.Arn?.endsWith(`oidc-provider/${GITHUB_OIDC_HOST}`))?.Arn;
413
+ if (existing) return existing;
414
+ return (await iam.send(new CreateOpenIDConnectProviderCommand({
415
+ Url: `https://${GITHUB_OIDC_HOST}`,
416
+ ClientIDList: [AUDIENCE]
417
+ }))).OpenIDConnectProviderArn;
418
+ }
419
+ function isNoSuchEntity(error) {
420
+ return error.name === "NoSuchEntityException";
421
+ }
422
+ /**
423
+ * The deploy role, created or brought back to this trust and access.
424
+ *
425
+ * Converges: re-running rewrites the trust, and moves the role from one access
426
+ * to another — detaching a managed policy only when the role's
427
+ * {@link POLICY_TAG} says gkm attached it.
428
+ */
429
+ async function ensureRole(iam, name, trust, access) {
430
+ let arn;
431
+ let created = false;
432
+ let previousSubject;
433
+ let recorded;
434
+ try {
435
+ const { Role } = await iam.send(new GetRoleCommand({ RoleName: name }));
436
+ previousSubject = trustedSubject(Role.AssumeRolePolicyDocument);
437
+ await iam.send(new UpdateAssumeRolePolicyCommand({
438
+ RoleName: name,
439
+ PolicyDocument: trust
440
+ }));
441
+ arn = Role.Arn;
442
+ const { Tags = [] } = await iam.send(new ListRoleTagsCommand({ RoleName: name }));
443
+ recorded = Tags.find((tag) => tag.Key === POLICY_TAG)?.Value;
444
+ } catch (error) {
445
+ if (!isNoSuchEntity(error)) throw error;
446
+ const { Role } = await iam.send(new CreateRoleCommand({
447
+ RoleName: name,
448
+ AssumeRolePolicyDocument: trust,
449
+ Description: "Deploys from GitHub Actions (gkm deploy:github)",
450
+ MaxSessionDuration: 7200
451
+ }));
452
+ arn = Role.Arn;
453
+ created = true;
454
+ }
455
+ const detached = [];
456
+ const leftAttached = [];
457
+ const detach = async (policyArn) => {
458
+ try {
459
+ await iam.send(new DetachRolePolicyCommand({
460
+ RoleName: name,
461
+ PolicyArn: policyArn
462
+ }));
463
+ detached.push(policyArn);
464
+ } catch (error) {
465
+ if (!isNoSuchEntity(error)) throw error;
466
+ }
467
+ };
468
+ const deleteInline = async () => {
469
+ try {
470
+ await iam.send(new DeleteRolePolicyCommand({
471
+ RoleName: name,
472
+ PolicyName: SCOPED_POLICY_NAME
473
+ }));
474
+ } catch (error) {
475
+ if (!isNoSuchEntity(error)) throw error;
476
+ }
477
+ };
478
+ if ("managed" in access) {
479
+ await iam.send(new AttachRolePolicyCommand({
480
+ RoleName: name,
481
+ PolicyArn: access.managed
482
+ }));
483
+ if (recorded && recorded !== access.managed) await detach(recorded);
484
+ if (recorded !== access.managed) await iam.send(new TagRoleCommand({
485
+ RoleName: name,
486
+ Tags: [{
487
+ Key: POLICY_TAG,
488
+ Value: access.managed
489
+ }]
490
+ }));
491
+ if (!created) await deleteInline();
492
+ return {
493
+ arn,
494
+ created,
495
+ previousSubject,
496
+ detached,
497
+ leftAttached
498
+ };
499
+ }
500
+ if (access.inline) await iam.send(new PutRolePolicyCommand({
501
+ RoleName: name,
502
+ PolicyName: SCOPED_POLICY_NAME,
503
+ PolicyDocument: access.inline
504
+ }));
505
+ else if (!created) await deleteInline();
506
+ if (created) return {
507
+ arn,
508
+ created,
509
+ previousSubject,
510
+ detached,
511
+ leftAttached
512
+ };
513
+ if (recorded) {
514
+ await detach(recorded);
515
+ await iam.send(new UntagRoleCommand({
516
+ RoleName: name,
517
+ TagKeys: [POLICY_TAG]
518
+ }));
519
+ }
520
+ const { AttachedPolicies = [] } = await iam.send(new ListAttachedRolePoliciesCommand({ RoleName: name }));
521
+ for (const policy of AttachedPolicies) if (policy.PolicyArn && !detached.includes(policy.PolicyArn)) leftAttached.push(policy.PolicyArn);
522
+ return {
523
+ arn,
524
+ created,
525
+ previousSubject,
526
+ detached,
527
+ leftAttached
528
+ };
529
+ }
530
+ /**
531
+ * An IAM client for the stage's account.
532
+ *
533
+ * `fromIni` for a named profile, deliberately: it resolves exactly that
534
+ * profile — keys, assume-role, `credential_process` or SSO, whichever
535
+ * ~/.aws/config says — and never falls back to AWS_* in the environment. The
536
+ * provider chain checks the environment first, so exported staging keys would
537
+ * quietly win over `--profile prod` and the production role would land in the
538
+ * staging account.
539
+ */
540
+ async function iamFor(profile, endpoint) {
541
+ const config = { region: "us-east-1" };
542
+ if (profile) {
543
+ const { fromIni } = await import("@aws-sdk/credential-providers");
544
+ config.credentials = fromIni({ profile });
545
+ }
546
+ if (endpoint) {
547
+ config.endpoint = endpoint;
548
+ config.credentials = {
549
+ accessKeyId: "test",
550
+ secretAccessKey: "test"
551
+ };
552
+ }
553
+ return new IAMClient(config);
554
+ }
555
+ async function deployGithubCommand(options, deps = {}) {
556
+ const gh = deps.gh ?? defaultGh;
557
+ const { workspace } = await loadWorkspaceConfig(deps.cwd);
558
+ assertDeployedStage(workspace.stages, options.stage);
559
+ const repo = options.repo ?? gh([
560
+ "repo",
561
+ "view",
562
+ "--json",
563
+ "nameWithOwner",
564
+ "-q",
565
+ ".nameWithOwner"
566
+ ]);
567
+ const role = roleName(workspace.name, options.stage);
568
+ const oidc = resolveOidcSubject(gh, repo, options.stage);
569
+ const access = deployAccess(workspace, options.stage, options.policyArn);
570
+ const key = await readKey(options.stage, keystoreProject(workspace));
571
+ const keyPath = getKeyPath(options.stage, keystoreProject(workspace));
572
+ const hasKey = key !== null;
573
+ const remote = isRemoteStore(workspace, options.stage);
574
+ const store = await secretsStoreFor(workspace, options.stage, { ...options.profile ? { profile: options.profile } : {} });
575
+ const hasSecrets = remote && await store.read(options.stage) !== null;
576
+ const via = options.profile ? `profile "${options.profile}"` : "the default credentials";
577
+ logger.log(`\n🔐 GitHub → AWS for stage "${options.stage}"\n`);
578
+ logger.log(` Repository: ${repo}`);
579
+ logger.log(` AWS profile: ${options.profile ?? "(default credentials)"}`);
580
+ logger.log(` Role: ${role}`);
581
+ if (access.kind === "managed") logger.log(` Policy: ${access.policyArn} (${access.reason})`);
582
+ else if (access.describe.length > 0) {
583
+ logger.log(` Policy: inline "${SCOPED_POLICY_NAME}", scoped to the compose deploy — only:`);
584
+ for (const line of access.describe) logger.log(` ${line}`);
585
+ } else logger.log(" Policy: none — a compose deploy with file secrets and local state reads nothing from AWS");
586
+ logger.log(` Trusted by: ${oidc.subject} only (${SUBJECT_KINDS[oidc.kind]})`);
587
+ if (oidc.warning) logger.log(` ⚠ ${oidc.warning}`);
588
+ if (remote) logger.log(` Secrets: ${hasSecrets ? `in the store (${store.name}), read with ${via}` : `none in the store yet — gkm secrets:set <KEY> '…' --stage ${options.stage}`}`);
589
+ else logger.log(` Secrets key: ${hasKey ? keyPath : `none at ${keyPath} — run gkm secrets:init --stage ${options.stage} first`}`);
590
+ if (options.dryRun) {
591
+ if (access.kind === "scoped") {
592
+ const document = scopedPolicyDocument(access.statements("<account>"));
593
+ if (document) logger.log(`\n Inline policy:\n${JSON.stringify(JSON.parse(document), null, 2).replace(/^/gm, " ")}`);
594
+ }
595
+ logger.log("\n --dry-run: nothing changed.\n");
596
+ return {
597
+ repo,
598
+ subject: oidc.subject
599
+ };
600
+ }
601
+ const iam = deps.iam ?? await iamFor(options.profile);
602
+ const providerArn = await ensureOidcProvider(iam).catch((error) => {
603
+ if (/SSO session/i.test(String(error?.message)) && options.profile) throw new SsoSessionExpired(options.profile);
604
+ throw error;
605
+ });
606
+ const outcome = await ensureRole(iam, role, trustPolicy(providerArn, oidc.subject), access.kind === "managed" ? { managed: access.policyArn } : { inline: scopedPolicyDocument(access.statements(accountOf(providerArn))) });
607
+ const roleArn = outcome.arn;
608
+ logger.log(`\n ✓ AWS: ${roleArn}${outcome.created ? " (created)" : ""}`);
609
+ if (!outcome.created) logger.log(outcome.previousSubject === oidc.subject ? ` ✓ Trust: unchanged, ${oidc.subject}` : ` ✓ Trust: ${outcome.previousSubject ?? "(no subject)"} → ${oidc.subject}`);
610
+ for (const detached of outcome.detached) logger.log(` ✓ Detached ${detached} (attached by gkm, no longer needed)`);
611
+ if (access.kind === "scoped") for (const attached of outcome.leftAttached) logger.log(` ⚠ ${attached} is still attached to ${role}. gkm has no record of attaching it, so it is left; the compose deploy does not need it. Detach it with: aws iam detach-role-policy --role-name ${role} --policy-arn ${attached}${options.profile ? ` --profile ${options.profile}` : ""}`);
612
+ gh([
613
+ "api",
614
+ "--method",
615
+ "PUT",
616
+ `repos/${repo}/environments/${options.stage}`
617
+ ]);
618
+ gh([
619
+ "variable",
620
+ "set",
621
+ "AWS_ROLE_ARN",
622
+ "--env",
623
+ options.stage,
624
+ "--repo",
625
+ repo,
626
+ "--body",
627
+ roleArn
628
+ ]);
629
+ logger.log(` ✓ GitHub: environment "${options.stage}", AWS_ROLE_ARN`);
630
+ if (remote) {
631
+ if (!hasSecrets) logger.log(` ⚠ The "${options.stage}" store holds no secrets yet; set them with gkm secrets:set <KEY> '…' --stage ${options.stage}${options.profile ? ` (AWS_PROFILE=${options.profile})` : ""}.`);
632
+ } else if (hasKey) {
633
+ gh([
634
+ "secret",
635
+ "set",
636
+ "GKM_SECRETS_KEY",
637
+ "--env",
638
+ options.stage,
639
+ "--repo",
640
+ repo
641
+ ], key);
642
+ logger.log(" ✓ GitHub: GKM_SECRETS_KEY");
643
+ } else logger.log(` ⚠ No secrets key for "${options.stage}"; run gkm secrets:init --stage ${options.stage}, then this again.`);
644
+ logger.log(`\n Protect it: add required reviewers to the "${options.stage}" environment in the repository settings.\n`);
645
+ return {
646
+ roleArn,
647
+ repo,
648
+ subject: oidc.subject
649
+ };
650
+ }
651
+ const SUBJECT_KINDS = {
652
+ default: "the repository's default subject",
653
+ immutable: "the repository's immutable subject",
654
+ custom: "the repository's custom subject template",
655
+ assumed: "assumed: the default format"
656
+ };
657
+ /** The profile's SSO login has lapsed; the fix is one command away. */
658
+ var SsoSessionExpired = class extends Error {
659
+ profile;
660
+ constructor(profile) {
661
+ super(`The SSO session for profile "${profile}" has expired or is invalid. Run: aws sso login --profile ${profile}`);
662
+ this.profile = profile;
663
+ this.name = "SsoSessionExpired";
664
+ }
665
+ };
666
+ //#endregion
667
+ export { deployGithubCommand };
668
+
669
+ //# sourceMappingURL=github-CVdbVlT5.mjs.map