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
@@ -172,7 +172,17 @@ function analyzeField(name, initializer, checker) {
172
172
  const app = literalTypeProp(fieldType, "__app", checker, initializer);
173
173
  const model = literalTypeProp(fieldType, "__model", checker, initializer);
174
174
  if (app && model) {
175
- return { name, type: "fk", relation: { app, model } };
175
+ const fk = { name, type: "fk", relation: { app, model } };
176
+ // `s.ref(() => X, { null: true })` — a nullable relation (a shared secret
177
+ // with no project, an unassigned task). The options object is the ref
178
+ // call's second argument, read the same way scalar constraints are.
179
+ if (ts.isCallExpression(initializer)) {
180
+ const opts = literalToValue(initializer.arguments[1]);
181
+ if (opts && typeof opts === "object" && opts.null === true) {
182
+ fk.null = true;
183
+ }
184
+ }
185
+ return fk;
176
186
  }
177
187
  return undefined;
178
188
  }
@@ -64,6 +64,11 @@ export interface Ref<App extends string, Model extends string> {
64
64
  readonly __model: Model;
65
65
  /** The authored `() => Resource` thunk — carried at runtime. */
66
66
  readonly target?: () => ResourceType<App, Model, any>;
67
+ /** Optional FK constraints — `null: true` makes the relation nullable
68
+ * (e.g. a shared secret with no project, or an unassigned task). */
69
+ readonly options?: {
70
+ null?: boolean;
71
+ };
67
72
  }
68
73
  export interface StringOptions {
69
74
  maxLength?: number;
@@ -105,7 +110,9 @@ export declare const s: {
105
110
  * the relation from the *type* `Ref<App, Model>` this returns, and the
106
111
  * runtime keeps the thunk so `manifestFromResources()` can resolve it too.
107
112
  */
108
- ref<A extends string, M extends string>(thunk: () => ResourceType<A, M, any>): Ref<A, M>;
113
+ ref<A extends string, M extends string>(thunk: () => ResourceType<A, M, any>, options?: {
114
+ null?: boolean;
115
+ }): Ref<A, M>;
109
116
  };
110
117
  export type AnyField = Scalar<string> | Ref<string, string>;
111
118
  /** Local-first sync configuration (mirrors the old Python `class Sync:`). */
@@ -56,8 +56,8 @@ exports.s = {
56
56
  * the relation from the *type* `Ref<App, Model>` this returns, and the
57
57
  * runtime keeps the thunk so `manifestFromResources()` can resolve it too.
58
58
  */
59
- ref(thunk) {
60
- return { __rastackRef: true, target: thunk };
59
+ ref(thunk, options) {
60
+ return { __rastackRef: true, target: thunk, options };
61
61
  },
62
62
  };
63
63
  /** Define a resource. Capture app/model (and options) as literal types. */
@@ -89,11 +89,17 @@ function runtimeField(res, name, value) {
89
89
  if (!isResourceValue(target)) {
90
90
  throw new Error(`manifestFromResources: ${res.app}.${res.model}.${name} is an s.ref(...) whose target thunk did not return a resource() value`);
91
91
  }
92
- return {
92
+ const fk = {
93
93
  name,
94
94
  type: "fk",
95
95
  relation: { app: target.app, model: target.model },
96
96
  };
97
+ // A nullable FK (`s.ref(() => X, { null: true })`) — e.g. a shared secret
98
+ // with no project — so the engine allows and stores a null relation.
99
+ if (field.options?.null === true) {
100
+ fk.null = true;
101
+ }
102
+ return fk;
97
103
  }
98
104
  if (typeof field.__rastackScalar !== "string")
99
105
  return undefined;
@@ -15,7 +15,7 @@
15
15
  * or emitted as a live `$(aws cloudformation describe-stacks …)` substitution so
16
16
  * the same plan works before and after they are wired into the config.
17
17
  */
18
- import { DeployConfig } from "./config";
18
+ import { DeployConfig, EnvName } from "./config";
19
19
  export type CiPhase = "check" | "build" | "deploy";
20
20
  export interface CiStep {
21
21
  /** Human label, echoed before the step runs. */
@@ -37,10 +37,17 @@ export interface CiPlanOptions {
37
37
  bin?: string;
38
38
  /** Skip the build steps in `deploy` (code already built this job). */
39
39
  skipBuild?: boolean;
40
- /** Override the function name (else from cfg.outputs, else resolved live). */
40
+ /** Override the function name (else from the env's outputs, else resolved live). */
41
41
  functionName?: string;
42
- /** Override the JWKS URL (else from cfg.outputs, else resolved live). */
42
+ /** Override the JWKS URL (else from the env's outputs, else resolved live). */
43
43
  jwksUrl?: string;
44
+ /**
45
+ * Which environment (dev/prod) to build+ship. When set, the stack name,
46
+ * outputs, and app label come from that environment — so `rastack ci deploy
47
+ * --env dev` targets the dev stack (dev's Cognito JWKS, dev's function). When
48
+ * unset the legacy single-stack fields (`cfg.stackName` / `cfg.outputs`) apply.
49
+ */
50
+ env?: EnvName;
44
51
  }
45
52
  /** `check` — validate the resource graph and compile the manifest. A CI gate. */
46
53
  export declare function checkSteps(cfg: DeployConfig, bin: string): CiStep[];
package/dist/deploy/ci.js CHANGED
@@ -21,6 +21,7 @@ exports.checkSteps = checkSteps;
21
21
  exports.buildSteps = buildSteps;
22
22
  exports.deploySteps = deploySteps;
23
23
  exports.ciPlan = ciPlan;
24
+ const config_1 = require("./config");
24
25
  /**
25
26
  * The in-process compile step shared by `check` and `build`: it produces
26
27
  * `schema.rastack.json` + `openapi.json` in `cfg.outDir` without a subprocess,
@@ -34,17 +35,25 @@ function compileStep(cfg) {
34
35
  action: "compile",
35
36
  };
36
37
  }
38
+ /** The stack name + outputs + app label the plan targets, honouring `opts.env`. */
39
+ function ciTarget(cfg, opts) {
40
+ if (opts.env) {
41
+ const e = (0, config_1.resolveEnvironment)(cfg, opts.env);
42
+ return { stackName: e.stackName, outputs: e.outputs, app: e.appName };
43
+ }
44
+ return { stackName: cfg.stackName, outputs: cfg.outputs, app: cfg.app };
45
+ }
37
46
  /**
38
- * A stack Output value: the literal from `cfg.outputs` if wired, else a shell
39
- * command substitution that resolves it live from CloudFormation. Keeping the
40
- * fallback inline (rather than a separate step) means the plan is a flat list of
41
- * self-contained commands that read correctly in any order.
47
+ * A stack Output value: the literal if wired, else a shell command substitution
48
+ * that resolves it live from CloudFormation. Keeping the fallback inline (rather
49
+ * than a separate step) means the plan is a flat list of self-contained commands
50
+ * that read correctly in any order.
42
51
  */
43
- function stackOutput(cfg, outputKey, literal) {
52
+ function stackOutput(stackName, region, outputKey, literal) {
44
53
  if (literal)
45
54
  return literal;
46
- return (`$(aws cloudformation describe-stacks --stack-name ${cfg.stackName} ` +
47
- `--region ${cfg.region} ` +
55
+ return (`$(aws cloudformation describe-stacks --stack-name ${stackName} ` +
56
+ `--region ${region} ` +
48
57
  `--query "Stacks[0].Outputs[?OutputKey=='${outputKey}'].OutputValue" ` +
49
58
  `--output text)`);
50
59
  }
@@ -69,8 +78,9 @@ function checkSteps(cfg, bin) {
69
78
  */
70
79
  function buildSteps(cfg, opts = {}) {
71
80
  const stage = stageDir(cfg);
81
+ const target = ciTarget(cfg, opts);
72
82
  const archFlag = cfg.lambda.arch === "arm64" ? "--arm64" : "--x86-64";
73
- const jwks = stackOutput(cfg, "JwksUrl", opts.jwksUrl ?? cfg.outputs?.jwksUrl);
83
+ const jwks = stackOutput(target.stackName, cfg.region, "JwksUrl", opts.jwksUrl ?? target.outputs?.jwksUrl);
74
84
  return [
75
85
  compileStep(cfg),
76
86
  {
@@ -100,8 +110,9 @@ function buildSteps(cfg, opts = {}) {
100
110
  function deploySteps(cfg, opts = {}) {
101
111
  const stage = stageDir(cfg);
102
112
  const zip = `${stage}/bootstrap.zip`;
103
- const fn = stackOutput(cfg, "FunctionName", opts.functionName ?? cfg.outputs?.functionName);
104
- const apiUrl = stackOutput(cfg, "ApiUrl", cfg.outputs?.apiUrl);
113
+ const target = ciTarget(cfg, opts);
114
+ const fn = stackOutput(target.stackName, cfg.region, "FunctionName", opts.functionName ?? target.outputs?.functionName);
115
+ const apiUrl = stackOutput(target.stackName, cfg.region, "ApiUrl", target.outputs?.apiUrl);
105
116
  const steps = opts.skipBuild ? [] : buildSteps(cfg, opts);
106
117
  steps.push({
107
118
  name: "Ship the code (update-function-code)",
@@ -113,7 +124,7 @@ function deploySteps(cfg, opts = {}) {
113
124
  run: `aws lambda wait function-updated --function-name ${fn} --region ${cfg.region}`,
114
125
  }, {
115
126
  name: "Report the API URL",
116
- run: `echo "Deployed ${cfg.app} → ${apiUrl}"`,
127
+ run: `echo "Deployed ${target.app} → ${apiUrl}"`,
117
128
  });
118
129
  return steps;
119
130
  }
@@ -32,6 +32,12 @@ export interface DeployOutputs {
32
32
  jwksUrl?: string;
33
33
  /** S3 Iceberg warehouse bucket — the system of record. */
34
34
  warehouseBucket?: string;
35
+ /** Cognito Hosted UI domain (scheme-less), for browser sign-in. */
36
+ userPoolDomain?: string;
37
+ /** Full Cognito Hosted UI base URL (`https://…`). */
38
+ hostedUiUrl?: string;
39
+ /** Base URL of the external-API proxy route, when the env provisions one. */
40
+ proxyUrl?: string;
35
41
  }
36
42
  export interface LambdaBuild {
37
43
  /** Cargo bin built for the custom runtime (`provided.al2023`). */
@@ -41,6 +47,35 @@ export interface LambdaBuild {
41
47
  /** Cargo features the Lambda build enables. */
42
48
  features: string;
43
49
  }
50
+ /**
51
+ * Per-environment overrides. The **prod** environment defaults its values from
52
+ * the top-level config (its branch is `github.branch`, its stack is `stackName`,
53
+ * its outputs are the top-level `outputs`); the **dev** environment defaults to
54
+ * the `development` branch and a `-dev` stack. Anything set here wins over those
55
+ * defaults. Only the fields that actually differ per environment live here —
56
+ * `app`/`namePrefix`/`region`/`github.owner`/`github.repo` are shared.
57
+ */
58
+ export interface EnvOverride {
59
+ /** GitHub branch whose push deploys this environment. */
60
+ branch?: string;
61
+ /** GitHub Actions **Environment** name (scoped Variables + protection rules). */
62
+ ghEnvironment?: string;
63
+ /** CloudFormation stack name for this environment. */
64
+ stackName?: string;
65
+ /** Iceberg namespace this environment's warehouse writes under. */
66
+ namespace?: string;
67
+ /**
68
+ * Base URL of the upstream this environment's **external-API proxy** forwards
69
+ * to (typically a UAT or prod backend). When set, the stack provisions a
70
+ * Cognito-authorized `ANY /proxy/{proxy+}` route on the HTTP API that forwards
71
+ * to `${proxyUpstream}/{proxy}` — so a locally-running app (signed in against
72
+ * this environment's Cognito pool) reaches external APIs *only* through the
73
+ * proxy, never directly, and credentials stay server-side. Empty = no proxy.
74
+ */
75
+ proxyUpstream?: string;
76
+ /** Values resolved from this environment's stack after it is created. */
77
+ outputs?: DeployOutputs;
78
+ }
44
79
  export interface DeployConfig {
45
80
  /** App slug — namespaces buckets/roles/SSM. Cognito-safe: `^[a-z0-9]{1,20}$`. */
46
81
  app: string;
@@ -48,9 +83,16 @@ export interface DeployConfig {
48
83
  namePrefix: string;
49
84
  /** AWS region the stack (and CI) target. */
50
85
  region: string;
51
- /** CloudFormation stack name — defaults to `${namePrefix}-${app}`. */
86
+ /**
87
+ * CloudFormation stack name of the **prod** environment — defaults to
88
+ * `${namePrefix}-${app}`. Dev's stack defaults to this + `-dev`.
89
+ */
52
90
  stackName: string;
53
- /** The GitHub repo whose CI is trusted to ship the Lambda code. */
91
+ /**
92
+ * The GitHub repo whose CI is trusted to ship the Lambda code. `branch` is the
93
+ * **prod** deploy branch (default `master`); the dev branch defaults to
94
+ * `development` and is overridable under `environments.dev.branch`.
95
+ */
54
96
  github: {
55
97
  owner: string;
56
98
  repo: string;
@@ -70,9 +112,80 @@ export interface DeployConfig {
70
112
  artifactsBucket?: string;
71
113
  /** Whether the function verifies the `access` or `id` token. */
72
114
  tokenUse: "access" | "id";
73
- /** Values resolved from the stack after it is created (optional). */
115
+ /** Values resolved from the (prod) stack after it is created (optional). */
116
+ outputs?: DeployOutputs;
117
+ /**
118
+ * Per-environment overrides. A `dev` + `prod` split is the default even with
119
+ * this omitted (see `resolveEnvironment`); this only records what a user
120
+ * changed (a custom dev branch, dev stack outputs, …).
121
+ */
122
+ environments?: Partial<Record<EnvName, EnvOverride>>;
123
+ }
124
+ /** The two deployment environments — dev (development branch) and prod (master). */
125
+ export type EnvName = "prod" | "dev";
126
+ export declare const ENV_NAMES: readonly EnvName[];
127
+ /** The fully-resolved shape of one environment (top-level + defaults + overrides). */
128
+ export interface EnvDef {
129
+ /** Which environment this is. */
130
+ name: EnvName;
131
+ /** Branch whose push deploys it (`master` for prod, `development` for dev). */
132
+ branch: string;
133
+ /** GitHub Actions Environment name (`production` / `development`). */
134
+ ghEnvironment: string;
135
+ /**
136
+ * The app.yaml `AppName` parameter — the infra namespace. Prod uses the app
137
+ * slug; dev uses `${app}dev` so its buckets/roles/pools/SSM never collide with
138
+ * prod's (AppName is Cognito-safe `^[a-z0-9]{1,20}$`, so the suffix is glued,
139
+ * not hyphenated).
140
+ */
141
+ appName: string;
142
+ /** CloudFormation stack name for this environment. */
143
+ stackName: string;
144
+ /** Iceberg namespace this environment writes under. */
145
+ namespace: string;
146
+ /** Upstream the external-API proxy forwards to (empty when no proxy). */
147
+ proxyUpstream: string;
148
+ /** Values resolved from this environment's stack, when known. */
74
149
  outputs?: DeployOutputs;
75
150
  }
151
+ /** Defaults for the dev environment when `environments.dev` is unset. */
152
+ export declare const DEV_DEFAULTS: {
153
+ readonly branch: "development";
154
+ readonly ghEnvironment: "development";
155
+ readonly namespace: "development";
156
+ };
157
+ /** The GitHub Actions Environment name the prod deploy uses. */
158
+ export declare const PROD_GH_ENVIRONMENT = "production";
159
+ /**
160
+ * The app.yaml `AppName` for an environment — the infra namespace. Dev glues a
161
+ * `dev` suffix on so its stack's buckets/roles/pools/SSM are physically distinct
162
+ * from prod's without a hyphen (Cognito `AppName`/`IdentityPoolName` forbid one).
163
+ */
164
+ export declare function envAppName(cfg: DeployConfig, env: EnvName): string;
165
+ /**
166
+ * Resolve one environment to its full definition: the prod environment inherits
167
+ * the top-level branch/stack/namespace/outputs, the dev environment defaults to
168
+ * the `development` branch and a `-dev` stack, and anything under
169
+ * `cfg.environments[env]` overrides those.
170
+ */
171
+ export declare function resolveEnvironment(cfg: DeployConfig, env: EnvName): EnvDef;
172
+ /** Both environments, prod first, fully resolved. */
173
+ export declare function resolveEnvironments(cfg: DeployConfig): EnvDef[];
174
+ /**
175
+ * The browser-facing Cognito config for one environment — what `rastack dev`
176
+ * serves at `/__rastack/cognito.json` so a locally-running app can sign in
177
+ * against the real (dev) Cognito pool via the Hosted UI. Shaped to feed
178
+ * `cognitoRuntimeToConfig` in `rastack/auth`. Returns null until the
179
+ * environment's stack Outputs (client id + Hosted UI url) are known.
180
+ */
181
+ export interface CognitoRuntimeConfig {
182
+ userPoolId?: string;
183
+ clientId: string;
184
+ /** Hosted UI base URL (`https://…amazoncognito.com`). */
185
+ hostedUiUrl: string;
186
+ region: string;
187
+ }
188
+ export declare function cognitoRuntimeConfig(cfg: DeployConfig, env: EnvName): CognitoRuntimeConfig | null;
76
189
  export declare const DEFAULT_LAMBDA: LambdaBuild;
77
190
  /** The out-of-the-box config — everything a fresh repo can default sensibly. */
78
191
  export declare const DEFAULTS: Omit<DeployConfig, "app" | "github">;
@@ -136,6 +249,16 @@ export declare function cognitoAdvancedSecuritySupported(region: string): boolea
136
249
  * the template's ENFORCED default stands).
137
250
  */
138
251
  export declare function stackParameters(cfg: DeployConfig): StackParameter[];
252
+ /**
253
+ * The app.yaml `--parameter-overrides` for a **specific environment** (dev or
254
+ * prod). Same as `stackParameters` but the AppName/branch/namespace come from the
255
+ * resolved environment, and it also passes `GitHubEnvironment` so the stack's
256
+ * deploy-role trust is scoped to the GitHub Actions Environment (the OIDC subject
257
+ * for an environment-gated job is `…:environment:<env>`, not `…:ref:…`). Prod's
258
+ * AppName is the bare slug, so prod's resource names are byte-for-byte what the
259
+ * pre-environment template produced.
260
+ */
261
+ export declare function stackParametersForEnv(cfg: DeployConfig, env: EnvName): StackParameter[];
139
262
  /**
140
263
  * The canonical repo-relative CloudFormation template paths. These are the paths
141
264
  * in the *framework* repo (theserverkid/reactapistack) and the default the argv
@@ -158,6 +281,10 @@ export declare function deployStackArgs(cfg: DeployConfig, templateFile?: string
158
281
  * for you; it is also printed by `--print` so it can be run by hand.
159
282
  */
160
283
  export declare function deployStackCommand(cfg: DeployConfig, templateFile?: string): string;
284
+ /** The `aws cloudformation deploy` argv for one environment's stack (dev/prod). */
285
+ export declare function deployStackArgsForEnv(cfg: DeployConfig, env: EnvName, templateFile?: string): string[];
286
+ /** The single-line `aws cloudformation deploy` command for one environment. */
287
+ export declare function deployStackCommandForEnv(cfg: DeployConfig, env: EnvName, templateFile?: string): string;
161
288
  /**
162
289
  * The tier-0 bootstrap stack (bootstrap.yaml) — the S3 buckets that hold the
163
290
  * Lambda placeholder + hosted templates, and the GitHub OIDC provider. One per
@@ -193,14 +320,28 @@ export interface RepoVariable {
193
320
  name: string;
194
321
  value: string;
195
322
  from: string;
323
+ /**
324
+ * The GitHub Actions **Environment** this Variable is scoped to (`development`
325
+ * / `production`). When set, `init` wires it under that environment so the
326
+ * dev and prod deploys each read their own role/region.
327
+ */
328
+ ghEnvironment?: string;
196
329
  }
197
330
  /**
198
331
  * The GitHub repo **Variables** CI reads — `AWS_DEPLOY_ROLE_ARN` and
199
332
  * `AWS_REGION` (see docs/serverless-deploy.md §3). Values come from the stack
200
333
  * Outputs when already resolved, else an empty string to fill in from the
201
- * Console.
334
+ * Console. This is the legacy single-environment view (the prod stack); the
335
+ * dev/prod split wires them per GitHub Environment via `repoVariablesForEnv`.
202
336
  */
203
337
  export declare function repoVariables(cfg: DeployConfig): RepoVariable[];
338
+ /**
339
+ * The same two Variables, scoped to one environment's GitHub Actions
340
+ * Environment. The dev deploy reads `development`'s copies, the prod deploy
341
+ * `production`'s — so the two never cross-assume each other's role. Values come
342
+ * from the environment's own stack Outputs.
343
+ */
344
+ export declare function repoVariablesForEnv(cfg: DeployConfig, env: EnvName): RepoVariable[];
204
345
  /**
205
346
  * The argv (after `gh`) that sets one GitHub repo **Variable** on the config's
206
347
  * repo. `init` runs this for each resolved `repoVariables` entry so the
@@ -210,7 +351,16 @@ export declare function repoVariables(cfg: DeployConfig): RepoVariable[];
210
351
  * user's GitHub) rather than an API token; a pure argv so it is inspectable and
211
352
  * shell-free (`execFileSync`).
212
353
  */
213
- export declare function ghVariableSetArgs(cfg: DeployConfig, name: string, value: string): string[];
354
+ export declare function ghVariableSetArgs(cfg: DeployConfig, name: string, value: string, ghEnvironment?: string): string[];
355
+ /**
356
+ * The argv (after `gh`) that creates (idempotently) a GitHub Actions
357
+ * **Environment** on the config's repo — `gh api --method PUT
358
+ * repos/{owner}/{repo}/environments/{env}`. `init` runs this for `development`
359
+ * and `production` before wiring their scoped Variables, since a Variable can
360
+ * only be set on an environment that already exists. PUT is idempotent, so
361
+ * re-running `init` is safe.
362
+ */
363
+ export declare function ghEnvironmentCreateArgs(cfg: DeployConfig, ghEnvironment: string): string[];
214
364
  /**
215
365
  * The `RASTACK_*` environment the Lambda reads (docs/serverless-deploy.md §1).
216
366
  * The IaC sets these on the function; surfaced here for `rastack deploy` to