From 64662afa2e4710eb69a375aef9afde7b190bb8ca Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 15 Jan 2026 09:59:07 -0800 Subject: [PATCH 1/3] feat: adds circleci to trust command --- lib/commands/trust/circleci.js | 170 +++++++ lib/commands/trust/index.js | 1 + .../test/lib/commands/completion.js.test.cjs | 1 + tap-snapshots/test/lib/docs.js.test.cjs | 13 + test/lib/commands/trust/circleci.js | 449 ++++++++++++++++++ 5 files changed, 634 insertions(+) create mode 100644 lib/commands/trust/circleci.js create mode 100644 test/lib/commands/trust/circleci.js diff --git a/lib/commands/trust/circleci.js b/lib/commands/trust/circleci.js new file mode 100644 index 0000000000000..3a47d5230a45e --- /dev/null +++ b/lib/commands/trust/circleci.js @@ -0,0 +1,170 @@ +const Definition = require('@npmcli/config/lib/definitions/definition.js') +const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js') +const TrustCommand = require('../../trust-cmd.js') + +// UUID validation regex +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + +class TrustCircleCI extends TrustCommand { + static description = 'Create a trusted relationship between a package and CircleCI' + static name = 'circleci' + static positionals = 1 // expects at most 1 positional (package name) + static providerName = 'CircleCI' + static providerEntity = 'CircleCI pipeline' + + static usage = [ + '[package] --org-id --project-id --pipeline-definition-id --vcs-origin [--context-id ...] [-y|--yes]', + ] + + static definitions = { + yes: globalDefinitions.yes, + json: globalDefinitions.json, + 'dry-run': globalDefinitions['dry-run'], + 'org-id': new Definition('org-id', { + default: null, + type: String, + description: 'CircleCI organization UUID', + }), + 'project-id': new Definition('project-id', { + default: null, + type: String, + description: 'CircleCI project UUID', + }), + 'pipeline-definition-id': new Definition('pipeline-definition-id', { + default: null, + type: String, + description: 'CircleCI pipeline definition UUID', + }), + 'vcs-origin': new Definition('vcs-origin', { + default: null, + type: String, + description: "CircleCI repository origin in format 'provider/owner/repo'", + }), + 'context-id': new Definition('context-id', { + default: null, + type: [null, String, Array], + description: 'CircleCI context UUID to match', + }), + } + + validateUuid (value, fieldName) { + if (!UUID_REGEX.test(value)) { + throw new Error(`${fieldName} must be a valid UUID`) + } + } + + validateVcsOrigin (value) { + // Expected format: provider/owner/repo (e.g., github.com/owner/repo, bitbucket.org/owner/repo) + const parts = value.split('/') + if (parts.length < 3) { + throw new Error("vcs-origin must be in format 'provider/owner/repo'") + } + } + + // Generate a URL from vcs-origin (e.g., github.com/npm/repo -> https://github.com/npm/repo) + getVcsOriginUrl (vcsOrigin) { + if (!vcsOrigin) { + return null + } + // vcs-origin format: github.com/owner/repo or bitbucket.org/owner/repo + return `https://${vcsOrigin}` + } + + static optionsToBody (options) { + const { orgId, projectId, pipelineDefinitionId, vcsOrigin, contextIds } = options + const trustConfig = { + type: 'circleci', + claims: { + org_id: orgId, + project_id: projectId, + pipeline_definition_id: pipelineDefinitionId, + vcs_origin: vcsOrigin, + }, + } + if (contextIds && contextIds.length > 0) { + trustConfig.claims.context_ids = contextIds + } + return trustConfig + } + + static bodyToOptions (body) { + return { + ...(body.id) && { id: body.id }, + ...(body.type) && { type: body.type }, + ...(body.claims?.org_id) && { orgId: body.claims.org_id }, + ...(body.claims?.project_id) && { projectId: body.claims.project_id }, + ...(body.claims?.pipeline_definition_id) && { + pipelineDefinitionId: body.claims.pipeline_definition_id, + }, + ...(body.claims?.vcs_origin) && { vcsOrigin: body.claims.vcs_origin }, + ...(body.claims?.context_ids) && { contextIds: body.claims.context_ids }, + } + } + + // Override flagsToOptions since CircleCI doesn't use file/entity pattern + async flagsToOptions ({ positionalArgs, flags }) { + const content = await this.optionalPkgJson() + const pkgName = positionalArgs[0] || content.name + + if (!pkgName) { + throw new Error('Package name must be specified either as an argument or in package.json file') + } + + const orgId = flags['org-id'] + const projectId = flags['project-id'] + const pipelineDefinitionId = flags['pipeline-definition-id'] + const vcsOrigin = flags['vcs-origin'] + const contextIds = flags['context-id'] + + // Validate required flags + if (!orgId) { + throw new Error('org-id is required') + } + if (!projectId) { + throw new Error('project-id is required') + } + if (!pipelineDefinitionId) { + throw new Error('pipeline-definition-id is required') + } + if (!vcsOrigin) { + throw new Error('vcs-origin is required') + } + + // Validate formats + this.validateUuid(orgId, 'org-id') + this.validateUuid(projectId, 'project-id') + this.validateUuid(pipelineDefinitionId, 'pipeline-definition-id') + this.validateVcsOrigin(vcsOrigin) + if (contextIds?.length > 0) { + for (const contextId of contextIds) { + this.validateUuid(contextId, 'context-id') + } + } + + return { + values: { + package: pkgName, + orgId, + projectId, + pipelineDefinitionId, + vcsOrigin, + ...(contextIds?.length > 0 && { contextIds }), + }, + fromPackageJson: {}, + warnings: [], + urls: { + package: this.getFrontendUrl({ pkgName }), + vcsOrigin: this.getVcsOriginUrl(vcsOrigin), + }, + } + } + + async exec (positionalArgs, flags) { + await this.createConfigCommand({ + positionalArgs, + flags, + }) + } +} + +module.exports = TrustCircleCI diff --git a/lib/commands/trust/index.js b/lib/commands/trust/index.js index cabcfa7c34cb8..9c3bf070a4ce1 100644 --- a/lib/commands/trust/index.js +++ b/lib/commands/trust/index.js @@ -7,6 +7,7 @@ class Trust extends BaseCommand { static subcommands = { github: require('./github.js'), gitlab: require('./gitlab.js'), + circleci: require('./circleci.js'), list: require('./list.js'), revoke: require('./revoke.js'), } diff --git a/tap-snapshots/test/lib/commands/completion.js.test.cjs b/tap-snapshots/test/lib/commands/completion.js.test.cjs index 6f6d225dff720..0dd229f38630a 100644 --- a/tap-snapshots/test/lib/commands/completion.js.test.cjs +++ b/tap-snapshots/test/lib/commands/completion.js.test.cjs @@ -139,6 +139,7 @@ Array [ String( github gitlab + circleci list revoke ), diff --git a/tap-snapshots/test/lib/docs.js.test.cjs b/tap-snapshots/test/lib/docs.js.test.cjs index 655b36937dfe7..9d74cb68595b1 100644 --- a/tap-snapshots/test/lib/docs.js.test.cjs +++ b/tap-snapshots/test/lib/docs.js.test.cjs @@ -5710,6 +5710,9 @@ Subcommands: gitlab Create a trusted relationship between a package and GitLab CI/CD + circleci + Create a trusted relationship between a package and CircleCI + list List trusted relationships for a package @@ -5745,6 +5748,16 @@ Note: This command is unaware of workspaces. #### \`registry\` #### \`dry-run\` #### Synopsis +#### Flags +#### \`org-id\` +#### \`project-id\` +#### \`pipeline-definition-id\` +#### \`vcs-origin\` +#### \`context-id\` +#### \`yes\` +#### \`json\` +#### \`dry-run\` +#### Synopsis #### Configuration #### \`json\` #### \`registry\` diff --git a/test/lib/commands/trust/circleci.js b/test/lib/commands/trust/circleci.js new file mode 100644 index 0000000000000..87fc72044ddce --- /dev/null +++ b/test/lib/commands/trust/circleci.js @@ -0,0 +1,449 @@ +const t = require('tap') +const { load: loadMockNpm } = require('../../../fixtures/mock-npm.js') +const MockRegistry = require('@npmcli/mock-registry') +const realProcLog = require('proc-log') + +const packageName = '@npmcli/test-package' + +t.test('circleci with all options provided', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + }, + mocks: { + 'proc-log': { + ...realProcLog, + input: { + ...realProcLog.input, + read: async () => 'y', + }, + }, + }, + }) + + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: 'test-auth-token', + }) + + registry.trustCreate({ packageName }) + + await npm.exec('trust', [ + 'circleci', + packageName, + '--yes', + '--org-id', '550e8400-e29b-41d4-a716-446655440000', + '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', + '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + '--vcs-origin', 'github.com/owner/repo', + '--context-id', '123e4567-e89b-12d3-a456-426614174000', + ]) +}) + +t.test('circleci without optional context-id', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + }, + mocks: { + 'proc-log': { + ...realProcLog, + input: { + ...realProcLog.input, + read: async () => 'y', + }, + }, + }, + }) + + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: 'test-auth-token', + }) + + registry.trustCreate({ packageName }) + + await npm.exec('trust', [ + 'circleci', + packageName, + '--yes', + '--org-id', '550e8400-e29b-41d4-a716-446655440000', + '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', + '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + '--vcs-origin', 'github.com/owner/repo', + ]) +}) + +t.test('circleci with multiple context-ids', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + }, + mocks: { + 'proc-log': { + ...realProcLog, + input: { + ...realProcLog.input, + read: async () => 'y', + }, + }, + }, + }) + + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: 'test-auth-token', + }) + + registry.trustCreate({ packageName }) + + await npm.exec('trust', [ + 'circleci', + packageName, + '--yes', + '--org-id', '550e8400-e29b-41d4-a716-446655440000', + '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', + '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + '--vcs-origin', 'github.com/owner/repo', + '--context-id', '123e4567-e89b-12d3-a456-426614174000', + '--context-id', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + ]) +}) + +t.test('circleci missing required org-id', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + }, + }) + + await t.rejects( + npm.exec('trust', [ + 'circleci', + packageName, + '--yes', + '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', + '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + '--vcs-origin', 'github.com/owner/repo', + ]), + { message: /org-id is required/ } + ) +}) + +t.test('circleci missing required project-id', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + }, + }) + + await t.rejects( + npm.exec('trust', [ + 'circleci', + packageName, + '--yes', + '--org-id', '550e8400-e29b-41d4-a716-446655440000', + '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + '--vcs-origin', 'github.com/owner/repo', + ]), + { message: /project-id is required/ } + ) +}) + +t.test('circleci missing required pipeline-definition-id', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + }, + }) + + await t.rejects( + npm.exec('trust', [ + 'circleci', + packageName, + '--yes', + '--org-id', '550e8400-e29b-41d4-a716-446655440000', + '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', + '--vcs-origin', 'github.com/owner/repo', + ]), + { message: /pipeline-definition-id is required/ } + ) +}) + +t.test('circleci missing required vcs-origin', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + }, + }) + + await t.rejects( + npm.exec('trust', [ + 'circleci', + packageName, + '--yes', + '--org-id', '550e8400-e29b-41d4-a716-446655440000', + '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', + '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + ]), + { message: /vcs-origin is required/ } + ) +}) + +t.test('circleci with invalid org-id uuid format', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + }, + }) + + await t.rejects( + npm.exec('trust', [ + 'circleci', + packageName, + '--yes', + '--org-id', 'not-a-uuid', + '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', + '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + '--vcs-origin', 'github.com/owner/repo', + ]), + { message: /org-id must be a valid UUID/ } + ) +}) + +t.test('circleci with invalid vcs-origin format', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + }, + }) + + await t.rejects( + npm.exec('trust', [ + 'circleci', + packageName, + '--yes', + '--org-id', '550e8400-e29b-41d4-a716-446655440000', + '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', + '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + '--vcs-origin', 'invalid-format', + ]), + { message: /vcs-origin must be in format 'provider\/owner\/repo'/ } + ) +}) + +t.test('circleci missing package name', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + }, + }) + + await t.rejects( + npm.exec('trust', [ + 'circleci', + '--yes', + '--org-id', '550e8400-e29b-41d4-a716-446655440000', + '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', + '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + '--vcs-origin', 'github.com/owner/repo', + ]), + { message: /Package name must be specified either as an argument or in package.json file/ } + ) +}) + +t.test('bodyToOptions with all fields', t => { + const TrustCircleCI = require('../../../../lib/commands/trust/circleci.js') + + const body = { + id: 'test-id', + type: 'circleci', + claims: { + org_id: '550e8400-e29b-41d4-a716-446655440000', + project_id: '7c9e6679-7425-40de-944b-e07fc1f90ae7', + pipeline_definition_id: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + vcs_origin: 'github.com/owner/repo', + context_ids: ['123e4567-e89b-12d3-a456-426614174000'], + }, + } + + const options = TrustCircleCI.bodyToOptions(body) + + t.equal(options.id, 'test-id', 'id should be set') + t.equal(options.type, 'circleci', 'type should be set') + t.equal(options.orgId, '550e8400-e29b-41d4-a716-446655440000', 'orgId should be set') + t.equal(options.projectId, '7c9e6679-7425-40de-944b-e07fc1f90ae7', 'projectId should be set') + t.equal(options.pipelineDefinitionId, '6ba7b810-9dad-11d1-80b4-00c04fd430c8', 'pipelineDefinitionId should be set') + t.equal(options.vcsOrigin, 'github.com/owner/repo', 'vcsOrigin should be set') + t.same(options.contextIds, ['123e4567-e89b-12d3-a456-426614174000'], 'contextIds should be set') + t.end() +}) + +t.test('bodyToOptions without optional context_ids', t => { + const TrustCircleCI = require('../../../../lib/commands/trust/circleci.js') + + const body = { + id: 'test-id', + type: 'circleci', + claims: { + org_id: '550e8400-e29b-41d4-a716-446655440000', + project_id: '7c9e6679-7425-40de-944b-e07fc1f90ae7', + pipeline_definition_id: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + vcs_origin: 'github.com/owner/repo', + }, + } + + const options = TrustCircleCI.bodyToOptions(body) + + t.equal(options.contextIds, undefined, 'contextIds should be undefined') + t.end() +}) + +t.test('optionsToBody with all fields', t => { + const TrustCircleCI = require('../../../../lib/commands/trust/circleci.js') + + const options = { + orgId: '550e8400-e29b-41d4-a716-446655440000', + projectId: '7c9e6679-7425-40de-944b-e07fc1f90ae7', + pipelineDefinitionId: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + vcsOrigin: 'github.com/owner/repo', + contextIds: ['123e4567-e89b-12d3-a456-426614174000'], + } + + const body = TrustCircleCI.optionsToBody(options) + + t.equal(body.type, 'circleci', 'type should be circleci') + t.equal(body.claims.org_id, '550e8400-e29b-41d4-a716-446655440000', 'org_id should be set') + t.equal(body.claims.project_id, '7c9e6679-7425-40de-944b-e07fc1f90ae7', 'project_id should be set') + t.equal(body.claims.pipeline_definition_id, '6ba7b810-9dad-11d1-80b4-00c04fd430c8', 'pipeline_definition_id should be set') + t.equal(body.claims.vcs_origin, 'github.com/owner/repo', 'vcs_origin should be set') + t.same(body.claims.context_ids, ['123e4567-e89b-12d3-a456-426614174000'], 'context_ids should be set') + t.end() +}) + +t.test('optionsToBody without optional contextIds', t => { + const TrustCircleCI = require('../../../../lib/commands/trust/circleci.js') + + const options = { + orgId: '550e8400-e29b-41d4-a716-446655440000', + projectId: '7c9e6679-7425-40de-944b-e07fc1f90ae7', + pipelineDefinitionId: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + vcsOrigin: 'github.com/owner/repo', + } + + const body = TrustCircleCI.optionsToBody(options) + + t.equal(body.claims.context_ids, undefined, 'context_ids should be undefined') + t.end() +}) + +t.test('optionsToBody with multiple contextIds', t => { + const TrustCircleCI = require('../../../../lib/commands/trust/circleci.js') + + const options = { + orgId: '550e8400-e29b-41d4-a716-446655440000', + projectId: '7c9e6679-7425-40de-944b-e07fc1f90ae7', + pipelineDefinitionId: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + vcsOrigin: 'github.com/owner/repo', + contextIds: [ + '123e4567-e89b-12d3-a456-426614174000', + 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + ], + } + + const body = TrustCircleCI.optionsToBody(options) + + t.same(body.claims.context_ids, [ + '123e4567-e89b-12d3-a456-426614174000', + 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + ], 'context_ids should contain both UUIDs') + t.end() +}) + +t.test('getVcsOriginUrl generates correct URL', t => { + const TrustCircleCI = require('../../../../lib/commands/trust/circleci.js') + + t.equal( + TrustCircleCI.prototype.getVcsOriginUrl('github.com/npm/cli'), + 'https://github.com/npm/cli', + 'should generate https URL from vcs-origin' + ) + t.equal( + TrustCircleCI.prototype.getVcsOriginUrl('bitbucket.org/owner/repo'), + 'https://bitbucket.org/owner/repo', + 'should work with bitbucket' + ) + t.equal( + TrustCircleCI.prototype.getVcsOriginUrl(null), + null, + 'should return null for null input' + ) + t.equal( + TrustCircleCI.prototype.getVcsOriginUrl(undefined), + null, + 'should return null for undefined input' + ) + t.end() +}) From 43efaa52de61bffa6c6644227b0570656feb7146 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 22 Jan 2026 13:33:32 -0800 Subject: [PATCH 2/3] use full claim for body --- lib/commands/trust/circleci.js | 22 +++++++++---------- test/lib/commands/trust/circleci.js | 34 ++++++++++++++--------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/commands/trust/circleci.js b/lib/commands/trust/circleci.js index 3a47d5230a45e..b3f61771a874e 100644 --- a/lib/commands/trust/circleci.js +++ b/lib/commands/trust/circleci.js @@ -75,14 +75,14 @@ class TrustCircleCI extends TrustCommand { const trustConfig = { type: 'circleci', claims: { - org_id: orgId, - project_id: projectId, - pipeline_definition_id: pipelineDefinitionId, - vcs_origin: vcsOrigin, + 'oidc.circleci.com/org-id': orgId, + 'oidc.circleci.com/project-id': projectId, + 'oidc.circleci.com/pipeline-definition-id': pipelineDefinitionId, + 'oidc.circleci.com/vcs-origin': vcsOrigin, }, } if (contextIds && contextIds.length > 0) { - trustConfig.claims.context_ids = contextIds + trustConfig.claims['oidc.circleci.com/context-ids'] = contextIds } return trustConfig } @@ -91,13 +91,13 @@ class TrustCircleCI extends TrustCommand { return { ...(body.id) && { id: body.id }, ...(body.type) && { type: body.type }, - ...(body.claims?.org_id) && { orgId: body.claims.org_id }, - ...(body.claims?.project_id) && { projectId: body.claims.project_id }, - ...(body.claims?.pipeline_definition_id) && { - pipelineDefinitionId: body.claims.pipeline_definition_id, + ...(body.claims?.['oidc.circleci.com/org-id']) && { orgId: body.claims['oidc.circleci.com/org-id'] }, + ...(body.claims?.['oidc.circleci.com/project-id']) && { projectId: body.claims['oidc.circleci.com/project-id'] }, + ...(body.claims?.['oidc.circleci.com/pipeline-definition-id']) && { + pipelineDefinitionId: body.claims['oidc.circleci.com/pipeline-definition-id'], }, - ...(body.claims?.vcs_origin) && { vcsOrigin: body.claims.vcs_origin }, - ...(body.claims?.context_ids) && { contextIds: body.claims.context_ids }, + ...(body.claims?.['oidc.circleci.com/vcs-origin']) && { vcsOrigin: body.claims['oidc.circleci.com/vcs-origin'] }, + ...(body.claims?.['oidc.circleci.com/context-ids']) && { contextIds: body.claims['oidc.circleci.com/context-ids'] }, } } diff --git a/test/lib/commands/trust/circleci.js b/test/lib/commands/trust/circleci.js index 87fc72044ddce..613609a564fcf 100644 --- a/test/lib/commands/trust/circleci.js +++ b/test/lib/commands/trust/circleci.js @@ -321,11 +321,11 @@ t.test('bodyToOptions with all fields', t => { id: 'test-id', type: 'circleci', claims: { - org_id: '550e8400-e29b-41d4-a716-446655440000', - project_id: '7c9e6679-7425-40de-944b-e07fc1f90ae7', - pipeline_definition_id: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', - vcs_origin: 'github.com/owner/repo', - context_ids: ['123e4567-e89b-12d3-a456-426614174000'], + 'oidc.circleci.com/org-id': '550e8400-e29b-41d4-a716-446655440000', + 'oidc.circleci.com/project-id': '7c9e6679-7425-40de-944b-e07fc1f90ae7', + 'oidc.circleci.com/pipeline-definition-id': '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + 'oidc.circleci.com/vcs-origin': 'github.com/owner/repo', + 'oidc.circleci.com/context-ids': ['123e4567-e89b-12d3-a456-426614174000'], }, } @@ -348,10 +348,10 @@ t.test('bodyToOptions without optional context_ids', t => { id: 'test-id', type: 'circleci', claims: { - org_id: '550e8400-e29b-41d4-a716-446655440000', - project_id: '7c9e6679-7425-40de-944b-e07fc1f90ae7', - pipeline_definition_id: '6ba7b810-9dad-11d1-80b4-00c04fd430c8', - vcs_origin: 'github.com/owner/repo', + 'oidc.circleci.com/org-id': '550e8400-e29b-41d4-a716-446655440000', + 'oidc.circleci.com/project-id': '7c9e6679-7425-40de-944b-e07fc1f90ae7', + 'oidc.circleci.com/pipeline-definition-id': '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + 'oidc.circleci.com/vcs-origin': 'github.com/owner/repo', }, } @@ -375,11 +375,11 @@ t.test('optionsToBody with all fields', t => { const body = TrustCircleCI.optionsToBody(options) t.equal(body.type, 'circleci', 'type should be circleci') - t.equal(body.claims.org_id, '550e8400-e29b-41d4-a716-446655440000', 'org_id should be set') - t.equal(body.claims.project_id, '7c9e6679-7425-40de-944b-e07fc1f90ae7', 'project_id should be set') - t.equal(body.claims.pipeline_definition_id, '6ba7b810-9dad-11d1-80b4-00c04fd430c8', 'pipeline_definition_id should be set') - t.equal(body.claims.vcs_origin, 'github.com/owner/repo', 'vcs_origin should be set') - t.same(body.claims.context_ids, ['123e4567-e89b-12d3-a456-426614174000'], 'context_ids should be set') + t.equal(body.claims['oidc.circleci.com/org-id'], '550e8400-e29b-41d4-a716-446655440000', 'org-id should be set') + t.equal(body.claims['oidc.circleci.com/project-id'], '7c9e6679-7425-40de-944b-e07fc1f90ae7', 'project-id should be set') + t.equal(body.claims['oidc.circleci.com/pipeline-definition-id'], '6ba7b810-9dad-11d1-80b4-00c04fd430c8', 'pipeline-definition-id should be set') + t.equal(body.claims['oidc.circleci.com/vcs-origin'], 'github.com/owner/repo', 'vcs-origin should be set') + t.same(body.claims['oidc.circleci.com/context-ids'], ['123e4567-e89b-12d3-a456-426614174000'], 'context-ids should be set') t.end() }) @@ -395,7 +395,7 @@ t.test('optionsToBody without optional contextIds', t => { const body = TrustCircleCI.optionsToBody(options) - t.equal(body.claims.context_ids, undefined, 'context_ids should be undefined') + t.equal(body.claims['oidc.circleci.com/context-ids'], undefined, 'context-ids should be undefined') t.end() }) @@ -415,10 +415,10 @@ t.test('optionsToBody with multiple contextIds', t => { const body = TrustCircleCI.optionsToBody(options) - t.same(body.claims.context_ids, [ + t.same(body.claims['oidc.circleci.com/context-ids'], [ '123e4567-e89b-12d3-a456-426614174000', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', - ], 'context_ids should contain both UUIDs') + ], 'context-ids should contain both UUIDs') t.end() }) From ba9778ad2ccbf0a0c13516d741b259a89055281d Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 29 Jan 2026 11:12:36 -0800 Subject: [PATCH 3/3] don't redact UUIDs for create/list trust commands --- lib/trust-cmd.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/trust-cmd.js b/lib/trust-cmd.js index 1e5c4e9a55bb6..ae5f5aa495817 100644 --- a/lib/trust-cmd.js +++ b/lib/trust-cmd.js @@ -3,7 +3,7 @@ const { otplease } = require('./utils/auth.js') const npmFetch = require('npm-registry-fetch') const npa = require('npm-package-arg') const { read: _read } = require('read') -const { input, output, log } = require('proc-log') +const { input, output, log, META } = require('proc-log') const gitinfo = require('hosted-git-info') const pkgJson = require('@npmcli/package-json') @@ -55,7 +55,8 @@ class TrustCommand extends BaseCommand { const json = this.config.get('json') if (json) { - output.standard(JSON.stringify(options.values, null, 2)) + // Disable redaction: trust config values (e.g. CircleCI UUIDs) are not secrets + output.standard(JSON.stringify(options.values, null, 2), { [META]: true, redact: false }) return } @@ -95,7 +96,7 @@ class TrustCommand extends BaseCommand { } if (urlLines.length > 0) { output.standard() - output.standard(urlLines.join('\n')) + output.standard(urlLines.join('\n'), { [META]: true, redact: false }) } } if (pad) {