rastack 0.0.40 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/deploy/config.d.ts +23 -7
- package/dist/deploy/config.js +29 -12
- package/dist/deploy/io.d.ts +15 -0
- package/dist/deploy/io.js +32 -2
- package/dist/rastack-bootstrap.d.ts +2 -1
- package/dist/rastack-bootstrap.js +14 -6
- package/dist/rastack-init.js +20 -6
- package/dist/rastack-update.js +12 -3
- package/dist/templates/cloudformation/app.yaml +635 -0
- package/dist/templates/cloudformation/bootstrap.yaml +342 -0
- package/dist/templates/cloudformation/placeholder/bootstrap +28 -0
- package/dist/update.d.ts +21 -0
- package/dist/update.js +31 -2
- package/package.json +2 -2
- package/scripts/copy-templates.js +42 -0
- package/src/deploy/config.ts +35 -10
- package/src/deploy/io.ts +34 -2
- package/src/rastack-bootstrap.ts +17 -6
- package/src/rastack-init.ts +25 -6
- package/src/rastack-update.ts +14 -3
- package/src/update.ts +31 -2
- package/test/deploy.spec.ts +53 -0
- package/test/update.spec.ts +35 -0
package/src/rastack-bootstrap.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* [--region <r>] [--prefix <p>]
|
|
10
10
|
* [--reuse-oidc] [--print] [--yes]
|
|
11
11
|
*
|
|
12
|
-
* It deploys `
|
|
12
|
+
* It deploys the `bootstrap.yaml` template bundled with the `rastack` package
|
|
13
|
+
* (resolved by `resolveDeployAsset`, so it works from any repo), which creates:
|
|
13
14
|
* • the S3 buckets that hold the Lambda placeholder + hosted templates, and
|
|
14
15
|
* • the GitHub OIDC provider + trust that lets CI assume a deploy role.
|
|
15
16
|
* It then uploads the Lambda placeholder bundle to the artifacts bucket, so the
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
canPrompt,
|
|
44
45
|
gitRemoteUrl,
|
|
45
46
|
loadConfig,
|
|
47
|
+
resolveDeployAsset,
|
|
46
48
|
stackOutput,
|
|
47
49
|
uploadPlaceholder,
|
|
48
50
|
withPrompt,
|
|
@@ -152,8 +154,9 @@ function printPlan(cfg: DeployConfig, reuseOidc: boolean): void {
|
|
|
152
154
|
console.log(
|
|
153
155
|
`\n Bootstrap plan (${bootstrapStackName(cfg)} @ ${cfg.region})\n`,
|
|
154
156
|
);
|
|
157
|
+
const template = resolveDeployAsset("bootstrap.yaml") ?? undefined;
|
|
155
158
|
console.log(" ── Tier-0 bootstrap stack (bootstrap.yaml) ──");
|
|
156
|
-
console.log("\n " + bootstrapStackCommand(cfg, reuseOidc) + "\n");
|
|
159
|
+
console.log("\n " + bootstrapStackCommand(cfg, reuseOidc, template) + "\n");
|
|
157
160
|
console.log(
|
|
158
161
|
" Then `rastack init` (or `rastack bootstrap` again) resolves this stack's\n" +
|
|
159
162
|
" ArtifactsBucket to create app backends. Nothing here is app-specific.\n",
|
|
@@ -166,8 +169,16 @@ function printPlan(cfg: DeployConfig, reuseOidc: boolean): void {
|
|
|
166
169
|
* already ask to reuse it.
|
|
167
170
|
*/
|
|
168
171
|
function deployBootstrap(cfg: DeployConfig, reuseOidc: boolean): void {
|
|
172
|
+
const template = resolveDeployAsset("bootstrap.yaml");
|
|
173
|
+
if (!template) {
|
|
174
|
+
console.error(
|
|
175
|
+
" ✗ bootstrap.yaml template not found in the rastack package. Reinstall\n" +
|
|
176
|
+
" rastack (`npm i -g rastack`) or run from the framework repo.",
|
|
177
|
+
);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
169
180
|
try {
|
|
170
|
-
awsRun(bootstrapStackArgs(cfg, reuseOidc));
|
|
181
|
+
awsRun(bootstrapStackArgs(cfg, reuseOidc, template));
|
|
171
182
|
} catch (err) {
|
|
172
183
|
if (reuseOidc) {
|
|
173
184
|
console.error(
|
|
@@ -182,7 +193,7 @@ function deployBootstrap(cfg: DeployConfig, reuseOidc: boolean): void {
|
|
|
182
193
|
" • retrying bootstrap, reusing the account's existing GitHub OIDC provider",
|
|
183
194
|
);
|
|
184
195
|
try {
|
|
185
|
-
awsRun(bootstrapStackArgs(cfg, true));
|
|
196
|
+
awsRun(bootstrapStackArgs(cfg, true, template));
|
|
186
197
|
} catch (retryErr) {
|
|
187
198
|
console.error(
|
|
188
199
|
`\n ✗ bootstrap stack deploy failed.\n ${(retryErr as Error).message}`,
|
|
@@ -237,8 +248,8 @@ function standUp(cfg: DeployConfig, reuseOidc: boolean): void {
|
|
|
237
248
|
console.log("\n▸ Uploading the Lambda placeholder bundle");
|
|
238
249
|
if (!uploadPlaceholder(artifactsBucket, cfg.region)) {
|
|
239
250
|
console.error(
|
|
240
|
-
" ✗
|
|
241
|
-
" was not uploaded. `rastack init` will upload it on the first app deploy.",
|
|
251
|
+
" ✗ Lambda placeholder bundle not found in the rastack package — the\n" +
|
|
252
|
+
" placeholder was not uploaded. `rastack init` will upload it on the first app deploy.",
|
|
242
253
|
);
|
|
243
254
|
} else {
|
|
244
255
|
console.log(" ✓ placeholder uploaded");
|
package/src/rastack-init.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
ghAvailable,
|
|
57
57
|
gitRemoteUrl,
|
|
58
58
|
loadConfig,
|
|
59
|
+
resolveDeployAsset,
|
|
59
60
|
saveConfig,
|
|
60
61
|
setRepoVariable,
|
|
61
62
|
stackOutput,
|
|
@@ -209,13 +210,15 @@ function printPlan(cfg: DeployConfig): void {
|
|
|
209
210
|
const withBucket = mergeConfig(cfg, {
|
|
210
211
|
artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
|
|
211
212
|
});
|
|
213
|
+
const bootstrapTemplate = resolveDeployAsset("bootstrap.yaml") ?? undefined;
|
|
214
|
+
const appTemplate = resolveDeployAsset("app.yaml") ?? undefined;
|
|
212
215
|
console.log(`\n Deployment plan (${cfg.stackName} @ ${cfg.region})\n`);
|
|
213
216
|
console.log(
|
|
214
217
|
` ── 1. Bootstrap stack (${bootstrapStackName(cfg)}) — run only if it does not exist ──`,
|
|
215
218
|
);
|
|
216
|
-
console.log("\n " + bootstrapStackCommand(cfg) + "\n");
|
|
219
|
+
console.log("\n " + bootstrapStackCommand(cfg, false, bootstrapTemplate) + "\n");
|
|
217
220
|
console.log(" ── 2. Per-app backend (app.yaml) ──");
|
|
218
|
-
console.log("\n " + deployStackCommand(withBucket) + "\n");
|
|
221
|
+
console.log("\n " + deployStackCommand(withBucket, appTemplate) + "\n");
|
|
219
222
|
console.log(
|
|
220
223
|
" ── 3. Wire these GitHub repo Variables from the stack Outputs ──",
|
|
221
224
|
);
|
|
@@ -285,7 +288,7 @@ async function standUp(
|
|
|
285
288
|
console.log("\n▸ Uploading the Lambda placeholder bundle");
|
|
286
289
|
if (!uploadPlaceholder(artifactsBucket, cfg.region)) {
|
|
287
290
|
console.error(
|
|
288
|
-
" ✗
|
|
291
|
+
" ✗ Lambda placeholder bundle not found in the rastack package — cannot create the function.",
|
|
289
292
|
);
|
|
290
293
|
process.exit(1);
|
|
291
294
|
}
|
|
@@ -295,9 +298,17 @@ async function standUp(
|
|
|
295
298
|
artifactsBucket,
|
|
296
299
|
templatesBucket: templatesBucket ?? undefined,
|
|
297
300
|
});
|
|
301
|
+
const appTemplate = resolveDeployAsset("app.yaml");
|
|
302
|
+
if (!appTemplate) {
|
|
303
|
+
console.error(
|
|
304
|
+
" ✗ app.yaml template not found in the rastack package. Reinstall rastack\n" +
|
|
305
|
+
" (`npm i -g rastack`) or run from the framework repo.",
|
|
306
|
+
);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
298
309
|
console.log(`\n▸ Deploying ${cfg.stackName}`);
|
|
299
310
|
try {
|
|
300
|
-
awsRun(deployStackArgs(cfg));
|
|
311
|
+
awsRun(deployStackArgs(cfg, appTemplate));
|
|
301
312
|
} catch (err) {
|
|
302
313
|
console.error(
|
|
303
314
|
`\n ✗ aws cloudformation deploy failed for ${cfg.stackName}.\n ${(err as Error).message}`,
|
|
@@ -372,8 +383,16 @@ function wireRepoVariables(cfg: DeployConfig, interactive: boolean): void {
|
|
|
372
383
|
|
|
373
384
|
/** Deploy the bootstrap stack, retrying once against an existing OIDC provider. */
|
|
374
385
|
function deployBootstrap(cfg: DeployConfig): void {
|
|
386
|
+
const template = resolveDeployAsset("bootstrap.yaml");
|
|
387
|
+
if (!template) {
|
|
388
|
+
console.error(
|
|
389
|
+
" ✗ bootstrap.yaml template not found in the rastack package. Reinstall\n" +
|
|
390
|
+
" rastack (`npm i -g rastack`) or run from the framework repo.",
|
|
391
|
+
);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
375
394
|
try {
|
|
376
|
-
awsRun(bootstrapStackArgs(cfg));
|
|
395
|
+
awsRun(bootstrapStackArgs(cfg, false, template));
|
|
377
396
|
} catch {
|
|
378
397
|
// The most common cause is an account that already has a GitHub OIDC
|
|
379
398
|
// provider (only one is allowed per URL). Retry telling the template to
|
|
@@ -382,7 +401,7 @@ function deployBootstrap(cfg: DeployConfig): void {
|
|
|
382
401
|
" • retrying bootstrap, reusing the account's existing GitHub OIDC provider",
|
|
383
402
|
);
|
|
384
403
|
try {
|
|
385
|
-
awsRun(bootstrapStackArgs(cfg, true));
|
|
404
|
+
awsRun(bootstrapStackArgs(cfg, true, template));
|
|
386
405
|
} catch (err) {
|
|
387
406
|
console.error(
|
|
388
407
|
`\n ✗ bootstrap stack deploy failed.\n ${(err as Error).message}`,
|
package/src/rastack-update.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
globalPrefixFromPackageDir,
|
|
25
25
|
npmBinary,
|
|
26
26
|
npmInstallArgs,
|
|
27
|
+
npmSpawnShell,
|
|
27
28
|
parseUpdateFlags,
|
|
28
29
|
updateAvailable,
|
|
29
30
|
} from "./update";
|
|
@@ -31,6 +32,13 @@ import {
|
|
|
31
32
|
/** The npm executable for this platform (`npm.cmd` on Windows, else `npm`). */
|
|
32
33
|
const NPM = npmBinary();
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Whether to launch npm through a shell. Required on Windows, where a bare
|
|
37
|
+
* `.cmd` spawn is rejected with `EINVAL` since the Node CVE-2024-27980 fix;
|
|
38
|
+
* false on POSIX so the direct spawn stays injection-free.
|
|
39
|
+
*/
|
|
40
|
+
const NPM_SHELL = npmSpawnShell();
|
|
41
|
+
|
|
34
42
|
/** The installed rastack package directory (…/rastack), one up from dist/. */
|
|
35
43
|
const PKG_DIR = path.join(__dirname, "..");
|
|
36
44
|
const PKG_JSON = path.join(PKG_DIR, "package.json");
|
|
@@ -49,6 +57,7 @@ function npmCapture(args: string[]): string | null {
|
|
|
49
57
|
try {
|
|
50
58
|
return execFileSync(NPM, args, {
|
|
51
59
|
stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
+
shell: NPM_SHELL,
|
|
52
61
|
})
|
|
53
62
|
.toString()
|
|
54
63
|
.trim();
|
|
@@ -103,10 +112,12 @@ function main(argv: string[]): void {
|
|
|
103
112
|
`\n▸ Updating rastack (${current} → ${flags.version}) in ${prefix}\n`,
|
|
104
113
|
);
|
|
105
114
|
try {
|
|
106
|
-
// Re-runs the developer's own npm to update this global package
|
|
107
|
-
//
|
|
115
|
+
// Re-runs the developer's own npm to update this global package. On Windows
|
|
116
|
+
// the `.cmd` shim is launched through a shell (EINVAL otherwise); the only
|
|
117
|
+
// caller-supplied token — the version — is sanitized in `parseUpdateFlags`,
|
|
118
|
+
// and every other arg is a fixed literal, so nothing injectable reaches it.
|
|
108
119
|
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
109
|
-
execFileSync(NPM, args, { stdio: "inherit", env });
|
|
120
|
+
execFileSync(NPM, args, { stdio: "inherit", env, shell: NPM_SHELL });
|
|
110
121
|
} catch (err) {
|
|
111
122
|
console.error(
|
|
112
123
|
`\n ✗ npm failed to update rastack. Re-run for detail:\n npm install -g ${npmInstallArgs(flags.version)[2]}\n ${(err as Error).message}`,
|
package/src/update.ts
CHANGED
|
@@ -40,27 +40,56 @@ export function parseUpdateFlags(argv: string[]): UpdateFlags {
|
|
|
40
40
|
break;
|
|
41
41
|
case "--version":
|
|
42
42
|
case "--to":
|
|
43
|
-
version = argv[++i]
|
|
43
|
+
version = sanitizeVersion(argv[++i]);
|
|
44
44
|
break;
|
|
45
45
|
default:
|
|
46
46
|
// A bare, non-flag token is taken as the target version.
|
|
47
|
-
if (!a.startsWith("-")) version = a;
|
|
47
|
+
if (!a.startsWith("-")) version = sanitizeVersion(a);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
return { version, check, dryRun };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Constrain the update target to a safe npm version or dist-tag. The Windows
|
|
55
|
+
* update path runs npm through a shell (see `npmSpawnShell`), so the target —
|
|
56
|
+
* the one caller-supplied token that reaches the command line — must not be
|
|
57
|
+
* able to carry shell metacharacters. A valid npm spec starts alphanumeric and
|
|
58
|
+
* uses only `A-Za-z0-9._-` (e.g. `latest`, `next`, `0.0.40`, `1.2.3-beta.1`);
|
|
59
|
+
* anything else falls back to `latest`.
|
|
60
|
+
*/
|
|
61
|
+
export function sanitizeVersion(version: string | undefined): string {
|
|
62
|
+
return version && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(version)
|
|
63
|
+
? version
|
|
64
|
+
: "latest";
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
/**
|
|
54
68
|
* The npm executable to spawn for a platform. On Windows npm is a `.cmd` shim,
|
|
55
69
|
* not a real executable, so `execFile("npm", …)` (no shell) fails with
|
|
56
70
|
* `spawnSync npm ENOENT` — the direct-spawn path only resolves real binaries,
|
|
57
71
|
* never `PATHEXT` scripts. Spawn `npm.cmd` there; POSIX keeps plain `npm`.
|
|
58
72
|
* (`aws` is unaffected — it installs as `aws.exe`, a real executable.)
|
|
73
|
+
*
|
|
74
|
+
* The `.cmd` must still be launched through a shell — see `npmSpawnShell`.
|
|
59
75
|
*/
|
|
60
76
|
export function npmBinary(platform: string = process.platform): string {
|
|
61
77
|
return platform === "win32" ? "npm.cmd" : "npm";
|
|
62
78
|
}
|
|
63
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Whether npm must be spawned through a shell on this platform. Since the Node
|
|
82
|
+
* fix for CVE-2024-27980 (18.20.2 / 20.12.2 / 21.7.2), spawning a `.cmd` shim
|
|
83
|
+
* like `npm.cmd` *without* a shell is rejected with `spawnSync npm.cmd EINVAL`
|
|
84
|
+
* — the very failure a Windows/PowerShell `rastack update` hits. cmd.exe knows
|
|
85
|
+
* how to run a `.cmd`, so Windows spawns through the shell; POSIX keeps the
|
|
86
|
+
* direct, shell-free spawn (npm is a real script resolved on PATH there, and no
|
|
87
|
+
* shell means no injection surface).
|
|
88
|
+
*/
|
|
89
|
+
export function npmSpawnShell(platform: string = process.platform): boolean {
|
|
90
|
+
return platform === "win32";
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
/** The npm package spec to install for a target version. */
|
|
65
94
|
export function installSpec(version: string): string {
|
|
66
95
|
return !version || version === "latest" ? "rastack" : `rastack@${version}`;
|
package/test/deploy.spec.ts
CHANGED
|
@@ -243,6 +243,19 @@ describe("stack parameters + launch command", () => {
|
|
|
243
243
|
"deploy",
|
|
244
244
|
]);
|
|
245
245
|
});
|
|
246
|
+
|
|
247
|
+
it("accepts a template-file override (the package-resolved path)", () => {
|
|
248
|
+
// `init` runs in a consumer repo that carries no framework IaC, so it passes
|
|
249
|
+
// the path the template was resolved to inside the `rastack` package.
|
|
250
|
+
const cfg = base({ artifactsBucket: "art" });
|
|
251
|
+
const abs = "/opt/rastack/dist/templates/cloudformation/app.yaml";
|
|
252
|
+
expect(deployStackArgs(cfg, abs)).toContain(abs);
|
|
253
|
+
expect(deployStackCommand(cfg, abs)).toContain(`--template-file ${abs}`);
|
|
254
|
+
// Default is unchanged when no override is given.
|
|
255
|
+
expect(deployStackCommand(cfg)).toContain(
|
|
256
|
+
"--template-file deploy/cloudformation/app.yaml",
|
|
257
|
+
);
|
|
258
|
+
});
|
|
246
259
|
});
|
|
247
260
|
|
|
248
261
|
describe("bootstrap stack", () => {
|
|
@@ -281,6 +294,21 @@ describe("bootstrap stack", () => {
|
|
|
281
294
|
expect(`aws ${bootstrapStackArgs(base()).join(" ")}`).toBe(cmd);
|
|
282
295
|
});
|
|
283
296
|
|
|
297
|
+
it("accepts a template-file override (the package-resolved path)", () => {
|
|
298
|
+
const abs = "/opt/rastack/dist/templates/cloudformation/bootstrap.yaml";
|
|
299
|
+
expect(bootstrapStackArgs(base(), false, abs)).toContain(abs);
|
|
300
|
+
expect(bootstrapStackCommand(base(), true, abs)).toContain(
|
|
301
|
+
`--template-file ${abs}`,
|
|
302
|
+
);
|
|
303
|
+
// Reuse-OIDC + override compose without disturbing the default path.
|
|
304
|
+
expect(bootstrapStackArgs(base(), true, abs)).toContain(
|
|
305
|
+
"CreateOIDCProvider=false",
|
|
306
|
+
);
|
|
307
|
+
expect(bootstrapStackCommand(base())).toContain(
|
|
308
|
+
"--template-file deploy/cloudformation/bootstrap.yaml",
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
284
312
|
it("validates only the account-level fields, not the per-app ones", () => {
|
|
285
313
|
// A config with no `app` is fine for bootstrap — it never touches app.yaml.
|
|
286
314
|
const cfg = normalizeConfig({
|
|
@@ -513,3 +541,28 @@ describe("scaffold", () => {
|
|
|
513
541
|
expect(files.map((f) => f.path)).toEqual([CONFIG_PATH]);
|
|
514
542
|
});
|
|
515
543
|
});
|
|
544
|
+
|
|
545
|
+
describe("resolveDeployAsset — bundled template resolution", () => {
|
|
546
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
547
|
+
const { resolveDeployAsset } = require("../src/deploy/io");
|
|
548
|
+
const path = require("path");
|
|
549
|
+
|
|
550
|
+
it("resolves the framework templates to an existing file", () => {
|
|
551
|
+
for (const name of ["app.yaml", "bootstrap.yaml"]) {
|
|
552
|
+
const resolved = resolveDeployAsset(name);
|
|
553
|
+
expect(resolved).not.toBeNull();
|
|
554
|
+
expect(path.isAbsolute(resolved)).toBe(true);
|
|
555
|
+
expect(resolved.endsWith(name)).toBe(true);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("resolves the Lambda placeholder bundle", () => {
|
|
560
|
+
const resolved = resolveDeployAsset(path.join("placeholder", "bootstrap"));
|
|
561
|
+
expect(resolved).not.toBeNull();
|
|
562
|
+
expect(resolved.endsWith(path.join("placeholder", "bootstrap"))).toBe(true);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("returns null for an asset that does not exist", () => {
|
|
566
|
+
expect(resolveDeployAsset("does-not-exist.yaml")).toBeNull();
|
|
567
|
+
});
|
|
568
|
+
});
|
package/test/update.spec.ts
CHANGED
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
installSpec,
|
|
5
5
|
npmBinary,
|
|
6
6
|
npmInstallArgs,
|
|
7
|
+
npmSpawnShell,
|
|
7
8
|
parseUpdateFlags,
|
|
9
|
+
sanitizeVersion,
|
|
8
10
|
updateAvailable,
|
|
9
11
|
} from "../src/update";
|
|
10
12
|
|
|
@@ -56,6 +58,39 @@ describe("update — npm spec + args", () => {
|
|
|
56
58
|
expect(npmBinary("linux")).toBe("npm");
|
|
57
59
|
expect(npmBinary("darwin")).toBe("npm");
|
|
58
60
|
});
|
|
61
|
+
|
|
62
|
+
it("runs npm through a shell only on Windows", () => {
|
|
63
|
+
// A `.cmd` shim can't be spawned directly since the Node CVE-2024-27980 fix
|
|
64
|
+
// (spawnSync npm.cmd EINVAL) — it must go through cmd.exe. POSIX spawns npm
|
|
65
|
+
// directly so there is no shell and no injection surface.
|
|
66
|
+
expect(npmSpawnShell("win32")).toBe(true);
|
|
67
|
+
expect(npmSpawnShell("linux")).toBe(false);
|
|
68
|
+
expect(npmSpawnShell("darwin")).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("update — version sanitization", () => {
|
|
73
|
+
it("keeps valid semver, dist-tags, and pinned versions", () => {
|
|
74
|
+
expect(sanitizeVersion("latest")).toBe("latest");
|
|
75
|
+
expect(sanitizeVersion("next")).toBe("next");
|
|
76
|
+
expect(sanitizeVersion("0.0.40")).toBe("0.0.40");
|
|
77
|
+
expect(sanitizeVersion("1.2.3-beta.1")).toBe("1.2.3-beta.1");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("falls back to latest for empty or shell-unsafe input", () => {
|
|
81
|
+
expect(sanitizeVersion(undefined)).toBe("latest");
|
|
82
|
+
expect(sanitizeVersion("")).toBe("latest");
|
|
83
|
+
// Shell metacharacters must never reach the Windows shell-spawned npm.
|
|
84
|
+
expect(sanitizeVersion("0.0.40; rm -rf /")).toBe("latest");
|
|
85
|
+
expect(sanitizeVersion("$(whoami)")).toBe("latest");
|
|
86
|
+
expect(sanitizeVersion("a b")).toBe("latest");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("is applied when parsing the bare positional and --version", () => {
|
|
90
|
+
expect(parseUpdateFlags(["0.0.40; evil"]).version).toBe("latest");
|
|
91
|
+
expect(parseUpdateFlags(["--version", "&& evil"]).version).toBe("latest");
|
|
92
|
+
expect(parseUpdateFlags(["0.0.40"]).version).toBe("0.0.40");
|
|
93
|
+
});
|
|
59
94
|
});
|
|
60
95
|
|
|
61
96
|
describe("update — global prefix derivation", () => {
|