@lunora/flags 0.0.1 → 1.0.0-alpha.1
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/LICENSE.md +105 -0
- package/README.md +80 -28
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.mts +56 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.mjs +2 -0
- package/dist/packem_shared/createFlags-BQ8eiIlo.mjs +94 -0
- package/dist/packem_shared/defineFlags-DAIO8cWQ.mjs +15 -0
- package/dist/packem_shared/types.d-BWygZiSc.d.mts +85 -0
- package/dist/packem_shared/types.d-BWygZiSc.d.ts +85 -0
- package/dist/providers/env.d.mts +43 -0
- package/dist/providers/env.d.ts +43 -0
- package/dist/providers/env.mjs +75 -0
- package/dist/providers/flagship.d.mts +62 -0
- package/dist/providers/flagship.d.ts +62 -0
- package/dist/providers/flagship.mjs +20 -0
- package/dist/providers/memory.d.mts +37 -0
- package/dist/providers/memory.d.ts +37 -0
- package/dist/providers/memory.mjs +16 -0
- package/dist/web.d.mts +1 -0
- package/dist/web.d.ts +1 -0
- package/dist/web.mjs +1 -0
- package/package.json +77 -7
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { a as FlagsConfig, b as FlagsDefinition, L as LunoraFlags } from "./packem_shared/types.d-BWygZiSc.mjs";
|
|
2
|
+
export type { c as FlagsAuth, F as FlagsProviderFactory } from "./packem_shared/types.d-BWygZiSc.mjs";
|
|
3
|
+
import { Hook, Logger, Provider } from '@openfeature/server-sdk';
|
|
4
|
+
export type { EvaluationContext, EvaluationDetails, Hook, JsonValue, Logger, Provider } from '@openfeature/server-sdk';
|
|
5
|
+
/**
|
|
6
|
+
* Declare the feature-flag provider for a Lunora app. Pure validation +
|
|
7
|
+
* branding — codegen discovers the default export of `lunora/flags.ts`, imports
|
|
8
|
+
* it into the generated worker, and wires `ctx.flags` from `provider` /
|
|
9
|
+
* `identify` (mirrors how `defineQueue` / `defineWorkflow` feed codegen).
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* // lunora/flags.ts
|
|
13
|
+
* import { defineFlags } from "@lunora/flags";
|
|
14
|
+
* import { flagshipProvider } from "@lunora/flags/providers/flagship";
|
|
15
|
+
*
|
|
16
|
+
* export default defineFlags({
|
|
17
|
+
* provider: flagshipProvider({ binding: "FLAGS" }), // Cloudflare Flagship (binding mode)
|
|
18
|
+
* identify: (auth) => auth.userId ?? undefined, // default targetingKey
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Any OpenFeature provider works — Flagship is just the first-class default:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* export default defineFlags({ provider: (env) => new SomeOpenFeatureProvider(env.SOME_KEY) });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
declare const defineFlags: (config: FlagsConfig) => FlagsDefinition;
|
|
29
|
+
/** True when a value is a {@link defineFlags} result (the runtime brand check). */
|
|
30
|
+
declare const isFlagsDefinition: (value: unknown) => value is FlagsDefinition;
|
|
31
|
+
/** Options for `createFlags`. Built by codegen from `defineFlags(...)`. */
|
|
32
|
+
interface CreateFlagsOptions {
|
|
33
|
+
/** OpenFeature hooks applied when the provider is bound. */
|
|
34
|
+
hooks?: Hook[];
|
|
35
|
+
/** Logger for the OpenFeature client. */
|
|
36
|
+
logger?: Logger;
|
|
37
|
+
/** Lazily constructs the OpenFeature provider from `env` (invoked once per isolate). */
|
|
38
|
+
provider: () => Provider;
|
|
39
|
+
/**
|
|
40
|
+
* Default `targetingKey` for every evaluation (from `defineFlags({ identify })`).
|
|
41
|
+
* May be a thunk — codegen passes one wrapping the user's `identify` — so a
|
|
42
|
+
* throwing `identify` fails open to no `targetingKey` rather than escaping
|
|
43
|
+
* ctx construction and taking down the whole request.
|
|
44
|
+
*/
|
|
45
|
+
targetingKey?: (() => string | undefined) | string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Builds the `ctx.flags` facade for a single request. Evaluations resolve
|
|
49
|
+
* through the OpenFeature client (the SDK applies hooks and never throws —
|
|
50
|
+
* provider errors surface as the default value with an `errorCode`). The default
|
|
51
|
+
* `targetingKey` is merged under any per-call context, and identical evaluations
|
|
52
|
+
* within the request are memoized so repeated reads of the same flag are
|
|
53
|
+
* internally consistent and hit the provider once.
|
|
54
|
+
*/
|
|
55
|
+
declare const createFlags: (options: CreateFlagsOptions) => LunoraFlags;
|
|
56
|
+
export { type CreateFlagsOptions, type FlagsConfig, type FlagsDefinition, type LunoraFlags, createFlags, defineFlags, isFlagsDefinition };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { a as FlagsConfig, b as FlagsDefinition, L as LunoraFlags } from "./packem_shared/types.d-BWygZiSc.js";
|
|
2
|
+
export type { c as FlagsAuth, F as FlagsProviderFactory } from "./packem_shared/types.d-BWygZiSc.js";
|
|
3
|
+
import { Hook, Logger, Provider } from '@openfeature/server-sdk';
|
|
4
|
+
export type { EvaluationContext, EvaluationDetails, Hook, JsonValue, Logger, Provider } from '@openfeature/server-sdk';
|
|
5
|
+
/**
|
|
6
|
+
* Declare the feature-flag provider for a Lunora app. Pure validation +
|
|
7
|
+
* branding — codegen discovers the default export of `lunora/flags.ts`, imports
|
|
8
|
+
* it into the generated worker, and wires `ctx.flags` from `provider` /
|
|
9
|
+
* `identify` (mirrors how `defineQueue` / `defineWorkflow` feed codegen).
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* // lunora/flags.ts
|
|
13
|
+
* import { defineFlags } from "@lunora/flags";
|
|
14
|
+
* import { flagshipProvider } from "@lunora/flags/providers/flagship";
|
|
15
|
+
*
|
|
16
|
+
* export default defineFlags({
|
|
17
|
+
* provider: flagshipProvider({ binding: "FLAGS" }), // Cloudflare Flagship (binding mode)
|
|
18
|
+
* identify: (auth) => auth.userId ?? undefined, // default targetingKey
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Any OpenFeature provider works — Flagship is just the first-class default:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* export default defineFlags({ provider: (env) => new SomeOpenFeatureProvider(env.SOME_KEY) });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
declare const defineFlags: (config: FlagsConfig) => FlagsDefinition;
|
|
29
|
+
/** True when a value is a {@link defineFlags} result (the runtime brand check). */
|
|
30
|
+
declare const isFlagsDefinition: (value: unknown) => value is FlagsDefinition;
|
|
31
|
+
/** Options for `createFlags`. Built by codegen from `defineFlags(...)`. */
|
|
32
|
+
interface CreateFlagsOptions {
|
|
33
|
+
/** OpenFeature hooks applied when the provider is bound. */
|
|
34
|
+
hooks?: Hook[];
|
|
35
|
+
/** Logger for the OpenFeature client. */
|
|
36
|
+
logger?: Logger;
|
|
37
|
+
/** Lazily constructs the OpenFeature provider from `env` (invoked once per isolate). */
|
|
38
|
+
provider: () => Provider;
|
|
39
|
+
/**
|
|
40
|
+
* Default `targetingKey` for every evaluation (from `defineFlags({ identify })`).
|
|
41
|
+
* May be a thunk — codegen passes one wrapping the user's `identify` — so a
|
|
42
|
+
* throwing `identify` fails open to no `targetingKey` rather than escaping
|
|
43
|
+
* ctx construction and taking down the whole request.
|
|
44
|
+
*/
|
|
45
|
+
targetingKey?: (() => string | undefined) | string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Builds the `ctx.flags` facade for a single request. Evaluations resolve
|
|
49
|
+
* through the OpenFeature client (the SDK applies hooks and never throws —
|
|
50
|
+
* provider errors surface as the default value with an `errorCode`). The default
|
|
51
|
+
* `targetingKey` is merged under any per-call context, and identical evaluations
|
|
52
|
+
* within the request are memoized so repeated reads of the same flag are
|
|
53
|
+
* internally consistent and hit the provider once.
|
|
54
|
+
*/
|
|
55
|
+
declare const createFlags: (options: CreateFlagsOptions) => LunoraFlags;
|
|
56
|
+
export { type CreateFlagsOptions, type FlagsConfig, type FlagsDefinition, type LunoraFlags, createFlags, defineFlags, isFlagsDefinition };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { OpenFeature, ErrorCode } from '@openfeature/server-sdk';
|
|
2
|
+
|
|
3
|
+
const DOMAIN = "lunora";
|
|
4
|
+
let clientBinding;
|
|
5
|
+
const bindClient = ({ hooks, logger, provider }) => {
|
|
6
|
+
if (clientBinding === void 0) {
|
|
7
|
+
clientBinding = (async () => {
|
|
8
|
+
await OpenFeature.setProviderAndWait(DOMAIN, provider());
|
|
9
|
+
const client = OpenFeature.getClient(DOMAIN);
|
|
10
|
+
if (logger) {
|
|
11
|
+
client.setLogger(logger);
|
|
12
|
+
}
|
|
13
|
+
if (hooks && hooks.length > 0) {
|
|
14
|
+
client.addHooks(...hooks);
|
|
15
|
+
}
|
|
16
|
+
return client;
|
|
17
|
+
})();
|
|
18
|
+
clientBinding.catch(() => {
|
|
19
|
+
clientBinding = void 0;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return clientBinding;
|
|
23
|
+
};
|
|
24
|
+
const resetFlags = async () => {
|
|
25
|
+
clientBinding = void 0;
|
|
26
|
+
await OpenFeature.clearProviders();
|
|
27
|
+
};
|
|
28
|
+
const memoKey = (type, flagKey, defaultValue, context) => {
|
|
29
|
+
const entries = Object.keys(context).toSorted((a, b) => a.localeCompare(b)).map((name) => [name, context[name]]);
|
|
30
|
+
return JSON.stringify([type, flagKey, defaultValue, entries]);
|
|
31
|
+
};
|
|
32
|
+
const resolveDetails = (client, type, flagKey, defaultValue, context) => {
|
|
33
|
+
switch (type) {
|
|
34
|
+
case "boolean": {
|
|
35
|
+
return client.getBooleanDetails(flagKey, defaultValue, context);
|
|
36
|
+
}
|
|
37
|
+
case "number": {
|
|
38
|
+
return client.getNumberDetails(flagKey, defaultValue, context);
|
|
39
|
+
}
|
|
40
|
+
case "object": {
|
|
41
|
+
return client.getObjectDetails(flagKey, defaultValue, context);
|
|
42
|
+
}
|
|
43
|
+
case "string": {
|
|
44
|
+
return client.getStringDetails(flagKey, defaultValue, context);
|
|
45
|
+
}
|
|
46
|
+
default: {
|
|
47
|
+
throw new Error(`createFlags: unknown flag type "${type}"`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const createFlags = (options) => {
|
|
52
|
+
const { hooks, logger, provider, targetingKey } = options;
|
|
53
|
+
let resolvedTargetingKey;
|
|
54
|
+
try {
|
|
55
|
+
resolvedTargetingKey = typeof targetingKey === "function" ? targetingKey() : targetingKey;
|
|
56
|
+
} catch {
|
|
57
|
+
resolvedTargetingKey = void 0;
|
|
58
|
+
}
|
|
59
|
+
const memo = /* @__PURE__ */ new Map();
|
|
60
|
+
const evaluate = (type, flagKey, defaultValue, context) => {
|
|
61
|
+
const merged = resolvedTargetingKey === void 0 ? { ...context } : { targetingKey: resolvedTargetingKey, ...context };
|
|
62
|
+
const key = memoKey(type, flagKey, defaultValue, merged);
|
|
63
|
+
const cached = memo.get(key);
|
|
64
|
+
if (cached) {
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
67
|
+
const pending = bindClient({ hooks, logger, provider }).then((client) => resolveDetails(client, type, flagKey, defaultValue, merged)).catch((error) => {
|
|
68
|
+
return {
|
|
69
|
+
errorCode: ErrorCode.GENERAL,
|
|
70
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
71
|
+
flagKey,
|
|
72
|
+
flagMetadata: {},
|
|
73
|
+
reason: "ERROR",
|
|
74
|
+
value: defaultValue
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
memo.set(key, pending);
|
|
78
|
+
return pending;
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
boolean: (flagKey, defaultValue, context) => evaluate("boolean", flagKey, defaultValue, context).then((details) => details.value),
|
|
82
|
+
details: {
|
|
83
|
+
boolean: (flagKey, defaultValue, context) => evaluate("boolean", flagKey, defaultValue, context),
|
|
84
|
+
number: (flagKey, defaultValue, context) => evaluate("number", flagKey, defaultValue, context),
|
|
85
|
+
object: (flagKey, defaultValue, context) => evaluate("object", flagKey, defaultValue, context),
|
|
86
|
+
string: (flagKey, defaultValue, context) => evaluate("string", flagKey, defaultValue, context)
|
|
87
|
+
},
|
|
88
|
+
number: (flagKey, defaultValue, context) => evaluate("number", flagKey, defaultValue, context).then((details) => details.value),
|
|
89
|
+
object: (flagKey, defaultValue, context) => evaluate("object", flagKey, defaultValue, context).then((details) => details.value),
|
|
90
|
+
string: (flagKey, defaultValue, context) => evaluate("string", flagKey, defaultValue, context).then((details) => details.value)
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export { createFlags, resetFlags };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const defineFlags = (config) => {
|
|
2
|
+
if (typeof config.provider !== "function") {
|
|
3
|
+
throw new TypeError('defineFlags: `provider` must be a function `(env) => Provider` (e.g. flagshipProvider({ binding: "FLAGS" }))');
|
|
4
|
+
}
|
|
5
|
+
if (config.identify !== void 0 && typeof config.identify !== "function") {
|
|
6
|
+
throw new TypeError("defineFlags: `identify` must be a function `(auth) => string | undefined` when provided");
|
|
7
|
+
}
|
|
8
|
+
if (config.hooks !== void 0 && !Array.isArray(config.hooks)) {
|
|
9
|
+
throw new TypeError("defineFlags: `hooks` must be an array of OpenFeature hooks when provided");
|
|
10
|
+
}
|
|
11
|
+
return { ...config, isLunoraFlags: true };
|
|
12
|
+
};
|
|
13
|
+
const isFlagsDefinition = (value) => typeof value === "object" && value !== null && value.isLunoraFlags === true;
|
|
14
|
+
|
|
15
|
+
export { defineFlags, isFlagsDefinition };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Hook, Logger, Provider, EvaluationContext, EvaluationDetails, JsonValue } from '@openfeature/server-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* The flag-evaluation surface spliced onto every Lunora `ctx` as `ctx.flags`
|
|
4
|
+
* (query / mutation / action) by codegen when an app wires `@lunora/flags`.
|
|
5
|
+
*
|
|
6
|
+
* Every method resolves through the configured OpenFeature provider and **never
|
|
7
|
+
* throws** — on a missing flag, type mismatch, provider error, or
|
|
8
|
+
* misconfiguration it resolves with the supplied `defaultValue`. Use the
|
|
9
|
+
* `details.*` variants when you need the full {@link EvaluationDetails} (reason,
|
|
10
|
+
* variant, `errorCode`, `errorMessage`) alongside the value.
|
|
11
|
+
*
|
|
12
|
+
* The optional per-call `context` is merged on top of the default targeting
|
|
13
|
+
* context (the `targetingKey` derived from `defineFlags({ identify })`), so a
|
|
14
|
+
* call can add or override targeting attributes for that single evaluation.
|
|
15
|
+
*/
|
|
16
|
+
interface LunoraFlags {
|
|
17
|
+
/** Resolve a boolean flag (feature on/off). */
|
|
18
|
+
boolean: (flagKey: string, defaultValue: boolean, context?: EvaluationContext) => Promise<boolean>;
|
|
19
|
+
/** Full {@link EvaluationDetails} variants — value plus reason / variant / error metadata. */
|
|
20
|
+
details: {
|
|
21
|
+
boolean: (flagKey: string, defaultValue: boolean, context?: EvaluationContext) => Promise<EvaluationDetails<boolean>>;
|
|
22
|
+
number: (flagKey: string, defaultValue: number, context?: EvaluationContext) => Promise<EvaluationDetails<number>>;
|
|
23
|
+
object: <T extends JsonValue>(flagKey: string, defaultValue: T, context?: EvaluationContext) => Promise<EvaluationDetails<T>>;
|
|
24
|
+
string: (flagKey: string, defaultValue: string, context?: EvaluationContext) => Promise<EvaluationDetails<string>>;
|
|
25
|
+
};
|
|
26
|
+
/** Resolve a number flag (rate limits, thresholds, percentages). */
|
|
27
|
+
number: (flagKey: string, defaultValue: number, context?: EvaluationContext) => Promise<number>;
|
|
28
|
+
/** Resolve an object/JSON flag (complex configuration). */
|
|
29
|
+
object: <T extends JsonValue>(flagKey: string, defaultValue: T, context?: EvaluationContext) => Promise<T>;
|
|
30
|
+
/** Resolve a string flag (A/B variants, copy experiments). */
|
|
31
|
+
string: (flagKey: string, defaultValue: string, context?: EvaluationContext) => Promise<string>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The resolved request auth handed to {@link FlagsConfig.identify} so an app can
|
|
35
|
+
* derive the default OpenFeature `targetingKey` (usually the authenticated user
|
|
36
|
+
* id). This mirrors the `auth` shape the runtime already threads into other ctx
|
|
37
|
+
* helpers, so `identify` never needs the full ctx.
|
|
38
|
+
*/
|
|
39
|
+
interface FlagsAuth {
|
|
40
|
+
/** The verified identity claims for the request, or `null` when anonymous. */
|
|
41
|
+
identity: Record<string, unknown> | null;
|
|
42
|
+
/** The authenticated user id for the request, or `null` when anonymous. */
|
|
43
|
+
userId: string | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Builds the OpenFeature {@link Provider} for the request from the Worker `env`.
|
|
47
|
+
*
|
|
48
|
+
* Receiving `env` (rather than a constructed provider) is what lets the Flagship
|
|
49
|
+
* binding provider read `env.FLAGS` at request time — matching the existing
|
|
50
|
+
* `config.ai?.(env)` thunk pattern. Construction is memoized per DO isolate, so
|
|
51
|
+
* this factory is invoked once per isolate, not once per request.
|
|
52
|
+
*/
|
|
53
|
+
type FlagsProviderFactory = (env: Record<string, unknown>) => Provider;
|
|
54
|
+
/** Options accepted by `defineFlags`. */
|
|
55
|
+
interface FlagsConfig {
|
|
56
|
+
/**
|
|
57
|
+
* OpenFeature hooks run on every evaluation (logging, telemetry, …). Applied
|
|
58
|
+
* to the Lunora flags client when the provider is bound.
|
|
59
|
+
*/
|
|
60
|
+
hooks?: Hook[];
|
|
61
|
+
/**
|
|
62
|
+
* Derives the default OpenFeature `targetingKey` from the request auth. Most
|
|
63
|
+
* apps return the user id: `identify: (auth) => auth.userId ?? undefined`.
|
|
64
|
+
* A per-call `context.targetingKey` still overrides this.
|
|
65
|
+
*/
|
|
66
|
+
identify?: (auth: FlagsAuth) => string | undefined;
|
|
67
|
+
/** Logger for the OpenFeature client (provider + SDK diagnostics). */
|
|
68
|
+
logger?: Logger;
|
|
69
|
+
/**
|
|
70
|
+
* The OpenFeature provider factory. Use `flagshipProvider(...)` from
|
|
71
|
+
* `@lunora/flags/providers/flagship`, or return any OpenFeature provider:
|
|
72
|
+
* `provider: (env) => new LaunchDarklyProvider(env.LD_KEY)`.
|
|
73
|
+
*/
|
|
74
|
+
provider: FlagsProviderFactory;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A branded {@link FlagsConfig} produced by `defineFlags`. This is the
|
|
78
|
+
* default export of `lunora/flags.ts`; codegen imports it into the generated
|
|
79
|
+
* worker and wires `ctx.flags` from its `provider` / `identify`.
|
|
80
|
+
*/
|
|
81
|
+
interface FlagsDefinition extends FlagsConfig {
|
|
82
|
+
/** Runtime brand used by `isFlagsDefinition` and codegen discovery. */
|
|
83
|
+
readonly isLunoraFlags: true;
|
|
84
|
+
}
|
|
85
|
+
export { FlagsProviderFactory as F, LunoraFlags as L, FlagsConfig as a, FlagsDefinition as b, FlagsAuth as c };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Hook, Logger, Provider, EvaluationContext, EvaluationDetails, JsonValue } from '@openfeature/server-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* The flag-evaluation surface spliced onto every Lunora `ctx` as `ctx.flags`
|
|
4
|
+
* (query / mutation / action) by codegen when an app wires `@lunora/flags`.
|
|
5
|
+
*
|
|
6
|
+
* Every method resolves through the configured OpenFeature provider and **never
|
|
7
|
+
* throws** — on a missing flag, type mismatch, provider error, or
|
|
8
|
+
* misconfiguration it resolves with the supplied `defaultValue`. Use the
|
|
9
|
+
* `details.*` variants when you need the full {@link EvaluationDetails} (reason,
|
|
10
|
+
* variant, `errorCode`, `errorMessage`) alongside the value.
|
|
11
|
+
*
|
|
12
|
+
* The optional per-call `context` is merged on top of the default targeting
|
|
13
|
+
* context (the `targetingKey` derived from `defineFlags({ identify })`), so a
|
|
14
|
+
* call can add or override targeting attributes for that single evaluation.
|
|
15
|
+
*/
|
|
16
|
+
interface LunoraFlags {
|
|
17
|
+
/** Resolve a boolean flag (feature on/off). */
|
|
18
|
+
boolean: (flagKey: string, defaultValue: boolean, context?: EvaluationContext) => Promise<boolean>;
|
|
19
|
+
/** Full {@link EvaluationDetails} variants — value plus reason / variant / error metadata. */
|
|
20
|
+
details: {
|
|
21
|
+
boolean: (flagKey: string, defaultValue: boolean, context?: EvaluationContext) => Promise<EvaluationDetails<boolean>>;
|
|
22
|
+
number: (flagKey: string, defaultValue: number, context?: EvaluationContext) => Promise<EvaluationDetails<number>>;
|
|
23
|
+
object: <T extends JsonValue>(flagKey: string, defaultValue: T, context?: EvaluationContext) => Promise<EvaluationDetails<T>>;
|
|
24
|
+
string: (flagKey: string, defaultValue: string, context?: EvaluationContext) => Promise<EvaluationDetails<string>>;
|
|
25
|
+
};
|
|
26
|
+
/** Resolve a number flag (rate limits, thresholds, percentages). */
|
|
27
|
+
number: (flagKey: string, defaultValue: number, context?: EvaluationContext) => Promise<number>;
|
|
28
|
+
/** Resolve an object/JSON flag (complex configuration). */
|
|
29
|
+
object: <T extends JsonValue>(flagKey: string, defaultValue: T, context?: EvaluationContext) => Promise<T>;
|
|
30
|
+
/** Resolve a string flag (A/B variants, copy experiments). */
|
|
31
|
+
string: (flagKey: string, defaultValue: string, context?: EvaluationContext) => Promise<string>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The resolved request auth handed to {@link FlagsConfig.identify} so an app can
|
|
35
|
+
* derive the default OpenFeature `targetingKey` (usually the authenticated user
|
|
36
|
+
* id). This mirrors the `auth` shape the runtime already threads into other ctx
|
|
37
|
+
* helpers, so `identify` never needs the full ctx.
|
|
38
|
+
*/
|
|
39
|
+
interface FlagsAuth {
|
|
40
|
+
/** The verified identity claims for the request, or `null` when anonymous. */
|
|
41
|
+
identity: Record<string, unknown> | null;
|
|
42
|
+
/** The authenticated user id for the request, or `null` when anonymous. */
|
|
43
|
+
userId: string | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Builds the OpenFeature {@link Provider} for the request from the Worker `env`.
|
|
47
|
+
*
|
|
48
|
+
* Receiving `env` (rather than a constructed provider) is what lets the Flagship
|
|
49
|
+
* binding provider read `env.FLAGS` at request time — matching the existing
|
|
50
|
+
* `config.ai?.(env)` thunk pattern. Construction is memoized per DO isolate, so
|
|
51
|
+
* this factory is invoked once per isolate, not once per request.
|
|
52
|
+
*/
|
|
53
|
+
type FlagsProviderFactory = (env: Record<string, unknown>) => Provider;
|
|
54
|
+
/** Options accepted by `defineFlags`. */
|
|
55
|
+
interface FlagsConfig {
|
|
56
|
+
/**
|
|
57
|
+
* OpenFeature hooks run on every evaluation (logging, telemetry, …). Applied
|
|
58
|
+
* to the Lunora flags client when the provider is bound.
|
|
59
|
+
*/
|
|
60
|
+
hooks?: Hook[];
|
|
61
|
+
/**
|
|
62
|
+
* Derives the default OpenFeature `targetingKey` from the request auth. Most
|
|
63
|
+
* apps return the user id: `identify: (auth) => auth.userId ?? undefined`.
|
|
64
|
+
* A per-call `context.targetingKey` still overrides this.
|
|
65
|
+
*/
|
|
66
|
+
identify?: (auth: FlagsAuth) => string | undefined;
|
|
67
|
+
/** Logger for the OpenFeature client (provider + SDK diagnostics). */
|
|
68
|
+
logger?: Logger;
|
|
69
|
+
/**
|
|
70
|
+
* The OpenFeature provider factory. Use `flagshipProvider(...)` from
|
|
71
|
+
* `@lunora/flags/providers/flagship`, or return any OpenFeature provider:
|
|
72
|
+
* `provider: (env) => new LaunchDarklyProvider(env.LD_KEY)`.
|
|
73
|
+
*/
|
|
74
|
+
provider: FlagsProviderFactory;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A branded {@link FlagsConfig} produced by `defineFlags`. This is the
|
|
78
|
+
* default export of `lunora/flags.ts`; codegen imports it into the generated
|
|
79
|
+
* worker and wires `ctx.flags` from its `provider` / `identify`.
|
|
80
|
+
*/
|
|
81
|
+
interface FlagsDefinition extends FlagsConfig {
|
|
82
|
+
/** Runtime brand used by `isFlagsDefinition` and codegen discovery. */
|
|
83
|
+
readonly isLunoraFlags: true;
|
|
84
|
+
}
|
|
85
|
+
export { FlagsProviderFactory as F, LunoraFlags as L, FlagsConfig as a, FlagsDefinition as b, FlagsAuth as c };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { F as FlagsProviderFactory } from "../packem_shared/types.d-BWygZiSc.mjs";
|
|
2
|
+
import '@openfeature/server-sdk';
|
|
3
|
+
/** Options for {@link envProvider}. */
|
|
4
|
+
interface EnvProviderOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Map a flag key to its Worker `env` variable name. Defaults to
|
|
7
|
+
* `prefix` + the key upper-snake-cased (`"dark-mode"` → `"FLAG_DARK_MODE"`).
|
|
8
|
+
* Provide this to override the whole derivation (the `prefix` is then unused).
|
|
9
|
+
*/
|
|
10
|
+
name?: (flagKey: string) => string;
|
|
11
|
+
/** Prefix prepended to the derived env variable name. Defaults to `"FLAG_"`. */
|
|
12
|
+
prefix?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A zero-dependency OpenFeature provider that reads flags from the Worker `env`
|
|
16
|
+
* (plain `vars` and Secrets Store / `.dev.vars` values). Each flag key maps to an
|
|
17
|
+
* env variable (default `"dark-mode"` → `env.FLAG_DARK_MODE`); the string value is
|
|
18
|
+
* coerced to the read's type:
|
|
19
|
+
*
|
|
20
|
+
* - **boolean** — `true`/`1`/`on`/`yes` → `true`, `false`/`0`/`off`/`no` → `false` (case-insensitive), anything else is a parse error.
|
|
21
|
+
* - **number** — `Number(value)`; non-numeric is a parse error.
|
|
22
|
+
* - **string** — the raw value.
|
|
23
|
+
* - **object** — `JSON.parse(value)`; invalid JSON is a parse error.
|
|
24
|
+
*
|
|
25
|
+
* A key with no matching env variable falls back to the call's default (reason
|
|
26
|
+
* `DEFAULT`); a parse error also returns the default (reason `ERROR`) so a
|
|
27
|
+
* malformed value degrades the read rather than throwing.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { defineFlags } from "@lunora/flags";
|
|
31
|
+
* import { envProvider } from "@lunora/flags/providers/env";
|
|
32
|
+
*
|
|
33
|
+
* // wrangler.jsonc: { "vars": { "FLAG_DARK_MODE": "true", "FLAG_PAGE_SIZE": "25" } }
|
|
34
|
+
* export default defineFlags({ provider: envProvider() });
|
|
35
|
+
* // ctx.flags.boolean("dark-mode", false) → true
|
|
36
|
+
* // ctx.flags.number("page-size", 10) → 25
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Values are static per deployment (env is fixed for the isolate's lifetime), so
|
|
40
|
+
* this suits build-time/per-environment toggles rather than live targeting.
|
|
41
|
+
*/
|
|
42
|
+
declare const envProvider: (options?: EnvProviderOptions) => FlagsProviderFactory;
|
|
43
|
+
export { type EnvProviderOptions, envProvider };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { F as FlagsProviderFactory } from "../packem_shared/types.d-BWygZiSc.js";
|
|
2
|
+
import '@openfeature/server-sdk';
|
|
3
|
+
/** Options for {@link envProvider}. */
|
|
4
|
+
interface EnvProviderOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Map a flag key to its Worker `env` variable name. Defaults to
|
|
7
|
+
* `prefix` + the key upper-snake-cased (`"dark-mode"` → `"FLAG_DARK_MODE"`).
|
|
8
|
+
* Provide this to override the whole derivation (the `prefix` is then unused).
|
|
9
|
+
*/
|
|
10
|
+
name?: (flagKey: string) => string;
|
|
11
|
+
/** Prefix prepended to the derived env variable name. Defaults to `"FLAG_"`. */
|
|
12
|
+
prefix?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A zero-dependency OpenFeature provider that reads flags from the Worker `env`
|
|
16
|
+
* (plain `vars` and Secrets Store / `.dev.vars` values). Each flag key maps to an
|
|
17
|
+
* env variable (default `"dark-mode"` → `env.FLAG_DARK_MODE`); the string value is
|
|
18
|
+
* coerced to the read's type:
|
|
19
|
+
*
|
|
20
|
+
* - **boolean** — `true`/`1`/`on`/`yes` → `true`, `false`/`0`/`off`/`no` → `false` (case-insensitive), anything else is a parse error.
|
|
21
|
+
* - **number** — `Number(value)`; non-numeric is a parse error.
|
|
22
|
+
* - **string** — the raw value.
|
|
23
|
+
* - **object** — `JSON.parse(value)`; invalid JSON is a parse error.
|
|
24
|
+
*
|
|
25
|
+
* A key with no matching env variable falls back to the call's default (reason
|
|
26
|
+
* `DEFAULT`); a parse error also returns the default (reason `ERROR`) so a
|
|
27
|
+
* malformed value degrades the read rather than throwing.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { defineFlags } from "@lunora/flags";
|
|
31
|
+
* import { envProvider } from "@lunora/flags/providers/env";
|
|
32
|
+
*
|
|
33
|
+
* // wrangler.jsonc: { "vars": { "FLAG_DARK_MODE": "true", "FLAG_PAGE_SIZE": "25" } }
|
|
34
|
+
* export default defineFlags({ provider: envProvider() });
|
|
35
|
+
* // ctx.flags.boolean("dark-mode", false) → true
|
|
36
|
+
* // ctx.flags.number("page-size", 10) → 25
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Values are static per deployment (env is fixed for the isolate's lifetime), so
|
|
40
|
+
* this suits build-time/per-environment toggles rather than live targeting.
|
|
41
|
+
*/
|
|
42
|
+
declare const envProvider: (options?: EnvProviderOptions) => FlagsProviderFactory;
|
|
43
|
+
export { type EnvProviderOptions, envProvider };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ErrorCode } from '@openfeature/server-sdk';
|
|
2
|
+
|
|
3
|
+
const upperSnake = (flagKey) => flagKey.replaceAll(/[^a-z0-9]+/gi, "_").toUpperCase();
|
|
4
|
+
const TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "on", "true", "yes"]);
|
|
5
|
+
const FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
6
|
+
const staticDetails = (value) => Promise.resolve({ reason: "STATIC", value });
|
|
7
|
+
const missing = (defaultValue) => Promise.resolve({ reason: "DEFAULT", value: defaultValue });
|
|
8
|
+
const parseError = (defaultValue, message) => Promise.resolve({ errorCode: ErrorCode.PARSE_ERROR, errorMessage: message, reason: "ERROR", value: defaultValue });
|
|
9
|
+
const envProvider = (options = {}) => {
|
|
10
|
+
const prefix = options.prefix ?? "FLAG_";
|
|
11
|
+
const nameOf = options.name ?? ((flagKey) => prefix + upperSnake(flagKey));
|
|
12
|
+
return (env) => {
|
|
13
|
+
const raw = (flagKey) => {
|
|
14
|
+
const value = env[nameOf(flagKey)];
|
|
15
|
+
if (value === void 0 || value === null) {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "string") {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
22
|
+
return String(value);
|
|
23
|
+
}
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
metadata: { name: "lunora-env" },
|
|
28
|
+
resolveBooleanEvaluation: (flagKey, defaultValue) => {
|
|
29
|
+
const value = raw(flagKey);
|
|
30
|
+
if (value === void 0) {
|
|
31
|
+
return missing(defaultValue);
|
|
32
|
+
}
|
|
33
|
+
const token = value.trim().toLowerCase();
|
|
34
|
+
if (TRUE_TOKENS.has(token)) {
|
|
35
|
+
return staticDetails(true);
|
|
36
|
+
}
|
|
37
|
+
if (FALSE_TOKENS.has(token)) {
|
|
38
|
+
return staticDetails(false);
|
|
39
|
+
}
|
|
40
|
+
return parseError(defaultValue, `env flag "${flagKey}" (${nameOf(flagKey)}) is not a boolean: "${value}"`);
|
|
41
|
+
},
|
|
42
|
+
resolveNumberEvaluation: (flagKey, defaultValue) => {
|
|
43
|
+
const value = raw(flagKey);
|
|
44
|
+
if (value === void 0) {
|
|
45
|
+
return missing(defaultValue);
|
|
46
|
+
}
|
|
47
|
+
const parsed = Number(value);
|
|
48
|
+
if (value.trim() === "" || Number.isNaN(parsed)) {
|
|
49
|
+
return parseError(defaultValue, `env flag "${flagKey}" (${nameOf(flagKey)}) is not a number: "${value}"`);
|
|
50
|
+
}
|
|
51
|
+
return staticDetails(parsed);
|
|
52
|
+
},
|
|
53
|
+
resolveObjectEvaluation: (flagKey, defaultValue) => {
|
|
54
|
+
const value = raw(flagKey);
|
|
55
|
+
if (value === void 0) {
|
|
56
|
+
return missing(defaultValue);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return staticDetails(JSON.parse(value));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return parseError(
|
|
62
|
+
defaultValue,
|
|
63
|
+
`env flag "${flagKey}" (${nameOf(flagKey)}) is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
resolveStringEvaluation: (flagKey, defaultValue) => {
|
|
68
|
+
const value = raw(flagKey);
|
|
69
|
+
return value === void 0 ? missing(defaultValue) : staticDetails(value);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export { envProvider };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { F as FlagsProviderFactory } from "../packem_shared/types.d-BWygZiSc.mjs";
|
|
2
|
+
import '@openfeature/server-sdk';
|
|
3
|
+
/**
|
|
4
|
+
* Flagship provider in **binding mode** (recommended for Workers): evaluations
|
|
5
|
+
* go through the wrangler binding — no HTTP, no auth token.
|
|
6
|
+
*/
|
|
7
|
+
interface FlagshipBindingOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The name of the Flagship binding on the Worker `env` (e.g. `"FLAGS"`). The
|
|
10
|
+
* factory resolves `env[binding]` at request time. Configure a matching
|
|
11
|
+
* `flagship` binding in `wrangler.jsonc` with this binding name and your app id.
|
|
12
|
+
*/
|
|
13
|
+
binding: string;
|
|
14
|
+
/** Max cached entries when `cacheTtl` is set (default 1000). */
|
|
15
|
+
cacheMaxSize?: number;
|
|
16
|
+
/** Opt-in per-context TTL cache, in ms. Enables caching when greater than 0. */
|
|
17
|
+
cacheTtl?: number;
|
|
18
|
+
/** Surface Flagship SDK logs (default false). */
|
|
19
|
+
logging?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Flagship provider in **HTTP mode** (non-binding Workers or other server
|
|
23
|
+
* runtimes): evaluations go to the Flagship API over HTTP.
|
|
24
|
+
*/
|
|
25
|
+
interface FlagshipHttpOptions {
|
|
26
|
+
/** Account id for multi-tenant routing (required with `appId`). */
|
|
27
|
+
accountId?: string;
|
|
28
|
+
/** Flagship app id; the SDK builds the evaluation URL (mutually exclusive with `endpoint`). */
|
|
29
|
+
appId?: string;
|
|
30
|
+
/** Bearer token added as an `Authorization: Bearer` header to every request. */
|
|
31
|
+
authToken?: string;
|
|
32
|
+
/** Base URL override (only used with `appId`). */
|
|
33
|
+
baseUrl?: string;
|
|
34
|
+
cacheMaxSize?: number;
|
|
35
|
+
cacheTtl?: number;
|
|
36
|
+
/** Full evaluation URL (mutually exclusive with `appId`). */
|
|
37
|
+
endpoint?: string;
|
|
38
|
+
logging?: boolean;
|
|
39
|
+
/** Retry attempts on transient errors (default 1, max 10). */
|
|
40
|
+
retries?: number;
|
|
41
|
+
/** Delay between retries in ms (default 1000, max 30000). */
|
|
42
|
+
retryDelay?: number;
|
|
43
|
+
/** Request timeout in ms (default 5000). */
|
|
44
|
+
timeout?: number;
|
|
45
|
+
}
|
|
46
|
+
/** Options for `flagshipProvider` — binding mode or HTTP mode. */
|
|
47
|
+
type FlagshipProviderOptions = FlagshipBindingOptions | FlagshipHttpOptions;
|
|
48
|
+
/**
|
|
49
|
+
* Builds a Cloudflare Flagship OpenFeature provider for `defineFlags({ provider })`.
|
|
50
|
+
* Flagship is Lunora's first-class default; the same `defineFlags` accepts any
|
|
51
|
+
* OpenFeature provider, so apps can swap it out without touching call sites.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* // Binding mode (recommended) — reads env.FLAGS at request time:
|
|
55
|
+
* flagshipProvider({ binding: "FLAGS" })
|
|
56
|
+
*
|
|
57
|
+
* // HTTP mode:
|
|
58
|
+
* flagshipProvider({ appId: "app-abc", accountId: "acct", authToken: "tok" })
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
declare const flagshipProvider: (options: FlagshipProviderOptions) => FlagsProviderFactory;
|
|
62
|
+
export { type FlagshipBindingOptions, type FlagshipHttpOptions, type FlagshipProviderOptions, flagshipProvider };
|