rastack 0.0.41 → 0.0.43

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.
@@ -0,0 +1,342 @@
1
+ AWSTemplateFormatVersion: "2010-09-09"
2
+ Description: >-
3
+ React API Stack bootstrap (tier-0). Creates the account+region S3 buckets that
4
+ host the framework CloudFormation templates and the Lambda placeholder, plus a
5
+ least-privilege GitHub OIDC publisher role. Run once per account and region.
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # Parameters — everything that varies between accounts / forks lives here, so
9
+ # the same template bootstraps any account without edits.
10
+ # ---------------------------------------------------------------------------
11
+ Parameters:
12
+ NamePrefix:
13
+ Type: String
14
+ Default: rastack
15
+ Description: Prefix for the bucket names and the publisher role.
16
+ # No hyphens, matching app.yaml (its NamePrefix feeds a Cognito name).
17
+ AllowedPattern: "^[a-z0-9]{1,20}$"
18
+
19
+ GitHubOwner:
20
+ Type: String
21
+ Default: theserverkid
22
+ Description: GitHub org/user that owns the framework repo allowed to publish.
23
+
24
+ PublisherRepo:
25
+ Type: String
26
+ Default: reactapistack
27
+ Description: >
28
+ The single repository whose CI (via OIDC) may publish templates/artifacts
29
+ into these buckets. Only this repo's workflows can assume the publisher
30
+ role — nothing else, and no long-lived keys.
31
+
32
+ PublisherBranch:
33
+ Type: String
34
+ Default: master
35
+ Description: Branch of PublisherRepo whose workflow may assume the publisher role.
36
+
37
+ CreateOIDCProvider:
38
+ Type: String
39
+ Default: "true"
40
+ AllowedValues: ["true", "false"]
41
+ Description: >
42
+ Create the GitHub Actions OIDC provider. Set "false" if the account
43
+ already has one (an account may have only a single provider per URL) and
44
+ pass its ARN in ExistingOIDCProviderArn.
45
+
46
+ ExistingOIDCProviderArn:
47
+ Type: String
48
+ Default: ""
49
+ Description: ARN of a pre-existing GitHub OIDC provider (when CreateOIDCProvider=false).
50
+
51
+ ReadableByAccount:
52
+ Type: String
53
+ Default: "true"
54
+ AllowedValues: ["true", "false"]
55
+ Description: >
56
+ Grant s3:GetObject on both buckets to the account root, so any IAM
57
+ principal in THIS account can launch a hosted template / read the
58
+ placeholder artifact from the Console without an extra identity policy.
59
+ Public (internet) access stays blocked regardless — account-root is not
60
+ public.
61
+
62
+ Conditions:
63
+ ShouldCreateOIDCProvider: !Equals [!Ref CreateOIDCProvider, "true"]
64
+ GrantAccountRead: !Equals [!Ref ReadableByAccount, "true"]
65
+ HasExistingOIDCArn: !Not [!Equals [!Ref ExistingOIDCProviderArn, ""]]
66
+
67
+ Resources:
68
+
69
+ # -- GitHub OIDC provider ----------------------------------------------------
70
+ # Short-lived OIDC tokens instead of stored AWS keys. Reused by the per-app
71
+ # deploy role (app.yaml) too; created here once so tier-0 owns the trust root.
72
+ GitHubOIDCProvider:
73
+ Type: AWS::IAM::OIDCProvider
74
+ Condition: ShouldCreateOIDCProvider
75
+ Properties:
76
+ Url: https://token.actions.githubusercontent.com
77
+ ClientIdList:
78
+ - sts.amazonaws.com
79
+ # AWS no longer validates these thumbprints for the GitHub host, but the
80
+ # property is still required; both historically-published values are listed.
81
+ ThumbprintList:
82
+ - 6938fd4d98bab03faadb97b34396831e3780aea1
83
+ - 1c58a3a8518e8759bf075b76b750d4f2df264fca
84
+
85
+ # -- Templates bucket --------------------------------------------------------
86
+ # Hosts the *.yaml CloudFormation templates (app.yaml + future ones). Private,
87
+ # versioned, encrypted, TLS-only, public access fully blocked. CloudFormation
88
+ # reads a template referenced by S3 URL using the *deployer's* credentials, so
89
+ # no public read is needed for a Console "Launch Stack" within the account.
90
+ TemplatesBucket:
91
+ Type: AWS::S3::Bucket
92
+ # The template store is durable and shared across every consumer stack —
93
+ # never let a `delete-stack` of the bootstrap take it (and its version
94
+ # history) down with it.
95
+ DeletionPolicy: Retain
96
+ UpdateReplacePolicy: Retain
97
+ Properties:
98
+ BucketName: !Sub "${NamePrefix}-cloudformation-${AWS::AccountId}-${AWS::Region}"
99
+ VersioningConfiguration:
100
+ Status: Enabled
101
+ BucketEncryption:
102
+ ServerSideEncryptionConfiguration:
103
+ - ServerSideEncryptionByDefault:
104
+ SSEAlgorithm: AES256
105
+ PublicAccessBlockConfiguration:
106
+ BlockPublicAcls: true
107
+ BlockPublicPolicy: true
108
+ IgnorePublicAcls: true
109
+ RestrictPublicBuckets: true
110
+ OwnershipControls:
111
+ Rules:
112
+ - ObjectOwnership: BucketOwnerEnforced
113
+ LifecycleConfiguration:
114
+ Rules:
115
+ - Id: expire-noncurrent-template-versions
116
+ Status: Enabled
117
+ NoncurrentVersionExpiration:
118
+ NoncurrentDays: 30
119
+ AbortIncompleteMultipartUpload:
120
+ DaysAfterInitiation: 7
121
+
122
+ TemplatesBucketPolicy:
123
+ Type: AWS::S3::BucketPolicy
124
+ Properties:
125
+ Bucket: !Ref TemplatesBucket
126
+ PolicyDocument:
127
+ Version: "2012-10-17"
128
+ Statement:
129
+ - Sid: DenyInsecureTransport
130
+ Effect: Deny
131
+ Principal: "*"
132
+ Action: "s3:*"
133
+ Resource:
134
+ - !GetAtt TemplatesBucket.Arn
135
+ - !Sub "${TemplatesBucket.Arn}/*"
136
+ Condition:
137
+ Bool:
138
+ aws:SecureTransport: "false"
139
+ - !If
140
+ - GrantAccountRead
141
+ - Sid: AccountReadTemplates
142
+ Effect: Allow
143
+ Principal:
144
+ AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
145
+ Action:
146
+ - s3:GetObject
147
+ - s3:ListBucket
148
+ Resource:
149
+ - !GetAtt TemplatesBucket.Arn
150
+ - !Sub "${TemplatesBucket.Arn}/*"
151
+ - !Ref "AWS::NoValue"
152
+
153
+ # -- Artifacts bucket --------------------------------------------------------
154
+ # Hosts the Lambda placeholder bundle (placeholders/bootstrap.zip) that the
155
+ # per-app template's function is created from, plus any build artifacts CI
156
+ # stages. Same hardening as the templates bucket.
157
+ ArtifactsBucket:
158
+ Type: AWS::S3::Bucket
159
+ DeletionPolicy: Retain
160
+ UpdateReplacePolicy: Retain
161
+ Properties:
162
+ BucketName: !Sub "${NamePrefix}-artifacts-${AWS::AccountId}-${AWS::Region}"
163
+ VersioningConfiguration:
164
+ Status: Enabled
165
+ BucketEncryption:
166
+ ServerSideEncryptionConfiguration:
167
+ - ServerSideEncryptionByDefault:
168
+ SSEAlgorithm: AES256
169
+ PublicAccessBlockConfiguration:
170
+ BlockPublicAcls: true
171
+ BlockPublicPolicy: true
172
+ IgnorePublicAcls: true
173
+ RestrictPublicBuckets: true
174
+ OwnershipControls:
175
+ Rules:
176
+ - ObjectOwnership: BucketOwnerEnforced
177
+ LifecycleConfiguration:
178
+ Rules:
179
+ - Id: expire-noncurrent-artifact-versions
180
+ Status: Enabled
181
+ NoncurrentVersionExpiration:
182
+ NoncurrentDays: 30
183
+ AbortIncompleteMultipartUpload:
184
+ DaysAfterInitiation: 7
185
+
186
+ ArtifactsBucketPolicy:
187
+ Type: AWS::S3::BucketPolicy
188
+ Properties:
189
+ Bucket: !Ref ArtifactsBucket
190
+ PolicyDocument:
191
+ Version: "2012-10-17"
192
+ Statement:
193
+ - Sid: DenyInsecureTransport
194
+ Effect: Deny
195
+ Principal: "*"
196
+ Action: "s3:*"
197
+ Resource:
198
+ - !GetAtt ArtifactsBucket.Arn
199
+ - !Sub "${ArtifactsBucket.Arn}/*"
200
+ Condition:
201
+ Bool:
202
+ aws:SecureTransport: "false"
203
+ - !If
204
+ - GrantAccountRead
205
+ - Sid: AccountReadArtifacts
206
+ Effect: Allow
207
+ Principal:
208
+ AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
209
+ Action:
210
+ - s3:GetObject
211
+ - s3:ListBucket
212
+ Resource:
213
+ - !GetAtt ArtifactsBucket.Arn
214
+ - !Sub "${ArtifactsBucket.Arn}/*"
215
+ - !Ref "AWS::NoValue"
216
+
217
+ # -- Publisher role (GitHub OIDC) --------------------------------------------
218
+ # The framework's CI assumes THIS role (no stored keys) to sync the templates
219
+ # and the placeholder artifact into the two buckets. Its trust is pinned to a
220
+ # single repo+branch, and its permission grant is scoped to exactly these two
221
+ # buckets — plus an explicit deny that stops it re-governing them or escalating.
222
+ TemplatePublisherRole:
223
+ Type: AWS::IAM::Role
224
+ Properties:
225
+ RoleName: !Sub "${NamePrefix}-template-publisher"
226
+ Description: CI role that publishes React API Stack templates + artifacts.
227
+ MaxSessionDuration: 3600
228
+ AssumeRolePolicyDocument:
229
+ Version: "2012-10-17"
230
+ Statement:
231
+ - Effect: Allow
232
+ Principal:
233
+ # When reusing an existing provider but no ARN was passed, derive
234
+ # the account's single standard GitHub provider ARN — never an
235
+ # empty principal (which IAM rejects as MalformedPolicyDocument).
236
+ Federated: !If
237
+ - ShouldCreateOIDCProvider
238
+ - !Ref GitHubOIDCProvider
239
+ - !If
240
+ - HasExistingOIDCArn
241
+ - !Ref ExistingOIDCProviderArn
242
+ - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com"
243
+ Action: sts:AssumeRoleWithWebIdentity
244
+ Condition:
245
+ StringEquals:
246
+ token.actions.githubusercontent.com:aud: sts.amazonaws.com
247
+ StringLike:
248
+ token.actions.githubusercontent.com:sub: !Sub "repo:${GitHubOwner}/${PublisherRepo}:ref:refs/heads/${PublisherBranch}"
249
+ Policies:
250
+ - PolicyName: publish-to-template-buckets
251
+ PolicyDocument:
252
+ Version: "2012-10-17"
253
+ Statement:
254
+ - Sid: WriteObjects
255
+ Effect: Allow
256
+ Action:
257
+ - s3:PutObject
258
+ - s3:GetObject
259
+ - s3:DeleteObject
260
+ Resource:
261
+ - !Sub "${TemplatesBucket.Arn}/*"
262
+ - !Sub "${ArtifactsBucket.Arn}/*"
263
+ - Sid: ListBuckets
264
+ Effect: Allow
265
+ Action:
266
+ - s3:ListBucket
267
+ - s3:GetBucketLocation
268
+ Resource:
269
+ - !GetAtt TemplatesBucket.Arn
270
+ - !GetAtt ArtifactsBucket.Arn
271
+ - PolicyName: deny-privilege-escalation
272
+ PolicyDocument:
273
+ Version: "2012-10-17"
274
+ Statement:
275
+ # Defense-in-depth: a compromised publish workflow can only write
276
+ # template/artifact objects. It can never re-govern the buckets,
277
+ # touch identity, or grant itself anything more.
278
+ - Sid: DenyPrivilegeEscalation
279
+ Effect: Deny
280
+ Action:
281
+ - iam:*
282
+ - sts:AssumeRole
283
+ - organizations:*
284
+ - account:*
285
+ - s3:PutBucketPolicy
286
+ - s3:DeleteBucketPolicy
287
+ - s3:PutBucketPublicAccessBlock
288
+ - s3:PutBucketAcl
289
+ - s3:PutObjectAcl
290
+ - s3:DeleteBucket
291
+ Resource: "*"
292
+
293
+ Outputs:
294
+ TemplatesBucketName:
295
+ Description: S3 bucket hosting the CloudFormation templates.
296
+ Value: !Ref TemplatesBucket
297
+ Export:
298
+ Name: !Sub "${AWS::StackName}-TemplatesBucket"
299
+
300
+ TemplatesBaseUrl:
301
+ Description: Base HTTPS URL of the templates bucket (append the object key).
302
+ Value: !Sub "https://${TemplatesBucket}.s3.${AWS::Region}.amazonaws.com"
303
+
304
+ AppTemplateUrl:
305
+ Description: >
306
+ URL the per-app backend template will live at once published
307
+ (deploy/cloudformation/app.yaml → app.yaml). Consumer repos launch from this.
308
+ Value: !Sub "https://${TemplatesBucket}.s3.${AWS::Region}.amazonaws.com/app.yaml"
309
+
310
+ LaunchAppStackUrl:
311
+ Description: >
312
+ One-click Console "Launch Stack" (quick-create) link for the hosted app
313
+ template. Fill in GitHubRepo/AppName parameters on the review screen.
314
+ Value: !Sub "https://${AWS::Region}.console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/create/review?templateURL=https://${TemplatesBucket}.s3.${AWS::Region}.amazonaws.com/app.yaml&stackName=rastack-app"
315
+
316
+ ArtifactsBucketName:
317
+ Description: S3 bucket hosting the Lambda placeholder + build artifacts.
318
+ Value: !Ref ArtifactsBucket
319
+ Export:
320
+ Name: !Sub "${AWS::StackName}-ArtifactsBucket"
321
+
322
+ ArtifactsBaseUrl:
323
+ Description: Base HTTPS URL of the artifacts bucket.
324
+ Value: !Sub "https://${ArtifactsBucket}.s3.${AWS::Region}.amazonaws.com"
325
+
326
+ PublisherRoleArn:
327
+ Description: >
328
+ Role ARN the framework CI assumes via OIDC to publish templates/artifacts.
329
+ Wire as the AWS_PUBLISH_ROLE_ARN repo Variable in the framework repo.
330
+ Value: !GetAtt TemplatePublisherRole.Arn
331
+ Export:
332
+ Name: !Sub "${AWS::StackName}-PublisherRoleArn"
333
+
334
+ OIDCProviderArn:
335
+ Description: GitHub OIDC provider ARN (created or reused) — reuse in app.yaml.
336
+ Value: !If
337
+ - ShouldCreateOIDCProvider
338
+ - !Ref GitHubOIDCProvider
339
+ - !If
340
+ - HasExistingOIDCArn
341
+ - !Ref ExistingOIDCProviderArn
342
+ - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com"
@@ -0,0 +1,28 @@
1
+ #!/bin/sh
2
+ # Placeholder React API Stack Lambda bootstrap (provided.al2023 custom runtime).
3
+ #
4
+ # Its one guaranteed job: be a valid code bundle so app.yaml can CREATE the
5
+ # function with no Rust toolchain (Lambda does not execute this at create time).
6
+ # CI then replaces it out of band with the compiled rastack-server via
7
+ # `aws lambda update-function-code`; app.yaml pins a fixed Code.S3Key, so stack
8
+ # updates never revert what CI shipped.
9
+ #
10
+ # Best-effort until real code lands: if the (minimal) runtime has curl, every
11
+ # invocation returns a clear 503 rather than an opaque error. The placeholder is
12
+ # never meant to serve real traffic — deploy the backend before pointing at it.
13
+ set -eu
14
+
15
+ while true; do
16
+ HEADERS="$(mktemp)"
17
+ # Long-poll for the next invocation.
18
+ curl -sS -LD "$HEADERS" -o /dev/null \
19
+ "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next"
20
+ REQUEST_ID="$(grep -Fi 'Lambda-Runtime-Aws-Request-Id' "$HEADERS" \
21
+ | tr -d '[:space:]' | cut -d: -f2)"
22
+ [ -n "${REQUEST_ID}" ] || continue
23
+
24
+ RESPONSE='{"statusCode":503,"headers":{"content-type":"application/json"},"body":"{\"detail\":\"React API Stack backend not yet deployed — CI has not shipped the rastack-server bundle to this function.\"}"}'
25
+ curl -sS -o /dev/null \
26
+ "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/${REQUEST_ID}/response" \
27
+ -d "${RESPONSE}"
28
+ done
package/dist/update.d.ts CHANGED
@@ -52,6 +52,17 @@ export declare function npmBinary(platform?: string): string;
52
52
  * shell means no injection surface).
53
53
  */
54
54
  export declare function npmSpawnShell(platform?: string): boolean;
55
+ /**
56
+ * The single command string used to launch npm on platforms that must go through
57
+ * a shell (Windows — see `npmSpawnShell`). Node 22+ deprecates passing an args
58
+ * *array* together with `shell: true` (DEP0190: the args are concatenated, not
59
+ * escaped), so the shell path builds one string and runs it via `execSync`
60
+ * instead. Joining with spaces is safe here because every token is a fixed
61
+ * literal except the version, which `sanitizeVersion` constrains to
62
+ * `[A-Za-z0-9._-]` (no shell metacharacters). POSIX never uses this — it keeps
63
+ * the shell-free, array-based `execFile`, which has no concatenation at all.
64
+ */
65
+ export declare function npmShellCommand(binary: string, args: string[]): string;
55
66
  /** The npm package spec to install for a target version. */
56
67
  export declare function installSpec(version: string): string;
57
68
  /** The `npm <args>` (after the `npm` binary) that installs the target globally. */
package/dist/update.js CHANGED
@@ -15,6 +15,7 @@ exports.parseUpdateFlags = parseUpdateFlags;
15
15
  exports.sanitizeVersion = sanitizeVersion;
16
16
  exports.npmBinary = npmBinary;
17
17
  exports.npmSpawnShell = npmSpawnShell;
18
+ exports.npmShellCommand = npmShellCommand;
18
19
  exports.installSpec = installSpec;
19
20
  exports.npmInstallArgs = npmInstallArgs;
20
21
  exports.globalPrefixFromPackageDir = globalPrefixFromPackageDir;
@@ -88,6 +89,19 @@ function npmBinary(platform = process.platform) {
88
89
  function npmSpawnShell(platform = process.platform) {
89
90
  return platform === "win32";
90
91
  }
92
+ /**
93
+ * The single command string used to launch npm on platforms that must go through
94
+ * a shell (Windows — see `npmSpawnShell`). Node 22+ deprecates passing an args
95
+ * *array* together with `shell: true` (DEP0190: the args are concatenated, not
96
+ * escaped), so the shell path builds one string and runs it via `execSync`
97
+ * instead. Joining with spaces is safe here because every token is a fixed
98
+ * literal except the version, which `sanitizeVersion` constrains to
99
+ * `[A-Za-z0-9._-]` (no shell metacharacters). POSIX never uses this — it keeps
100
+ * the shell-free, array-based `execFile`, which has no concatenation at all.
101
+ */
102
+ function npmShellCommand(binary, args) {
103
+ return [binary, ...args].join(" ");
104
+ }
91
105
  /** The npm package spec to install for a target version. */
92
106
  function installSpec(version) {
93
107
  return !version || version === "latest" ? "rastack" : `rastack@${version}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rastack",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
4
4
  "description": "",
5
5
  "main": "runtime.ts",
6
6
  "types": "runtime.ts",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "scripts": {
15
15
  "format": "prettier --write \"**/*.{ts,tsx,md}\"",
16
- "build": "tsc && cp src/.prettierrc dist/.prettierrc",
16
+ "build": "tsc && cp src/.prettierrc dist/.prettierrc && node scripts/copy-templates.js",
17
17
  "start": "node dist/index.js",
18
18
  "test": "jest"
19
19
  },
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copy the canonical CloudFormation templates (deploy/cloudformation/*.yaml + the
4
+ * Lambda placeholder bundle) into the built package so `rastack init` /
5
+ * `rastack bootstrap` can deploy them from ANY repo.
6
+ *
7
+ * A consumer repo (the demo, any app) deliberately does not carry a copy of the
8
+ * framework IaC (deploy/cloudformation/README.md), so the CLI ships the templates
9
+ * itself. `resolveDeployAsset` (src/deploy/io.ts) reads them from
10
+ * `dist/templates/cloudformation/`, which this script populates from the single
11
+ * source of truth at repo-root `deploy/cloudformation/`.
12
+ */
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+
16
+ const repoRoot = path.resolve(__dirname, "..", "..");
17
+ const srcDir = path.join(repoRoot, "deploy", "cloudformation");
18
+ const destDir = path.join(__dirname, "..", "dist", "templates", "cloudformation");
19
+
20
+ const assets = [
21
+ "app.yaml",
22
+ "bootstrap.yaml",
23
+ path.join("placeholder", "bootstrap"),
24
+ ];
25
+
26
+ fs.mkdirSync(path.join(destDir, "placeholder"), { recursive: true });
27
+
28
+ for (const rel of assets) {
29
+ const src = path.join(srcDir, rel);
30
+ const dest = path.join(destDir, rel);
31
+ if (!fs.existsSync(src)) {
32
+ console.error(`copy-templates: missing source ${src}`);
33
+ process.exit(1);
34
+ }
35
+ fs.copyFileSync(src, dest);
36
+ // Preserve the executable bit on the Lambda placeholder binary.
37
+ if (rel.endsWith("bootstrap")) fs.chmodSync(dest, 0o755);
38
+ }
39
+
40
+ console.log(
41
+ `copy-templates: bundled ${assets.length} CloudFormation asset(s) into dist/templates/cloudformation`,
42
+ );
@@ -314,7 +314,14 @@ export function stackParameters(cfg: DeployConfig): StackParameter[] {
314
314
  return params;
315
315
  }
316
316
 
317
- /** The in-repo CloudFormation templates `rastack init` deploys from. */
317
+ /**
318
+ * The canonical repo-relative CloudFormation template paths. These are the paths
319
+ * in the *framework* repo (theserverkid/reactapistack) and the default the argv
320
+ * builders below embed. A **consumer** repo (the demo, any app) deliberately does
321
+ * NOT carry a copy of the framework IaC — the templates ship inside the `rastack`
322
+ * package instead — so `init`/`bootstrap` resolve the real on-disk path via
323
+ * `resolveDeployAsset` (io.ts) and pass it in as the `templateFile` override.
324
+ */
318
325
  export const APP_TEMPLATE_FILE = "deploy/cloudformation/app.yaml";
319
326
  export const BOOTSTRAP_TEMPLATE_FILE = "deploy/cloudformation/bootstrap.yaml";
320
327
 
@@ -327,7 +334,8 @@ export const BOOTSTRAP_TEMPLATE_FILE = "deploy/cloudformation/bootstrap.yaml";
327
334
  * accepts *only* a local template file. `--template-url` is exclusive to the
328
335
  * low-level `create-stack`/`update-stack`/`create-change-set` commands, so
329
336
  * pairing it with `deploy` fails with `the following arguments are required:
330
- * --template-file`. `init` runs in the repo, where both templates are present.
337
+ * --template-file`. The file is resolved from the `rastack` package (see
338
+ * `resolveDeployAsset`), so a consumer repo need not carry the framework IaC.
331
339
  */
332
340
  function cfnDeployArgs(
333
341
  templateFile: string,
@@ -362,10 +370,17 @@ function awsCommand(args: string[]): string {
362
370
  return `aws ${args.join(" ")}`;
363
371
  }
364
372
 
365
- /** The argv that creates the app's infrastructure (app.yaml). */
366
- export function deployStackArgs(cfg: DeployConfig): string[] {
373
+ /**
374
+ * The argv that creates the app's infrastructure (app.yaml). `templateFile`
375
+ * defaults to the canonical repo-relative path; `init` passes the resolved
376
+ * package-bundled path so it works from a consumer repo that carries no IaC.
377
+ */
378
+ export function deployStackArgs(
379
+ cfg: DeployConfig,
380
+ templateFile: string = APP_TEMPLATE_FILE,
381
+ ): string[] {
367
382
  return cfnDeployArgs(
368
- APP_TEMPLATE_FILE,
383
+ templateFile,
369
384
  cfg.stackName,
370
385
  cfg.region,
371
386
  stackParameters(cfg),
@@ -377,8 +392,11 @@ export function deployStackArgs(cfg: DeployConfig): string[] {
377
392
  * infrastructure (app.yaml), from the in-repo template. `rastack init` runs this
378
393
  * for you; it is also printed by `--print` so it can be run by hand.
379
394
  */
380
- export function deployStackCommand(cfg: DeployConfig): string {
381
- return awsCommand(deployStackArgs(cfg));
395
+ export function deployStackCommand(
396
+ cfg: DeployConfig,
397
+ templateFile: string = APP_TEMPLATE_FILE,
398
+ ): string {
399
+ return awsCommand(deployStackArgs(cfg, templateFile));
382
400
  }
383
401
 
384
402
  /**
@@ -410,13 +428,19 @@ export function bootstrapStackParameters(
410
428
  return params;
411
429
  }
412
430
 
413
- /** The argv that creates the bootstrap stack (bootstrap.yaml). */
431
+ /**
432
+ * The argv that creates the bootstrap stack (bootstrap.yaml). `templateFile`
433
+ * defaults to the canonical repo-relative path; `init`/`bootstrap` pass the
434
+ * resolved package-bundled path so it works from a consumer repo that carries no
435
+ * IaC.
436
+ */
414
437
  export function bootstrapStackArgs(
415
438
  cfg: DeployConfig,
416
439
  reuseOidc = false,
440
+ templateFile: string = BOOTSTRAP_TEMPLATE_FILE,
417
441
  ): string[] {
418
442
  return cfnDeployArgs(
419
- BOOTSTRAP_TEMPLATE_FILE,
443
+ templateFile,
420
444
  bootstrapStackName(cfg),
421
445
  cfg.region,
422
446
  bootstrapStackParameters(cfg, reuseOidc),
@@ -427,8 +451,9 @@ export function bootstrapStackArgs(
427
451
  export function bootstrapStackCommand(
428
452
  cfg: DeployConfig,
429
453
  reuseOidc = false,
454
+ templateFile: string = BOOTSTRAP_TEMPLATE_FILE,
430
455
  ): string {
431
- return awsCommand(bootstrapStackArgs(cfg, reuseOidc));
456
+ return awsCommand(bootstrapStackArgs(cfg, reuseOidc, templateFile));
432
457
  }
433
458
 
434
459
  /** Stack Output keys `init` reads back off the bootstrap stack. */
package/src/deploy/io.ts CHANGED
@@ -18,6 +18,38 @@ export function configPath(cwd = process.cwd()): string {
18
18
  return path.join(cwd, CONFIG_PATH);
19
19
  }
20
20
 
21
+ /**
22
+ * Resolve a CloudFormation asset (`app.yaml`, `bootstrap.yaml`, or the
23
+ * `placeholder/bootstrap` Lambda bundle) to an absolute on-disk path.
24
+ *
25
+ * The templates are the **framework's** IaC, not the consumer's — the demo repo
26
+ * and every future app repo deliberately do not carry a copy (see
27
+ * deploy/cloudformation/README.md). So `init`/`bootstrap`, which run in those
28
+ * repos, cannot assume `deploy/cloudformation/<asset>` exists relative to the
29
+ * cwd — that is exactly the `Invalid template path deploy/cloudformation/…`
30
+ * failure. Instead the assets ship *inside* the `rastack` package (copied into
31
+ * `dist/templates/cloudformation/` at build time) and are resolved from the
32
+ * install location, with fallbacks so running from source or from the framework
33
+ * repo itself still works. Returns null when no candidate exists.
34
+ */
35
+ export function resolveDeployAsset(
36
+ relPath: string,
37
+ cwd = process.cwd(),
38
+ ): string | null {
39
+ const candidates = [
40
+ // Installed package: dist/deploy/io.js → dist/templates/cloudformation/…
41
+ path.join(__dirname, "..", "templates", "cloudformation", relPath),
42
+ // Running from TS source: tools/src/deploy/io.ts → repo-root deploy/…
43
+ path.join(__dirname, "..", "..", "..", "deploy", "cloudformation", relPath),
44
+ // Invoked inside the framework repo itself (cwd is the repo root).
45
+ path.join(cwd, "deploy", "cloudformation", relPath),
46
+ ];
47
+ for (const candidate of candidates) {
48
+ if (fs.existsSync(candidate)) return candidate;
49
+ }
50
+ return null;
51
+ }
52
+
21
53
  /** Read + normalize the deploy config, or null when it does not exist yet. */
22
54
  export function loadConfig(cwd = process.cwd()): DeployConfig | null {
23
55
  const file = configPath(cwd);
@@ -126,8 +158,8 @@ export function uploadPlaceholder(
126
158
  region: string,
127
159
  cwd = process.cwd(),
128
160
  ): boolean {
129
- const src = path.join(cwd, "deploy/cloudformation/placeholder/bootstrap");
130
- if (!fs.existsSync(src)) return false;
161
+ const src = resolveDeployAsset("placeholder/bootstrap", cwd);
162
+ if (!src) return false;
131
163
  const zip = zipStore([
132
164
  { name: "bootstrap", data: fs.readFileSync(src), mode: 0o755 },
133
165
  ]);