@redpennon/node-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # @redpennon/node-sdk
2
+
3
+ [![npm](https://img.shields.io/npm/v/@redpennon/node-sdk)](https://www.npmjs.com/package/@redpennon/node-sdk)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/redpennon/sdks/blob/main/LICENSE)
5
+
6
+ Thin HTTP client for the [RedPennon](https://redpennon.dev) feature flag evaluation API.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @redpennon/node-sdk
12
+ ```
13
+
14
+ Requires Node 20+. No runtime dependencies — uses the global `fetch`.
15
+
16
+ ## Quick start
17
+
18
+ ```ts
19
+ import { RedPennonClient } from "@redpennon/node-sdk";
20
+
21
+ const client = new RedPennonClient({ apiKey: "YOUR_ENV_API_KEY" });
22
+
23
+ // Simple: get the resolved value or your default on any failure.
24
+ const enabled = await client.variableValue("dark-mode-enabled", {
25
+ default: false,
26
+ });
27
+
28
+ // With user context (for targeting rules).
29
+ const user = { id: "user_123", email: "alice@example.com" };
30
+ const enabled = await client.variableValue("dark-mode-enabled", {
31
+ default: false,
32
+ user,
33
+ });
34
+
35
+ // Full result: value + variation + reason.
36
+ const result = await client.variable("dark-mode-enabled", { user });
37
+ console.log(result.value, result.variation, result.reason);
38
+
39
+ // Batch: resolve many flags in one round-trip.
40
+ const results = await client.variables(["flag-a", "flag-b"], { user });
41
+ const flagA = results["flag-a"].value;
42
+ ```
43
+
44
+ ## API
45
+
46
+ ### `new RedPennonClient({ apiKey, baseUrl?, fetchImpl? })`
47
+
48
+ | Option | Type | Description |
49
+ |--------|------|-------------|
50
+ | `apiKey` | `string` | Environment API key from the RedPennon dashboard. |
51
+ | `baseUrl` | `string?` | Override the API base URL (default: `https://api.redpennon.dev`). |
52
+ | `fetchImpl` | `typeof fetch?` | Custom fetch — useful for testing. |
53
+
54
+ ### `client.variableValue(key, { default, user? }) → Promise<T>`
55
+
56
+ Returns the resolved value or `default` on any failure (network error, non-2xx, `value=null`). Use this in production code — your default is the load-bearing contract.
57
+
58
+ ### `client.variable(key, { user? }) → Promise<VariableResult>`
59
+
60
+ Returns the full evaluation result (`value`, `variation`, `reason`, `feature`). Throws `APIError` on network or non-2xx errors.
61
+
62
+ ### `client.variables(keys, { user? }) → Promise<Record<string, VariableResult>>`
63
+
64
+ Batch evaluation — one HTTP round-trip for multiple flags. Unknown keys return `reason: "variable_not_found"` inline rather than throwing.
65
+
66
+ ## Local development
67
+
68
+ ```bash
69
+ npm ci
70
+ npm test
71
+ npm run build
72
+ ```
73
+
74
+ ## License
75
+
76
+ [MIT](../LICENSE) © RedPennon
@@ -0,0 +1,152 @@
1
+ /**
2
+ * RedPennon Node SDK — variable-key evaluation client.
3
+ *
4
+ * Two evaluation methods cover the two SDK ergonomics teams reach
5
+ * for:
6
+ *
7
+ * * {@link RedPennonClient.variableValue} — "give me the value, or
8
+ * this fallback". Returns the resolved value or the
9
+ * developer-supplied default. Network failures swallow into the
10
+ * default so the calling app never crashes because the API is
11
+ * unreachable; the developer's code default is the contract.
12
+ *
13
+ * * {@link RedPennonClient.variable} — "give me the full result
14
+ * object". Returns the typed result (`value`, `variation`,
15
+ * `reason`, `feature`) so callers can branch on `reason` for
16
+ * telemetry, dashboards, or per-state UI. Network failures
17
+ * surface as {@link APIError}; callers opting into the result
18
+ * shape opt into error handling.
19
+ *
20
+ * Batch evaluation ({@link RedPennonClient.variables}) lets one HTTP
21
+ * round-trip resolve many flags for the same user context.
22
+ */
23
+ export declare const DEFAULT_API_BASE_URL = "https://api.redpennon.dev";
24
+ export type UserContext = {
25
+ id?: string;
26
+ email?: string;
27
+ organisation_id?: string;
28
+ ip?: string;
29
+ audiences?: string[];
30
+ /**
31
+ * Application version string supplied by the calling app. The
32
+ * evaluator treats it as opaque text — operators like `is`,
33
+ * `contains`, and `starts_with` work; numeric/semver comparisons do
34
+ * not. Server-side SDKs can't auto-detect this, so populate it
35
+ * manually if you need to target on it.
36
+ */
37
+ app_version?: string;
38
+ /**
39
+ * Free-form platform identifier (e.g. `"ios"`, `"android"`,
40
+ * `"web"`). Server-side SDKs don't auto-populate this either.
41
+ */
42
+ platform?: string;
43
+ /**
44
+ * ISO-3166 alpha-2 country code (e.g. `"AU"`, `"US"`).
45
+ */
46
+ country?: string;
47
+ /**
48
+ * Arbitrary user attributes for `custom_property` targeting
49
+ * conditions. Keys are looked up by the rule's `custom_key`; values
50
+ * may be strings, numbers, booleans, or arrays of strings.
51
+ */
52
+ custom_data?: Record<string, unknown>;
53
+ };
54
+ /**
55
+ * Reasons surfaced by the evaluation engine. Mirrors the server-side
56
+ * constants 1:1 so SDK consumers can switch on the string value.
57
+ */
58
+ export type EvaluationReason = "targeting_rule_matched" | "default_variation" | "no_rule_matched" | "targeting_disabled" | "feature_complete" | "feature_deleted" | "feature_archived" | "self_targeting_override" | "variable_not_found";
59
+ export type VariableResult<T> = {
60
+ key: string;
61
+ /** Resolved value, or `null` when the platform served no value. */
62
+ value: T | null;
63
+ /** Variation slug served, or `null` when no value was served. */
64
+ variation: string | null;
65
+ reason: EvaluationReason;
66
+ /** Parent feature slug, or `null` when the key didn't resolve. */
67
+ feature: string | null;
68
+ /** Opaque signed trace token returned by the server when the user has stable identity (`id` or `email`). `null` or absent when not available. Pass to `trackEvents` to correlate metric events with evaluations. */
69
+ evaluation_trace?: string | null;
70
+ };
71
+ export type BatchResults = Record<string, VariableResult<unknown>>;
72
+ export type EventPayload = {
73
+ event: string;
74
+ variable: string;
75
+ variation: string;
76
+ user?: UserContext;
77
+ value?: number;
78
+ /** ISO-8601 timestamp; defaults to server receive time when omitted. */
79
+ occurred_at?: string;
80
+ /** Opaque trace token from a prior {@link VariableResult.evaluation_trace}. */
81
+ evaluation_trace?: string;
82
+ };
83
+ export type TrackEventsResult = {
84
+ accepted: number;
85
+ };
86
+ export declare class APIError extends Error {
87
+ readonly statusCode: number;
88
+ readonly body: string;
89
+ /**
90
+ * Platform governance error code parsed from the response body's
91
+ * structured `{"error", "code"}` payload (e.g. `rate_limit_exceeded`,
92
+ * `organisation_suspended`, `monthly_active_users_exceeded`).
93
+ * `null` for transport errors or unstructured responses. Callers
94
+ * branching on governance state should match on `code`, not `message`.
95
+ */
96
+ readonly code: string | null;
97
+ constructor(statusCode: number, message: string, body: string, code?: string | null);
98
+ }
99
+ export type ClientOptions = {
100
+ apiKey: string;
101
+ /** Override the API origin. Defaults to {@link DEFAULT_API_BASE_URL}. */
102
+ baseUrl?: string;
103
+ /** Override the fetch implementation (handy for tests). */
104
+ fetchImpl?: typeof fetch;
105
+ };
106
+ export type EvalOptions = {
107
+ user?: UserContext;
108
+ };
109
+ /**
110
+ * Thin client around the variable-key evaluation endpoints.
111
+ * Stateless — one instance can serve any number of concurrent
112
+ * evaluations and is safe to keep as a module-level singleton.
113
+ */
114
+ export declare class RedPennonClient {
115
+ readonly origin: string;
116
+ readonly apiKey: string;
117
+ readonly fetchImpl: typeof fetch;
118
+ constructor(options: ClientOptions);
119
+ /**
120
+ * Resolve a single variable to its full result object.
121
+ *
122
+ * Network errors and non-2xx responses surface as {@link APIError};
123
+ * callers that want fail-open semantics should use
124
+ * {@link variableValue} instead.
125
+ */
126
+ variable<T = unknown>(key: string, options?: EvalOptions): Promise<VariableResult<T>>;
127
+ /**
128
+ * Resolve a single variable to its value, falling back to
129
+ * `defaultValue` whenever the platform served no value (network
130
+ * error, unknown key, deleted/archived feature, targeting
131
+ * disabled). This is the method that should be reached for in
132
+ * application code: the developer-supplied default is the
133
+ * load-bearing contract.
134
+ */
135
+ variableValue<T>(key: string, defaultValue: T, options?: EvalOptions): Promise<T>;
136
+ /**
137
+ * Record one or more metric events. Pass the `evaluation_trace` from a
138
+ * prior {@link variable} call to correlate events with the evaluation
139
+ * that triggered them.
140
+ *
141
+ * Throws {@link APIError} on any non-2xx response, including governance
142
+ * errors (`events_batch_too_large`, `rate_limit_exceeded`, etc.).
143
+ */
144
+ trackEvents(events: EventPayload[]): Promise<TrackEventsResult>;
145
+ /**
146
+ * Resolve multiple variables in one HTTP round-trip. Each result
147
+ * has the same shape as {@link variable}; unknown keys surface as
148
+ * `variable_not_found` inline.
149
+ */
150
+ variables(keys: string[], options?: EvalOptions): Promise<BatchResults>;
151
+ }
152
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * RedPennon Node SDK — variable-key evaluation client.
3
+ *
4
+ * Two evaluation methods cover the two SDK ergonomics teams reach
5
+ * for:
6
+ *
7
+ * * {@link RedPennonClient.variableValue} — "give me the value, or
8
+ * this fallback". Returns the resolved value or the
9
+ * developer-supplied default. Network failures swallow into the
10
+ * default so the calling app never crashes because the API is
11
+ * unreachable; the developer's code default is the contract.
12
+ *
13
+ * * {@link RedPennonClient.variable} — "give me the full result
14
+ * object". Returns the typed result (`value`, `variation`,
15
+ * `reason`, `feature`) so callers can branch on `reason` for
16
+ * telemetry, dashboards, or per-state UI. Network failures
17
+ * surface as {@link APIError}; callers opting into the result
18
+ * shape opt into error handling.
19
+ *
20
+ * Batch evaluation ({@link RedPennonClient.variables}) lets one HTTP
21
+ * round-trip resolve many flags for the same user context.
22
+ */
23
+ export const DEFAULT_API_BASE_URL = "https://api.redpennon.dev";
24
+ export class APIError extends Error {
25
+ statusCode;
26
+ body;
27
+ /**
28
+ * Platform governance error code parsed from the response body's
29
+ * structured `{"error", "code"}` payload (e.g. `rate_limit_exceeded`,
30
+ * `organisation_suspended`, `monthly_active_users_exceeded`).
31
+ * `null` for transport errors or unstructured responses. Callers
32
+ * branching on governance state should match on `code`, not `message`.
33
+ */
34
+ code;
35
+ constructor(statusCode, message, body, code = null) {
36
+ super(`redpennon: api error ${statusCode}: ${message}`);
37
+ this.name = "APIError";
38
+ this.statusCode = statusCode;
39
+ this.body = body;
40
+ this.code = code;
41
+ }
42
+ }
43
+ function parseErrorCode(body) {
44
+ try {
45
+ const parsed = JSON.parse(body);
46
+ if (parsed && typeof parsed === "object" && typeof parsed.code === "string" && parsed.code) {
47
+ return parsed.code;
48
+ }
49
+ }
50
+ catch {
51
+ // Body is not JSON; leave code unparsed.
52
+ }
53
+ return null;
54
+ }
55
+ /**
56
+ * Thin client around the variable-key evaluation endpoints.
57
+ * Stateless — one instance can serve any number of concurrent
58
+ * evaluations and is safe to keep as a module-level singleton.
59
+ */
60
+ export class RedPennonClient {
61
+ origin;
62
+ apiKey;
63
+ fetchImpl;
64
+ constructor(options) {
65
+ this.origin = (options.baseUrl ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
66
+ this.apiKey = options.apiKey;
67
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
68
+ }
69
+ /**
70
+ * Resolve a single variable to its full result object.
71
+ *
72
+ * Network errors and non-2xx responses surface as {@link APIError};
73
+ * callers that want fail-open semantics should use
74
+ * {@link variableValue} instead.
75
+ */
76
+ async variable(key, options = {}) {
77
+ const body = {};
78
+ if (options.user)
79
+ body.user = options.user;
80
+ const response = await this.fetchImpl(`${this.origin}/v1/variables/${encodeURIComponent(key)}`, {
81
+ method: "POST",
82
+ headers: {
83
+ "X-API-Key": this.apiKey,
84
+ "Content-Type": "application/json",
85
+ },
86
+ body: JSON.stringify(body),
87
+ });
88
+ if (!response.ok) {
89
+ const text = await response.text();
90
+ throw new APIError(response.status, response.statusText, text, parseErrorCode(text));
91
+ }
92
+ return (await response.json());
93
+ }
94
+ /**
95
+ * Resolve a single variable to its value, falling back to
96
+ * `defaultValue` whenever the platform served no value (network
97
+ * error, unknown key, deleted/archived feature, targeting
98
+ * disabled). This is the method that should be reached for in
99
+ * application code: the developer-supplied default is the
100
+ * load-bearing contract.
101
+ */
102
+ async variableValue(key, defaultValue, options = {}) {
103
+ try {
104
+ const result = await this.variable(key, options);
105
+ return result.value === null ? defaultValue : result.value;
106
+ }
107
+ catch {
108
+ // Swallow all errors — network, API, JSON parse — and serve the
109
+ // developer-supplied default. The SDK's job is to keep the
110
+ // calling app running, not to surface infra problems.
111
+ return defaultValue;
112
+ }
113
+ }
114
+ /**
115
+ * Record one or more metric events. Pass the `evaluation_trace` from a
116
+ * prior {@link variable} call to correlate events with the evaluation
117
+ * that triggered them.
118
+ *
119
+ * Throws {@link APIError} on any non-2xx response, including governance
120
+ * errors (`events_batch_too_large`, `rate_limit_exceeded`, etc.).
121
+ */
122
+ async trackEvents(events) {
123
+ const response = await this.fetchImpl(`${this.origin}/v1/events`, {
124
+ method: "POST",
125
+ headers: {
126
+ "X-API-Key": this.apiKey,
127
+ "Content-Type": "application/json",
128
+ },
129
+ body: JSON.stringify({ events }),
130
+ });
131
+ if (!response.ok) {
132
+ const text = await response.text();
133
+ throw new APIError(response.status, response.statusText, text, parseErrorCode(text));
134
+ }
135
+ return (await response.json());
136
+ }
137
+ /**
138
+ * Resolve multiple variables in one HTTP round-trip. Each result
139
+ * has the same shape as {@link variable}; unknown keys surface as
140
+ * `variable_not_found` inline.
141
+ */
142
+ async variables(keys, options = {}) {
143
+ const body = { keys };
144
+ if (options.user)
145
+ body.user = options.user;
146
+ const response = await this.fetchImpl(`${this.origin}/v1/variables`, {
147
+ method: "POST",
148
+ headers: {
149
+ "X-API-Key": this.apiKey,
150
+ "Content-Type": "application/json",
151
+ },
152
+ body: JSON.stringify(body),
153
+ });
154
+ if (!response.ok) {
155
+ const text = await response.text();
156
+ throw new APIError(response.status, response.statusText, text, parseErrorCode(text));
157
+ }
158
+ const parsed = (await response.json());
159
+ return parsed.results;
160
+ }
161
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@redpennon/node-sdk",
3
+ "version": "1.0.0",
4
+ "description": "RedPennon feature flag evaluation API client for Node.js",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": ["dist", "README.md"],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/redpennon/sdks.git",
18
+ "directory": "node"
19
+ },
20
+ "homepage": "https://docs.redpennon.dev",
21
+ "bugs": "https://github.com/redpennon/sdks/issues",
22
+ "keywords": ["feature-flags", "feature-toggles", "redpennon"],
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.build.json",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "prepublishOnly": "npm test && npm run build"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.14.1",
34
+ "typescript": "^5.8.3",
35
+ "vitest": "^3.1.2"
36
+ },
37
+ "license": "MIT"
38
+ }