Skip to content

Commit 3a1f876

Browse files
authored
fix: pass expected @scope:team format to backend (#90)
1 parent 1ed54a3 commit 3a1f876

10 files changed

Lines changed: 100 additions & 9 deletions

File tree

app/components/OrgMembersPanel.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { NewOperation } from '~/composables/useConnector'
3+
import { buildScopeTeam } from '~/utils/npm'
34
45
const props = defineProps<{
56
orgName: string
@@ -144,10 +145,10 @@ async function loadTeamMemberships() {
144145
try {
145146
const teamsResult = await listOrgTeams(props.orgName)
146147
if (teamsResult) {
147-
// Teams come as "org:team" format
148+
// Teams come as "org:team" format from npm, need @scope:team for API calls
148149
const teamPromises = teamsResult.map(async (fullTeamName: string) => {
149150
const teamName = fullTeamName.replace(`${props.orgName}:`, '')
150-
const membersResult = await listTeamUsers(fullTeamName)
151+
const membersResult = await listTeamUsers(buildScopeTeam(props.orgName, teamName))
151152
if (membersResult) {
152153
teamMembers.value[teamName] = membersResult
153154
}
@@ -183,7 +184,7 @@ async function handleAddMember() {
183184
// Second operation: add user to team (if a team is selected)
184185
// This depends on the org operation completing first
185186
if (newTeam.value && addedOrgOp) {
186-
const scopeTeam = `${props.orgName}:${newTeam.value}`
187+
const scopeTeam = buildScopeTeam(props.orgName, newTeam.value)
187188
const teamOperation: NewOperation = {
188189
type: 'team:add-user',
189190
params: {

app/components/OrgTeamsPanel.vue

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { NewOperation } from '~/composables/useConnector'
3+
import { buildScopeTeam } from '~/utils/npm'
34
45
const props = defineProps<{
56
orgName: string
@@ -103,7 +104,7 @@ async function loadTeamUsers(teamName: string) {
103104
isLoadingUsers.value[teamName] = true
104105
105106
try {
106-
const scopeTeam = `${props.orgName}:${teamName}`
107+
const scopeTeam = buildScopeTeam(props.orgName, teamName)
107108
const result = await listTeamUsers(scopeTeam)
108109
if (result) {
109110
teamUsers.value[teamName] = result
@@ -135,7 +136,7 @@ async function handleCreateTeam() {
135136
isCreatingTeam.value = true
136137
try {
137138
const teamName = newTeamName.value.trim()
138-
const scopeTeam = `${props.orgName}:${teamName}`
139+
const scopeTeam = buildScopeTeam(props.orgName, teamName)
139140
const operation: NewOperation = {
140141
type: 'team:create',
141142
params: { scopeTeam },
@@ -153,7 +154,7 @@ async function handleCreateTeam() {
153154
154155
// Destroy team
155156
async function handleDestroyTeam(teamName: string) {
156-
const scopeTeam = `${props.orgName}:${teamName}`
157+
const scopeTeam = buildScopeTeam(props.orgName, teamName)
157158
const operation: NewOperation = {
158159
type: 'team:destroy',
159160
params: { scopeTeam },
@@ -171,7 +172,7 @@ async function handleAddUser(teamName: string) {
171172
isAddingUser.value = true
172173
try {
173174
const username = newUserUsername.value.trim().replace(/^@/, '')
174-
const scopeTeam = `${props.orgName}:${teamName}`
175+
const scopeTeam = buildScopeTeam(props.orgName, teamName)
175176
176177
let dependsOnId: string | undefined
177178
@@ -213,7 +214,7 @@ async function handleAddUser(teamName: string) {
213214
214215
// Remove user from team
215216
async function handleRemoveUser(teamName: string, username: string) {
216-
const scopeTeam = `${props.orgName}:${teamName}`
217+
const scopeTeam = buildScopeTeam(props.orgName, teamName)
217218
const operation: NewOperation = {
218219
type: 'team:rm-user',
219220
params: { scopeTeam, user: username },

app/components/PackageAccessControls.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { NewOperation } from '~/composables/useConnector'
3+
import { buildScopeTeam } from '~/utils/npm'
34
45
const props = defineProps<{
56
packageName: string
@@ -96,7 +97,7 @@ async function handleGrantAccess() {
9697
9798
isGranting.value = true
9899
try {
99-
const scopeTeam = `${orgName.value}:${selectedTeam.value}`
100+
const scopeTeam = buildScopeTeam(orgName.value, selectedTeam.value)
100101
const operation: NewOperation = {
101102
type: 'access:grant',
102103
params: {

app/utils/npm.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Constructs a scope:team string in the format expected by npm.
3+
* npm operations require the format @scope:team (with @ prefix).
4+
*
5+
* @param orgName - The organization name (with or without @)
6+
* @param teamName - The team name
7+
* @returns The scope:team string in @scope:team format
8+
*/
9+
export function buildScopeTeam(orgName: string, teamName: string): `@${string}:${string}` {
10+
return `@${orgName.replace(/^@/, '')}:${teamName}`
11+
}

cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@clack/prompts": "1.0.0-alpha.9",
3535
"citty": "^0.2.0",
3636
"h3-next": "npm:h3@^2.0.1-rc.11",
37+
"obug": "^2.1.1",
3738
"picocolors": "^1.1.1",
3839
"srvx": "^0.10.1",
3940
"validate-npm-package-name": "^7.0.2"

cli/src/logger.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as p from '@clack/prompts'
22
import pc from 'picocolors'
3+
import { createDebug } from 'obug'
34

45
let isInitialized = false
56

@@ -47,6 +48,11 @@ export function logInfo(message: string): void {
4748
p.log.info(message)
4849
}
4950

51+
/**
52+
* Log a debug message with `obug` (minimal fork of `debug`)
53+
*/
54+
export const logDebug = createDebug('npmx-connector')
55+
5056
/**
5157
* Log a message (generic)
5258
*/

cli/src/server.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { H3, HTTPError, handleCors, type H3Event } from 'h3-next'
33
import type { CorsOptions } from 'h3-next'
44

55
import type { ConnectorState, PendingOperation, OperationType, ApiResponse } from './types.ts'
6+
import { logDebug, logError } from './logger.ts'
67
import {
78
getNpmUser,
89
orgAddUser,
@@ -20,6 +21,7 @@ import {
2021
ownerAdd,
2122
ownerRemove,
2223
packageInit,
24+
validateScopeTeam,
2325
type NpmExecResult,
2426
} from './npm-client.ts'
2527

@@ -447,6 +449,17 @@ export function createConnectorApp(expectedToken: string) {
447449
// Decode the team name (handles encoded colons like nuxt%3Adevelopers)
448450
const scopeTeam = decodeURIComponent(scopeTeamRaw)
449451

452+
try {
453+
validateScopeTeam(scopeTeam)
454+
} catch (err) {
455+
logError('scope:team validation failed')
456+
logDebug(err, { scopeTeamRaw, scopeTeam })
457+
throw new HTTPError({
458+
statusCode: 400,
459+
message: `Invalid scope:team format: ${scopeTeam}. Expected @scope:team`,
460+
})
461+
}
462+
450463
const result = await teamListUsers(scopeTeam)
451464
if (result.exitCode !== 0) {
452465
return {

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/unit/cli-server.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { createConnectorApp } from '../../cli/src/server.ts'
3+
4+
const TEST_TOKEN = 'test-token-123'
5+
6+
describe('connector server', () => {
7+
describe('GET /team/:scopeTeam/users', () => {
8+
it('returns 400 for invalid scope:team format (missing @ prefix)', async () => {
9+
const app = createConnectorApp(TEST_TOKEN)
10+
11+
const response = await app.fetch(
12+
new Request('http://localhost/team/netlify%3Adevelopers/users', {
13+
headers: { Authorization: `Bearer ${TEST_TOKEN}` },
14+
}),
15+
)
16+
17+
expect(response.status).toBe(400)
18+
const body = await response.json()
19+
expect(body.message).toContain('Invalid scope:team format')
20+
})
21+
22+
it('returns 401 without auth token', async () => {
23+
const app = createConnectorApp(TEST_TOKEN)
24+
25+
const response = await app.fetch(
26+
new Request('http://localhost/team/@netlify%3Adevelopers/users'),
27+
)
28+
29+
expect(response.status).toBe(401)
30+
})
31+
})
32+
})

test/unit/npm-utils.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { buildScopeTeam } from '../../app/utils/npm'
4+
import { validateScopeTeam } from '../../cli/src/npm-client'
5+
6+
describe('buildScopeTeam', () => {
7+
it('constructs scope:team with @ prefix', () => {
8+
expect(buildScopeTeam('netlify', 'developers')).toBe('@netlify:developers')
9+
expect(buildScopeTeam('nuxt', 'core')).toBe('@nuxt:core')
10+
})
11+
12+
it('strips existing @ prefix from orgName', () => {
13+
expect(buildScopeTeam('@netlify', 'developers')).toBe('@netlify:developers')
14+
expect(buildScopeTeam('@nuxt', 'core')).toBe('@nuxt:core')
15+
})
16+
17+
it('produces format accepted by validateScopeTeam', () => {
18+
expect(() => validateScopeTeam(buildScopeTeam('netlify', 'developers'))).not.toThrow()
19+
expect(() => validateScopeTeam(buildScopeTeam('nuxt', 'core'))).not.toThrow()
20+
expect(() => validateScopeTeam(buildScopeTeam('my-org', 'my-team'))).not.toThrow()
21+
})
22+
})

0 commit comments

Comments
 (0)