@ship.zone/szci 7.1.6 → 7.1.7

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,138 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import childProcess from 'node:child_process';
5
+
6
+ import {
7
+ parseNpmCredentialEnvironment,
8
+ validateNpmRegistryUrl,
9
+ } from '../ts/manager.npm/npmcredential.shared.js';
10
+
11
+ const defaultRegistryUrl = 'registry.npmjs.org';
12
+
13
+ const configError = () => new Error('Invalid @ship.zone/szci npm registry configuration.');
14
+
15
+ /**
16
+ * @param {string} cwdArg
17
+ * @returns {string}
18
+ */
19
+ export const readDefaultRegistryUrl = (cwdArg) => {
20
+ const configPath = path.join(cwdArg, '.smartconfig.json');
21
+ if (!fs.existsSync(configPath)) {
22
+ return defaultRegistryUrl;
23
+ }
24
+
25
+ let config;
26
+ try {
27
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
28
+ } catch {
29
+ throw configError();
30
+ }
31
+
32
+ const szciConfig = config?.['@ship.zone/szci'];
33
+ let configuredRegistry = defaultRegistryUrl;
34
+ if (szciConfig !== undefined) {
35
+ if (
36
+ !szciConfig ||
37
+ typeof szciConfig !== 'object' ||
38
+ Array.isArray(szciConfig)
39
+ ) {
40
+ throw configError();
41
+ }
42
+ if (Object.prototype.hasOwnProperty.call(szciConfig, 'npmRegistryUrl')) {
43
+ configuredRegistry = szciConfig.npmRegistryUrl;
44
+ }
45
+ }
46
+ if (typeof configuredRegistry !== 'string') {
47
+ throw configError();
48
+ }
49
+
50
+ try {
51
+ return validateNpmRegistryUrl(configuredRegistry);
52
+ } catch {
53
+ throw configError();
54
+ }
55
+ };
56
+
57
+ /**
58
+ * @param {{
59
+ * environment?: Record<string, string | undefined>;
60
+ * cwd?: string;
61
+ * }} [optionsArg]
62
+ */
63
+ export const createNpmPreparePlan = (optionsArg = {}) => {
64
+ const environment = optionsArg.environment ?? process.env;
65
+ const cwd = optionsArg.cwd ?? process.cwd();
66
+ const npmCredentials = parseNpmCredentialEnvironment(environment);
67
+ if (npmCredentials.length === 0) {
68
+ throw new Error('No SZCI_TOKEN_NPM credentials were provided.');
69
+ }
70
+ const registryUrl = readDefaultRegistryUrl(cwd);
71
+
72
+ let npmrcFileString = '';
73
+ for (const npmCredential of npmCredentials) {
74
+ npmrcFileString += `//${npmCredential.registryUrl}/:_authToken="${npmCredential.token}"\n`;
75
+ }
76
+ npmrcFileString += `registry=https://${registryUrl}\n`;
77
+
78
+ return {
79
+ cwd,
80
+ npmCredentials,
81
+ npmrcFileString,
82
+ registryUrl,
83
+ };
84
+ };
85
+
86
+ /**
87
+ * @param {{
88
+ * environment?: Record<string, string | undefined>;
89
+ * cwd?: string;
90
+ * homeDirectory?: string;
91
+ * log?: (messageArg: string) => void;
92
+ * writeFile?: (pathArg: string, contentArg: string) => void;
93
+ * runNpm?: (commandArg: string, argsArg: string[]) => void;
94
+ * }} [optionsArg]
95
+ */
96
+ export const runNpmPrepare = (optionsArg = {}) => {
97
+ const plan = createNpmPreparePlan(optionsArg);
98
+ const homeDirectory = optionsArg.homeDirectory ?? os.homedir();
99
+ const log = optionsArg.log ?? ((messageArg) => console.log(messageArg));
100
+ const writeFile = optionsArg.writeFile ??
101
+ ((pathArg, contentArg) => fs.writeFileSync(pathArg, contentArg));
102
+ const runNpm = optionsArg.runNpm ??
103
+ ((commandArg, argsArg) => {
104
+ const result = childProcess.spawnSync(commandArg, argsArg, {
105
+ stdio: 'inherit',
106
+ shell: false,
107
+ });
108
+ if (result.error) {
109
+ throw new Error('Failed to configure the npm cache.');
110
+ }
111
+ if (result.status !== 0) {
112
+ throw new Error('Failed to configure the npm cache.');
113
+ }
114
+ });
115
+
116
+ log('running >>npm prepare<<');
117
+ for (const npmCredential of plan.npmCredentials) {
118
+ log(`found token for ${npmCredential.registryUrl}`);
119
+ log(
120
+ npmCredential.tokenEncoding === 'plain'
121
+ ? 'npm token not base64 encoded.'
122
+ : 'npm token base64 encoded.',
123
+ );
124
+ }
125
+ log(`setting default npm registry to ${plan.registryUrl}`);
126
+ log('found one or more access tokens');
127
+
128
+ writeFile(path.join(homeDirectory, '.npmrc'), plan.npmrcFileString);
129
+ runNpm('npm', [
130
+ 'config',
131
+ 'set',
132
+ 'cache',
133
+ path.join(plan.cwd, '.szci_cache'),
134
+ '--global',
135
+ ]);
136
+
137
+ return plan.npmCredentials;
138
+ };
@@ -145,5 +145,22 @@ function executeBinary() {
145
145
  }
146
146
  }
147
147
 
148
- // Execute
149
- executeBinary();
148
+ const cliArguments = process.argv.slice(2);
149
+ if (
150
+ cliArguments.length === 2 &&
151
+ cliArguments[0] === 'npm' &&
152
+ cliArguments[1] === 'prepare'
153
+ ) {
154
+ const runNodeNpmPrepare = async () => {
155
+ const { runNpmPrepare } = await import('./npm-prepare.js');
156
+ runNpmPrepare();
157
+ };
158
+ runNodeNpmPrepare().catch((error) => {
159
+ console.error(
160
+ error instanceof Error ? error.message : 'SZCI npm prepare failed.',
161
+ );
162
+ process.exitCode = 1;
163
+ });
164
+ } else {
165
+ executeBinary();
166
+ }
package/changelog.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-07-28 - 7.1.7
4
+
5
+ ### Fixes
6
+
7
+ - run `szci npm prepare` through the Node wrapper without starting a platform binary
8
+ - Keep npm credential validation shared between the Node and Deno paths.
9
+ - Allow cross-platform Docker builds to prepare npm authentication when Deno cannot execute under CPU emulation.
10
+ - Validate the configured default registry before logging or writing `.npmrc`.
11
+ - Require Node 14.13.1 or newer for the Node-native prepare path.
12
+
3
13
  ## 2026-07-28 - 7.1.6
4
14
 
5
15
  ### Fixes
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship.zone/szci",
3
- "version": "7.1.6",
3
+ "version": "7.1.7",
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.6",
3
+ "version": "7.1.7",
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",
@@ -45,7 +45,7 @@
45
45
  "changelog.md"
46
46
  ],
47
47
  "engines": {
48
- "node": ">=14.0.0"
48
+ "node": ">=14.13.1"
49
49
  },
50
50
  "os": [
51
51
  "darwin",
package/readme.md CHANGED
@@ -118,6 +118,10 @@ szci npm prepare # Generates ~/.npmrc from SZCI_TOKEN_NPM* env vars
118
118
  szci npm publish # Full workflow: prepare → install → build → clean → npm publish
119
119
  ```
120
120
 
121
+ `szci npm prepare` runs directly through the Node wrapper, so npm authentication
122
+ setup remains available during cross-platform Docker builds where a Deno binary
123
+ cannot execute under CPU emulation.
124
+
121
125
  The `publish` command supports multi-registry publishing. If `npmAccessLevel` is `public` and a Verdaccio registry is configured, it publishes to both npm and Verdaccio automatically.
122
126
 
123
127
  ### `szci node` — Node.js Version Management
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@ship.zone/szci',
6
- version: '7.1.6',
6
+ version: '7.1.7',
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
  }
@@ -65,6 +65,7 @@ export class SzciNpmManager {
65
65
  if (npmCredentials.length === 0) {
66
66
  throw new Error('No SZCI_TOKEN_NPM credentials were provided.');
67
67
  }
68
+ const defaultRegistryUrl = validateNpmRegistryUrl(config.npmRegistryUrl);
68
69
 
69
70
  let npmrcFileString: string = '';
70
71
  for (const npmCredential of npmCredentials) {
@@ -78,7 +79,6 @@ export class SzciNpmManager {
78
79
  npmrcFileString += `//${npmCredential.registryUrl}/:_authToken="${npmCredential.token}"\n`;
79
80
  }
80
81
 
81
- const defaultRegistryUrl = validateNpmRegistryUrl(config.npmRegistryUrl);
82
82
  logger.log('info', `setting default npm registry to ${defaultRegistryUrl}`);
83
83
  npmrcFileString += `registry=https://${defaultRegistryUrl}\n`;
84
84
  logger.log('info', 'found one or more access tokens');
@@ -0,0 +1,111 @@
1
+ const invalidCredentialError = () =>
2
+ new Error(
3
+ 'Invalid SZCI_TOKEN_NPM credential. Expected registry|token[|plain].',
4
+ );
5
+
6
+ const registryUrlRegex = /^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::[0-9]{1,5})?$/;
7
+ const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
8
+ const unsafeTokenRegex = /[\u0000-\u001f\u007f"\\]/;
9
+
10
+ /**
11
+ * @param {string} registryUrlArg
12
+ * @returns {string}
13
+ */
14
+ export const validateNpmRegistryUrl = (registryUrlArg) => {
15
+ if (
16
+ !registryUrlArg ||
17
+ registryUrlArg.length > 512 ||
18
+ !registryUrlRegex.test(registryUrlArg)
19
+ ) {
20
+ throw invalidCredentialError();
21
+ }
22
+ return registryUrlArg;
23
+ };
24
+
25
+ /**
26
+ * @param {string} npmEnvironmentValueArg
27
+ * @returns {{
28
+ * registryUrl: string;
29
+ * token: string;
30
+ * tokenEncoding: 'base64' | 'plain';
31
+ * }}
32
+ */
33
+ export const parseNpmCredential = (npmEnvironmentValueArg) => {
34
+ const fields = npmEnvironmentValueArg.split('|');
35
+ if (fields.length < 2 || fields.length > 3) {
36
+ throw invalidCredentialError();
37
+ }
38
+
39
+ const registryUrl = validateNpmRegistryUrl(fields[0]);
40
+ const encodedToken = fields[1];
41
+ const encodingMarker = fields[2];
42
+ if (
43
+ !encodedToken ||
44
+ (encodingMarker !== undefined && encodingMarker !== 'plain')
45
+ ) {
46
+ throw invalidCredentialError();
47
+ }
48
+
49
+ let token;
50
+ /** @type {'base64' | 'plain'} */
51
+ let tokenEncoding;
52
+ if (encodingMarker === 'plain') {
53
+ token = encodedToken;
54
+ tokenEncoding = 'plain';
55
+ } else {
56
+ if (!base64Regex.test(encodedToken)) {
57
+ throw invalidCredentialError();
58
+ }
59
+ const tokenBuffer = Buffer.from(encodedToken, 'base64');
60
+ if (tokenBuffer.toString('base64') !== encodedToken) {
61
+ throw invalidCredentialError();
62
+ }
63
+ try {
64
+ token = new TextDecoder('utf-8', { fatal: true }).decode(tokenBuffer);
65
+ } catch {
66
+ throw invalidCredentialError();
67
+ }
68
+ if (Buffer.from(token, 'utf8').toString('base64') !== encodedToken) {
69
+ throw invalidCredentialError();
70
+ }
71
+ tokenEncoding = 'base64';
72
+ }
73
+
74
+ if (
75
+ !token ||
76
+ token.length > 65536 ||
77
+ unsafeTokenRegex.test(token) ||
78
+ token.includes('${')
79
+ ) {
80
+ throw invalidCredentialError();
81
+ }
82
+
83
+ return {
84
+ registryUrl,
85
+ token,
86
+ tokenEncoding,
87
+ };
88
+ };
89
+
90
+ /**
91
+ * @param {Record<string, string | undefined>} environmentArg
92
+ * @returns {Array<{
93
+ * registryUrl: string;
94
+ * token: string;
95
+ * tokenEncoding: 'base64' | 'plain';
96
+ * }>}
97
+ */
98
+ export const parseNpmCredentialEnvironment = (environmentArg) => {
99
+ const credentialValues = Object.entries(environmentArg)
100
+ .filter(
101
+ ([environmentVariableNameArg, environmentVariableValueArg]) =>
102
+ environmentVariableNameArg.startsWith('SZCI_TOKEN_NPM') &&
103
+ environmentVariableValueArg !== undefined,
104
+ )
105
+ .sort(([environmentVariableNameA], [environmentVariableNameB]) =>
106
+ environmentVariableNameA.localeCompare(environmentVariableNameB)
107
+ )
108
+ .map(([, environmentVariableValueArg]) => environmentVariableValueArg);
109
+
110
+ return credentialValues.map((credentialValueArg) => parseNpmCredential(credentialValueArg));
111
+ };
@@ -1,91 +1,23 @@
1
- import * as plugins from './mod.plugins.ts';
2
-
3
1
  export interface INpmCredential {
4
2
  registryUrl: string;
5
3
  token: string;
6
4
  tokenEncoding: 'base64' | 'plain';
7
5
  }
8
6
 
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"\\]/;
7
+ import {
8
+ parseNpmCredential as parseNpmCredentialShared,
9
+ parseNpmCredentialEnvironment as parseNpmCredentialEnvironmentShared,
10
+ validateNpmRegistryUrl as validateNpmRegistryUrlShared,
11
+ } from './npmcredential.shared.js';
16
12
 
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
- };
13
+ export const validateNpmRegistryUrl = (
14
+ registryUrlArg: string,
15
+ ): string => validateNpmRegistryUrlShared(registryUrlArg);
27
16
 
28
17
  export const parseNpmCredential = (
29
18
  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
- };
19
+ ): INpmCredential => parseNpmCredentialShared(npmEnvironmentValueArg);
77
20
 
78
21
  export const parseNpmCredentialEnvironment = (
79
22
  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
- };
23
+ ): INpmCredential[] => parseNpmCredentialEnvironmentShared(environmentArg);