rastack 0.0.62 → 0.0.64

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 (44) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/admin/AdminApp.tsx +56 -3
  3. package/admin/index.tsx +8 -2
  4. package/admin/ui/icons.tsx +9 -0
  5. package/admin/ui/styles.ts +3 -0
  6. package/admin/useAdmin.ts +41 -4
  7. package/auth/provider.ts +71 -8
  8. package/dist/admin.js +17 -14
  9. package/dist/compile/analyze.js +11 -1
  10. package/dist/define/index.d.ts +8 -1
  11. package/dist/define/index.js +2 -2
  12. package/dist/define/manifest.js +7 -1
  13. package/dist/deploy/ci.d.ts +10 -3
  14. package/dist/deploy/ci.js +22 -11
  15. package/dist/deploy/config.d.ts +155 -5
  16. package/dist/deploy/config.js +216 -6
  17. package/dist/deploy/io.d.ts +57 -0
  18. package/dist/deploy/io.js +141 -0
  19. package/dist/deploy/workflows.d.ts +30 -0
  20. package/dist/deploy/workflows.js +117 -12
  21. package/dist/rastack-bootstrap.js +18 -47
  22. package/dist/rastack-ci.js +19 -1
  23. package/dist/rastack-dev.js +48 -2
  24. package/dist/rastack-init.d.ts +6 -3
  25. package/dist/rastack-init.js +231 -92
  26. package/dist/rastack.js +1 -0
  27. package/dist/templates/cloudformation/app.yaml +133 -2
  28. package/dist/wasm/rastack_wasm.js +1 -1
  29. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  30. package/package.json +1 -1
  31. package/provider/provider.tsx +23 -0
  32. package/provider/types.ts +9 -0
  33. package/src/compile/analyze.ts +11 -1
  34. package/src/define/index.ts +5 -1
  35. package/src/define/manifest.ts +7 -1
  36. package/src/deploy/ci.ts +50 -16
  37. package/src/deploy/config.ts +340 -7
  38. package/src/deploy/io.ts +179 -2
  39. package/src/deploy/workflows.ts +122 -12
  40. package/src/rastack-bootstrap.ts +24 -69
  41. package/src/rastack-ci.ts +20 -2
  42. package/src/rastack-dev.ts +61 -6
  43. package/src/rastack-init.ts +283 -115
  44. package/src/rastack.ts +1 -0
@@ -18,7 +18,11 @@
18
18
  * thin IO wrappers that read/write the file and shell out.
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.APP_STACK_OUTPUTS = exports.BOOTSTRAP_TEMPLATES_OUTPUT = exports.BOOTSTRAP_ARTIFACTS_OUTPUT = exports.BOOTSTRAP_TEMPLATE_FILE = exports.APP_TEMPLATE_FILE = exports.REGIONS_WITHOUT_COGNITO_ASF = exports.DEFAULTS = exports.DEFAULT_LAMBDA = void 0;
21
+ exports.APP_STACK_OUTPUTS = exports.BOOTSTRAP_TEMPLATES_OUTPUT = exports.BOOTSTRAP_ARTIFACTS_OUTPUT = exports.BOOTSTRAP_TEMPLATE_FILE = exports.APP_TEMPLATE_FILE = exports.REGIONS_WITHOUT_COGNITO_ASF = exports.DEFAULTS = exports.DEFAULT_LAMBDA = exports.PROD_GH_ENVIRONMENT = exports.DEV_DEFAULTS = exports.ENV_NAMES = void 0;
22
+ exports.envAppName = envAppName;
23
+ exports.resolveEnvironment = resolveEnvironment;
24
+ exports.resolveEnvironments = resolveEnvironments;
25
+ exports.cognitoRuntimeConfig = cognitoRuntimeConfig;
22
26
  exports.normalizeConfig = normalizeConfig;
23
27
  exports.validateConfig = validateConfig;
24
28
  exports.validateBootstrapConfig = validateBootstrapConfig;
@@ -26,15 +30,83 @@ exports.mergeConfig = mergeConfig;
26
30
  exports.parseGitHubRemote = parseGitHubRemote;
27
31
  exports.cognitoAdvancedSecuritySupported = cognitoAdvancedSecuritySupported;
28
32
  exports.stackParameters = stackParameters;
33
+ exports.stackParametersForEnv = stackParametersForEnv;
29
34
  exports.deployStackArgs = deployStackArgs;
30
35
  exports.deployStackCommand = deployStackCommand;
36
+ exports.deployStackArgsForEnv = deployStackArgsForEnv;
37
+ exports.deployStackCommandForEnv = deployStackCommandForEnv;
31
38
  exports.bootstrapStackName = bootstrapStackName;
32
39
  exports.bootstrapStackParameters = bootstrapStackParameters;
33
40
  exports.bootstrapStackArgs = bootstrapStackArgs;
34
41
  exports.bootstrapStackCommand = bootstrapStackCommand;
35
42
  exports.repoVariables = repoVariables;
43
+ exports.repoVariablesForEnv = repoVariablesForEnv;
36
44
  exports.ghVariableSetArgs = ghVariableSetArgs;
45
+ exports.ghEnvironmentCreateArgs = ghEnvironmentCreateArgs;
37
46
  exports.lambdaEnv = lambdaEnv;
47
+ exports.ENV_NAMES = ["prod", "dev"];
48
+ /** Defaults for the dev environment when `environments.dev` is unset. */
49
+ exports.DEV_DEFAULTS = {
50
+ branch: "development",
51
+ ghEnvironment: "development",
52
+ namespace: "development",
53
+ };
54
+ /** The GitHub Actions Environment name the prod deploy uses. */
55
+ exports.PROD_GH_ENVIRONMENT = "production";
56
+ /**
57
+ * The app.yaml `AppName` for an environment — the infra namespace. Dev glues a
58
+ * `dev` suffix on so its stack's buckets/roles/pools/SSM are physically distinct
59
+ * from prod's without a hyphen (Cognito `AppName`/`IdentityPoolName` forbid one).
60
+ */
61
+ function envAppName(cfg, env) {
62
+ return env === "dev" ? `${cfg.app}dev` : cfg.app;
63
+ }
64
+ /**
65
+ * Resolve one environment to its full definition: the prod environment inherits
66
+ * the top-level branch/stack/namespace/outputs, the dev environment defaults to
67
+ * the `development` branch and a `-dev` stack, and anything under
68
+ * `cfg.environments[env]` overrides those.
69
+ */
70
+ function resolveEnvironment(cfg, env) {
71
+ const o = cfg.environments?.[env] ?? {};
72
+ if (env === "prod") {
73
+ return {
74
+ name: "prod",
75
+ branch: o.branch ?? cfg.github.branch,
76
+ ghEnvironment: o.ghEnvironment ?? exports.PROD_GH_ENVIRONMENT,
77
+ appName: cfg.app,
78
+ stackName: o.stackName ?? cfg.stackName,
79
+ namespace: o.namespace ?? cfg.namespace,
80
+ proxyUpstream: o.proxyUpstream ?? "",
81
+ outputs: o.outputs ?? cfg.outputs,
82
+ };
83
+ }
84
+ return {
85
+ name: "dev",
86
+ branch: o.branch ?? exports.DEV_DEFAULTS.branch,
87
+ ghEnvironment: o.ghEnvironment ?? exports.DEV_DEFAULTS.ghEnvironment,
88
+ appName: envAppName(cfg, "dev"),
89
+ stackName: o.stackName ?? (cfg.stackName ? `${cfg.stackName}-dev` : ""),
90
+ namespace: o.namespace ?? exports.DEV_DEFAULTS.namespace,
91
+ proxyUpstream: o.proxyUpstream ?? "",
92
+ outputs: o.outputs,
93
+ };
94
+ }
95
+ /** Both environments, prod first, fully resolved. */
96
+ function resolveEnvironments(cfg) {
97
+ return exports.ENV_NAMES.map((e) => resolveEnvironment(cfg, e));
98
+ }
99
+ function cognitoRuntimeConfig(cfg, env) {
100
+ const o = resolveEnvironment(cfg, env).outputs;
101
+ if (!o?.userPoolClientId || !o?.hostedUiUrl)
102
+ return null;
103
+ return {
104
+ userPoolId: o.userPoolId,
105
+ clientId: o.userPoolClientId,
106
+ hostedUiUrl: o.hostedUiUrl,
107
+ region: cfg.region,
108
+ };
109
+ }
38
110
  exports.DEFAULT_LAMBDA = {
39
111
  bin: "bootstrap",
40
112
  arch: "arm64",
@@ -82,8 +154,36 @@ function normalizeConfig(partial = {}) {
82
154
  artifactsBucket: partial.artifactsBucket?.trim() || undefined,
83
155
  tokenUse: partial.tokenUse ?? exports.DEFAULTS.tokenUse,
84
156
  outputs: partial.outputs ? { ...partial.outputs } : undefined,
157
+ environments: normalizeEnvironments(partial.environments),
85
158
  };
86
159
  }
160
+ /** Copy through per-environment overrides, dropping an empty map to undefined. */
161
+ function normalizeEnvironments(envs) {
162
+ if (!envs)
163
+ return undefined;
164
+ const out = {};
165
+ for (const name of exports.ENV_NAMES) {
166
+ const o = envs[name];
167
+ if (!o)
168
+ continue;
169
+ const entry = {};
170
+ if (o.branch?.trim())
171
+ entry.branch = o.branch.trim();
172
+ if (o.ghEnvironment?.trim())
173
+ entry.ghEnvironment = o.ghEnvironment.trim();
174
+ if (o.stackName?.trim())
175
+ entry.stackName = o.stackName.trim();
176
+ if (o.namespace?.trim())
177
+ entry.namespace = o.namespace.trim();
178
+ if (o.proxyUpstream?.trim())
179
+ entry.proxyUpstream = o.proxyUpstream.trim();
180
+ if (o.outputs)
181
+ entry.outputs = { ...o.outputs };
182
+ if (Object.keys(entry).length)
183
+ out[name] = entry;
184
+ }
185
+ return Object.keys(out).length ? out : undefined;
186
+ }
87
187
  const SLUG = /^[a-z0-9]{1,20}$/;
88
188
  const REGION = /^[a-z]{2}-[a-z]+-\d+$/;
89
189
  const GH_NAME = /^[A-Za-z0-9_.-]+$/;
@@ -99,6 +199,12 @@ function validateConfig(cfg) {
99
199
  errors.push("app is required (a short slug, e.g. `airline`).");
100
200
  else if (!SLUG.test(cfg.app))
101
201
  errors.push(`app "${cfg.app}" must match ${SLUG} (lowercase letters/digits, 1–20).`);
202
+ // The dev environment appends "dev" to the app slug for its infra namespace
203
+ // (buckets/roles/pools/SSM). That derived name must still be a valid slug, so
204
+ // the app slug is effectively capped at 17 chars once a dev env is in play.
205
+ else if (!SLUG.test(envAppName(cfg, "dev")))
206
+ errors.push(`app "${cfg.app}" is too long: the dev environment uses "${envAppName(cfg, "dev")}" ` +
207
+ `for its infra names, which must still match ${SLUG} (so app ≤ 17 chars).`);
102
208
  if (!SLUG.test(cfg.namePrefix))
103
209
  errors.push(`namePrefix "${cfg.namePrefix}" must match ${SLUG}.`);
104
210
  if (!cfg.region)
@@ -158,8 +264,27 @@ function mergeConfig(base, patch) {
158
264
  github: { ...base.github, ...patch.github },
159
265
  lambda: { ...base.lambda, ...patch.lambda },
160
266
  outputs: { ...base.outputs, ...patch.outputs },
267
+ environments: mergeEnvironments(base.environments, patch.environments),
161
268
  });
162
269
  }
270
+ /** Deep-merge per-environment overrides (per env, and per env's outputs). */
271
+ function mergeEnvironments(base, patch) {
272
+ if (!base && !patch)
273
+ return undefined;
274
+ const out = {};
275
+ for (const name of exports.ENV_NAMES) {
276
+ const b = base?.[name];
277
+ const p = patch?.[name];
278
+ if (!b && !p)
279
+ continue;
280
+ out[name] = {
281
+ ...b,
282
+ ...p,
283
+ outputs: b?.outputs || p?.outputs ? { ...b?.outputs, ...p?.outputs } : undefined,
284
+ };
285
+ }
286
+ return Object.keys(out).length ? out : undefined;
287
+ }
163
288
  /**
164
289
  * Recover `{ owner, repo }` from a GitHub remote URL — both the SSH
165
290
  * (`git@github.com:owner/repo.git`) and HTTPS
@@ -232,6 +357,36 @@ function stackParameters(cfg) {
232
357
  params.push({ key: "AdvancedSecurity", value: "OFF" });
233
358
  return params;
234
359
  }
360
+ /**
361
+ * The app.yaml `--parameter-overrides` for a **specific environment** (dev or
362
+ * prod). Same as `stackParameters` but the AppName/branch/namespace come from the
363
+ * resolved environment, and it also passes `GitHubEnvironment` so the stack's
364
+ * deploy-role trust is scoped to the GitHub Actions Environment (the OIDC subject
365
+ * for an environment-gated job is `…:environment:<env>`, not `…:ref:…`). Prod's
366
+ * AppName is the bare slug, so prod's resource names are byte-for-byte what the
367
+ * pre-environment template produced.
368
+ */
369
+ function stackParametersForEnv(cfg, env) {
370
+ const e = resolveEnvironment(cfg, env);
371
+ const params = [
372
+ { key: "AppName", value: e.appName },
373
+ { key: "NamePrefix", value: cfg.namePrefix },
374
+ { key: "GitHubOwner", value: cfg.github.owner },
375
+ { key: "GitHubRepo", value: cfg.github.repo },
376
+ { key: "GitHubBranch", value: e.branch },
377
+ { key: "GitHubEnvironment", value: e.ghEnvironment },
378
+ { key: "IcebergNamespace", value: e.namespace },
379
+ ];
380
+ if (cfg.artifactsBucket)
381
+ params.push({ key: "ArtifactsBucket", value: cfg.artifactsBucket });
382
+ // The external-API proxy is provisioned only when an upstream is configured
383
+ // (typically dev → a UAT/prod backend). Empty leaves the route/authorizer out.
384
+ if (e.proxyUpstream)
385
+ params.push({ key: "ExternalApiUpstream", value: e.proxyUpstream });
386
+ if (!cognitoAdvancedSecuritySupported(cfg.region))
387
+ params.push({ key: "AdvancedSecurity", value: "OFF" });
388
+ return params;
389
+ }
235
390
  /**
236
391
  * The canonical repo-relative CloudFormation template paths. These are the paths
237
392
  * in the *framework* repo (theserverkid/reactapistack) and the default the argv
@@ -296,6 +451,14 @@ function deployStackArgs(cfg, templateFile = exports.APP_TEMPLATE_FILE) {
296
451
  function deployStackCommand(cfg, templateFile = exports.APP_TEMPLATE_FILE) {
297
452
  return awsCommand(deployStackArgs(cfg, templateFile));
298
453
  }
454
+ /** The `aws cloudformation deploy` argv for one environment's stack (dev/prod). */
455
+ function deployStackArgsForEnv(cfg, env, templateFile = exports.APP_TEMPLATE_FILE) {
456
+ return cfnDeployArgs(templateFile, resolveEnvironment(cfg, env).stackName, cfg.region, stackParametersForEnv(cfg, env));
457
+ }
458
+ /** The single-line `aws cloudformation deploy` command for one environment. */
459
+ function deployStackCommandForEnv(cfg, env, templateFile = exports.APP_TEMPLATE_FILE) {
460
+ return awsCommand(deployStackArgsForEnv(cfg, env, templateFile));
461
+ }
299
462
  /**
300
463
  * The tier-0 bootstrap stack (bootstrap.yaml) — the S3 buckets that hold the
301
464
  * Lambda placeholder + hosted templates, and the GitHub OIDC provider. One per
@@ -346,12 +509,16 @@ exports.APP_STACK_OUTPUTS = [
346
509
  { key: "UserPoolClientId", field: "userPoolClientId" },
347
510
  { key: "JwksUrl", field: "jwksUrl" },
348
511
  { key: "WarehouseBucketName", field: "warehouseBucket" },
512
+ { key: "UserPoolDomain", field: "userPoolDomain" },
513
+ { key: "HostedUiUrl", field: "hostedUiUrl" },
514
+ { key: "ProxyUrl", field: "proxyUrl" },
349
515
  ];
350
516
  /**
351
517
  * The GitHub repo **Variables** CI reads — `AWS_DEPLOY_ROLE_ARN` and
352
518
  * `AWS_REGION` (see docs/serverless-deploy.md §3). Values come from the stack
353
519
  * Outputs when already resolved, else an empty string to fill in from the
354
- * Console.
520
+ * Console. This is the legacy single-environment view (the prod stack); the
521
+ * dev/prod split wires them per GitHub Environment via `repoVariablesForEnv`.
355
522
  */
356
523
  function repoVariables(cfg) {
357
524
  return [
@@ -363,6 +530,29 @@ function repoVariables(cfg) {
363
530
  { name: "AWS_REGION", value: cfg.region, from: "Region stack output" },
364
531
  ];
365
532
  }
533
+ /**
534
+ * The same two Variables, scoped to one environment's GitHub Actions
535
+ * Environment. The dev deploy reads `development`'s copies, the prod deploy
536
+ * `production`'s — so the two never cross-assume each other's role. Values come
537
+ * from the environment's own stack Outputs.
538
+ */
539
+ function repoVariablesForEnv(cfg, env) {
540
+ const e = resolveEnvironment(cfg, env);
541
+ return [
542
+ {
543
+ name: "AWS_DEPLOY_ROLE_ARN",
544
+ value: e.outputs?.deployRoleArn ?? "",
545
+ from: "DeployRoleArn stack output",
546
+ ghEnvironment: e.ghEnvironment,
547
+ },
548
+ {
549
+ name: "AWS_REGION",
550
+ value: cfg.region,
551
+ from: "Region stack output",
552
+ ghEnvironment: e.ghEnvironment,
553
+ },
554
+ ];
555
+ }
366
556
  /**
367
557
  * The argv (after `gh`) that sets one GitHub repo **Variable** on the config's
368
558
  * repo. `init` runs this for each resolved `repoVariables` entry so the
@@ -372,15 +562,35 @@ function repoVariables(cfg) {
372
562
  * user's GitHub) rather than an API token; a pure argv so it is inspectable and
373
563
  * shell-free (`execFileSync`).
374
564
  */
375
- function ghVariableSetArgs(cfg, name, value) {
376
- return [
565
+ function ghVariableSetArgs(cfg, name, value, ghEnvironment) {
566
+ const args = [
377
567
  "variable",
378
568
  "set",
379
569
  name,
380
570
  "--repo",
381
571
  `${cfg.github.owner}/${cfg.github.repo}`,
382
- "--body",
383
- value,
572
+ ];
573
+ // Scope the Variable to a GitHub Actions Environment when one is given, so the
574
+ // dev and prod deploys each read their own role/region.
575
+ if (ghEnvironment)
576
+ args.push("--env", ghEnvironment);
577
+ args.push("--body", value);
578
+ return args;
579
+ }
580
+ /**
581
+ * The argv (after `gh`) that creates (idempotently) a GitHub Actions
582
+ * **Environment** on the config's repo — `gh api --method PUT
583
+ * repos/{owner}/{repo}/environments/{env}`. `init` runs this for `development`
584
+ * and `production` before wiring their scoped Variables, since a Variable can
585
+ * only be set on an environment that already exists. PUT is idempotent, so
586
+ * re-running `init` is safe.
587
+ */
588
+ function ghEnvironmentCreateArgs(cfg, ghEnvironment) {
589
+ return [
590
+ "api",
591
+ "--method",
592
+ "PUT",
593
+ `repos/${cfg.github.owner}/${cfg.github.repo}/environments/${ghEnvironment}`,
384
594
  ];
385
595
  }
386
596
  /**
@@ -22,6 +22,27 @@ export declare function configPath(cwd?: string): string;
22
22
  * repo itself still works. Returns null when no candidate exists.
23
23
  */
24
24
  export declare function resolveDeployAsset(relPath: string, cwd?: string): string | null;
25
+ /**
26
+ * Base URL the framework's CloudFormation templates are published at. The same
27
+ * site that hosts the installer (`reactapistack.com/setup.sh`) serves the base
28
+ * templates, so a CLI whose bundled copy is missing (a partial install, or a
29
+ * globally-linked dev build) can still fetch them.
30
+ */
31
+ export declare const TEMPLATE_BASE_URL = "https://reactapistack.com/cloudformation";
32
+ /**
33
+ * Download a published CloudFormation template to a temp file, returning its
34
+ * path — or null on any failure (offline, 404, write error). Used as the
35
+ * fallback when the template is not bundled with the install.
36
+ */
37
+ export declare function downloadTemplate(name: string, base?: string): Promise<string | null>;
38
+ /**
39
+ * Resolve a CloudFormation template to a local file path for `aws cloudformation
40
+ * deploy` (which accepts only `--template-file`). Prefers the copy bundled with
41
+ * the `rastack` install; when that is missing, downloads it from the React API
42
+ * Stack website. Returns null only when the template is neither bundled nor
43
+ * fetchable.
44
+ */
45
+ export declare function resolveTemplateFile(name: string, cwd?: string): Promise<string | null>;
25
46
  /** Read + normalize the deploy config, or null when it does not exist yet. */
26
47
  export declare function loadConfig(cwd?: string): DeployConfig | null;
27
48
  /** Write the deploy config, creating `.rastack/` if needed. */
@@ -39,6 +60,14 @@ export declare function awsCapture(args: string[], cwd?: string): string | null;
39
60
  export declare function awsRun(args: string[], cwd?: string): void;
40
61
  /** True when the AWS CLI is on PATH. */
41
62
  export declare function awsAvailable(): boolean;
63
+ /**
64
+ * The AWS account id the CLI's active credentials resolve to, or null when the
65
+ * CLI has no usable credentials (not logged in, an expired SSO session, or no
66
+ * configured profile). `init`/`bootstrap` call this to fail fast with an
67
+ * actionable "log in" message before attempting a stack deploy, instead of
68
+ * letting the first `cloudformation deploy` fail with a cryptic AWS error.
69
+ */
70
+ export declare function awsIdentity(region?: string): string | null;
42
71
  /**
43
72
  * A single Output value of a CloudFormation stack, or null when the stack does
44
73
  * not exist or the key is unset. `describe-stacks` prints `None` for a missing
@@ -52,6 +81,27 @@ export declare function stackOutput(stackName: string, key: string, region: stri
52
81
  * on every `init`. Returns false when the placeholder file is missing.
53
82
  */
54
83
  export declare function uploadPlaceholder(artifactsBucket: string, region: string, cwd?: string): boolean;
84
+ /**
85
+ * Deploy the tier-0 bootstrap stack, retrying once against an account that
86
+ * already has a GitHub OIDC provider — AWS allows only one provider per URL, so
87
+ * a second create is rejected and the retry tells the template to reuse the
88
+ * existing one. Throws on failure (after the retry). Shared by `rastack init`
89
+ * and `rastack bootstrap` so both stand the stack up identically.
90
+ */
91
+ export declare function deployBootstrap(cfg: DeployConfig, reuseOidc: boolean, template: string): void;
92
+ /**
93
+ * Ensure the tier-0 bootstrap stack exists for this account+region, **creating
94
+ * it when missing**, and return its artifacts/templates buckets. This is why
95
+ * `rastack init` is a single command that stands up *everything* — the
96
+ * account-level bootstrap stack and the per-app backend — with no separate
97
+ * `rastack bootstrap` step. Idempotent: an existing stack is reconciled in
98
+ * place. Throws with an actionable message when the template cannot be resolved,
99
+ * the deploy fails, or the buckets can't be read back.
100
+ */
101
+ export declare function ensureBootstrapStack(cfg: DeployConfig, reuseOidc?: boolean): Promise<{
102
+ artifactsBucket: string;
103
+ templatesBucket: string | null;
104
+ }>;
55
105
  /** True when the GitHub CLI is on PATH. */
56
106
  export declare function ghAvailable(): boolean;
57
107
  /**
@@ -75,6 +125,13 @@ export declare function ghAuthLogin(cwd?: string): boolean;
75
125
  * into a command line.
76
126
  */
77
127
  export declare function setRepoVariable(args: string[], cwd?: string): boolean;
128
+ /**
129
+ * Create (idempotently, via `gh api --method PUT`) a GitHub Actions Environment
130
+ * so its scoped Variables can be set. Returns true on success. Best-effort — a
131
+ * missing/unauthenticated `gh` or an API error degrades to the printed
132
+ * instructions, exactly like `setRepoVariable`.
133
+ */
134
+ export declare function ghEnvironmentCreate(args: string[], cwd?: string): boolean;
78
135
  /**
79
136
  * Ask a single question with a default, echoed as `label [default]: `. Returns
80
137
  * the trimmed answer, or the default on an empty line.
package/dist/deploy/io.js CHANGED
@@ -38,8 +38,11 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  };
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.TEMPLATE_BASE_URL = void 0;
41
42
  exports.configPath = configPath;
42
43
  exports.resolveDeployAsset = resolveDeployAsset;
44
+ exports.downloadTemplate = downloadTemplate;
45
+ exports.resolveTemplateFile = resolveTemplateFile;
43
46
  exports.loadConfig = loadConfig;
44
47
  exports.saveConfig = saveConfig;
45
48
  exports.gitRemoteUrl = gitRemoteUrl;
@@ -47,16 +50,21 @@ exports.canPrompt = canPrompt;
47
50
  exports.awsCapture = awsCapture;
48
51
  exports.awsRun = awsRun;
49
52
  exports.awsAvailable = awsAvailable;
53
+ exports.awsIdentity = awsIdentity;
50
54
  exports.stackOutput = stackOutput;
51
55
  exports.uploadPlaceholder = uploadPlaceholder;
56
+ exports.deployBootstrap = deployBootstrap;
57
+ exports.ensureBootstrapStack = ensureBootstrapStack;
52
58
  exports.ghAvailable = ghAvailable;
53
59
  exports.ghAuthenticated = ghAuthenticated;
54
60
  exports.ghAuthLogin = ghAuthLogin;
55
61
  exports.setRepoVariable = setRepoVariable;
62
+ exports.ghEnvironmentCreate = ghEnvironmentCreate;
56
63
  exports.ask = ask;
57
64
  exports.withPrompt = withPrompt;
58
65
  const child_process_1 = require("child_process");
59
66
  const fs = __importStar(require("fs"));
67
+ const https = __importStar(require("https"));
60
68
  const os = __importStar(require("os"));
61
69
  const path = __importStar(require("path"));
62
70
  const readline = __importStar(require("readline"));
@@ -96,6 +104,54 @@ function resolveDeployAsset(relPath, cwd = process.cwd()) {
96
104
  }
97
105
  return null;
98
106
  }
107
+ /**
108
+ * Base URL the framework's CloudFormation templates are published at. The same
109
+ * site that hosts the installer (`reactapistack.com/setup.sh`) serves the base
110
+ * templates, so a CLI whose bundled copy is missing (a partial install, or a
111
+ * globally-linked dev build) can still fetch them.
112
+ */
113
+ exports.TEMPLATE_BASE_URL = "https://reactapistack.com/cloudformation";
114
+ /**
115
+ * Download a published CloudFormation template to a temp file, returning its
116
+ * path — or null on any failure (offline, 404, write error). Used as the
117
+ * fallback when the template is not bundled with the install.
118
+ */
119
+ function downloadTemplate(name, base = exports.TEMPLATE_BASE_URL) {
120
+ const url = `${base}/${name}`;
121
+ return new Promise((resolve) => {
122
+ https
123
+ .get(url, (res) => {
124
+ if (res.statusCode !== 200) {
125
+ res.resume();
126
+ resolve(null);
127
+ return;
128
+ }
129
+ const chunks = [];
130
+ res.on("data", (c) => chunks.push(c));
131
+ res.on("end", () => {
132
+ try {
133
+ const dest = path.join(os.tmpdir(), `rastack-${path.basename(name)}`);
134
+ fs.writeFileSync(dest, Buffer.concat(chunks));
135
+ resolve(dest);
136
+ }
137
+ catch {
138
+ resolve(null);
139
+ }
140
+ });
141
+ })
142
+ .on("error", () => resolve(null));
143
+ });
144
+ }
145
+ /**
146
+ * Resolve a CloudFormation template to a local file path for `aws cloudformation
147
+ * deploy` (which accepts only `--template-file`). Prefers the copy bundled with
148
+ * the `rastack` install; when that is missing, downloads it from the React API
149
+ * Stack website. Returns null only when the template is neither bundled nor
150
+ * fetchable.
151
+ */
152
+ async function resolveTemplateFile(name, cwd = process.cwd()) {
153
+ return resolveDeployAsset(name, cwd) ?? (await downloadTemplate(name));
154
+ }
99
155
  /** Read + normalize the deploy config, or null when it does not exist yet. */
100
156
  function loadConfig(cwd = process.cwd()) {
101
157
  const file = configPath(cwd);
@@ -162,6 +218,26 @@ function awsRun(args, cwd = process.cwd()) {
162
218
  function awsAvailable() {
163
219
  return awsCapture(["--version"]) !== null;
164
220
  }
221
+ /**
222
+ * The AWS account id the CLI's active credentials resolve to, or null when the
223
+ * CLI has no usable credentials (not logged in, an expired SSO session, or no
224
+ * configured profile). `init`/`bootstrap` call this to fail fast with an
225
+ * actionable "log in" message before attempting a stack deploy, instead of
226
+ * letting the first `cloudformation deploy` fail with a cryptic AWS error.
227
+ */
228
+ function awsIdentity(region) {
229
+ const args = [
230
+ "sts",
231
+ "get-caller-identity",
232
+ "--query",
233
+ "Account",
234
+ "--output",
235
+ "text",
236
+ ];
237
+ if (region)
238
+ args.push("--region", region);
239
+ return awsCapture(args);
240
+ }
165
241
  /**
166
242
  * A single Output value of a CloudFormation stack, or null when the stack does
167
243
  * not exist or the key is unset. `describe-stacks` prints `None` for a missing
@@ -209,6 +285,55 @@ function uploadPlaceholder(artifactsBucket, region, cwd = process.cwd()) {
209
285
  ]);
210
286
  return true;
211
287
  }
288
+ /**
289
+ * Deploy the tier-0 bootstrap stack, retrying once against an account that
290
+ * already has a GitHub OIDC provider — AWS allows only one provider per URL, so
291
+ * a second create is rejected and the retry tells the template to reuse the
292
+ * existing one. Throws on failure (after the retry). Shared by `rastack init`
293
+ * and `rastack bootstrap` so both stand the stack up identically.
294
+ */
295
+ function deployBootstrap(cfg, reuseOidc, template) {
296
+ try {
297
+ awsRun((0, config_1.bootstrapStackArgs)(cfg, reuseOidc, template));
298
+ }
299
+ catch (err) {
300
+ if (reuseOidc)
301
+ throw err;
302
+ console.log(" • retrying bootstrap, reusing the account's existing GitHub OIDC provider");
303
+ awsRun((0, config_1.bootstrapStackArgs)(cfg, true, template));
304
+ }
305
+ }
306
+ /**
307
+ * Ensure the tier-0 bootstrap stack exists for this account+region, **creating
308
+ * it when missing**, and return its artifacts/templates buckets. This is why
309
+ * `rastack init` is a single command that stands up *everything* — the
310
+ * account-level bootstrap stack and the per-app backend — with no separate
311
+ * `rastack bootstrap` step. Idempotent: an existing stack is reconciled in
312
+ * place. Throws with an actionable message when the template cannot be resolved,
313
+ * the deploy fails, or the buckets can't be read back.
314
+ */
315
+ async function ensureBootstrapStack(cfg, reuseOidc = false) {
316
+ const stack = (0, config_1.bootstrapStackName)(cfg);
317
+ const existing = stackOutput(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
318
+ if (existing) {
319
+ console.log(` ✓ bootstrap stack ${stack} already exists — reconciling`);
320
+ }
321
+ else {
322
+ console.log(`\n▸ Creating the tier-0 bootstrap stack ${stack} (@ ${cfg.region})`);
323
+ }
324
+ const template = await resolveTemplateFile("bootstrap.yaml");
325
+ if (!template) {
326
+ throw new Error("bootstrap.yaml template not found in the rastack package and could not be\n" +
327
+ " downloaded from reactapistack.com. Reinstall rastack (`npm i -g rastack`).");
328
+ }
329
+ deployBootstrap(cfg, reuseOidc, template);
330
+ const artifactsBucket = stackOutput(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
331
+ const templatesBucket = stackOutput(stack, config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
332
+ if (!artifactsBucket) {
333
+ throw new Error(`bootstrap stack ${stack} deployed but its ArtifactsBucket output could not be read.`);
334
+ }
335
+ return { artifactsBucket, templatesBucket };
336
+ }
212
337
  // ── GitHub CLI boundary ──────────────────────────────────────────────────────
213
338
  // After the stack is up `init` wires the two repo Variables CI reads
214
339
  // (AWS_DEPLOY_ROLE_ARN / AWS_REGION) through the developer's own `gh` — already
@@ -278,6 +403,22 @@ function setRepoVariable(args, cwd = process.cwd()) {
278
403
  return false;
279
404
  }
280
405
  }
406
+ /**
407
+ * Create (idempotently, via `gh api --method PUT`) a GitHub Actions Environment
408
+ * so its scoped Variables can be set. Returns true on success. Best-effort — a
409
+ * missing/unauthenticated `gh` or an API error degrades to the printed
410
+ * instructions, exactly like `setRepoVariable`.
411
+ */
412
+ function ghEnvironmentCreate(args, cwd = process.cwd()) {
413
+ try {
414
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
415
+ (0, child_process_1.execFileSync)("gh", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
416
+ return true;
417
+ }
418
+ catch {
419
+ return false;
420
+ }
421
+ }
281
422
  /**
282
423
  * Ask a single question with a default, echoed as `label [default]: `. Returns
283
424
  * the trimmed answer, or the default on an empty line.
@@ -34,6 +34,36 @@ export declare function renderConfigFile(cfg: DeployConfig): string;
34
34
  export declare const CONFIG_PATH = ".rastack/deploy.json";
35
35
  export declare const CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
36
36
  export declare const DEPLOY_WORKFLOW_PATH = ".github/workflows/deploy-backend.yml";
37
+ export declare const ENV_PATH = ".env";
38
+ export declare const GITIGNORE_PATH = ".gitignore";
39
+ /**
40
+ * The generated `.env` — the app's **local** environment. Every value is
41
+ * non-secret (client ids, pool ids, public URLs from the stack Outputs), but the
42
+ * file is git-ignored by convention (`gitignoreWith`) so a developer can add
43
+ * their own secrets beside these without ever committing them. Values are blank
44
+ * until the matching stack is deployed; `rastack init` rewrites the file with the
45
+ * resolved Outputs after standing the backends up.
46
+ *
47
+ * Keys are `RASTACK_<ENV>_*` (a neutral prefix). For a browser bundle, re-export
48
+ * the ones you need under your bundler's public prefix (EXPO_PUBLIC_/VITE_/…);
49
+ * `rastack dev` also serves the dev pool config live at `/__rastack/cognito.json`.
50
+ */
51
+ export declare function renderEnvFile(cfg: DeployConfig): string;
52
+ /**
53
+ * Upsert the generated `RASTACK_*` keys from `renderEnvFile(cfg)` into an
54
+ * existing `.env`, preserving every other line (a developer's own secrets,
55
+ * comments, extra keys). A matching `RASTACK_*=` line is updated in place;
56
+ * generated keys not yet present are appended. When there is no existing file,
57
+ * this is exactly `renderEnvFile(cfg)`. Pure — the IO wrapper reads/writes.
58
+ */
59
+ export declare function applyEnvValues(existing: string, cfg: DeployConfig): string;
60
+ /**
61
+ * Return `.gitignore` content with a `.env` entry present (idempotent). Appends
62
+ * under a labelled block when missing, leaves the file untouched when `.env` is
63
+ * already ignored — so `rastack init` never duplicates the entry or clobbers a
64
+ * developer's existing ignore rules.
65
+ */
66
+ export declare function gitignoreWith(existing: string, entry?: string): string;
37
67
  export interface ScaffoldOptions {
38
68
  /** Omit the GitHub Actions workflows (config only). */
39
69
  noWorkflows?: boolean;