11const { log, output } = require ( 'proc-log' )
22const { listTokens, createToken, removeToken } = require ( 'npm-profile' )
33const { otplease } = require ( '../utils/auth.js' )
4- const readUserInfo = require ( '../utils/read-user-info.js' )
54const BaseCommand = require ( '../base-cmd.js' )
65
76class Token extends BaseCommand {
87 static description = 'Manage your authentication tokens'
98 static name = 'token'
10- static usage = [ 'list' , 'revoke <id|token>' , 'create [--read-only] [--cidr=list]' ]
11- static params = [ 'read-only' , 'cidr' , 'registry' , 'otp' ]
9+ static usage = [ 'list' , 'revoke <id|token>' , 'create --name=<name> --access=<read-only|read-write> [--expires=<YYYY-MM-DD>] [--packages=<pkg1,pkg2>] [--scopes=<scope1,scope2>] [--orgs=<org1,org2>] [--cidr=<ip-range>] [--bypass-2fa]' ]
10+ static params = [ 'name' ,
11+ 'expires' ,
12+ 'access' ,
13+ 'packages' ,
14+ 'scopes' ,
15+ 'orgs' ,
16+ 'cidr' ,
17+ 'bypass-2fa' ,
18+ 'registry' ,
19+ 'otp' ,
20+ 'read-only' ,
21+ ]
1222
1323 static async completion ( opts ) {
1424 const argv = opts . conf . argv . remain
@@ -127,15 +137,91 @@ class Token extends BaseCommand {
127137 const json = this . npm . config . get ( 'json' )
128138 const parseable = this . npm . config . get ( 'parseable' )
129139 const cidr = this . npm . config . get ( 'cidr' )
130- const readonly = this . npm . config . get ( 'read-only' )
140+ const name = this . npm . config . get ( 'name' )
141+ const expires = this . npm . config . get ( 'expires' )
142+ const access = this . npm . config . get ( 'access' )
143+ const packages = this . npm . config . get ( 'packages' )
144+ const scopes = this . npm . config . get ( 'scopes' )
145+ const orgs = this . npm . config . get ( 'orgs' )
146+ const bypassTwoFactor = this . npm . config . get ( 'bypass-2fa' )
147+
148+ // Validate required parameters
149+ if ( ! name ) {
150+ throw this . usageError ( '--name is required for token creation' )
151+ }
152+ if ( ! access ) {
153+ throw this . usageError ( '--access is required (use "read-only" or "read-write")' )
154+ }
155+ if ( ! [ 'read-only' , 'read-write' ] . includes ( access ) ) {
156+ throw this . usageError ( '--access must be either "read-only" or "read-write"' )
157+ }
131158
132159 const validCIDR = await this . validateCIDRList ( cidr )
133- const password = await readUserInfo . password ( )
160+ // Build GAT token data structure
161+ const tokenData = {
162+ token_type : 'granular' ,
163+ token_name : name ,
164+ }
165+
166+ // Convert access to permission action (read-only -> read, read-write -> write)
167+ const permissionAction = access === 'read-only' ? 'read' : 'write'
168+
169+ // Build scopes array (combines packages, scopes, and orgs)
170+ const scopesArray = [ ]
171+ if ( packages ?. length > 0 ) {
172+ packages . forEach ( pkg => {
173+ scopesArray . push ( { type : 'package' , name : pkg } )
174+ } )
175+ }
176+ if ( scopes ?. length > 0 ) {
177+ scopes . forEach ( scope => {
178+ scopesArray . push ( { type : 'package' , name : scope } )
179+ } )
180+ }
181+ if ( orgs ?. length > 0 ) {
182+ orgs . forEach ( org => {
183+ scopesArray . push ( { type : 'org' , name : org } )
184+ } )
185+ }
186+ tokenData . scopes = scopesArray
187+
188+ // Build permissions array based on what types are present
189+ const permissionsArray = [ ]
190+ const hasPackageScope = packages ?. length > 0 || scopes ?. length > 0
191+ const hasOrgScope = orgs ?. length > 0
192+
193+ if ( hasPackageScope ) {
194+ permissionsArray . push ( { name : 'package' , action : permissionAction } )
195+ }
196+ if ( hasOrgScope ) {
197+ permissionsArray . push ( { name : 'org' , action : permissionAction } )
198+ }
199+ tokenData . permissions = permissionsArray
200+
201+ // Add expiration in days (default to 7 days if not provided)
202+ if ( expires ) {
203+ const expiresDate = new Date ( expires )
204+ const now = new Date ( )
205+ const diffDays = Math . ceil ( ( expiresDate - now ) / ( 1000 * 60 * 60 * 24 ) )
206+ tokenData . expirationInDays = Math . max ( 1 , diffDays ) // minimum 1 day
207+ } else {
208+ tokenData . expirationInDays = 7
209+ }
210+
211+ // Add optional fields
212+ if ( validCIDR ?. length > 0 ) {
213+ tokenData . cidr_whitelist = validCIDR
214+ }
215+ if ( bypassTwoFactor ) {
216+ tokenData . bypass_2fa = true
217+ }
218+
134219 log . info ( 'token' , 'creating' )
220+ log . silly ( 'token' , 'request body:' , JSON . stringify ( tokenData , null , 2 ) )
135221 const result = await otplease (
136222 this . npm ,
137223 { ...this . npm . flatOptions } ,
138- c => createToken ( password , readonly , validCIDR , c )
224+ c => createToken ( tokenData , c )
139225 )
140226 delete result . key
141227 delete result . updated
@@ -145,12 +231,15 @@ class Token extends BaseCommand {
145231 Object . keys ( result ) . forEach ( k => output . standard ( k + '\t' + result [ k ] ) )
146232 } else {
147233 const chalk = this . npm . chalk
148- // Identical to list
149- const level = result . readonly ? 'read only' : 'publish'
234+ // Display based on access level
235+ const level = result . access === 'read-only' || result . readonly ? 'read only' : 'publish'
150236 output . standard ( `Created ${ chalk . blue ( level ) } token ${ result . token } ` )
151237 if ( result . cidr_whitelist ?. length ) {
152238 output . standard ( `with IP whitelist: ${ chalk . green ( result . cidr_whitelist . join ( ',' ) ) } ` )
153239 }
240+ if ( result . expires ) {
241+ output . standard ( `expires: ${ result . expires } ` )
242+ }
154243 }
155244 }
156245
@@ -180,7 +269,7 @@ class Token extends BaseCommand {
180269 for ( const cidr of list ) {
181270 if ( isCidrV6 ( cidr ) ) {
182271 throw this . invalidCIDRError (
183- `CIDR whitelist can only contain IPv4 addresses${ cidr } is IPv6`
272+ `CIDR whitelist can only contain IPv4 addresses, ${ cidr } is IPv6`
184273 )
185274 }
186275
0 commit comments