diff --git a/.changeset/few-rings-nail.md b/.changeset/few-rings-nail.md new file mode 100644 index 00000000..8ccb66a8 --- /dev/null +++ b/.changeset/few-rings-nail.md @@ -0,0 +1,11 @@ +--- +"@dmno/encrypted-vault-plugin": patch +"@dmno/remix-integration": patch +"@dmno/vite-integration": patch +"@dmno/1password-plugin": patch +"@dmno/bitwarden-plugin": patch +"@dmno/infisical-plugin": patch +"dmno": patch +--- + +add `includeInDmnoConfig` option so items can be exluded from DMNO_CONFIG diff --git a/example-repo/packages/astro-web/.dmno/config.mts b/example-repo/packages/astro-web/.dmno/config.mts index c34097e0..42e8e92e 100644 --- a/example-repo/packages/astro-web/.dmno/config.mts +++ b/example-repo/packages/astro-web/.dmno/config.mts @@ -20,6 +20,18 @@ export default defineDmnoService({ 'SOME_API_KEY', ], schema: { + + INTERNAL_ITEM: { + description: 'will not be included in DMNO_CONFIG', + value: 'dont-include-me', + includeInDmnoConfig: false, + }, + + FN_INTERNAL_CHECK: { + value: () => DMNO_CONFIG.INTERNAL_ITEM, + }, + + OP_TOKEN: { extends: OnePasswordTypes.serviceAccountToken }, FOO: { diff --git a/packages/core/src/cli/commands/printenv.command.ts b/packages/core/src/cli/commands/printenv.command.ts index 66c83a5a..7b697fbe 100644 --- a/packages/core/src/cli/commands/printenv.command.ts +++ b/packages/core/src/cli/commands/printenv.command.ts @@ -34,13 +34,14 @@ program.action(async (itemPath: string, opts: {}, thisCommand) => { checkForSchemaErrors(workspace); checkForConfigErrors(service); - - const injectedEnv = await ctx.dmnoServer.makeRequest('getInjectedJson', service.serviceName); + // TODO: process env version or dmno env? what if the key is renamed as an env var? flag to select? // TODO: could be smarter about not caring about errors unless they affect the item(s) being printed // TODO: support nested paths // TODO: do we want to support multiple items? - if (!injectedEnv[itemPath]) { + const { injectedProcessEnv } = await ctx.dmnoServer.makeRequest('getServiceResolvedConfig', service.serviceName); + + if (!(itemPath in injectedProcessEnv)) { throw new CliExitError(`Config item ${itemPath} not found in config schema`, { details: [ 'Perhaps you meant one of:', @@ -49,10 +50,9 @@ program.action(async (itemPath: string, opts: {}, thisCommand) => { }); } - // TODO: what to do about formatting of arrays/objects/etc // now just print the resolved value - ctx.logOutput(injectedEnv[itemPath].value); + ctx.logOutput(injectedProcessEnv[itemPath]); }); export const PrintEnvCommand = program; diff --git a/packages/core/src/cli/commands/resolve.command.ts b/packages/core/src/cli/commands/resolve.command.ts index 8d2e5483..a43984fd 100644 --- a/packages/core/src/cli/commands/resolve.command.ts +++ b/packages/core/src/cli/commands/resolve.command.ts @@ -60,6 +60,7 @@ program.action(async (opts: { checkForSchemaErrors(workspace); checkForConfigErrors(service, { showAll: opts?.showAll }); + // this logic could probably move to the service itself? function getExposedConfigValues() { const values = {} as Record; for (const itemKey in service.configNodes) { @@ -71,14 +72,18 @@ program.action(async (opts: { return values; } + // when we are using the non default format (which includes everything) we have the same questions + // here about what values to include, and how to handle env var keys that may be renamed + // probably need a flag to select which we are talking about + // console.log(service.config); if (opts.format === 'json') { console.log(JSON.stringify(getExposedConfigValues())); } else if (opts.format === 'json-full') { console.dir(service, { depth: null }); } else if (opts.format === 'json-injected') { - const injectedJson = await ctx.dmnoServer.makeRequest('getInjectedJson', ctx.selectedService.serviceName); - console.log(JSON.stringify(injectedJson)); + const { injectedDmnoEnv } = await ctx.dmnoServer.makeRequest('getServiceResolvedConfig', ctx.selectedService.serviceName); + console.log(JSON.stringify(injectedDmnoEnv)); } else if (opts.format === 'env') { console.log(stringifyObjectAsEnvFile(getExposedConfigValues())); } else { diff --git a/packages/core/src/cli/commands/run.command.ts b/packages/core/src/cli/commands/run.command.ts index 233f445c..d4b63bbe 100644 --- a/packages/core/src/cli/commands/run.command.ts +++ b/packages/core/src/cli/commands/run.command.ts @@ -62,25 +62,18 @@ program.action(async (_command, opts: { //! await workspace.resolveConfig(); checkForConfigErrors(service); - const injectedJson = await ctx.dmnoServer.makeRequest('getInjectedJson', ctx.selectedService.serviceName); + const { + injectedProcessEnv, + injectedDmnoEnv, + } = await ctx.dmnoServer.makeRequest('getServiceResolvedConfig', ctx.selectedService.serviceName); const fullInjectedEnv = { + // TODO: not sure if we want to overwrite or not ...process.env, + ...injectedProcessEnv, }; - // we need to add any config items that are defined in dmno config, but we dont want to modify existing items - for (const key in injectedJson) { - // must skip $SETTINGS - if (key.startsWith('$')) continue; - - // TODO: need to think about how we deal with nested items - // TODO: let config nodes expose themselves in inject env vars with aliases - if (!Object.hasOwn(process.env, key)) { - const strVal = injectedJson[key]?.value?.toString(); - if (strVal !== undefined) fullInjectedEnv[key] = strVal; - } - } - fullInjectedEnv.DMNO_INJECTED_ENV = JSON.stringify(injectedJson); + fullInjectedEnv.DMNO_INJECTED_ENV = JSON.stringify(injectedDmnoEnv); // this is what signals to the child process that is has a parent dmno server to use fullInjectedEnv.DMNO_PARENT_SERVER = ctx.dmnoServer.parentServerInfo; diff --git a/packages/core/src/config-engine/config-engine.ts b/packages/core/src/config-engine/config-engine.ts index 5be93292..b8620269 100644 --- a/packages/core/src/config-engine/config-engine.ts +++ b/packages/core/src/config-engine/config-engine.ts @@ -409,10 +409,24 @@ export class DmnoService { return this.configraphEntity.isValid; } - getEnv() { - const env: Record = _.mapValues(this.configraphEntity.configNodes, (node) => { - return node.resolvedValue; - }); + /** + * key/value object of resolved config ready to be injected as actual environment variables (process.env) + */ + getInjectedProcessEnv() { + const env: Record = {}; + for (const node of _.values(this.configraphEntity.configNodes)) { + // TODO: handle key renaming + // TODO: better handling of nested keys / object + const val = node.resolvedValue; + // not sure about this handling of _empty_ items + if (val === null || val === undefined || val === '') { + env[node.key] = undefined; + } else if (_.isPlainObject(val)) { + env[node.key] = JSON.stringify(val); + } else { + env[node.key] = val.toString(); + } + } return env; } diff --git a/packages/core/src/config-engine/configraph-adapter.ts b/packages/core/src/config-engine/configraph-adapter.ts index 87931b50..958cf7a1 100644 --- a/packages/core/src/config-engine/configraph-adapter.ts +++ b/packages/core/src/config-engine/configraph-adapter.ts @@ -62,6 +62,9 @@ export type DmnoDataTypeMetadata = { /** opt in/out of build-type code replacements - default is false unless changed at the service level */ dynamic?: boolean; + + /** set to false to keep this item out of DMNO_CONFIG */ + includeInDmnoConfig?: boolean, }; // way more legible, but super weird that we are involving a class like this @@ -142,7 +145,11 @@ DmnoEntityMetadata, DmnoDataTypeMetadata, DmnoConfigraphNode getInjectedEnvJSON(): InjectedDmnoEnv { // some funky ts stuff going on here... doesn't like how I set the values, // but otherwise the type seems to work ok? - const env: any = _.mapValues(this.configNodes, (item) => item.toInjectedJSON()); + const env: any = {}; + _.each(this.configNodes, (item, itemKey) => { + if (!item.includeInDmnoConfig) return; + env[itemKey] = item.toInjectedJSON(); + }); // simple way to get settings passed through to injected stuff - we may want env.$SETTINGS = this.settings; return env as any; @@ -159,6 +166,10 @@ export class DmnoConfigraphNode extends ConfigraphNode { return !!this.type.getMetadata('sensitive'); } + get includeInDmnoConfig() { + return this.type.getMetadata('includeInDmnoConfig') !== false; + } + get isDynamic() { // this resolves whether the item should actually be treated as static or dynamic // which takes into account the specific item's `dynamic` override diff --git a/packages/core/src/config-engine/type-generation.ts b/packages/core/src/config-engine/type-generation.ts index aa5603ba..c5055e11 100644 --- a/packages/core/src/config-engine/type-generation.ts +++ b/packages/core/src/config-engine/type-generation.ts @@ -29,22 +29,22 @@ export async function generateServiceTypes(service: DmnoService, writeToFile = f // write global file which defines a DMNO_CONFIG global // this used in our config.mts files and in front-end apps where we inject rollup rewrites await fs.promises.writeFile(`${typeGenFolderPath}/global.d.ts`, `${AUTOGENERATED_FILE_BANNER} -import type { DmnoGeneratedConfigSchema } from './schema.d.ts'; +import type { DmnoConfigSchema } from './schema.d.ts'; declare global { /** ${service.serviceName} config global obj */ - const DMNO_CONFIG: DmnoGeneratedConfigSchema; + const DMNO_CONFIG: DmnoConfigSchema; } `, 'utf-8'); // write global file which defines a DMNO_CONFIG global // this used in our config.mts files and in front-end apps where we inject rollup rewrites await fs.promises.writeFile(`${typeGenFolderPath}/global-public.d.ts`, `${AUTOGENERATED_FILE_BANNER} -import type { DmnoGeneratedPublicConfigSchema } from './schema.d.ts'; +import type { DmnoPublicConfigSchema } from './schema.d.ts'; declare global { /** ${service.serviceName} config global obj - public (non-sensitive) items only */ - const DMNO_PUBLIC_CONFIG: DmnoGeneratedPublicConfigSchema; + const DMNO_PUBLIC_CONFIG: DmnoPublicConfigSchema; } `, 'utf-8'); } @@ -53,20 +53,28 @@ declare global { export async function generateTypescriptTypes(service: DmnoService) { const tsSrc = [ AUTOGENERATED_FILE_BANNER, - 'export type DmnoGeneratedConfigSchema = {', + 'export type FullDmnoConfigSchema = {', ]; - const publicKeys: Array = []; + const dmnoConfigKeys: Array = []; + const dmnoPublicConfigKeys: Array = []; for (const itemKey in service.config) { const configItem = service.config[itemKey]; - if (!configItem.isSensitive) publicKeys.push(itemKey); + // generate the TS type for the item in the full schema tsSrc.push(...await getTsDefinitionForNode(configItem, 1)); + // then include in DMNO_CONFIG and DMNO_PUBLIC_CONFIG based on settings + if (configItem.includeInDmnoConfig) { + dmnoConfigKeys.push(itemKey); + if (!configItem.isSensitive) dmnoPublicConfigKeys.push(itemKey); + } } tsSrc.push('}'); tsSrc.push('\n'); - const publicKeysForPick = _.map(publicKeys, JSON.stringify).join(' | '); - tsSrc.push(`export type DmnoGeneratedPublicConfigSchema = Pick`); + const keysForPick = _.map(dmnoConfigKeys, JSON.stringify).join(' | '); + tsSrc.push(`export type DmnoConfigSchema = Pick`); + const publicKeysForPick = _.map(dmnoPublicConfigKeys, JSON.stringify).join(' | '); + tsSrc.push(`export type DmnoPublicConfigSchema = Pick`); return tsSrc.join('\n'); } export async function getPublicConfigKeys(service: DmnoService) { diff --git a/packages/core/src/config-loader/dmno-server.ts b/packages/core/src/config-loader/dmno-server.ts index 7c1dc27f..290ae131 100644 --- a/packages/core/src/config-loader/dmno-server.ts +++ b/packages/core/src/config-loader/dmno-server.ts @@ -426,10 +426,13 @@ export class DmnoServer { // TODO: ideally resolution would be a second step that we could trigger as needed return workspace.toJSON(); }, - getInjectedJson: async (serviceId: string) => { + getServiceResolvedConfig: async (serviceId: string) => { const workspace = await this.configLoader?.getWorkspace()!; const service = workspace.getService(serviceId); - return service.getInjectedEnvJSON(); + return { + injectedProcessEnv: service.getInjectedProcessEnv(), + injectedDmnoEnv: service.getInjectedEnvJSON(), + }; }, clearCache: async () => { const workspace = await this.configLoader?.getWorkspace()!; @@ -485,11 +488,12 @@ export class DmnoServer { throw new Error(`Unable to select service by package name - ${packageName}`); } - const injectedEnv = await this.makeRequest('getInjectedJson', service.serviceName); + const { injectedProcessEnv, injectedDmnoEnv } = await this.makeRequest('getServiceResolvedConfig', service.serviceName); return { serviceDetails: service, - injectedEnv, + injectedProcessEnv, + injectedDmnoEnv, }; } } diff --git a/packages/integrations/astro/src/index.ts b/packages/integrations/astro/src/index.ts index 2c71e568..4cf82e6b 100644 --- a/packages/integrations/astro/src/index.ts +++ b/packages/integrations/astro/src/index.ts @@ -30,7 +30,7 @@ async function reloadDmnoConfig() { (process as any).dmnoServer ||= new DmnoServer({ watch: true }); dmnoServer = (process as any).dmnoServer; const resolvedService = await dmnoServer.getCurrentPackageConfig(); - const injectedConfig = resolvedService.injectedEnv; + const injectedConfig = resolvedService.injectedDmnoEnv; dmnoConfigValid = resolvedService.serviceDetails.isValid; configItemKeysAccessed = {}; diff --git a/packages/integrations/remix/src/index.ts b/packages/integrations/remix/src/index.ts index 32c5ace4..bbb0d234 100644 --- a/packages/integrations/remix/src/index.ts +++ b/packages/integrations/remix/src/index.ts @@ -29,7 +29,7 @@ async function reloadDmnoConfig() { (process as any).dmnoServer ||= new DmnoServer({ watch: true }); dmnoServer = (process as any).dmnoServer; const resolvedService = await dmnoServer.getCurrentPackageConfig(); - const injectedConfig = resolvedService.injectedEnv; + const injectedConfig = resolvedService.injectedDmnoEnv; dmnoConfigValid = resolvedService.serviceDetails.isValid; configItemKeysAccessed = {}; diff --git a/packages/integrations/vite/src/index.ts b/packages/integrations/vite/src/index.ts index 23837966..4119ef58 100644 --- a/packages/integrations/vite/src/index.ts +++ b/packages/integrations/vite/src/index.ts @@ -20,7 +20,7 @@ async function reloadDmnoConfig() { (process as any).dmnoServer ||= new DmnoServer({ watch: true }); dmnoServer = (process as any).dmnoServer; const resolvedService = await dmnoServer.getCurrentPackageConfig(); - const injectedConfig = resolvedService.injectedEnv; + const injectedConfig = resolvedService.injectedDmnoEnv; dmnoConfigValid = resolvedService.serviceDetails.isValid; configItemKeysAccessed = {}; diff --git a/packages/plugins/1password/src/data-types.ts b/packages/plugins/1password/src/data-types.ts index 7bbaa71a..305ae85c 100644 --- a/packages/plugins/1password/src/data-types.ts +++ b/packages/plugins/1password/src/data-types.ts @@ -17,6 +17,7 @@ const OnePasswordServiceAccountToken = createDmnoDataType({ sensitive: { redactMode: 'show_last_2', }, + includeInDmnoConfig: false, }); const OnePasswordUUID = createDmnoDataType({ diff --git a/packages/plugins/bitwarden/src/data-types.ts b/packages/plugins/bitwarden/src/data-types.ts index 598fb8da..38f1fed4 100644 --- a/packages/plugins/bitwarden/src/data-types.ts +++ b/packages/plugins/bitwarden/src/data-types.ts @@ -16,6 +16,7 @@ const BitwardenMachineIdentityAccessToken = createDmnoDataType({ icon: BITWARDEN_ICON, }, sensitive: true, + includeInDmnoConfig: false, }); const BitwardenSecretId = createDmnoDataType({ @@ -29,6 +30,7 @@ const BitwardenSecretId = createDmnoDataType({ ui: { icon: BITWARDEN_ICON, }, + includeInDmnoConfig: false, }); const BitwardenProjectId = createDmnoDataType({ @@ -42,6 +44,7 @@ const BitwardenProjectId = createDmnoDataType({ ui: { icon: BITWARDEN_ICON, }, + includeInDmnoConfig: false, }); diff --git a/packages/plugins/encrypted-vault/src/data-types.ts b/packages/plugins/encrypted-vault/src/data-types.ts index 271f1e7c..eee790de 100644 --- a/packages/plugins/encrypted-vault/src/data-types.ts +++ b/packages/plugins/encrypted-vault/src/data-types.ts @@ -15,6 +15,7 @@ const DmnoEncryptionKey = createDmnoDataType({ icon: 'material-symbols:key', }, sensitive: true, + includeInDmnoConfig: false, }); export const EncryptedVaultTypes = { diff --git a/packages/plugins/infisical/src/data-types.ts b/packages/plugins/infisical/src/data-types.ts index 8a3c6538..1618808c 100644 --- a/packages/plugins/infisical/src/data-types.ts +++ b/packages/plugins/infisical/src/data-types.ts @@ -15,7 +15,7 @@ const InfisicalClientId = createDmnoDataType({ ui: { icon: INFISICAL_ICON, }, - sensitive: true, + includeInDmnoConfig: false, }); const InfisicalClientSecret = createDmnoDataType({ @@ -29,6 +29,8 @@ const InfisicalClientSecret = createDmnoDataType({ ui: { icon: INFISICAL_ICON, }, + sensitive: true, + includeInDmnoConfig: false, }); const InfisicalEnvironment = createDmnoDataType({ @@ -42,6 +44,7 @@ const InfisicalEnvironment = createDmnoDataType({ ui: { icon: INFISICAL_ICON, }, + includeInDmnoConfig: false, }); const InfisicalProjectId = createDmnoDataType({ @@ -55,6 +58,7 @@ const InfisicalProjectId = createDmnoDataType({ ui: { icon: INFISICAL_ICON, }, + includeInDmnoConfig: false, });