@jewel998/config 0.2.2 → 0.2.4
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 +30 -14
- package/dist/cache/types.d.ts +1 -1
- package/dist/createConfig.js +9 -6
- package/dist/errors/index.d.ts +18 -0
- package/dist/errors/index.js +12 -0
- package/dist/events/EventEmitter.d.ts +21 -0
- package/dist/events/EventEmitter.js +54 -8
- package/dist/fetch/TierFetcher.d.ts +71 -0
- package/dist/fetch/TierFetcher.js +154 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/initConfig.d.ts +79 -54
- package/dist/initConfig.js +255 -82
- package/dist/plugins/lifecycle/index.d.ts +1 -1
- package/dist/plugins/lifecycle/index.js +1 -1
- package/dist/plugins/rollout/murmurhash3.js +1 -5
- package/dist/retry/RetryEngine.js +1 -1
- package/dist/server/initServerConfig.d.ts +110 -0
- package/dist/server/initServerConfig.js +159 -0
- package/dist/server/serverContext.d.ts +63 -0
- package/dist/server/serverContext.js +64 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +20 -0
- package/dist/transport/HttpTransport.js +26 -16
- package/dist/types.d.ts +3 -1
- package/dist/webhook-formats.js +1 -6
- package/package.json +36 -25
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { EvaluationContext, EvaluationPlugin } from "../plugins/types.js";
|
|
2
|
+
import type { CacheStorage, ConfigClient, FetchGranularity, RetryConfig } from "../types.js";
|
|
3
|
+
export interface InitServerConfigOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Required. Server API key from the portal (must start with `svr_`).
|
|
6
|
+
*
|
|
7
|
+
* Server keys return full flag data (targeting rules, rollouts, segments)
|
|
8
|
+
* for local evaluation. The Cloud Function does NOT validate the
|
|
9
|
+
* request origin for server keys — no domain allowlist is enforced.
|
|
10
|
+
*/
|
|
11
|
+
clientId: string;
|
|
12
|
+
/**
|
|
13
|
+
* Your self-hosted API URL. Points to your Firebase deployment.
|
|
14
|
+
* Default: https://jewel998-config.web.app/api (demo instance)
|
|
15
|
+
*
|
|
16
|
+
* @example "https://your-project.web.app/api"
|
|
17
|
+
*/
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Evaluation context for targeting rules and rollouts.
|
|
21
|
+
* Use `serverContext()` helper to build this from request/session data.
|
|
22
|
+
*
|
|
23
|
+
* Can be updated post-init via `setContext()` for per-request evaluation.
|
|
24
|
+
*/
|
|
25
|
+
context?: EvaluationContext;
|
|
26
|
+
/**
|
|
27
|
+
* Evaluation plugins to register (tree-shakeable pipeline steps).
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { targetingPlugin } from "@jewel998/config/targeting";
|
|
32
|
+
* import { rolloutPlugin } from "@jewel998/config/rollout";
|
|
33
|
+
*
|
|
34
|
+
* const config = await initServerConfig({
|
|
35
|
+
* clientId: "svr_xxx",
|
|
36
|
+
* plugins: [targetingPlugin(), rolloutPlugin()],
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
plugins?: EvaluationPlugin[];
|
|
41
|
+
/**
|
|
42
|
+
* Fetch granularity.
|
|
43
|
+
* - "batch" (default): Fetch all flags in one request (recommended for servers)
|
|
44
|
+
* - "projected": Fetch only specific keys on demand
|
|
45
|
+
*/
|
|
46
|
+
fetchGranularity?: FetchGranularity;
|
|
47
|
+
/** Cache storage adapter. Default: memoryStorage() (in-process Map) */
|
|
48
|
+
storage?: CacheStorage;
|
|
49
|
+
/** Retry configuration for failed fetches */
|
|
50
|
+
retry?: RetryConfig;
|
|
51
|
+
/** Timeout for initial fetch (ms). Default: 10000 */
|
|
52
|
+
timeout?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Background refresh interval in milliseconds.
|
|
55
|
+
* The SDK will periodically re-fetch flags to keep them current.
|
|
56
|
+
*
|
|
57
|
+
* Default: 0 (disabled). Set to e.g. 60_000 for 1-minute refresh.
|
|
58
|
+
*
|
|
59
|
+
* For most server apps, calling `refresh()` after receiving a webhook
|
|
60
|
+
* notification is more efficient than background polling.
|
|
61
|
+
*/
|
|
62
|
+
refreshInterval?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface ServerFlags extends ConfigClient {
|
|
65
|
+
/**
|
|
66
|
+
* Gracefully shut down the SDK.
|
|
67
|
+
* Stops background refresh timers and cleans up listeners.
|
|
68
|
+
* Call this on server shutdown (SIGTERM handler, etc.)
|
|
69
|
+
*/
|
|
70
|
+
close(): void;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Initialize the server-side feature flags SDK.
|
|
74
|
+
*
|
|
75
|
+
* Returns a Promise that resolves once flags are fetched from the API.
|
|
76
|
+
* This is a pessimistic strategy — your server won't serve requests
|
|
77
|
+
* with stale/empty flags.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* import { initServerConfig, serverContext } from "@jewel998/config/server";
|
|
82
|
+
* import { targetingPlugin } from "@jewel998/config/targeting";
|
|
83
|
+
* import { rolloutPlugin } from "@jewel998/config/rollout";
|
|
84
|
+
*
|
|
85
|
+
* // Initialize once at server startup
|
|
86
|
+
* const flags = await initServerConfig({
|
|
87
|
+
* clientId: "svr_xxx",
|
|
88
|
+
* baseUrl: "https://your-project.web.app/api",
|
|
89
|
+
* plugins: [targetingPlugin(), rolloutPlugin()],
|
|
90
|
+
* refreshInterval: 60_000, // re-fetch every minute
|
|
91
|
+
* });
|
|
92
|
+
*
|
|
93
|
+
* // In request handlers:
|
|
94
|
+
* app.get("/api/feature", (req, res) => {
|
|
95
|
+
* flags.setContext(serverContext({
|
|
96
|
+
* userId: req.user.id,
|
|
97
|
+
* plan: req.user.plan,
|
|
98
|
+
* }));
|
|
99
|
+
*
|
|
100
|
+
* res.json({
|
|
101
|
+
* newCheckout: flags.getFlag("feature.checkout_v2"),
|
|
102
|
+
* uploadLimit: flags.getValue("app.upload_limit", 50),
|
|
103
|
+
* });
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* // Graceful shutdown
|
|
107
|
+
* process.on("SIGTERM", () => flags.close());
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare function initServerConfig(options: InitServerConfigOptions): Promise<ServerFlags>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// initServerConfig — Server-side SDK entry point
|
|
3
|
+
//
|
|
4
|
+
// Designed for Node.js, Deno, Bun, and server-side runtimes.
|
|
5
|
+
// Does NOT use window, document, navigator, localStorage,
|
|
6
|
+
// or any browser-specific APIs.
|
|
7
|
+
//
|
|
8
|
+
// Key differences from initConfig (browser):
|
|
9
|
+
// - Always uses pessimistic strategy (await flags before serving)
|
|
10
|
+
// - No polling (server processes are long-lived; use refresh())
|
|
11
|
+
// - No visibility change detection
|
|
12
|
+
// - Plugins always run locally (svr_ keys provide full flag data)
|
|
13
|
+
// - Optional background refresh via configurable interval
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════
|
|
15
|
+
import { memoryStorage } from "../cache/memoryStorage.js";
|
|
16
|
+
import { buildConfigClient } from "../client/ConfigClient.js";
|
|
17
|
+
import { ConfigError } from "../errors/index.js";
|
|
18
|
+
import { TypedEventEmitter } from "../events/EventEmitter.js";
|
|
19
|
+
import { createBatchFetcher } from "../fetch/batchFetcher.js";
|
|
20
|
+
import { createProjectedFetcher } from "../fetch/projectedFetcher.js";
|
|
21
|
+
import { executePessimistic } from "../loading/pessimistic.js";
|
|
22
|
+
import { createHttpTransport } from "../transport/HttpTransport.js";
|
|
23
|
+
import { DEFAULT_RETRY, DEFAULT_TIMEOUT } from "../types.js";
|
|
24
|
+
/**
|
|
25
|
+
* Initialize the server-side feature flags SDK.
|
|
26
|
+
*
|
|
27
|
+
* Returns a Promise that resolves once flags are fetched from the API.
|
|
28
|
+
* This is a pessimistic strategy — your server won't serve requests
|
|
29
|
+
* with stale/empty flags.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { initServerConfig, serverContext } from "@jewel998/config/server";
|
|
34
|
+
* import { targetingPlugin } from "@jewel998/config/targeting";
|
|
35
|
+
* import { rolloutPlugin } from "@jewel998/config/rollout";
|
|
36
|
+
*
|
|
37
|
+
* // Initialize once at server startup
|
|
38
|
+
* const flags = await initServerConfig({
|
|
39
|
+
* clientId: "svr_xxx",
|
|
40
|
+
* baseUrl: "https://your-project.web.app/api",
|
|
41
|
+
* plugins: [targetingPlugin(), rolloutPlugin()],
|
|
42
|
+
* refreshInterval: 60_000, // re-fetch every minute
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* // In request handlers:
|
|
46
|
+
* app.get("/api/feature", (req, res) => {
|
|
47
|
+
* flags.setContext(serverContext({
|
|
48
|
+
* userId: req.user.id,
|
|
49
|
+
* plan: req.user.plan,
|
|
50
|
+
* }));
|
|
51
|
+
*
|
|
52
|
+
* res.json({
|
|
53
|
+
* newCheckout: flags.getFlag("feature.checkout_v2"),
|
|
54
|
+
* uploadLimit: flags.getValue("app.upload_limit", 50),
|
|
55
|
+
* });
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* // Graceful shutdown
|
|
59
|
+
* process.on("SIGTERM", () => flags.close());
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export async function initServerConfig(options) {
|
|
63
|
+
// ── Validation ─────────────────────────────────────────────
|
|
64
|
+
if (!options.clientId) {
|
|
65
|
+
throw new ConfigError("clientId is required", "MISSING_CLIENT_ID");
|
|
66
|
+
}
|
|
67
|
+
if (!options.clientId.startsWith("svr_")) {
|
|
68
|
+
throw new ConfigError("Server SDK requires a server key (svr_ prefix). " +
|
|
69
|
+
"Client keys (cid_) are for the browser SDK — they don't return " +
|
|
70
|
+
"full flag data needed for local evaluation.", "INITIALIZATION_FAILED");
|
|
71
|
+
}
|
|
72
|
+
// ── Resolve defaults ───────────────────────────────────────
|
|
73
|
+
const baseUrl = options.baseUrl ?? "https://jewel998-config.web.app/api";
|
|
74
|
+
const granularity = options.fetchGranularity ?? "batch";
|
|
75
|
+
const storage = options.storage ?? memoryStorage();
|
|
76
|
+
const retry = { ...DEFAULT_RETRY, ...options.retry };
|
|
77
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
78
|
+
const refreshInterval = options.refreshInterval ?? 0;
|
|
79
|
+
const plugins = options.plugins ?? [];
|
|
80
|
+
// Server keys always evaluate locally (client mode)
|
|
81
|
+
const evaluationMode = "client";
|
|
82
|
+
// Mutable context reference
|
|
83
|
+
let currentContext = options.context ?? {};
|
|
84
|
+
// ── Create internal modules ────────────────────────────────
|
|
85
|
+
const transport = createHttpTransport({
|
|
86
|
+
baseUrl,
|
|
87
|
+
clientId: options.clientId,
|
|
88
|
+
evaluationMode,
|
|
89
|
+
getContext: () => currentContext,
|
|
90
|
+
});
|
|
91
|
+
const events = new TypedEventEmitter();
|
|
92
|
+
const fetcher = granularity === "batch" ? createBatchFetcher(transport) : createProjectedFetcher(transport);
|
|
93
|
+
// ── Pessimistic load — await flags before returning ────────
|
|
94
|
+
const loadResult = await executePessimistic({
|
|
95
|
+
clientId: options.clientId,
|
|
96
|
+
fetcher,
|
|
97
|
+
cache: storage,
|
|
98
|
+
events,
|
|
99
|
+
retry,
|
|
100
|
+
timeout,
|
|
101
|
+
granularity,
|
|
102
|
+
});
|
|
103
|
+
// ── Build the config client ────────────────────────────────
|
|
104
|
+
const client = buildConfigClient({
|
|
105
|
+
data: loadResult.initialData,
|
|
106
|
+
cache: storage,
|
|
107
|
+
fetcher,
|
|
108
|
+
events,
|
|
109
|
+
retry,
|
|
110
|
+
granularity,
|
|
111
|
+
isDeferred: false,
|
|
112
|
+
plugins,
|
|
113
|
+
context: options.context,
|
|
114
|
+
evaluationMode,
|
|
115
|
+
onContextChange: (ctx) => {
|
|
116
|
+
currentContext = ctx;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
events.emit("ready", {
|
|
120
|
+
loadingStrategy: "pessimistic",
|
|
121
|
+
cachedKeys: Object.keys(loadResult.initialData).length,
|
|
122
|
+
});
|
|
123
|
+
// ── Background refresh (optional) ─────────────────────────
|
|
124
|
+
let refreshTimer = null;
|
|
125
|
+
if (refreshInterval > 0) {
|
|
126
|
+
refreshTimer = setInterval(() => {
|
|
127
|
+
void client.refresh();
|
|
128
|
+
}, refreshInterval);
|
|
129
|
+
// Ensure the timer doesn't prevent Node.js process from exiting.
|
|
130
|
+
// In Node.js, setInterval returns an object with .unref().
|
|
131
|
+
// We use a safe dynamic check to avoid depending on @types/node.
|
|
132
|
+
const timer = refreshTimer;
|
|
133
|
+
if (typeof timer.unref === "function") {
|
|
134
|
+
timer.unref();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ── Return ServerFlags (extends ConfigClient + close()) ────
|
|
138
|
+
const serverFlags = {
|
|
139
|
+
getValue: client.getValue,
|
|
140
|
+
getFlag: client.getFlag,
|
|
141
|
+
getAll: client.getAll,
|
|
142
|
+
refresh: client.refresh,
|
|
143
|
+
setContext: client.setContext,
|
|
144
|
+
on: client.on,
|
|
145
|
+
off: client.off,
|
|
146
|
+
destroy() {
|
|
147
|
+
if (refreshTimer) {
|
|
148
|
+
clearInterval(refreshTimer);
|
|
149
|
+
refreshTimer = null;
|
|
150
|
+
}
|
|
151
|
+
client.destroy();
|
|
152
|
+
},
|
|
153
|
+
close() {
|
|
154
|
+
serverFlags.destroy();
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
return serverFlags;
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=initServerConfig.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { EvaluationContext } from "../plugins/types.js";
|
|
2
|
+
export interface ServerContextOptions {
|
|
3
|
+
/** The authenticated user's ID. Should be stable (database ID, not session). */
|
|
4
|
+
userId?: string;
|
|
5
|
+
/**
|
|
6
|
+
* Custom attributes for targeting rules.
|
|
7
|
+
* Typically derived from the authenticated user, request headers, or environment.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* serverContext({
|
|
12
|
+
* userId: req.user.id,
|
|
13
|
+
* plan: req.user.plan,
|
|
14
|
+
* country: req.headers["cf-ipcountry"],
|
|
15
|
+
* locale: req.headers["accept-language"]?.split(",")[0],
|
|
16
|
+
* })
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
[key: string]: string | number | boolean | string[] | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build an evaluation context from server-side data.
|
|
23
|
+
*
|
|
24
|
+
* Unlike `autoContext()` (browser-side), this does NOT auto-detect anything.
|
|
25
|
+
* In server environments, context comes from your request/session/database
|
|
26
|
+
* — not from `navigator` or `window`.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { initServerConfig, serverContext } from "@jewel998/config/server";
|
|
31
|
+
*
|
|
32
|
+
* // In an Express route handler:
|
|
33
|
+
* app.get("/dashboard", (req, res) => {
|
|
34
|
+
* const flags = initServerConfig({
|
|
35
|
+
* clientId: "svr_xxx",
|
|
36
|
+
* context: serverContext({
|
|
37
|
+
* userId: req.user.id,
|
|
38
|
+
* plan: req.user.plan,
|
|
39
|
+
* country: req.headers["cf-ipcountry"],
|
|
40
|
+
* role: req.user.role,
|
|
41
|
+
* }),
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* if (flags.getFlag("feature.new_dashboard")) {
|
|
45
|
+
* // serve new dashboard
|
|
46
|
+
* }
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* // For per-request context updates (shared instance):
|
|
53
|
+
* const config = await initServerConfig({ clientId: "svr_xxx" });
|
|
54
|
+
*
|
|
55
|
+
* // On each request:
|
|
56
|
+
* config.setContext(serverContext({
|
|
57
|
+
* userId: req.user.id,
|
|
58
|
+
* plan: req.user.plan,
|
|
59
|
+
* }));
|
|
60
|
+
* config.getFlag("feature.checkout_v2");
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function serverContext(options?: ServerContextOptions): EvaluationContext;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// Server Context Helper
|
|
3
|
+
//
|
|
4
|
+
// The server-side equivalent of autoContext(). Provides a clean
|
|
5
|
+
// way to build evaluation context from server-side data sources
|
|
6
|
+
// (HTTP requests, environment variables, session objects, etc.)
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════
|
|
8
|
+
/**
|
|
9
|
+
* Build an evaluation context from server-side data.
|
|
10
|
+
*
|
|
11
|
+
* Unlike `autoContext()` (browser-side), this does NOT auto-detect anything.
|
|
12
|
+
* In server environments, context comes from your request/session/database
|
|
13
|
+
* — not from `navigator` or `window`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { initServerConfig, serverContext } from "@jewel998/config/server";
|
|
18
|
+
*
|
|
19
|
+
* // In an Express route handler:
|
|
20
|
+
* app.get("/dashboard", (req, res) => {
|
|
21
|
+
* const flags = initServerConfig({
|
|
22
|
+
* clientId: "svr_xxx",
|
|
23
|
+
* context: serverContext({
|
|
24
|
+
* userId: req.user.id,
|
|
25
|
+
* plan: req.user.plan,
|
|
26
|
+
* country: req.headers["cf-ipcountry"],
|
|
27
|
+
* role: req.user.role,
|
|
28
|
+
* }),
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* if (flags.getFlag("feature.new_dashboard")) {
|
|
32
|
+
* // serve new dashboard
|
|
33
|
+
* }
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* // For per-request context updates (shared instance):
|
|
40
|
+
* const config = await initServerConfig({ clientId: "svr_xxx" });
|
|
41
|
+
*
|
|
42
|
+
* // On each request:
|
|
43
|
+
* config.setContext(serverContext({
|
|
44
|
+
* userId: req.user.id,
|
|
45
|
+
* plan: req.user.plan,
|
|
46
|
+
* }));
|
|
47
|
+
* config.getFlag("feature.checkout_v2");
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function serverContext(options) {
|
|
51
|
+
if (!options)
|
|
52
|
+
return {};
|
|
53
|
+
const { userId, ...attrs } = options;
|
|
54
|
+
const attributes = {};
|
|
55
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
56
|
+
if (v !== undefined)
|
|
57
|
+
attributes[k] = v;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
userId,
|
|
61
|
+
attributes: Object.keys(attributes).length > 0 ? attributes : undefined,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=serverContext.js.map
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { initServerConfig } from "./server/initServerConfig.js";
|
|
2
|
+
export type { InitServerConfigOptions, ServerFlags } from "./server/initServerConfig.js";
|
|
3
|
+
export { serverContext } from "./server/serverContext.js";
|
|
4
|
+
export type { ServerContextOptions } from "./server/serverContext.js";
|
|
5
|
+
export type { CacheStorage, ConfigClient, ConfigEventCallback, ConfigEventPayloads, ConfigEventType, ConfigRecord, CreateConfigOptions, EvaluationWarning, FetchGranularity, LoadingStrategy, RetryConfig, } from "./types.js";
|
|
6
|
+
export type { EvaluationContext, EvaluationPlugin, PipelineStepId, PipelineStepResult, PipelineHelpers, } from "./plugins/types.js";
|
|
7
|
+
export { PIPELINE_ORDER } from "./plugins/types.js";
|
|
8
|
+
export { memoryStorage } from "./cache/memoryStorage.js";
|
|
9
|
+
export { AuthenticationError, ConfigError, InitializationError, RateLimitError, TimeoutError, } from "./errors/index.js";
|
|
10
|
+
export type { ConfigErrorCode } from "./errors/index.js";
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// @jewel998/config — Server (Node.js) Entry Point
|
|
3
|
+
//
|
|
4
|
+
// This module provides the backend SDK that runs in Node.js,
|
|
5
|
+
// Deno, Bun, or any server-side JavaScript runtime.
|
|
6
|
+
//
|
|
7
|
+
// It does NOT import any browser-specific code (localStorage,
|
|
8
|
+
// navigator, document, window). Tree-shaking ensures that
|
|
9
|
+
// frontend-only modules are never bundled into server builds.
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════
|
|
11
|
+
// Server-specific entry point
|
|
12
|
+
export { initServerConfig } from "./server/initServerConfig.js";
|
|
13
|
+
// Server context helper (equivalent of autoContext for Node.js)
|
|
14
|
+
export { serverContext } from "./server/serverContext.js";
|
|
15
|
+
export { PIPELINE_ORDER } from "./plugins/types.js";
|
|
16
|
+
// Cache adapters (memory only — no browser storage)
|
|
17
|
+
export { memoryStorage } from "./cache/memoryStorage.js";
|
|
18
|
+
// Errors
|
|
19
|
+
export { AuthenticationError, ConfigError, InitializationError, RateLimitError, TimeoutError, } from "./errors/index.js";
|
|
20
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import { AuthenticationError, ConfigError, RateLimitError
|
|
2
|
+
import { AuthenticationError, ConfigError, RateLimitError } from "../errors/index.js";
|
|
3
3
|
import { CircuitBreaker } from "./CircuitBreaker.js";
|
|
4
4
|
/** Errors that should permanently stop retrying (circuit opens) */
|
|
5
5
|
const FATAL_STATUS_CODES = new Set([400, 401, 403]);
|
|
@@ -32,15 +32,29 @@ export const createHttpTransport = (config) => {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
// Client keys (cid_) use POST with body (server-side evaluation needs context).
|
|
36
|
+
// Server keys (svr_) use GET with query params (fetches full flag data).
|
|
37
|
+
const isServerKey = config.clientId.startsWith("svr_");
|
|
38
|
+
let response;
|
|
39
|
+
if (isServerKey) {
|
|
40
|
+
// GET request — clientId as query param
|
|
41
|
+
const params = new URLSearchParams({ clientId: config.clientId });
|
|
42
|
+
if (body?.keys && Array.isArray(body.keys)) {
|
|
43
|
+
params.set("keys", body.keys.join(","));
|
|
44
|
+
}
|
|
45
|
+
response = await fetch(`${url}?${params.toString()}`, {
|
|
46
|
+
method: "GET",
|
|
47
|
+
headers: { Accept: "application/json" },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// POST request — full body with context
|
|
52
|
+
response = await fetch(url, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({ data: requestData }),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
44
58
|
if (!response.ok) {
|
|
45
59
|
const errorBody = await response.json().catch(() => ({}));
|
|
46
60
|
const errorData = errorBody;
|
|
@@ -70,12 +84,8 @@ export const createHttpTransport = (config) => {
|
|
|
70
84
|
break;
|
|
71
85
|
case 429: {
|
|
72
86
|
const retryAfterHeader = response.headers.get("Retry-After");
|
|
73
|
-
const retryAfterSeconds = retryAfterHeader
|
|
74
|
-
|
|
75
|
-
: undefined;
|
|
76
|
-
error = new RateLimitError(message, Number.isFinite(retryAfterSeconds)
|
|
77
|
-
? retryAfterSeconds
|
|
78
|
-
: undefined);
|
|
87
|
+
const retryAfterSeconds = retryAfterHeader ? parseInt(retryAfterHeader, 10) : undefined;
|
|
88
|
+
error = new RateLimitError(message, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined);
|
|
79
89
|
break;
|
|
80
90
|
}
|
|
81
91
|
case 500:
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { EvaluationContext, EvaluationPlugin } from "./plugins/types.js";
|
|
2
2
|
/** Loading strategy for SDK initialization */
|
|
3
3
|
export type LoadingStrategy = "optimistic" | "pessimistic" | "deferred";
|
|
4
|
+
/** Fetch tier for priority-based key fetching */
|
|
5
|
+
export type FetchTier = "critical" | "page" | "prefetch";
|
|
4
6
|
/** Evaluation mode for the SDK */
|
|
5
7
|
export type EvaluationMode = "server" | "client";
|
|
6
8
|
/** Fetch granularity for remote config retrieval */
|
|
@@ -106,7 +108,7 @@ export interface ConfigEventPayloads {
|
|
|
106
108
|
};
|
|
107
109
|
updated: {
|
|
108
110
|
keys: string[];
|
|
109
|
-
source: "background" | "refresh" | "version-check";
|
|
111
|
+
source: "background" | "refresh" | "version-check" | "prefetch" | "page" | "idle";
|
|
110
112
|
};
|
|
111
113
|
fetchError: {
|
|
112
114
|
error: Error;
|
package/dist/webhook-formats.js
CHANGED
|
@@ -105,12 +105,7 @@ export const TEMPLATE_VARIABLES = [
|
|
|
105
105
|
/**
|
|
106
106
|
* Event types that can trigger webhooks.
|
|
107
107
|
*/
|
|
108
|
-
export const WEBHOOK_EVENT_TYPES = [
|
|
109
|
-
"create",
|
|
110
|
-
"update",
|
|
111
|
-
"delete",
|
|
112
|
-
"state_change",
|
|
113
|
-
];
|
|
108
|
+
export const WEBHOOK_EVENT_TYPES = ["create", "update", "delete", "state_change"];
|
|
114
109
|
/**
|
|
115
110
|
* Resource categories for webhook filtering.
|
|
116
111
|
*/
|
package/package.json
CHANGED
|
@@ -1,33 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jewel998/config",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"private": false,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"sideEffects": false,
|
|
7
5
|
"description": "Free, self-hosted feature flags and remote configuration SDK",
|
|
8
|
-
"author": "Jyotirmoy Barman",
|
|
9
|
-
"homepage": "https://github.com/jewel998/config#readme",
|
|
10
|
-
"bugs": {
|
|
11
|
-
"url": "https://github.com/jewel998/config/issues"
|
|
12
|
-
},
|
|
13
6
|
"keywords": [
|
|
7
|
+
"a-b-testing",
|
|
8
|
+
"configuration",
|
|
14
9
|
"feature-flags",
|
|
15
|
-
"
|
|
10
|
+
"feature-management",
|
|
16
11
|
"feature-toggles",
|
|
17
|
-
"self-hosted",
|
|
18
12
|
"firebase",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
13
|
+
"free",
|
|
14
|
+
"launchdarkly-alternative",
|
|
15
|
+
"open-source",
|
|
16
|
+
"percentage-rollout",
|
|
17
|
+
"remote-config",
|
|
22
18
|
"rollout",
|
|
23
|
-
"
|
|
19
|
+
"sdk",
|
|
20
|
+
"segments",
|
|
21
|
+
"self-hosted",
|
|
22
|
+
"server-side-evaluation",
|
|
23
|
+
"targeting"
|
|
24
24
|
],
|
|
25
|
+
"homepage": "https://github.com/jewel998/config#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/jewel998/config/issues"
|
|
28
|
+
},
|
|
29
|
+
"license": "Elastic-2.0",
|
|
30
|
+
"author": "Jyotirmoy Barman",
|
|
25
31
|
"repository": {
|
|
26
32
|
"type": "git",
|
|
27
33
|
"url": "git+https://github.com/jewel998/config.git",
|
|
28
34
|
"directory": "packages/config"
|
|
29
35
|
},
|
|
30
|
-
"
|
|
36
|
+
"files": [
|
|
37
|
+
"dist/**/*.js",
|
|
38
|
+
"dist/**/*.d.ts",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"type": "module",
|
|
43
|
+
"sideEffects": false,
|
|
31
44
|
"main": "./dist/index.js",
|
|
32
45
|
"types": "./dist/index.d.ts",
|
|
33
46
|
"exports": {
|
|
@@ -35,6 +48,10 @@
|
|
|
35
48
|
"types": "./dist/index.d.ts",
|
|
36
49
|
"import": "./dist/index.js"
|
|
37
50
|
},
|
|
51
|
+
"./server": {
|
|
52
|
+
"types": "./dist/server.d.ts",
|
|
53
|
+
"import": "./dist/server.js"
|
|
54
|
+
},
|
|
38
55
|
"./storage": {
|
|
39
56
|
"types": "./dist/cache/memoryStorage.d.ts",
|
|
40
57
|
"import": "./dist/cache/memoryStorage.js"
|
|
@@ -68,15 +85,6 @@
|
|
|
68
85
|
"import": "./dist/plugins/overrides/index.js"
|
|
69
86
|
}
|
|
70
87
|
},
|
|
71
|
-
"files": [
|
|
72
|
-
"dist/**/*.js",
|
|
73
|
-
"dist/**/*.d.ts",
|
|
74
|
-
"README.md",
|
|
75
|
-
"LICENSE"
|
|
76
|
-
],
|
|
77
|
-
"engines": {
|
|
78
|
-
"node": ">=22.0.0"
|
|
79
|
-
},
|
|
80
88
|
"scripts": {
|
|
81
89
|
"build": "tsc -p tsconfig.json",
|
|
82
90
|
"dev": "tsc -p tsconfig.json --watch",
|
|
@@ -86,6 +94,9 @@
|
|
|
86
94
|
},
|
|
87
95
|
"devDependencies": {
|
|
88
96
|
"typescript": "^5.9.3",
|
|
89
|
-
"vitest": "^
|
|
97
|
+
"vitest": "^5.0.3"
|
|
98
|
+
},
|
|
99
|
+
"engines": {
|
|
100
|
+
"node": ">=22.0.0"
|
|
90
101
|
}
|
|
91
102
|
}
|