Skip to content

Commit b9297b8

Browse files
authored
chore: add node-client-sdk types, options validation, and basic logger (#1408)
<!-- CURSOR_SUMMARY --> > [!NOTE] > **Low Risk** > Mostly new types, validation, and tests with no full client implementation wired yet; TLS disable warning is intentional security messaging. > > **Overview** > Expands the **Node client-side SDK** surface with typed public APIs and runtime option handling ahead of wiring up `createClient`. > > **`NodeOptions`** now extends shared **`LDOptions`** and adds **`initialConnectionMode`**, **`plugins`**, **`hash`**, plus existing Node fields (TLS, cache path, event compression). New **`validateOptions`** applies defaults (e.g. streaming mode, empty plugins), type-checks each Node-specific field with compile-time coverage for new options, warns on bad types, and flags insecure **`tlsParams.rejectUnauthorized: false`**. **`filterToBaseOptions`** strips Node-only keys before the common validator runs. > > Adds **`LDClient`** (Node-specific **`identify`**, **`start`**, connection mode helpers), **`LDPlugin`**, and a large **`LDCommon`** type re-export barrel. **`basicLogger`** wraps shared **`BasicLogger`** with Node **`console`** + **`util.format`**. > > **`NodePlatform`** now takes a **`ValidatedOptions`** pick instead of raw **`NodeOptions`**. Contract-test ESLint override removed; **`@types/node`** added as a dev dependency. Jest tests cover defaults, passthrough, invalid values, TLS warning, and base-option stripping. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit cea18dc. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 7be8030 commit b9297b8

10 files changed

Lines changed: 374 additions & 14 deletions

File tree

packages/sdk/node-client/.eslintrc.cjs

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { NodeOptions } from '../src/NodeOptions';
2+
import validateOptions, { filterToBaseOptions, ValidatedOptions } from '../src/options';
3+
import { createMockLogger } from './testHelpers';
4+
5+
// A value no option validator should accept regardless of the field's expected type
6+
const BOGUS_VALUE = Symbol('invalid-option-value');
7+
8+
// Exhaustive over keyof ValidatedOptions: adding a new node-specific option fails to compile
9+
// here until a bogus case is added, which forces the wrong-type-warning test below to cover
10+
// it.
11+
const wrongTypedOptions: Record<keyof ValidatedOptions, unknown> = {
12+
tlsParams: BOGUS_VALUE,
13+
enableEventCompression: BOGUS_VALUE,
14+
initialConnectionMode: BOGUS_VALUE,
15+
plugins: BOGUS_VALUE,
16+
localStoragePath: BOGUS_VALUE,
17+
hash: BOGUS_VALUE,
18+
};
19+
20+
const nodeOptionKeys = Object.keys(wrongTypedOptions) as (keyof ValidatedOptions)[];
21+
22+
let logger: ReturnType<typeof createMockLogger>;
23+
24+
beforeEach(() => {
25+
logger = createMockLogger();
26+
});
27+
28+
it('applies defaults when no node-specific options are provided', () => {
29+
const out = validateOptions({}, logger);
30+
31+
expect(out.initialConnectionMode).toBe('streaming');
32+
expect(out.plugins).toEqual([]);
33+
expect(out.tlsParams).toBeUndefined();
34+
expect(out.enableEventCompression).toBeUndefined();
35+
expect(out.localStoragePath).toBeUndefined();
36+
expect(out.hash).toBeUndefined();
37+
expect(logger.warn).not.toHaveBeenCalled();
38+
});
39+
40+
it('passes through valid node-specific options', () => {
41+
const out = validateOptions(
42+
{
43+
initialConnectionMode: 'polling',
44+
enableEventCompression: true,
45+
localStoragePath: '/tmp/ld-cache',
46+
hash: 'abc123',
47+
},
48+
logger,
49+
);
50+
51+
expect(out.initialConnectionMode).toBe('polling');
52+
expect(out.enableEventCompression).toBe(true);
53+
expect(out.localStoragePath).toBe('/tmp/ld-cache');
54+
expect(out.hash).toBe('abc123');
55+
expect(logger.warn).not.toHaveBeenCalled();
56+
});
57+
58+
it('warns and falls back to the default for an invalid initialConnectionMode', () => {
59+
const out = validateOptions({ initialConnectionMode: 'STREAMING' as any }, logger);
60+
61+
expect(out.initialConnectionMode).toBe('streaming');
62+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('initialConnectionMode'));
63+
});
64+
65+
it('warns when TLS certificate verification is disabled', () => {
66+
validateOptions({ tlsParams: { rejectUnauthorized: false } }, logger);
67+
68+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('rejectUnauthorized'));
69+
});
70+
71+
it('strips every node-specific option from the base options but keeps base options', () => {
72+
const opts: NodeOptions = {
73+
initialConnectionMode: 'polling',
74+
plugins: [],
75+
tlsParams: {},
76+
enableEventCompression: true,
77+
localStoragePath: '/tmp/ld-cache',
78+
hash: 'abc123',
79+
sendEvents: false,
80+
};
81+
82+
const base = filterToBaseOptions(opts) as Record<string, unknown>;
83+
84+
nodeOptionKeys.forEach((key) => {
85+
expect(base).not.toHaveProperty(key);
86+
});
87+
expect(base).toHaveProperty('sendEvents', false);
88+
});
89+
90+
it('warns for every validated option when given a value of the wrong type', () => {
91+
nodeOptionKeys.forEach((key) => {
92+
const fieldLogger = createMockLogger();
93+
validateOptions({ [key]: wrongTypedOptions[key] } as unknown as NodeOptions, fieldLogger);
94+
95+
expect(fieldLogger.warn).toHaveBeenCalledWith(expect.stringContaining(key));
96+
});
97+
});

packages/sdk/node-client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"devDependencies": {
4848
"@eslint/js": "^9.0.0",
4949
"@types/jest": "^29.4.0",
50+
"@types/node": "^25.9.1",
5051
"eslint": "^9.0.0",
5152
"eslint-import-resolver-typescript": "^4.0.0",
5253
"eslint-plugin-import-x": "^4.0.0",
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type {
2+
ConnectionMode,
3+
LDClient as LDClientBase,
4+
LDContext,
5+
LDIdentifyOptions,
6+
LDIdentifyResult,
7+
LDStartOptions,
8+
LDWaitForInitializationResult,
9+
} from '@launchdarkly/js-client-sdk-common';
10+
11+
export type { LDStartOptions };
12+
13+
export interface LDClient extends Omit<LDClientBase, 'identify'> {
14+
/**
15+
* Identifies a context to LaunchDarkly and returns a promise which resolves to an object
16+
* containing the result of the identify operation.
17+
*
18+
* Unlike the server-side SDKs, the client-side Node.js SDK maintains a current context
19+
* state, which is set when you call `identify()`.
20+
*
21+
* Changing the current context also causes all feature flag values to be reloaded. Until
22+
* that has finished, calls to variation methods will still return flag values for the
23+
* previous context. You can await the Promise to determine when the new flag values are
24+
* available.
25+
*
26+
* Use {@link start} to set the initial context at startup.
27+
*
28+
* @param context The context to identify. @see {@link LDContext}
29+
* @param identifyOptions Optional configuration. @see {@link LDIdentifyOptions}.
30+
* @returns A promise which resolves to an object containing the result of the identify operation.
31+
*/
32+
identify(context: LDContext, identifyOptions?: LDIdentifyOptions): Promise<LDIdentifyResult>;
33+
34+
/**
35+
* Starts the client and returns a promise that resolves to the initialization result.
36+
*
37+
* The promise will resolve to a {@link LDWaitForInitializationResult} object containing the
38+
* status of the waitForInitialization operation.
39+
*
40+
* @param options Optional configuration. See {@link LDStartOptions}.
41+
*/
42+
start(options?: LDStartOptions): Promise<LDWaitForInitializationResult>;
43+
44+
/**
45+
* Sets the data source connection mode.
46+
*
47+
* @remarks
48+
* Switches between 'offline', 'streaming', and 'polling' at runtime without restarting
49+
* the client. Use 'offline' to pause all LaunchDarkly network activity.
50+
*
51+
* @see {@link ConnectionMode}
52+
*/
53+
setConnectionMode(mode: ConnectionMode): Promise<void>;
54+
55+
/**
56+
* Returns the current data source connection mode.
57+
*/
58+
getConnectionMode(): ConnectionMode;
59+
60+
/**
61+
* Returns true if the client is in offline mode.
62+
*/
63+
isOffline(): boolean;
64+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
export type {
2+
LDIdentifyOptions,
3+
AutoEnvAttributes,
4+
BasicLogger,
5+
BasicLoggerOptions,
6+
EvaluationSeriesContext,
7+
EvaluationSeriesData,
8+
Hook,
9+
HookMetadata,
10+
IdentifySeriesContext,
11+
IdentifySeriesData,
12+
IdentifySeriesResult,
13+
IdentifySeriesStatus,
14+
LDContext,
15+
LDContextCommon,
16+
LDContextMeta,
17+
LDContextStrict,
18+
LDEvaluationDetail,
19+
LDEvaluationDetailTyped,
20+
LDEvaluationReason,
21+
LDFlagSet,
22+
LDFlagValue,
23+
LDTimeoutError,
24+
LDInspection,
25+
LDLogger,
26+
LDLogLevel,
27+
LDMultiKindContext,
28+
LDSingleKindContext,
29+
TrackSeriesContext,
30+
LDPluginBase,
31+
LDPluginEnvironmentMetadata,
32+
LDPluginSdkMetadata,
33+
LDPluginApplicationMetadata,
34+
LDPluginMetadata,
35+
LDIdentifyResult,
36+
LDIdentifySuccess,
37+
LDIdentifyError,
38+
LDIdentifyTimeout,
39+
LDIdentifyShed,
40+
LDDebugOverride,
41+
LDWaitForInitializationOptions,
42+
LDWaitForInitializationResult,
43+
LDWaitForInitializationComplete,
44+
LDWaitForInitializationFailed,
45+
LDWaitForInitializationTimeout,
46+
} from '@launchdarkly/js-client-sdk-common';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Hook, LDPluginBase } from '@launchdarkly/js-client-sdk-common';
2+
3+
import { LDClient } from './LDClient';
4+
5+
/**
6+
* Interface for plugins to the LaunchDarkly SDK.
7+
*/
8+
export interface LDPlugin extends LDPluginBase<LDClient, Hook> {}

packages/sdk/node-client/src/NodeOptions.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { ConnectionMode, LDOptions as LDOptionsBase } from '@launchdarkly/js-client-sdk-common';
2+
3+
import type { LDPlugin } from './LDPlugin';
4+
15
/**
26
* Additional parameters to pass to the Node HTTPS API for secure requests. These can include any
37
* of the TLS-related parameters supported by `https.request()`, such as `ca`, `cert`, and `key`.
@@ -20,7 +24,7 @@ export interface LDTLSOptions {
2024
/**
2125
* Configuration options for the Node client-side SDK.
2226
*/
23-
export interface NodeOptions {
27+
export interface NodeOptions extends LDOptionsBase {
2428
/**
2529
* Additional parameters to pass to the Node HTTPS API for secure requests. These can include any
2630
* of the TLS-related parameters supported by `https.request()`, such as `ca`, `cert`, and `key`.
@@ -42,4 +46,26 @@ export interface NodeOptions {
4246
* Defaults to `<cwd>/ldclient-user-cache`.
4347
*/
4448
localStoragePath?: string;
49+
50+
/**
51+
* Sets the mode to use for connections when the SDK is initialized.
52+
*
53+
* @remarks
54+
* Possible values are offline, streaming, or polling. See {@link ConnectionMode} for more information.
55+
*
56+
* Defaults to streaming.
57+
*/
58+
initialConnectionMode?: ConnectionMode;
59+
60+
/**
61+
* A list of plugins to be used with the SDK.
62+
*/
63+
plugins?: LDPlugin[];
64+
65+
/**
66+
* The Secure Mode hash for the configured context.
67+
*
68+
* @see https://docs.launchdarkly.com/sdk/features/secure-mode
69+
*/
70+
hash?: string;
4571
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { format } from 'util';
2+
3+
import {
4+
BasicLogger,
5+
BasicLoggerOptions,
6+
LDLogger,
7+
} from '@launchdarkly/js-client-sdk-common';
8+
9+
/**
10+
* Provides a basic {@link LDLogger} implementation.
11+
*
12+
* Output is written to `console.log` using Node's `util.format` so multiple arguments and
13+
* format specifiers (`%s`, `%d`, etc.) are formatted the way Node consumers expect.
14+
*
15+
* If you do not pass a logger via {@link LDOptions.logger}, the SDK falls back to
16+
* a logger equivalent to `basicLogger({ level: 'info' })`.
17+
*
18+
* @example
19+
* ```javascript
20+
* const ldOptions = {
21+
* logger: basicLogger({ level: 'warn' }),
22+
* };
23+
* ```
24+
*/
25+
export default function basicLogger(options: BasicLoggerOptions = {}): LDLogger {
26+
return new BasicLogger({
27+
...options,
28+
destination: options.destination ?? {
29+
// eslint-disable-next-line no-console
30+
debug: console.debug,
31+
// eslint-disable-next-line no-console
32+
info: console.info,
33+
// eslint-disable-next-line no-console
34+
warn: console.warn,
35+
// eslint-disable-next-line no-console
36+
error: console.error,
37+
},
38+
formatter: options.formatter ?? format,
39+
});
40+
}

0 commit comments

Comments
 (0)