11import type PolykeyClient from 'polykey/PolykeyClient.js' ;
2- import type { JSONSchema , ParsedSecretPathValue } from '../types.js' ;
2+ import type {
3+ JSONSchema ,
4+ JSONSchemaInfo ,
5+ ParsedSecretPathValue ,
6+ } from '../types.js' ;
37import path from 'node:path' ;
48import os from 'node:os' ;
59import $RefParser from '@apidevtools/json-schema-ref-parser' ;
610import { Ajv2019 as Ajv } from 'ajv/dist/2019.js' ;
711import { InvalidArgumentError } from 'commander' ;
8- import * as utils from 'polykey/utils/index.js' ;
912import CommandPolykey from '../CommandPolykey.js' ;
1013import * as binProcessors from '../utils/processors.js' ;
1114import * as binUtils from '../utils/index.js' ;
@@ -37,6 +40,7 @@ class CommandEnv extends CommandPolykey {
3740 const { default : PolykeyClient } = await import (
3841 'polykey/PolykeyClient.js'
3942 ) ;
43+ const utils = await import ( 'polykey/utils/index.js' ) ;
4044 const {
4145 envInvalid,
4246 envDuplicate,
@@ -122,27 +126,66 @@ class CommandEnv extends CommandPolykey {
122126 logger : this . logger . getChild ( PolykeyClient . name ) ,
123127 } ) ;
124128
129+ let schema : JSONSchema | undefined = undefined ;
130+ let unwrappedSchema : JSONSchemaInfo | undefined = undefined ;
131+ if ( options . egressSchema != null ) {
132+ schema = ( await $RefParser . bundle (
133+ options . egressSchema ,
134+ ) ) satisfies JSONSchema ;
135+ unwrappedSchema = binUtils . loadSchema ( schema ! ) ;
136+ }
137+
125138 // Getting envs
126139 const [ envp ] = await binUtils . retryAuthentication ( async ( auth ) => {
127140 const responseStream =
128141 await pkClient . rpcClient . methods . vaultsSecretsEnv ( ) ;
142+
129143 // Writing desired secrets
130144 const secretRenameMap = new Map < string , string | undefined > ( ) ;
131- const writeP = ( async ( ) => {
132- const writer = responseStream . writable . getWriter ( ) ;
133- let first = true ;
134- for ( const envVariable of envVariables ) {
135- const [ nameOrId , secretName , secretNameNew ] = envVariable ;
136- secretRenameMap . set ( secretName ?? '/' , secretNameNew ) ;
145+ const writer = responseStream . writable . getWriter ( ) ;
146+ let first = true ;
147+ for ( const envVariable of envVariables ) {
148+ const [ nameOrId , secretName , secretNameNew ] = envVariable ;
149+ secretRenameMap . set ( secretName ?? '/' , secretNameNew ) ;
150+
151+ // If there is no secret name provided, then attempt to export the
152+ // secrets from the entire vault. Otherwise, check if the selected
153+ // secret exists in the schema before requesting it. This will
154+ // only run if a schema has been specified.
155+ if ( schema != null && unwrappedSchema != null ) {
156+ const { allKeys } = unwrappedSchema ;
157+ if ( nameOrId != null && secretName == null ) {
158+ // Only vault specified
159+ for ( const key of allKeys ) {
160+ // TODO: handle secret renames, allKeys key might not be the same in vault
161+ await writer . write ( {
162+ nameOrId : nameOrId ,
163+ secretName : key ,
164+ metadata : first ? auth : undefined ,
165+ } ) ;
166+ }
167+ } else {
168+ // Individual secret name specified
169+ const name : string = secretNameNew != null ? secretNameNew : secretName ! ;
170+ if ( allKeys . includes ( name ) ) {
171+ await writer . write ( {
172+ nameOrId : nameOrId ,
173+ secretName : name ,
174+ metadata : first ? auth : undefined ,
175+ } ) ;
176+ }
177+ }
178+ } else {
179+ // No schema specified
137180 await writer . write ( {
138181 nameOrId : nameOrId ,
139182 secretName : secretName ?? '/' ,
140183 metadata : first ? auth : undefined ,
141184 } ) ;
142- first = false ;
143185 }
144- await writer . close ( ) ;
145- } ) ( ) ;
186+ first = false ;
187+ }
188+ await writer . close ( ) ;
146189
147190 const envp : Record < string , string > = { } ;
148191 const envpPath : Record <
@@ -153,6 +196,34 @@ class CommandEnv extends CommandPolykey {
153196 }
154197 > = { } ;
155198 for await ( const value of responseStream . readable ) {
199+ if ( value . type === 'ErrorMessage' ) {
200+ switch ( value . code ) {
201+ case 'EINVAL' :
202+ // It is expected for the data to be populated with the offending
203+ // vault name if the vault was not found.
204+ process . stderr . write (
205+ binUtils . outputFormatterError (
206+ `Vault "${ value . data ?. nameOrId } " does not exist` ,
207+ ) ,
208+ ) ;
209+ break ;
210+ case 'ENOENT' :
211+ // It is expected for the data to be populated with the offending
212+ // secret and vault name if a secret was not found.
213+ process . stderr . write (
214+ binUtils . outputFormatterError (
215+ `Secret "${ value . data ?. secretName } " does not exist in vault "${ value . data ?. nameOrId } "` ,
216+ ) ,
217+ ) ;
218+ break ;
219+ default :
220+ utils . never (
221+ `Expected code to be one of EINVAL, ENOENT, received ${ value . code } ` ,
222+ ) ;
223+ }
224+ continue ;
225+ }
226+
156227 const { nameOrId, secretName, secretContent } = value ;
157228 let newName = secretRenameMap . get ( secretName ) ;
158229 if ( newName == null ) {
@@ -229,30 +300,16 @@ class CommandEnv extends CommandPolykey {
229300 secretName,
230301 } ;
231302 }
232- await writeP ;
233303
234- // Apply validation using the schema
235- // TODO: filter before pulling instead of after
304+ // Apply defaults using the schema
236305 const filteredEnvp : Record < string , string > = { } ;
237- if ( options . egressSchema != null ) {
238- // Resolve references and bundle schema
239- const schema : JSONSchema = await $RefParser . bundle (
240- options . egressSchema ,
241- ) ;
242-
243- // Validate the incoming secrets against the schema
244- const ajv = new Ajv ( {
245- coerceTypes : true ,
246- useDefaults : false ,
247- allErrors : true ,
248- } ) ;
249- const validate = ajv . compile ( schema ) ;
250- validate ( envp ) ;
251-
252- // Extract relevant keys, discarding the rest
253- const { requiredKeys, allKeys, defaults } =
254- binUtils . loadSchema ( schema ) ;
306+ if ( unwrappedSchema != null ) {
307+ // Parse the schema for manual filtering
308+ const { requiredKeys, allKeys, defaults } = unwrappedSchema ;
255309
310+ // Add allowed secrets to a filtered set of secrets. This runs after
311+ // the duplication is processed, so all secrets here are guaranteed
312+ // to be unique.
256313 for ( const key of allKeys ) {
257314 let value = envp [ key ] ;
258315 if ( value == null && defaults [ key ] != null ) {
0 commit comments