@sanlam-fintech-digital/mfe-platform-cli 0.2.8 → 0.4.0

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.
@@ -13,25 +13,59 @@ const os_1 = __importDefault(require("os"));
13
13
  const chalk_1 = __importDefault(require("chalk"));
14
14
  const fast_xml_parser_1 = require("fast-xml-parser");
15
15
  /**
16
- * Get platform-specific NuGet config path
16
+ * Get platform-specific NuGet config paths (primary and legacy fallback)
17
17
  */
18
- function getNugetConfigPath(configPath) {
19
- if (configPath)
20
- return configPath;
18
+ function getNugetConfigPaths() {
21
19
  const platform = process.platform;
22
20
  if (platform === 'win32') {
23
21
  const appData = process.env.APPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Roaming');
24
- return path_1.default.join(appData, 'NuGet', 'NuGet.Config');
22
+ const primary = path_1.default.join(appData, 'NuGet', 'NuGet.Config');
23
+ return { primary, legacy: null };
25
24
  }
26
25
  else {
27
- return path_1.default.join(os_1.default.homedir(), '.config', 'NuGet', 'NuGet.Config');
26
+ const primary = path_1.default.join(os_1.default.homedir(), '.nuget', 'NuGet', 'NuGet.Config');
27
+ const legacy = path_1.default.join(os_1.default.homedir(), '.config', 'NuGet', 'NuGet.Config');
28
+ return { primary, legacy };
29
+ }
30
+ }
31
+ /**
32
+ * Resolve the NuGet config path to use, checking both new and legacy locations.
33
+ * If a custom path is provided it is used as-is.
34
+ * Otherwise the new primary path (~/.nuget/NuGet/NuGet.Config) takes precedence,
35
+ * falling back to the legacy path (~/.config/NuGet/NuGet.Config) when it exists
36
+ * and the primary does not.
37
+ */
38
+ async function resolveNugetConfigPath(configPath) {
39
+ if (configPath)
40
+ return configPath;
41
+ const { primary, legacy } = getNugetConfigPaths();
42
+ if (!legacy)
43
+ return primary;
44
+ // Prefer primary; fall back to legacy only when primary is absent (ENOENT)
45
+ try {
46
+ await promises_1.default.access(primary);
47
+ return primary;
48
+ }
49
+ catch (err) {
50
+ // Only fall back to legacy when the primary path truly does not exist.
51
+ // For any other error (e.g. EACCES, EIO) keep the primary path so the
52
+ // caller sees the real failure rather than silently switching paths.
53
+ if (err.code !== 'ENOENT')
54
+ return primary;
55
+ try {
56
+ await promises_1.default.access(legacy);
57
+ return legacy;
58
+ }
59
+ catch {
60
+ return primary;
61
+ }
28
62
  }
29
63
  }
30
64
  /**
31
65
  * Backup existing NuGet.config
32
66
  */
33
67
  async function backupNugetConfig(configPath) {
34
- const nugetPath = getNugetConfigPath(configPath);
68
+ const nugetPath = await resolveNugetConfigPath(configPath);
35
69
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
36
70
  const backupPath = `${nugetPath}.backup.${timestamp}`;
37
71
  try {
@@ -49,7 +83,7 @@ async function backupNugetConfig(configPath) {
49
83
  * Read and parse NuGet.config
50
84
  */
51
85
  async function readNugetConfig(configPath) {
52
- const nugetPath = getNugetConfigPath(configPath);
86
+ const nugetPath = await resolveNugetConfigPath(configPath);
53
87
  try {
54
88
  const content = await promises_1.default.readFile(nugetPath, 'utf-8');
55
89
  const parser = new fast_xml_parser_1.XMLParser({
@@ -103,9 +137,7 @@ async function addNuGetFeeds(feeds, tokenMap, configPath) {
103
137
  // Add credentials if token provided
104
138
  const token = tokenMap?.get(feed.url);
105
139
  if (token) {
106
- // Normalize credential key (replace invalid XML chars)
107
- const credKey = sourceName.replace(/[^a-zA-Z0-9_]/g, '_');
108
- credentials[credKey] = {
140
+ credentials[sourceName] = {
109
141
  add: [
110
142
  { '@_key': 'Username', '@_value': 'PersonalAccessToken' },
111
143
  { '@_key': 'ClearTextPassword', '@_value': token }
@@ -119,7 +151,7 @@ async function addNuGetFeeds(feeds, tokenMap, configPath) {
119
151
  * Write NuGet.config
120
152
  */
121
153
  async function writeNugetConfig(config, configPath) {
122
- const nugetPath = getNugetConfigPath(configPath);
154
+ const nugetPath = await resolveNugetConfigPath(configPath);
123
155
  const dir = path_1.default.dirname(nugetPath);
124
156
  // Ensure directory exists
125
157
  await promises_1.default.mkdir(dir, { recursive: true });
@@ -140,6 +172,6 @@ async function writeNugetConfig(config, configPath) {
140
172
  * Restore from backup
141
173
  */
142
174
  async function restoreNugetFromBackup(backupPath, configPath) {
143
- const nugetPath = getNugetConfigPath(configPath);
175
+ const nugetPath = await resolveNugetConfigPath(configPath);
144
176
  await promises_1.default.copyFile(backupPath, nugetPath);
145
177
  }
@@ -6,6 +6,29 @@ const zod_1 = require("zod");
6
6
  * Package feed type
7
7
  */
8
8
  const FeedTypeSchema = zod_1.z.enum(['npm', 'nuget']).default('npm');
9
+ /**
10
+ * Validates if a string is a valid XML element name.
11
+ * XML 1.0 specification (https://www.w3.org/TR/xml/#NT-Name):
12
+ * - Must start with a letter (A-Z, a-z) or underscore (_)
13
+ * - Can contain letters, digits, hyphens (-), underscores (_), and periods (.)
14
+ * - Cannot start with "xml" (case-insensitive)
15
+ * - Cannot contain spaces or special characters like @, $, %, etc.
16
+ */
17
+ function isValidXmlElementName(name) {
18
+ // Empty string is invalid
19
+ if (!name)
20
+ return false;
21
+ // Must start with letter or underscore (not digit, hyphen, or period)
22
+ if (!/^[A-Za-z_]/.test(name))
23
+ return false;
24
+ // Can only contain letters, digits, hyphens, underscores, and periods
25
+ if (!/^[A-Za-z_][A-Za-z0-9._-]*$/.test(name))
26
+ return false;
27
+ // Cannot start with "xml" (case-insensitive)
28
+ if (/^xml/i.test(name))
29
+ return false;
30
+ return true;
31
+ }
9
32
  /**
10
33
  * Individual registry entry (npm or NuGet feed)
11
34
  */
@@ -13,6 +36,19 @@ const RegistryEntrySchema = zod_1.z.object({
13
36
  scope: zod_1.z.string().describe('npm scope (e.g., "@org/package") or NuGet source name'),
14
37
  url: zod_1.z.url().describe('Registry/Feed URL (Azure DevOps, GitHub, or public)').transform((url) => url.replace(/\/+$/, '')),
15
38
  feedType: FeedTypeSchema.optional().describe('Package feed type (defaults to npm for backward compatibility)'),
39
+ }).superRefine((data, ctx) => {
40
+ // For NuGet feeds, the scope becomes an XML element name in NuGet.Config
41
+ // Validate it meets XML element name requirements
42
+ const feedType = data.feedType || 'npm';
43
+ if (feedType === 'nuget' && !isValidXmlElementName(data.scope)) {
44
+ ctx.addIssue({
45
+ code: zod_1.z.ZodIssueCode.custom,
46
+ message: `NuGet source name "${data.scope}" is not a valid XML element name. ` +
47
+ `It must start with a letter or underscore, contain only letters, digits, hyphens, underscores, and periods, ` +
48
+ `and cannot start with "xml" (case-insensitive).`,
49
+ path: ['scope'],
50
+ });
51
+ }
16
52
  });
17
53
  /**
18
54
  * Azure DevOps auth group (requires tenantId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanlam-fintech-digital/mfe-platform-cli",
3
- "version": "0.2.8",
3
+ "version": "0.4.0",
4
4
  "description": "Bootstrapping and orchestration CLI for the Sanlam Fintech Digital platform",
5
5
  "main": "dist/index.js",
6
6
  "bin": {