@@ -176,11 +176,48 @@ class DeviceFlowUnsupported extends Error {
176176 constructor ( ) { super ( "device flow not supported by this server" ) ; }
177177}
178178
179+ // A vault is addressed as <project>/<env>, so one team can hold web/prod,
180+ // web/staging and api/prod side by side. The split lives entirely in the CLI —
181+ // the server still stores a single opaque vault name — so this join and
182+ // splitVaultName() below are the only places that know about the convention.
183+ // Neither half may contain a slash, which keeps the join unambiguous and makes
184+ // splitVaultName a true inverse.
185+ export function vaultName ( project : string , env : string ) : string {
186+ const parts : ReadonlyArray < readonly [ string , string ] > = [
187+ [ "project" , project ] ,
188+ [ "env" , env ]
189+ ] ;
190+ for ( const [ label , value ] of parts ) {
191+ if ( ! value || ! value . trim ( ) ) {
192+ throw new Error ( `Missing ${ label } . Usage: logicsrc teams push <team> <project> <env>` ) ;
193+ }
194+ if ( value . includes ( "/" ) ) {
195+ throw new Error ( `The ${ label } "${ value } " cannot contain "/" — it separates project from env in a vault name.` ) ;
196+ }
197+ }
198+ return `${ project } /${ env } ` ;
199+ }
200+
201+ /** Inverse of vaultName; null for names that predate the convention. */
202+ export function splitVaultName ( name : string ) : { project : string ; env : string } | null {
203+ const slash = name . indexOf ( "/" ) ;
204+ if ( slash <= 0 || slash === name . length - 1 ) return null ;
205+ const env = name . slice ( slash + 1 ) ;
206+ if ( env . includes ( "/" ) ) return null ;
207+ return { project : name . slice ( 0 , slash ) , env } ;
208+ }
209+
179210async function resolveVaultId ( client : TeamClient , slug : string , vault : string ) : Promise < string > {
180211 const { vaults } = await client . listVaults ( slug ) ;
181212 const found = vaults . find ( ( v ) => v . name === vault ) ;
182- if ( ! found ) throw new Error ( `Vault "${ vault } " not found in team "${ slug } ". Create it by pushing to it.` ) ;
183- return found . id ;
213+ if ( found ) return found . id ;
214+ // Vault names were a single word before they became <project>/<env>, so a
215+ // team can still hold legacy rows. Name them instead of silently retargeting
216+ // — picking a different vault than the one asked for would mean pushing
217+ // secrets somewhere the caller didn't say.
218+ const known = vaults . map ( ( v ) => v . name ) ;
219+ const hint = known . length ? ` Existing vaults: ${ known . join ( ", " ) } .` : "" ;
220+ throw new Error ( `Vault "${ vault } " not found in team "${ slug } ". Create it by pushing to it.${ hint } ` ) ;
184221}
185222
186223export async function loginAction ( options : { apiUrl ?: string ; token ?: string ; device ?: boolean ; web ?: boolean } ) : Promise < void > {
@@ -288,13 +325,25 @@ export async function teamsVaultsAction(slug: string, format: OutputFormat): Pro
288325 const { client } = authedClient ( ) ;
289326 const { vaults } = await client . listVaults ( slug ) ;
290327 print (
291- vaults . length ? vaults . map ( ( v ) => ( { vault : v . name , secrets : v . secretCount , youHaveAccess : v . hasAccess } ) ) : [ { note : "No vaults yet. Push to create one: logicsrc teams push <team> <vault>" } ] ,
328+ vaults . length
329+ ? vaults . map ( ( v ) => {
330+ const parts = splitVaultName ( v . name ) ;
331+ return {
332+ vault : v . name ,
333+ project : parts ?. project ?? v . name ,
334+ env : parts ?. env ?? "—" ,
335+ secrets : v . secretCount ,
336+ youHaveAccess : v . hasAccess
337+ } ;
338+ } )
339+ : [ { note : "No vaults yet. Push to create one: logicsrc teams push <team> <project> <env>" } ] ,
292340 format
293341 ) ;
294342}
295343
296- export async function teamsGrantAction ( slug : string , vault : string , email : string , format : OutputFormat ) : Promise < void > {
344+ export async function teamsGrantAction ( slug : string , project : string , env : string , email : string , format : OutputFormat ) : Promise < void > {
297345 const { client, identity } = authedClient ( ) ;
346+ const vault = vaultName ( project , env ) ;
298347 const vaultId = await resolveVaultId ( client , slug , vault ) ;
299348
300349 // Unwrap the vault DEK with our own key, then re-wrap it to the target member.
@@ -314,44 +363,48 @@ export async function teamsGrantAction(slug: string, vault: string, email: strin
314363 if ( ! target . publicKey ) throw new Error ( `${ email } has not registered a key yet. Ask them to run: logicsrc login --email ${ email } ` ) ;
315364
316365 await client . putGrant ( vaultId , email , await wrapVaultKey ( dek , target . publicKey ) ) ;
317- console . error ( `Granted ${ email } access to ${ slug } /${ vault } . They can now: logicsrc teams pull ${ slug } ${ vault } ` ) ;
318- print ( { granted : email , team : slug , vault } , format ) ;
366+ console . error ( `Granted ${ email } access to ${ slug } /${ vault } . They can now: logicsrc teams pull ${ slug } ${ project } ${ env } ` ) ;
367+ print ( { granted : email , team : slug , project , env , vault } , format ) ;
319368}
320369
321370function teamEndpoint ( slug : string , vault : string ) : CredentialEndpoint {
322371 return { provider : "team" , project : slug , config : vault } ;
323372}
324373
325- export async function teamsPushAction ( slug : string , vault : string , options : { env : string ; format : OutputFormat } ) : Promise < void > {
374+ // Note the two different "env"s: `envName` is the environment half of the vault
375+ // address (prod, staging), while `options.env` is the local .env file path.
376+ export async function teamsPushAction ( slug : string , project : string , envName : string , options : { env : string ; format : OutputFormat } ) : Promise < void > {
326377 requireAuth ( ) ;
378+ const vault = vaultName ( project , envName ) ;
327379 const engine = createCredentialEngine ( ) ;
328380 const from : CredentialEndpoint = { provider : "env" , path : options . env } ;
329381 const plan = await engine . createCredentialSyncPlan ( { from, to : teamEndpoint ( slug , vault ) } ) ;
330382 if ( plan . changes . length === 0 ) {
331383 console . error ( `${ slug } /${ vault } is already up to date with ${ options . env } .` ) ;
332- print ( { team : slug , vault, changes : 0 } , options . format ) ;
384+ print ( { team : slug , project , env : envName , vault, changes : 0 } , options . format ) ;
333385 return ;
334386 }
335387 const approval = engine . approveCredentialSync ( plan . id ) ;
336388 const run = await engine . runCredentialSync ( plan . id , { dryRun : false , approval } ) ;
337389 const applied = run . results . filter ( ( r ) => r . applied ) . length ;
338390 console . error ( `Pushed ${ applied } secret(s) from ${ options . env } to ${ slug } /${ vault } (end-to-end encrypted).` ) ;
339- print ( { team : slug , vault, applied, keys : run . results . map ( ( r ) => ( { key : r . key , op : r . op , applied : r . applied } ) ) } , options . format ) ;
391+ print ( { team : slug , project , env : envName , vault, applied, keys : run . results . map ( ( r ) => ( { key : r . key , op : r . op , applied : r . applied } ) ) } , options . format ) ;
340392}
341393
342- export async function teamsPullAction ( slug : string , vault : string , options : { env : string ; format : OutputFormat } ) : Promise < void > {
394+ export async function teamsPullAction ( slug : string , project : string , envName : string , options : { env : string ; format : OutputFormat } ) : Promise < void > {
343395 requireAuth ( ) ;
396+ const vault = vaultName ( project , envName ) ;
344397 const engine = createCredentialEngine ( ) ;
345398 const to : CredentialEndpoint = { provider : "env" , path : options . env } ;
346399 const plan = await engine . createCredentialSyncPlan ( { from : teamEndpoint ( slug , vault ) , to } ) ;
347400 if ( plan . changes . length === 0 ) {
348401 console . error ( `${ options . env } is already up to date with ${ slug } /${ vault } .` ) ;
349- print ( { team : slug , vault, changes : 0 } , options . format ) ;
402+ print ( { team : slug , project , env : envName , vault, changes : 0 } , options . format ) ;
350403 return ;
351404 }
352405 const approval = engine . approveCredentialSync ( plan . id ) ;
353406 const run = await engine . runCredentialSync ( plan . id , { dryRun : false , approval } ) ;
354407 const applied = run . results . filter ( ( r ) => r . applied ) . length ;
355408 console . error ( `Pulled ${ applied } secret(s) from ${ slug } /${ vault } into ${ options . env } .` ) ;
356- print ( { team : slug , vault, applied, keys : run . results . map ( ( r ) => ( { key : r . key , op : r . op , applied : r . applied } ) ) } , options . format ) ;
409+ print ( { team : slug , project , env : envName , vault, applied, keys : run . results . map ( ( r ) => ( { key : r . key , op : r . op , applied : r . applied } ) ) } , options . format ) ;
357410}
0 commit comments