From 60576120dc2eaea245fc9f72cdc150b21d336d15 Mon Sep 17 00:00:00 2001 From: Mark Faga Date: Fri, 28 Nov 2025 16:16:31 -0800 Subject: [PATCH] refactor: improve public interface + don't expose resolver --- .eslintrc.cjs | 2 + src/__tests__/integration.test.ts | 2 +- src/__tests__/reforge.test.ts | 14 +-- src/__tests__/reforgeClient.test.ts | 162 ++++++++++++++++++++++++++++ src/evaluate.ts | 20 +++- src/reforge.ts | 74 ++----------- src/reforgeClient.ts | 110 +++++++++++++++++++ src/resolver.ts | 38 ++++++- src/types.ts | 66 ++++++++++++ src/unwrap.ts | 27 +++-- 10 files changed, 427 insertions(+), 88 deletions(-) create mode 100644 src/__tests__/reforgeClient.test.ts create mode 100644 src/reforgeClient.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 11a0e87..a5414cd 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -12,6 +12,8 @@ module.exports = { }, rules: { "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/method-signature-style": "off", + "@typescript-eslint/strict-boolean-expressions": "off", "promise/param-names": "off", }, }; diff --git a/src/__tests__/integration.test.ts b/src/__tests__/integration.test.ts index bf5108b..fdcf674 100644 --- a/src/__tests__/integration.test.ts +++ b/src/__tests__/integration.test.ts @@ -1,6 +1,6 @@ import { jsonStringifyWithBigInt } from "../bigIntUtils"; import { Reforge } from "../reforge"; -import type { ReforgeInterface } from "../reforge"; +import type { ReforgeInterface } from "../types"; import type { ResolverAPI } from "../resolver"; import type { GetValue } from "../unwrap"; import { tests } from "./integrationHelper"; diff --git a/src/__tests__/reforge.test.ts b/src/__tests__/reforge.test.ts index 5dd1b89..69a94f5 100644 --- a/src/__tests__/reforge.test.ts +++ b/src/__tests__/reforge.test.ts @@ -10,12 +10,14 @@ import rolloutFlag from "./fixtures/rolloutFlag"; import envConfig from "./fixtures/envConfig"; import propIsOneOf from "./fixtures/propIsOneOf"; import propIsOneOfAndEndsWith from "./fixtures/propIsOneOfAndEndsWith"; -import { - Reforge, - type TypedNodeServerConfigurationRaw, - MULTIPLE_INIT_WARNING, -} from "../reforge"; -import type { Contexts, ProjectEnvId, Config, ConfigValue } from "../types"; +import { Reforge, MULTIPLE_INIT_WARNING } from "../reforge"; +import type { + Contexts, + ProjectEnvId, + Config, + ConfigValue, + TypedNodeServerConfigurationRaw, +} from "../types"; import { LogLevel, Criterion_CriterionOperator, diff --git a/src/__tests__/reforgeClient.test.ts b/src/__tests__/reforgeClient.test.ts new file mode 100644 index 0000000..1bcd00d --- /dev/null +++ b/src/__tests__/reforgeClient.test.ts @@ -0,0 +1,162 @@ +import { Reforge } from "../reforge"; +import { ConfigType, ConfigValueType } from "../types"; +import type { Config } from "../types"; +import { projectEnvIdUnderTest, irrelevant } from "./testHelpers"; + +const createSimpleConfig = (key: string, value: string): Config => { + return { + id: "1", + projectId: 1, + key, + changedBy: undefined, + rows: [ + { + properties: {}, + values: [ + { + criteria: [ + { + propertyName: "user.country", + operator: "PROP_IS_ONE_OF" as any, + valueToMatch: { + stringList: { + values: ["US"], + }, + }, + }, + ], + value: { string: value }, + }, + { + criteria: [], + value: { string: "default" }, + }, + ], + }, + ], + allowableValues: [], + configType: ConfigType.Config, + valueType: ConfigValueType.String, + sendToClientSdk: false, + }; +}; + +describe("ReforgeClient", () => { + describe("withContext", () => { + it("returns a context-scoped client that doesn't expose internal resolver", () => { + const reforge = new Reforge({ sdkKey: irrelevant }); + const config = createSimpleConfig("test.key", "us-value"); + reforge.setConfig([config], projectEnvIdUnderTest, new Map()); + + const scopedClient = reforge.withContext({ user: { country: "US" } }); + + // Should have the public API methods + expect(typeof scopedClient.get).toBe("function"); + expect(typeof scopedClient.isFeatureEnabled).toBe("function"); + expect(typeof scopedClient.logger).toBe("function"); + expect(typeof scopedClient.shouldLog).toBe("function"); + expect(typeof scopedClient.getLogLevel).toBe("function"); + expect(typeof scopedClient.withContext).toBe("function"); + expect(typeof scopedClient.inContext).toBe("function"); + expect(typeof scopedClient.updateIfStalerThan).toBe("function"); + expect(typeof scopedClient.addConfigChangeListener).toBe("function"); + + // Should NOT expose internal resolver methods + expect((scopedClient as any).raw).toBeUndefined(); + expect((scopedClient as any).set).toBeUndefined(); + expect((scopedClient as any).keys).toBeUndefined(); + expect((scopedClient as any).cloneWithContext).toBeUndefined(); + expect((scopedClient as any).update).toBeUndefined(); + + // Should apply context correctly + expect(scopedClient.get("test.key")).toBe("us-value"); + }); + + it("allows chaining withContext calls", () => { + const reforge = new Reforge({ sdkKey: irrelevant }); + const config = createSimpleConfig("test.key", "us-value"); + reforge.setConfig([config], projectEnvIdUnderTest, new Map()); + + const client1 = reforge.withContext({ user: { country: "FR" } }); + expect(client1.get("test.key")).toBe("default"); + + const client2 = client1.withContext({ user: { country: "US" } }); + expect(client2.get("test.key")).toBe("us-value"); + }); + + it("merges context when additional context is provided to methods", () => { + const reforge = new Reforge({ sdkKey: irrelevant }); + const config = createSimpleConfig("test.key", "us-value"); + reforge.setConfig([config], projectEnvIdUnderTest, new Map()); + + const client = reforge.withContext({ user: { name: "Alice" } }); + + // Provide additional context that includes the country + expect(client.get("test.key", { user: { country: "US" } })).toBe( + "us-value" + ); + }); + }); + + describe("inContext", () => { + it("provides a context-scoped client to the callback", () => { + const reforge = new Reforge({ sdkKey: irrelevant }); + const config = createSimpleConfig("test.key", "us-value"); + reforge.setConfig([config], projectEnvIdUnderTest, new Map()); + + const result = reforge.inContext( + { user: { country: "US" } }, + (client) => { + // Should not expose internal methods + expect((client as any).raw).toBeUndefined(); + expect((client as any).set).toBeUndefined(); + + // Should apply context + expect(client.get("test.key")).toBe("us-value"); + + return "success"; + } + ); + + expect(result).toBe("success"); + }); + + it("allows nested inContext calls", () => { + const reforge = new Reforge({ sdkKey: irrelevant }); + const config = createSimpleConfig("test.key", "us-value"); + reforge.setConfig([config], projectEnvIdUnderTest, new Map()); + + reforge.inContext({ user: { country: "FR" } }, (outer) => { + expect(outer.get("test.key")).toBe("default"); + + outer.inContext({ user: { country: "US" } }, (inner) => { + expect(inner.get("test.key")).toBe("us-value"); + }); + }); + }); + }); + + describe("shared state", () => { + it("shares telemetry with parent", () => { + const reforge = new Reforge({ sdkKey: irrelevant }); + reforge.setConfig([], projectEnvIdUnderTest, new Map()); + + const client = reforge.withContext({ user: { country: "US" } }); + + expect(client.telemetry).toBe(reforge.telemetry); + }); + + it("delegates methods to parent reforge instance", () => { + const reforge = new Reforge({ sdkKey: irrelevant }); + reforge.setConfig([], projectEnvIdUnderTest, new Map()); + + const client = reforge.withContext({ user: { country: "US" } }); + + // Telemetry is shared + expect(client.telemetry).toBe(reforge.telemetry); + + // getLogLevel delegates to parent + expect(typeof client.getLogLevel).toBe("function"); + }); + }); +}); diff --git a/src/evaluate.ts b/src/evaluate.ts index 8e789df..cef88fa 100644 --- a/src/evaluate.ts +++ b/src/evaluate.ts @@ -10,7 +10,7 @@ import { type HashByPropertyValue, type ProjectEnvId, } from "./types"; -import type { MinimumConfig, Resolver } from "./resolver"; +import type { MinimumConfig } from "./resolver"; import { type GetValue, unwrap } from "./unwrap"; import { contextLookup } from "./contextLookup"; @@ -18,6 +18,16 @@ import { sortRows } from "./sortRows"; import SemanticVersion from "./semanticversion"; import { isBigInt, jsonStringifyWithBigInt } from "./bigIntUtils"; +/** + * Minimal interface for resolving segments and encryption keys during evaluation. + * This interface is used internally to decouple evaluate.ts from the full Resolver. + * @internal + */ +interface SegmentResolver { + raw(key: string): MinimumConfig | undefined; + get(key: string, contexts?: Contexts): unknown; +} + const getHashByPropertyValue = ( value: ConfigValue | undefined, contexts: Contexts @@ -102,7 +112,7 @@ const propContainsOneOf = ( const inSegment = ( criterion: Criterion, contexts: Contexts, - resolver: Resolver + resolver: SegmentResolver ): boolean => { const segmentKey = criterion.valueToMatch?.string; @@ -282,7 +292,7 @@ const allCriteriaMatch = ( value: ConditionalValue, namespace: string | undefined, contexts: Contexts, - resolver: Resolver + resolver: SegmentResolver ): boolean => { if (value.criteria === undefined) { return true; @@ -380,7 +390,7 @@ const matchingConfigValue = ( projectEnvId: ProjectEnvId, namespace: string | undefined, contexts: Contexts, - resolver: Resolver + resolver: SegmentResolver ): [number, number, ConfigValue | undefined] => { let match: ConfigValue | undefined; let conditionalValueIndex: number = -1; @@ -413,7 +423,7 @@ export interface EvaluateArgs { projectEnvId: ProjectEnvId; namespace: string | undefined; contexts: Contexts; - resolver: Resolver; + resolver: SegmentResolver; } export interface Evaluation { diff --git a/src/reforge.ts b/src/reforge.ts index db71513..f0a2a0d 100644 --- a/src/reforge.ts +++ b/src/reforge.ts @@ -1,6 +1,6 @@ import { apiClient, type ApiClient } from "./apiClient"; import { loadConfig } from "./loadConfig"; -import { Resolver, type MinimumConfig, type ResolverAPI } from "./resolver"; +import { Resolver, type MinimumConfig } from "./resolver"; import { Sources } from "./sources"; import { jsonStringifyWithBigInt } from "./bigIntUtils"; import { @@ -21,10 +21,14 @@ import type { ConfigValue, ConfigRow, Provided, + TypedNodeServerConfigurationRaw, + Telemetry, + ReforgeInterface, } from "./types"; import { LOG_LEVEL_RANK_LOOKUP, type makeLogger } from "./logger"; import { SSEConnection } from "./sseConnection"; import { TelemetryReporter } from "./telemetry/reporter"; +import { ReforgeClient } from "./reforgeClient"; import type { ContextUploadMode } from "./telemetry/types"; import { knownLoggers } from "./telemetry/knownLoggers"; @@ -51,65 +55,6 @@ function requireResolver( } } -// @reforge-com/cli#generate will create interfaces into this namespace for Node to consume -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface NodeServerConfigurationRaw {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface NodeServerConfigurationAccessor {} - -export type TypedNodeServerConfigurationRaw = - keyof NodeServerConfigurationRaw extends never - ? Record - : { - [TypedFlagKey in keyof NodeServerConfigurationRaw]: NodeServerConfigurationRaw[TypedFlagKey]; - }; - -export type TypedNodeServerConfigurationAccessor = - keyof NodeServerConfigurationAccessor extends never - ? Record - : { - [TypedFlagKey in keyof NodeServerConfigurationAccessor]: NodeServerConfigurationAccessor[TypedFlagKey]; - }; - -export interface ReforgeInterface { - get: ( - key: K, - contexts?: Contexts | ContextObj, - defaultValue?: TypedNodeServerConfigurationRaw[K] - ) => TypedNodeServerConfigurationRaw[K]; - isFeatureEnabled: ( - key: K, - contexts?: Contexts | ContextObj - ) => boolean; - logger: ( - loggerName: string, - defaultLevel: LogLevel - ) => ReturnType; - shouldLog: ({ - loggerName, - desiredLevel, - defaultLevel, - contexts, - }: { - loggerName: string; - desiredLevel: LogLevel; - defaultLevel?: LogLevel; - contexts?: Contexts | ContextObj; - }) => boolean; - getLogLevel: (loggerName: string) => LogLevel; - telemetry?: Telemetry; - updateIfStalerThan: (durationInMs: number) => Promise | undefined; - withContext: (contexts: Contexts | ContextObj) => ResolverAPI; - addConfigChangeListener: (callback: GlobalListenerCallback) => () => void; -} - -export interface Telemetry { - knownLoggers: ReturnType; - contextShapes: ReturnType; - exampleContexts: ReturnType; - evaluationSummaries: ReturnType; -} - interface ConstructorProps { sdkKey: string; sources?: string[]; @@ -411,17 +356,17 @@ class Reforge implements ReforgeInterface { inContext( contexts: Contexts | ContextObj, - func: (reforge: Resolver) => T + func: (reforge: ReforgeInterface) => T ): T { requireResolver(this.resolver); - return func(this.resolver.cloneWithContext(contexts)); + return func(new ReforgeClient(this, contexts)); } - withContext(contexts: Contexts | ContextObj): ResolverAPI { + withContext(contexts: Contexts | ContextObj): ReforgeInterface { requireResolver(this.resolver); - return this.resolver.cloneWithContext(contexts); + return new ReforgeClient(this, contexts); } get( @@ -562,5 +507,4 @@ export { type Contexts, SchemaType, type Provided, - Resolver, }; diff --git a/src/reforgeClient.ts b/src/reforgeClient.ts new file mode 100644 index 0000000..e60e026 --- /dev/null +++ b/src/reforgeClient.ts @@ -0,0 +1,110 @@ +import { contextObjToMap, mergeContexts } from "./mergeContexts"; +import type { + Contexts, + ContextObj, + LogLevel, + ReforgeInterface, + Telemetry, + TypedNodeServerConfigurationRaw, +} from "./types"; +import type { makeLogger } from "./logger"; +import type { GlobalListenerCallback } from "./configChangeNotifier"; + +/** + * A context-scoped Reforge client that shares the underlying resolver + * with its parent Reforge instance but applies a specific context to all operations. + * + * This allows creating request/call-scoped clients without duplicating + * the resolver, polling, or SSE connections. All scoped clients share + * the same parent Reforge's state. + */ +export class ReforgeClient implements ReforgeInterface { + private readonly parent: ReforgeInterface; + private readonly context: Contexts; + + constructor(parent: ReforgeInterface, contexts: Contexts | ContextObj) { + this.parent = parent; + this.context = + contexts instanceof Map ? contexts : contextObjToMap(contexts); + } + + get telemetry(): Telemetry | undefined { + return this.parent.telemetry; + } + + get( + key: K, + contexts?: Contexts | ContextObj, + defaultValue?: TypedNodeServerConfigurationRaw[K] + ): TypedNodeServerConfigurationRaw[K] { + const mergedContexts = contexts + ? mergeContexts(this.context, contexts) + : this.context; + return this.parent.get(key, mergedContexts, defaultValue); + } + + isFeatureEnabled(key: string, contexts?: Contexts | ContextObj): boolean { + const mergedContexts = contexts + ? mergeContexts(this.context, contexts) + : this.context; + return this.parent.isFeatureEnabled(key, mergedContexts); + } + + logger( + loggerName: string, + defaultLevel?: LogLevel, + contexts?: Contexts | ContextObj + ): ReturnType { + const mergedContexts = contexts + ? mergeContexts(this.context, contexts) + : this.context; + return this.parent.logger(loggerName, defaultLevel, mergedContexts); + } + + shouldLog({ + loggerName, + desiredLevel, + defaultLevel, + contexts, + }: { + loggerName: string; + desiredLevel: LogLevel; + defaultLevel?: LogLevel; + contexts?: Contexts | ContextObj; + }): boolean { + const mergedContexts = contexts + ? mergeContexts(this.context, contexts) + : this.context; + return this.parent.shouldLog({ + loggerName, + desiredLevel, + defaultLevel, + contexts: mergedContexts, + }); + } + + getLogLevel(loggerName: string): LogLevel { + return this.parent.getLogLevel(loggerName); + } + + updateIfStalerThan(durationInMs: number): Promise | undefined { + return this.parent.updateIfStalerThan(durationInMs); + } + + withContext(contexts: Contexts | ContextObj): ReforgeInterface { + const mergedContexts = mergeContexts(this.context, contexts); + return new ReforgeClient(this.parent, mergedContexts); + } + + inContext( + contexts: Contexts | ContextObj, + func: (reforge: ReforgeInterface) => T + ): T { + const mergedContexts = mergeContexts(this.context, contexts); + return func(new ReforgeClient(this.parent, mergedContexts)); + } + + addConfigChangeListener(callback: GlobalListenerCallback): () => void { + return this.parent.addConfigChangeListener(callback); + } +} diff --git a/src/resolver.ts b/src/resolver.ts index e7b5eb7..37319a9 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -8,10 +8,11 @@ import { type Config, type ConfigValue, type LogLevel, + type Telemetry, + type TypedNodeServerConfigurationRaw, ConfigType, LogLevel as LogLevelEnum, } from "./types"; -import type { Telemetry, TypedNodeServerConfigurationRaw } from "./reforge"; import { REFORGE_DEFAULT_LOG_LEVEL } from "./reforge"; import { mergeContexts, contextObjToMap } from "./mergeContexts"; @@ -24,45 +25,68 @@ const emptyContexts: Contexts = new Map(); export const NOT_PROVIDED = Symbol("NOT_PROVIDED"); -// Interface for Resolver's public API +/** + * Internal resolver API. + * This interface is used internally by Reforge and should not be used by external consumers. + * Use the ReforgeInterface instead for public-facing operations. + * @internal + */ export interface ResolverAPI { + /** @internal */ id: number; + /** @internal */ contexts?: Contexts; + /** @internal */ readonly telemetry: Telemetry | undefined; + /** @internal */ readonly defaultContext?: Contexts; + /** @internal */ readonly loggerKey?: string; + /** @internal */ updateIfStalerThan: | ((durationInMs: number) => Promise | undefined) | undefined; + /** @internal - Create resolver with additional context (cloning pattern) */ cloneWithContext: (contexts: Contexts | ContextObj) => ResolverAPI; + /** @internal - Alias for cloneWithContext */ withContext: (contexts: Contexts | ContextObj) => ResolverAPI; + /** @internal - Update configs in place */ update: ( configs: Array, defaultContext?: Contexts ) => void; + /** @internal - Get raw config without evaluation */ raw: (key: string) => MinimumConfig | undefined; + /** @internal - Set runtime config value */ set: (key: string, value: ConfigValue) => void; + /** @internal - Evaluate config with context */ get: ( key: K, localContexts?: Contexts | ContextObj, defaultValue?: TypedNodeServerConfigurationRaw[K], onNoDefault?: OnNoDefault ) => TypedNodeServerConfigurationRaw[K]; + /** @internal - Check if feature is enabled */ isFeatureEnabled: (key: string, contexts?: Contexts | ContextObj) => boolean; + /** @internal - List all config keys */ keys: () => string[]; + /** @internal - Create logger for namespace */ logger: ( loggerName: string, defaultLevel: LogLevel, contexts?: Contexts | ContextObj ) => ReturnType; + /** @internal - Check if log level should produce output */ shouldLog: (args: { loggerName: string; desiredLevel: LogLevel; defaultLevel?: LogLevel; contexts?: Contexts | ContextObj; }) => boolean; + /** @internal - Get current log level for logger */ getLogLevel: (loggerName: string) => LogLevel; + /** @internal - Set callback for config updates */ setOnUpdate: ( onUpdate: (configs: Array) => void ) => void; @@ -103,6 +127,13 @@ const mergeDefaultContexts = ( let id = 0; +/** + * Internal resolver for evaluating configurations. + * This class is used internally by Reforge to resolve feature flags and configurations. + * It should not be instantiated or used directly by external consumers. + * Use the Reforge class and ReforgeInterface instead. + * @internal + */ class Resolver implements ResolverAPI { // Implement the new interface private readonly config = new Map(); @@ -379,4 +410,7 @@ class Resolver implements ResolverAPI { } } +/** + * @internal - This is for internal use only. Use ReforgeInterface instead. + */ export { Resolver }; diff --git a/src/types.ts b/src/types.ts index a21d322..e3b02b8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -429,3 +429,69 @@ export enum SchemaType { export const isNonNullable = (value: T): value is NonNullable => { return value !== null && value !== undefined; }; + +// Reforge public interface types + +// @reforge-com/cli#generate will create interfaces into this namespace for Node to consume +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface NodeServerConfigurationRaw {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface NodeServerConfigurationAccessor {} + +export type TypedNodeServerConfigurationRaw = + keyof NodeServerConfigurationRaw extends never + ? Record + : { + [TypedFlagKey in keyof NodeServerConfigurationRaw]: NodeServerConfigurationRaw[TypedFlagKey]; + }; + +export type TypedNodeServerConfigurationAccessor = + keyof NodeServerConfigurationAccessor extends never + ? Record + : { + [TypedFlagKey in keyof NodeServerConfigurationAccessor]: NodeServerConfigurationAccessor[TypedFlagKey]; + }; + +export interface Telemetry { + knownLoggers: any; // Runtime value from telemetry/knownLoggers + contextShapes: any; // Runtime value from telemetry/contextShapes + exampleContexts: any; // Runtime value from telemetry/exampleContexts + evaluationSummaries: any; // Runtime value from telemetry/evaluationSummaries +} + +export interface ReforgeInterface { + get: ( + key: K, + contexts?: Contexts | ContextObj, + defaultValue?: TypedNodeServerConfigurationRaw[K] + ) => TypedNodeServerConfigurationRaw[K]; + isFeatureEnabled: ( + key: K, + contexts?: Contexts | ContextObj + ) => boolean; + logger: ( + loggerName: string, + defaultLevel?: LogLevel, + contexts?: Contexts | ContextObj + ) => any; // ReturnType + shouldLog: ({ + loggerName, + desiredLevel, + defaultLevel, + contexts, + }: { + loggerName: string; + desiredLevel: LogLevel; + defaultLevel?: LogLevel; + contexts?: Contexts | ContextObj; + }) => boolean; + getLogLevel: (loggerName: string) => LogLevel; + telemetry?: Telemetry; + updateIfStalerThan: (durationInMs: number) => Promise | undefined; + withContext: (contexts: Contexts | ContextObj) => ReforgeInterface; + inContext: ( + contexts: Contexts | ContextObj, + func: (reforge: ReforgeInterface) => T + ) => T; + addConfigChangeListener: (callback: any) => () => void; // GlobalListenerCallback +} diff --git a/src/unwrap.ts b/src/unwrap.ts index 606547b..738d23b 100644 --- a/src/unwrap.ts +++ b/src/unwrap.ts @@ -1,3 +1,9 @@ +import { decrypt } from "./encryption"; +import { durationToMilliseconds } from "./duration"; +import forge from "node-forge"; + +import murmurhash from "murmurhash"; +import { isBigInt, jsonStringifyWithBigInt } from "./bigIntUtils"; import { ConfigValueType, type ConfigValue, @@ -7,13 +13,16 @@ import { type HashByPropertyValue, } from "./types"; import { isNonNullable } from "./types"; -import type { MinimumConfig, Resolver } from "./resolver"; -import { decrypt } from "./encryption"; -import { durationToMilliseconds } from "./duration"; -import forge from "node-forge"; - -import murmurhash from "murmurhash"; -import { isBigInt, jsonStringifyWithBigInt } from "./bigIntUtils"; +import type { MinimumConfig } from "./resolver"; + +/** + * Minimal interface for resolving configuration values during unwrapping. + * This interface is used internally to decouple unwrap.ts from the full Resolver. + * @internal + */ +interface ConfigResolver { + get(key: string): unknown; +} const CONFIDENTIAL_PREFIX = "*****"; @@ -204,7 +213,7 @@ export const unwrapValue = ({ hashByPropertyValue: HashByPropertyValue; primitivesOnly: boolean; config?: MinimumConfig; - resolver?: Resolver; + resolver?: ConfigResolver; }): Omit => { if (primitivesOnly) { if (isNonNullable(value.provided) || isNonNullable(value.decryptWith)) { @@ -307,7 +316,7 @@ export const unwrap = ({ hashByPropertyValue?: HashByPropertyValue; primitivesOnly?: boolean; config?: MinimumConfig; - resolver?: Resolver; + resolver?: ConfigResolver; }): UnwrappedValue => { if (value === undefined) { return NULL_UNWRAPPED_VALUE;