@lpm-registry/cli 0.2.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +15 -0
  3. package/README.md +406 -0
  4. package/bin/lpm.js +334 -0
  5. package/index.d.ts +131 -0
  6. package/index.js +31 -0
  7. package/lib/api.js +324 -0
  8. package/lib/commands/add.js +1217 -0
  9. package/lib/commands/audit.js +283 -0
  10. package/lib/commands/cache.js +209 -0
  11. package/lib/commands/check-name.js +112 -0
  12. package/lib/commands/config.js +174 -0
  13. package/lib/commands/doctor.js +142 -0
  14. package/lib/commands/info.js +215 -0
  15. package/lib/commands/init.js +146 -0
  16. package/lib/commands/install.js +217 -0
  17. package/lib/commands/login.js +547 -0
  18. package/lib/commands/logout.js +94 -0
  19. package/lib/commands/marketplace-compare.js +164 -0
  20. package/lib/commands/marketplace-earnings.js +89 -0
  21. package/lib/commands/mcp-setup.js +363 -0
  22. package/lib/commands/open.js +82 -0
  23. package/lib/commands/outdated.js +291 -0
  24. package/lib/commands/pool-stats.js +100 -0
  25. package/lib/commands/publish.js +707 -0
  26. package/lib/commands/quality.js +211 -0
  27. package/lib/commands/remove.js +82 -0
  28. package/lib/commands/run.js +14 -0
  29. package/lib/commands/search.js +143 -0
  30. package/lib/commands/setup.js +92 -0
  31. package/lib/commands/skills.js +863 -0
  32. package/lib/commands/token-rotate.js +25 -0
  33. package/lib/commands/whoami.js +129 -0
  34. package/lib/config.js +240 -0
  35. package/lib/constants.js +190 -0
  36. package/lib/ecosystem.js +501 -0
  37. package/lib/editors.js +215 -0
  38. package/lib/import-rewriter.js +364 -0
  39. package/lib/install-targets/mcp-server.js +245 -0
  40. package/lib/install-targets/vscode-extension.js +178 -0
  41. package/lib/install-targets.js +82 -0
  42. package/lib/integrity.js +179 -0
  43. package/lib/lpm-config-prompts.js +102 -0
  44. package/lib/lpm-config.js +408 -0
  45. package/lib/project-utils.js +152 -0
  46. package/lib/quality/checks.js +654 -0
  47. package/lib/quality/display.js +139 -0
  48. package/lib/quality/score.js +115 -0
  49. package/lib/quality/swift-checks.js +447 -0
  50. package/lib/safe-path.js +180 -0
  51. package/lib/secure-store.js +288 -0
  52. package/lib/swift-project.js +637 -0
  53. package/lib/ui.js +40 -0
  54. package/package.json +74 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Safe Path Resolution
3
+ *
4
+ * Provides path traversal protection for file operations.
5
+ * Ensures all paths stay within a designated base directory.
6
+ *
7
+ * @module cli/lib/safe-path
8
+ */
9
+
10
+ import { isAbsolute, normalize, relative, resolve } from "node:path"
11
+ import { ERROR_MESSAGES } from "./constants.js"
12
+
13
+ /**
14
+ * Resolve a path safely within a base directory.
15
+ * Prevents path traversal attacks using ../ sequences or absolute paths.
16
+ *
17
+ * @param {string} basePath - The base directory (must be absolute)
18
+ * @param {string} userPath - The user-provided path to resolve
19
+ * @returns {{ safe: boolean, resolvedPath?: string, error?: string }}
20
+ */
21
+ export function resolveSafePath(basePath, userPath) {
22
+ // Base path must be absolute
23
+ if (!isAbsolute(basePath)) {
24
+ return {
25
+ safe: false,
26
+ error: "Base path must be absolute",
27
+ }
28
+ }
29
+
30
+ // Reject absolute paths from user input
31
+ if (isAbsolute(userPath)) {
32
+ return {
33
+ safe: false,
34
+ error: ERROR_MESSAGES.pathTraversal,
35
+ }
36
+ }
37
+
38
+ // Normalize the base path
39
+ const normalizedBase = normalize(basePath)
40
+
41
+ // Resolve the full path
42
+ const resolvedPath = resolve(normalizedBase, userPath)
43
+
44
+ // Normalize the resolved path
45
+ const normalizedResolved = normalize(resolvedPath)
46
+
47
+ // Check if resolved path is within base
48
+ const relativePath = relative(normalizedBase, normalizedResolved)
49
+
50
+ // Path is outside base if:
51
+ // 1. It starts with '..' (goes above base)
52
+ // 2. It's an absolute path (on Windows, this can happen with drive changes)
53
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
54
+ return {
55
+ safe: false,
56
+ error: ERROR_MESSAGES.pathTraversal,
57
+ }
58
+ }
59
+
60
+ return {
61
+ safe: true,
62
+ resolvedPath: normalizedResolved,
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Check if a path contains dangerous patterns.
68
+ * This is a quick check before more expensive resolution.
69
+ *
70
+ * @param {string} userPath - The path to check
71
+ * @returns {boolean} - True if the path looks dangerous
72
+ */
73
+ export function hasDangerousPatterns(userPath) {
74
+ if (!userPath || typeof userPath !== "string") {
75
+ return true
76
+ }
77
+
78
+ // Dangerous patterns
79
+ const dangerousPatterns = [
80
+ // Null bytes (can bypass checks in some systems)
81
+ "\0",
82
+ // Windows drive letters
83
+ /^[a-zA-Z]:/,
84
+ // UNC paths
85
+ /^\\\\|^\/\//,
86
+ // Excessive parent traversal
87
+ /\.\.[/\\]\.\.[/\\]\.\.[/\\]/,
88
+ // Hidden files/directories (optional, depends on use case)
89
+ // /\/\./,
90
+ ]
91
+
92
+ for (const pattern of dangerousPatterns) {
93
+ if (typeof pattern === "string") {
94
+ if (userPath.includes(pattern)) return true
95
+ } else {
96
+ if (pattern.test(userPath)) return true
97
+ }
98
+ }
99
+
100
+ return false
101
+ }
102
+
103
+ /**
104
+ * Sanitize a filename by removing or replacing dangerous characters.
105
+ *
106
+ * @param {string} filename - The filename to sanitize
107
+ * @returns {string} - Sanitized filename
108
+ */
109
+ export function sanitizeFilename(filename) {
110
+ if (!filename || typeof filename !== "string") {
111
+ return ""
112
+ }
113
+
114
+ return (
115
+ filename
116
+ // Remove null bytes
117
+ .replace(/\0/g, "")
118
+ // Replace path separators
119
+ .replace(/[/\\]/g, "-")
120
+ // Remove other dangerous characters
121
+ .replace(/[<>:"|?*]/g, "")
122
+ // Collapse multiple dashes
123
+ .replace(/-+/g, "-")
124
+ // Trim dashes from ends
125
+ .replace(/^-+|-+$/g, "")
126
+ // Limit length
127
+ .slice(0, 255)
128
+ )
129
+ }
130
+
131
+ /**
132
+ * Validate a package component path.
133
+ * Used by the `lpm add` command to ensure extracted files stay in bounds.
134
+ *
135
+ * @param {string} projectRoot - The project root directory
136
+ * @param {string} componentPath - The path where the component should be extracted
137
+ * @returns {{ valid: boolean, resolvedPath?: string, error?: string }}
138
+ */
139
+ export function validateComponentPath(projectRoot, componentPath) {
140
+ // Quick dangerous pattern check
141
+ if (hasDangerousPatterns(componentPath)) {
142
+ return {
143
+ valid: false,
144
+ error: ERROR_MESSAGES.pathTraversal,
145
+ }
146
+ }
147
+
148
+ // Full path resolution and validation
149
+ const result = resolveSafePath(projectRoot, componentPath)
150
+
151
+ return {
152
+ valid: result.safe,
153
+ resolvedPath: result.resolvedPath,
154
+ error: result.error,
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Validate each file path in a tarball before extraction.
160
+ * Prevents zip slip attacks.
161
+ *
162
+ * @param {string} extractDir - The extraction directory
163
+ * @param {string[]} filePaths - Array of file paths from the tarball
164
+ * @returns {{ valid: boolean, invalidPaths: string[] }}
165
+ */
166
+ export function validateTarballPaths(extractDir, filePaths) {
167
+ const invalidPaths = []
168
+
169
+ for (const filePath of filePaths) {
170
+ const result = resolveSafePath(extractDir, filePath)
171
+ if (!result.safe) {
172
+ invalidPaths.push(filePath)
173
+ }
174
+ }
175
+
176
+ return {
177
+ valid: invalidPaths.length === 0,
178
+ invalidPaths,
179
+ }
180
+ }
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Secure Store - OS Keychain Storage with Fallback
3
+ *
4
+ * Provides secure credential storage using the system keychain.
5
+ * Falls back to encrypted file storage if keytar is unavailable.
6
+ *
7
+ * @module cli/lib/secure-store
8
+ */
9
+
10
+ import {
11
+ createCipheriv,
12
+ createDecipheriv,
13
+ randomBytes,
14
+ scryptSync,
15
+ } from "node:crypto"
16
+ import {
17
+ existsSync,
18
+ mkdirSync,
19
+ readFileSync,
20
+ unlinkSync,
21
+ writeFileSync,
22
+ } from "node:fs"
23
+ import { homedir } from "node:os"
24
+ import { join } from "node:path"
25
+ import { KEYTAR_ACCOUNT_NAME, KEYTAR_SERVICE_NAME } from "./constants.js"
26
+
27
+ /** @type {import('keytar') | null} */
28
+ let keytar = null
29
+
30
+ /**
31
+ * Try to load keytar for native keychain access.
32
+ * Falls back gracefully if not available.
33
+ */
34
+ async function loadKeytar() {
35
+ if (keytar !== null) return keytar
36
+
37
+ try {
38
+ // Dynamic import to avoid hard dependency
39
+ const mod = await import("keytar")
40
+ // ESM dynamic import returns { default: keytarModule }
41
+ keytar = mod.default || mod
42
+ return keytar
43
+ } catch {
44
+ // keytar not available (missing native dependencies)
45
+ keytar = false
46
+ return null
47
+ }
48
+ }
49
+
50
+ // ============================================================================
51
+ // Fallback Encrypted File Storage
52
+ // ============================================================================
53
+
54
+ const ENCRYPTED_STORE_DIR = join(homedir(), ".lpm")
55
+ const ENCRYPTED_STORE_FILE = join(ENCRYPTED_STORE_DIR, ".credentials")
56
+ const SALT_FILE = join(ENCRYPTED_STORE_DIR, ".salt")
57
+
58
+ /**
59
+ * Get or create encryption salt.
60
+ * @returns {Buffer}
61
+ */
62
+ function getOrCreateSalt() {
63
+ if (existsSync(SALT_FILE)) {
64
+ return readFileSync(SALT_FILE)
65
+ }
66
+
67
+ const salt = randomBytes(32)
68
+ if (!existsSync(ENCRYPTED_STORE_DIR)) {
69
+ mkdirSync(ENCRYPTED_STORE_DIR, { recursive: true, mode: 0o700 })
70
+ }
71
+ writeFileSync(SALT_FILE, salt, { mode: 0o600 })
72
+ return salt
73
+ }
74
+
75
+ /**
76
+ * Derive encryption key from machine-specific data.
77
+ * Uses a combination of hostname, username, and random salt.
78
+ * @returns {Buffer}
79
+ */
80
+ function deriveKey() {
81
+ const salt = getOrCreateSalt()
82
+ // Use machine-specific data as part of the key derivation
83
+ const machineId = `${homedir()}-${process.env.USER || "user"}`
84
+ return scryptSync(machineId, salt, 32)
85
+ }
86
+
87
+ /**
88
+ * Encrypt a value using AES-256-GCM.
89
+ * @param {string} value - The value to encrypt
90
+ * @returns {string} - Base64 encoded encrypted data
91
+ */
92
+ function encrypt(value) {
93
+ const key = deriveKey()
94
+ const iv = randomBytes(16)
95
+ const cipher = createCipheriv("aes-256-gcm", key, iv)
96
+
97
+ let encrypted = cipher.update(value, "utf8", "base64")
98
+ encrypted += cipher.final("base64")
99
+
100
+ const authTag = cipher.getAuthTag()
101
+
102
+ // Format: iv:authTag:encrypted
103
+ return `${iv.toString("base64")}:${authTag.toString("base64")}:${encrypted}`
104
+ }
105
+
106
+ /**
107
+ * Decrypt a value using AES-256-GCM.
108
+ * @param {string} encryptedValue - Base64 encoded encrypted data
109
+ * @returns {string | null} - Decrypted value or null if failed
110
+ */
111
+ function decrypt(encryptedValue) {
112
+ try {
113
+ const key = deriveKey()
114
+ const [ivBase64, authTagBase64, encrypted] = encryptedValue.split(":")
115
+
116
+ const iv = Buffer.from(ivBase64, "base64")
117
+ const authTag = Buffer.from(authTagBase64, "base64")
118
+
119
+ const decipher = createDecipheriv("aes-256-gcm", key, iv)
120
+ decipher.setAuthTag(authTag)
121
+
122
+ let decrypted = decipher.update(encrypted, "base64", "utf8")
123
+ decrypted += decipher.final("utf8")
124
+
125
+ return decrypted
126
+ } catch {
127
+ // Decryption failed (wrong key, corrupted data, etc.)
128
+ return null
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Read encrypted store from file.
134
+ * @returns {Record<string, string>}
135
+ */
136
+ function readEncryptedStore() {
137
+ if (!existsSync(ENCRYPTED_STORE_FILE)) {
138
+ return {}
139
+ }
140
+
141
+ try {
142
+ const content = readFileSync(ENCRYPTED_STORE_FILE, "utf8")
143
+ const decrypted = decrypt(content)
144
+ if (!decrypted) return {}
145
+ return JSON.parse(decrypted)
146
+ } catch {
147
+ return {}
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Write encrypted store to file.
153
+ * @param {Record<string, string>} store
154
+ */
155
+ function writeEncryptedStore(store) {
156
+ if (!existsSync(ENCRYPTED_STORE_DIR)) {
157
+ mkdirSync(ENCRYPTED_STORE_DIR, { recursive: true, mode: 0o700 })
158
+ }
159
+
160
+ const content = encrypt(JSON.stringify(store))
161
+ writeFileSync(ENCRYPTED_STORE_FILE, content, { mode: 0o600 })
162
+ }
163
+
164
+ // ============================================================================
165
+ // Public API
166
+ // ============================================================================
167
+
168
+ /**
169
+ * Store a secret securely.
170
+ * Uses OS keychain if available, falls back to encrypted file.
171
+ *
172
+ * @param {string} key - The key to store the secret under
173
+ * @param {string} value - The secret value
174
+ * @returns {Promise<void>}
175
+ */
176
+ export async function setSecret(key, value) {
177
+ const kt = await loadKeytar()
178
+
179
+ if (kt) {
180
+ await kt.setPassword(KEYTAR_SERVICE_NAME, key, value)
181
+ return
182
+ }
183
+
184
+ // Fallback to encrypted file
185
+ const store = readEncryptedStore()
186
+ store[key] = value
187
+ writeEncryptedStore(store)
188
+ }
189
+
190
+ /**
191
+ * Retrieve a secret.
192
+ * Uses OS keychain if available, falls back to encrypted file.
193
+ *
194
+ * @param {string} key - The key to retrieve
195
+ * @returns {Promise<string | null>}
196
+ */
197
+ export async function getSecret(key) {
198
+ const kt = await loadKeytar()
199
+
200
+ if (kt) {
201
+ return kt.getPassword(KEYTAR_SERVICE_NAME, key)
202
+ }
203
+
204
+ // Fallback to encrypted file
205
+ const store = readEncryptedStore()
206
+ return store[key] || null
207
+ }
208
+
209
+ /**
210
+ * Delete a secret.
211
+ * Uses OS keychain if available, falls back to encrypted file.
212
+ *
213
+ * @param {string} key - The key to delete
214
+ * @returns {Promise<boolean>}
215
+ */
216
+ export async function deleteSecret(key) {
217
+ const kt = await loadKeytar()
218
+
219
+ if (kt) {
220
+ return kt.deletePassword(KEYTAR_SERVICE_NAME, key)
221
+ }
222
+
223
+ // Fallback to encrypted file
224
+ const store = readEncryptedStore()
225
+ if (key in store) {
226
+ delete store[key]
227
+ writeEncryptedStore(store)
228
+ return true
229
+ }
230
+ return false
231
+ }
232
+
233
+ /**
234
+ * Clear all stored secrets.
235
+ * @returns {Promise<void>}
236
+ */
237
+ export async function clearAllSecrets() {
238
+ const kt = await loadKeytar()
239
+
240
+ if (kt) {
241
+ // keytar doesn't have a "clear all" - delete known keys
242
+ await kt.deletePassword(KEYTAR_SERVICE_NAME, KEYTAR_ACCOUNT_NAME)
243
+ return
244
+ }
245
+
246
+ // Fallback: delete the encrypted store file
247
+ if (existsSync(ENCRYPTED_STORE_FILE)) {
248
+ unlinkSync(ENCRYPTED_STORE_FILE)
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Check if secure storage is using native keychain.
254
+ * @returns {Promise<boolean>}
255
+ */
256
+ export async function isUsingKeychain() {
257
+ const kt = await loadKeytar()
258
+ return kt !== null && kt !== false
259
+ }
260
+
261
+ // ============================================================================
262
+ // Token-Specific Helpers
263
+ // ============================================================================
264
+
265
+ /**
266
+ * Get the stored auth token.
267
+ * @returns {Promise<string | null>}
268
+ */
269
+ export async function getToken() {
270
+ return getSecret(KEYTAR_ACCOUNT_NAME)
271
+ }
272
+
273
+ /**
274
+ * Set the auth token.
275
+ * @param {string} token
276
+ * @returns {Promise<void>}
277
+ */
278
+ export async function setToken(token) {
279
+ return setSecret(KEYTAR_ACCOUNT_NAME, token)
280
+ }
281
+
282
+ /**
283
+ * Clear the auth token.
284
+ * @returns {Promise<boolean>}
285
+ */
286
+ export async function clearToken() {
287
+ return deleteSecret(KEYTAR_ACCOUNT_NAME)
288
+ }