@ship.zone/szci 7.1.5 → 7.1.6

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-07-28 - 7.1.6
4
+
5
+ ### Fixes
6
+
7
+ - validate npm credentials before publishing (npm)
8
+ - Validate all SZCI_TOKEN_NPM values before logging or writing .npmrc
9
+ - Reject malformed registry, encoding, or token values without exposing credential input
10
+ - Await credential preparation and reuse the validated registry list during npm publish
11
+ - remove duplicate pending npm credential fix entries (changelog)
12
+ - Consolidate repeated pending fix bullets under the existing npm credential validation entry.
13
+
3
14
  ## 2026-07-28 - 7.1.5
4
15
 
5
16
  ### Fixes
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship.zone/szci",
3
- "version": "7.1.5",
3
+ "version": "7.1.6",
4
4
  "exports": "./mod.ts",
5
5
  "nodeModulesDir": "auto",
6
6
  "tasks": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship.zone/szci",
3
- "version": "7.1.5",
3
+ "version": "7.1.6",
4
4
  "description": "Serve Zone CI - A tool to streamline Node.js and Docker workflows within CI environments, particularly GitLab CI, providing various CI/CD utilities. Powered by Deno with standalone executables.",
5
5
  "keywords": [
6
6
  "Node.js",
package/readme.md CHANGED
@@ -199,6 +199,9 @@ SZCI_TOKEN_NPM_1="registry.npmjs.org|dGhlLXRva2VuLWhlcmU="
199
199
  SZCI_TOKEN_NPM_2="verdaccio.example.com|the-token-here|plain"
200
200
  ```
201
201
 
202
+ SZCI validates every configured npm credential before writing `.npmrc`. Malformed
203
+ registry, encoding, or token values fail without logging the credential.
204
+
202
205
  ### SSH Keys
203
206
 
204
207
  Pipe-delimited format: `host|privKeyBase64|pubKeyBase64`
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@ship.zone/szci',
6
- version: '7.1.5',
6
+ version: '7.1.6',
7
7
  description: 'Serve Zone CI - A tool to streamline Node.js and Docker workflows within CI environments, particularly GitLab CI, providing various CI/CD utilities. Powered by Deno with standalone executables.'
8
8
  }
@@ -4,6 +4,11 @@ import * as paths from '../szci.paths.ts';
4
4
  import { logger } from '../szci.logging.ts';
5
5
  import { bash, bashNoError, nvmAvailable } from '../szci.bash.ts';
6
6
  import { Szci } from '../szci.classes.szci.ts';
7
+ import {
8
+ type INpmCredential,
9
+ parseNpmCredentialEnvironment,
10
+ validateNpmRegistryUrl,
11
+ } from './npmcredential.ts';
7
12
 
8
13
  export class SzciNpmManager {
9
14
  public szciRef: Szci;
@@ -42,7 +47,7 @@ export class SzciNpmManager {
42
47
  } else {
43
48
  logger.log(
44
49
  'info',
45
- `>>szci npm ...<< cli arguments invalid... Please read the documentation.`
50
+ `>>szci npm ...<< cli arguments invalid... Please read the documentation.`,
46
51
  );
47
52
  Deno.exit(1);
48
53
  }
@@ -51,66 +56,55 @@ export class SzciNpmManager {
51
56
  /**
52
57
  * authenticates npm with token from env var
53
58
  */
54
- public async prepare() {
59
+ public async prepare(): Promise<INpmCredential[]> {
55
60
  logger.log('info', 'running >>npm prepare<<');
56
61
  const config = this.szciRef.szciConfig.getConfig();
57
- let npmrcFileString: string = '';
58
- await plugins.smartobject.forEachMinimatch(
62
+ const npmCredentials = parseNpmCredentialEnvironment(
59
63
  Deno.env.toObject(),
60
- 'SZCI_TOKEN_NPM*',
61
- (npmEnvArg: string) => {
62
- if (!npmEnvArg) {
63
- logger.log('note','found empty token...');
64
- return;
65
- }
66
- const npmRegistryUrl = npmEnvArg.split('|')[0];
67
- logger.log('ok', `found token for ${npmRegistryUrl}`);
68
- let npmToken = npmEnvArg.split('|')[1];
69
- if (npmEnvArg.split('|')[2] && npmEnvArg.split('|')[2] === 'plain') {
70
- logger.log('ok', 'npm token not base64 encoded.');
71
- } else {
72
- logger.log('ok', 'npm token base64 encoded.');
73
- npmToken = plugins.smartstring.base64.decode(npmToken);
74
- }
75
- npmrcFileString += `//${npmRegistryUrl}/:_authToken="${npmToken}"\n`;
76
- }
77
64
  );
78
- logger.log('info', `setting default npm registry to ${config.npmRegistryUrl}`);
79
- npmrcFileString += `registry=https://${config.npmRegistryUrl}\n`;
65
+ if (npmCredentials.length === 0) {
66
+ throw new Error('No SZCI_TOKEN_NPM credentials were provided.');
67
+ }
80
68
 
81
- // final check
82
- if (npmrcFileString.length > 0) {
83
- logger.log('info', 'found one or more access tokens');
84
- } else {
85
- logger.log('error', 'no access token found! Exiting!');
86
- Deno.exit(1);
69
+ let npmrcFileString: string = '';
70
+ for (const npmCredential of npmCredentials) {
71
+ logger.log('ok', `found token for ${npmCredential.registryUrl}`);
72
+ logger.log(
73
+ 'ok',
74
+ npmCredential.tokenEncoding === 'plain'
75
+ ? 'npm token not base64 encoded.'
76
+ : 'npm token base64 encoded.',
77
+ );
78
+ npmrcFileString += `//${npmCredential.registryUrl}/:_authToken="${npmCredential.token}"\n`;
87
79
  }
88
80
 
81
+ const defaultRegistryUrl = validateNpmRegistryUrl(config.npmRegistryUrl);
82
+ logger.log('info', `setting default npm registry to ${defaultRegistryUrl}`);
83
+ npmrcFileString += `registry=https://${defaultRegistryUrl}\n`;
84
+ logger.log('info', 'found one or more access tokens');
85
+
89
86
  // lets save it to disk
90
87
  plugins.smartfile.memory.toFsSync(npmrcFileString, '/root/.npmrc');
91
88
 
92
89
  // lets set the cache directory
93
90
  await bash(`npm config set cache ${paths.SzciCacheDir} --global `);
94
91
 
95
- return;
92
+ return npmCredentials;
96
93
  }
97
94
 
98
95
  /**
99
96
  * publish a package to npm
100
97
  */
101
98
  public async publish() {
102
- const buildPublishCommand = async () => {
99
+ const buildPublishCommand = async (
100
+ npmCredentialsArg: INpmCredential[],
101
+ ) => {
103
102
  let npmAccessCliString = ``;
104
103
  let npmRegistryCliString = ``;
105
104
  let publishVerdaccioAsWell = false;
106
105
  const config = this.szciRef.szciConfig.getConfig();
107
- const availableRegistries: string[] = [];
108
- await plugins.smartobject.forEachMinimatch(
109
- Deno.env.toObject(),
110
- 'SZCI_TOKEN_NPM*',
111
- (npmEnvArg: string) => {
112
- availableRegistries.push(npmEnvArg.split('|')[0]);
113
- }
106
+ const availableRegistries = npmCredentialsArg.map(
107
+ (npmCredentialArg) => npmCredentialArg.registryUrl,
114
108
  );
115
109
 
116
110
  // -> configure package access level
@@ -140,13 +134,14 @@ export class SzciNpmManager {
140
134
  if (verdaccioRegistry) {
141
135
  logger.log(
142
136
  'info',
143
- `package is public and verdaccio registry is specified. Also publishing to Verdaccio!`
137
+ `package is public and verdaccio registry is specified. Also publishing to Verdaccio!`,
144
138
  );
145
- publishCommand = `${publishCommand} && npm publish ${npmAccessCliString} --registry=https://${verdaccioRegistry}`;
139
+ publishCommand =
140
+ `${publishCommand} && npm publish ${npmAccessCliString} --registry=https://${verdaccioRegistry}`;
146
141
  } else {
147
142
  logger.log(
148
143
  'error',
149
- `This package should also be published to Verdaccio, however there is no Verdaccio registry data available!`
144
+ `This package should also be published to Verdaccio, however there is no Verdaccio registry data available!`,
150
145
  );
151
146
  }
152
147
  }
@@ -155,7 +150,7 @@ export class SzciNpmManager {
155
150
 
156
151
  // -> preparing
157
152
  logger.log('info', `now preparing environment:`);
158
- this.prepare();
153
+ const npmCredentials = await this.prepare();
159
154
  await bash(`npm -v`);
160
155
  await bash(`pnpm -v`);
161
156
 
@@ -173,7 +168,7 @@ export class SzciNpmManager {
173
168
 
174
169
  // -> publish it
175
170
  logger.log('info', `now invoking npm to publish the package!`);
176
- await bash(await buildPublishCommand());
171
+ await bash(await buildPublishCommand(npmCredentials));
177
172
  logger.log('success', `Package was successfully published!`);
178
173
  }
179
174
 
@@ -0,0 +1,91 @@
1
+ import * as plugins from './mod.plugins.ts';
2
+
3
+ export interface INpmCredential {
4
+ registryUrl: string;
5
+ token: string;
6
+ tokenEncoding: 'base64' | 'plain';
7
+ }
8
+
9
+ const invalidCredentialError = () =>
10
+ new Error(
11
+ 'Invalid SZCI_TOKEN_NPM credential. Expected registry|token[|plain].',
12
+ );
13
+
14
+ const registryUrlRegex = /^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?$/;
15
+ const unsafeTokenRegex = /[\u0000-\u001f\u007f"\\]/;
16
+
17
+ export const validateNpmRegistryUrl = (registryUrlArg: string): string => {
18
+ if (
19
+ !registryUrlArg ||
20
+ registryUrlArg.length > 512 ||
21
+ !registryUrlRegex.test(registryUrlArg)
22
+ ) {
23
+ throw invalidCredentialError();
24
+ }
25
+ return registryUrlArg;
26
+ };
27
+
28
+ export const parseNpmCredential = (
29
+ npmEnvironmentValueArg: string,
30
+ ): INpmCredential => {
31
+ const fields = npmEnvironmentValueArg.split('|');
32
+ if (fields.length < 2 || fields.length > 3) {
33
+ throw invalidCredentialError();
34
+ }
35
+
36
+ const registryUrl = validateNpmRegistryUrl(fields[0]);
37
+ const encodedToken = fields[1];
38
+ const encodingMarker = fields[2];
39
+ if (
40
+ !encodedToken ||
41
+ (encodingMarker !== undefined && encodingMarker !== 'plain')
42
+ ) {
43
+ throw invalidCredentialError();
44
+ }
45
+
46
+ let token: string;
47
+ let tokenEncoding: INpmCredential['tokenEncoding'];
48
+ if (encodingMarker === 'plain') {
49
+ token = encodedToken;
50
+ tokenEncoding = 'plain';
51
+ } else {
52
+ if (!plugins.smartstring.base64.isBase64(encodedToken)) {
53
+ throw invalidCredentialError();
54
+ }
55
+ token = plugins.smartstring.base64.decode(encodedToken);
56
+ if (plugins.smartstring.base64.encode(token) !== encodedToken) {
57
+ throw invalidCredentialError();
58
+ }
59
+ tokenEncoding = 'base64';
60
+ }
61
+
62
+ if (
63
+ !token ||
64
+ token.length > 65536 ||
65
+ unsafeTokenRegex.test(token) ||
66
+ token.includes('${')
67
+ ) {
68
+ throw invalidCredentialError();
69
+ }
70
+
71
+ return {
72
+ registryUrl,
73
+ token,
74
+ tokenEncoding,
75
+ };
76
+ };
77
+
78
+ export const parseNpmCredentialEnvironment = (
79
+ environmentArg: Record<string, string>,
80
+ ): INpmCredential[] => {
81
+ const credentialValues = Object.entries(environmentArg)
82
+ .filter(([environmentVariableNameArg]) =>
83
+ environmentVariableNameArg.startsWith('SZCI_TOKEN_NPM')
84
+ )
85
+ .sort(([environmentVariableNameA], [environmentVariableNameB]) =>
86
+ environmentVariableNameA.localeCompare(environmentVariableNameB)
87
+ )
88
+ .map(([, environmentVariableValueArg]) => environmentVariableValueArg);
89
+
90
+ return credentialValues.map((credentialValueArg) => parseNpmCredential(credentialValueArg));
91
+ };