Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"obug": "^2.1.1",
"picocolors": "^1.1.1",
"srvx": "^0.10.1",
"valibot": "^1.2.0",
"validate-npm-package-name": "^7.0.2"
},
"devDependencies": {
Expand Down
38 changes: 13 additions & 25 deletions cli/src/npm-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,21 @@ import { promisify } from 'node:util'
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import validateNpmPackageName from 'validate-npm-package-name'
import * as v from 'valibot'
import { PackageNameSchema, UsernameSchema, OrgNameSchema, ScopeTeamSchema } from './schemas.ts'
import { logCommand, logSuccess, logError } from './logger.ts'

const execFileAsync = promisify(execFile)

// Validation pattern for npm usernames/org names
// These follow similar rules: lowercase alphanumeric with hyphens, can't start/end with hyphen
const NPM_USERNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i

/**
* Validates an npm package name using the official npm validation package
* @throws Error if the name is invalid
*/
export function validatePackageName(name: string): void {
const result = validateNpmPackageName(name)
if (!result.validForNewPackages && !result.validForOldPackages) {
const errors = result.errors || result.warnings || ['Invalid package name']
throw new Error(`Invalid package name "${name}": ${errors.join(', ')}`)
const result = v.safeParse(PackageNameSchema, name)
if (!result.success) {
const message = result.issues[0]?.message || 'Invalid package name'
throw new Error(`Invalid package name "${name}": ${message}`)
}
}

Expand All @@ -31,7 +28,8 @@ export function validatePackageName(name: string): void {
* @throws Error if the username is invalid
*/
export function validateUsername(name: string): void {
if (!name || name.length > 50 || !NPM_USERNAME_RE.test(name)) {
const result = v.safeParse(UsernameSchema, name)
if (!result.success) {
throw new Error(`Invalid username: ${name}`)
}
}
Expand All @@ -41,7 +39,8 @@ export function validateUsername(name: string): void {
* @throws Error if the org name is invalid
*/
export function validateOrgName(name: string): void {
if (!name || name.length > 50 || !NPM_USERNAME_RE.test(name)) {
const result = v.safeParse(OrgNameSchema, name)
if (!result.success) {
throw new Error(`Invalid org name: ${name}`)
}
}
Expand All @@ -51,20 +50,9 @@ export function validateOrgName(name: string): void {
* @throws Error if the scope:team is invalid
*/
export function validateScopeTeam(scopeTeam: string): void {
if (!scopeTeam || scopeTeam.length > 100) {
throw new Error(`Invalid scope:team: ${scopeTeam}`)
}
// Format: @scope:team
const match = scopeTeam.match(/^@([^:]+):(.+)$/)
if (!match) {
throw new Error(`Invalid scope:team format: ${scopeTeam}`)
}
const [, scope, team] = match
if (!scope || !NPM_USERNAME_RE.test(scope)) {
throw new Error(`Invalid scope in scope:team: ${scopeTeam}`)
}
if (!team || !NPM_USERNAME_RE.test(team)) {
throw new Error(`Invalid team name in scope:team: ${scopeTeam}`)
const result = v.safeParse(ScopeTeamSchema, scopeTeam)
if (!result.success) {
throw new Error(`Invalid scope:team format: ${scopeTeam}. Expected @scope:team`)
}
}

Expand Down
Loading