@cqrs-ddd/pipeline-feature-flags 0.0.0-stage → 0.5.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.
Files changed (44) hide show
  1. package/COMMERCIAL_LICENSE.txt +34 -0
  2. package/LICENSE +661 -0
  3. package/README.md +35 -2
  4. package/dist/errors/feature-disabled.error.d.ts +15 -0
  5. package/dist/errors/feature-disabled.error.d.ts.map +1 -0
  6. package/dist/errors/feature-disabled.error.js +21 -0
  7. package/dist/errors/feature-disabled.error.js.map +1 -0
  8. package/dist/errors/feature-flag-evaluation.error.d.ts +17 -0
  9. package/dist/errors/feature-flag-evaluation.error.d.ts.map +1 -0
  10. package/dist/errors/feature-flag-evaluation.error.js +24 -0
  11. package/dist/errors/feature-flag-evaluation.error.js.map +1 -0
  12. package/dist/feature-flag.behavior.d.ts +148 -0
  13. package/dist/feature-flag.behavior.d.ts.map +1 -0
  14. package/dist/feature-flag.behavior.js +261 -0
  15. package/dist/feature-flag.behavior.js.map +1 -0
  16. package/dist/helpers/build-attributes.d.ts +48 -0
  17. package/dist/helpers/build-attributes.d.ts.map +1 -0
  18. package/dist/helpers/build-attributes.js +63 -0
  19. package/dist/helpers/build-attributes.js.map +1 -0
  20. package/dist/helpers/evaluation-context.d.ts +17 -0
  21. package/dist/helpers/evaluation-context.d.ts.map +1 -0
  22. package/dist/helpers/evaluation-context.js +33 -0
  23. package/dist/helpers/evaluation-context.js.map +1 -0
  24. package/dist/helpers/feature-flag-client.d.ts +27 -0
  25. package/dist/helpers/feature-flag-client.d.ts.map +1 -0
  26. package/dist/helpers/feature-flag-client.js +57 -0
  27. package/dist/helpers/feature-flag-client.js.map +1 -0
  28. package/dist/helpers/feature-flag.intent.d.ts +22 -0
  29. package/dist/helpers/feature-flag.intent.d.ts.map +1 -0
  30. package/dist/helpers/feature-flag.intent.js +20 -0
  31. package/dist/helpers/feature-flag.intent.js.map +1 -0
  32. package/dist/http.d.ts +21 -0
  33. package/dist/http.d.ts.map +1 -0
  34. package/dist/http.js +31 -0
  35. package/dist/http.js.map +1 -0
  36. package/dist/index.d.ts +14 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +14 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/interfaces/feature-flags-options.interface.d.ts +154 -0
  41. package/dist/interfaces/feature-flags-options.interface.d.ts.map +1 -0
  42. package/dist/interfaces/feature-flags-options.interface.js +3 -0
  43. package/dist/interfaces/feature-flags-options.interface.js.map +1 -0
  44. package/package.json +67 -3
package/README.md CHANGED
@@ -1,3 +1,36 @@
1
- # Temporary Holding Version
1
+ # @cqrs-ddd/pipeline-feature-flags
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Feature flags for `@cqrs-ddd/pipeline` through [OpenFeature](https://openfeature.dev): an
4
+ operation runs only while its boolean flag is on, with any OpenFeature provider
5
+ (Unleash, Flagsmith, LaunchDarkly, a file). `createFeatureFlagClient()` registers the
6
+ provider and resolves the client; `toHttpResponse()` from
7
+ `@cqrs-ddd/pipeline-feature-flags/http` turns a disabled feature into a 403, or a 404 that
8
+ hides it.
9
+
10
+ **Documentation:** [guide](https://aristoteliss.github.io/ddd-cqrs/packages/pipeline-feature-flags/) · [API reference](https://aristoteliss.github.io/ddd-cqrs/api/cqrs-ddd/pipeline-feature-flags/) · [all packages](https://aristoteliss.github.io/ddd-cqrs/)
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pnpm add @cqrs-ddd/pipeline-feature-flags @cqrs-ddd/pipeline @openfeature/server-sdk
16
+ ```
17
+
18
+ Requires Node.js 22.12 or later. Published as an ES module; a CommonJS application loads
19
+ it with `require()`.
20
+
21
+ ## Example
22
+
23
+ ```ts
24
+ const client = await createFeatureFlagClient({ provider: new UnleashProvider(config) });
25
+ const pipeline = createPipeline({ behaviors: [new FeatureFlagBehavior(client)] });
26
+
27
+ export const checkout = pipeline.wrap(
28
+ { name: 'checkout', kind: 'command' },
29
+ featureFlag({ flag: 'new-checkout' }),
30
+ )(async (cart: Cart) => orders.place(cart));
31
+ ```
32
+
33
+ ## License
34
+
35
+ Dual-licensed under **AGPLv3** and a **Commercial License**. See `LICENSE` and
36
+ `COMMERCIAL_LICENSE.txt`.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Thrown by {@link FeatureFlagBehavior} when a handler is gated behind a flag
3
+ * that resolves to disabled and no `fallback` was configured.
4
+ *
5
+ * Over HTTP, register {@link FeatureDisabledFilter}: it answers `403 Forbidden`
6
+ * with the flag, or a plain `404 Not Found` to hide the feature.
7
+ */
8
+ export declare class FeatureDisabledError extends Error {
9
+ /** The flag key that gated the request. */
10
+ readonly flag: string;
11
+ /** The request that was blocked, e.g. `CreateUserCommand`. */
12
+ readonly requestName: string;
13
+ constructor(flag: string, requestName: string);
14
+ }
15
+ //# sourceMappingURL=feature-disabled.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-disabled.error.d.ts","sourceRoot":"","sources":["../../src/errors/feature-disabled.error.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,YAAY,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAK5C;CACF"}
@@ -0,0 +1,21 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ /**
3
+ * Thrown by {@link FeatureFlagBehavior} when a handler is gated behind a flag
4
+ * that resolves to disabled and no `fallback` was configured.
5
+ *
6
+ * Over HTTP, register {@link FeatureDisabledFilter}: it answers `403 Forbidden`
7
+ * with the flag, or a plain `404 Not Found` to hide the feature.
8
+ */
9
+ export class FeatureDisabledError extends Error {
10
+ /** The flag key that gated the request. */
11
+ flag;
12
+ /** The request that was blocked, e.g. `CreateUserCommand`. */
13
+ requestName;
14
+ constructor(flag, requestName) {
15
+ super(`Feature "${flag}" is disabled for ${requestName}`);
16
+ this.name = 'FeatureDisabledError';
17
+ this.flag = flag;
18
+ this.requestName = requestName;
19
+ }
20
+ }
21
+ //# sourceMappingURL=feature-disabled.error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-disabled.error.js","sourceRoot":"","sources":["../../src/errors/feature-disabled.error.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE;;;;;;GAMG;AACH,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,2CAA2C;IAClC,IAAI,CAAS;IACtB,8DAA8D;IACrD,WAAW,CAAS;IAE7B,YAAY,IAAY,EAAE,WAAmB;QAC3C,KAAK,CAAC,YAAY,IAAI,qBAAqB,WAAW,EAAE,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;CACF"}
@@ -0,0 +1,17 @@
1
+ /** Raised when feature evaluation is configured to surface provider failures. */
2
+ export declare class FeatureFlagEvaluationError extends Error {
3
+ readonly flag: string;
4
+ readonly requestName: string;
5
+ readonly errorCode?: string | undefined;
6
+ readonly providerMessage?: string | undefined;
7
+ readonly name = "FeatureFlagEvaluationError";
8
+ constructor(flag: string, requestName: string, errorCode?: string | undefined, providerMessage?: string | undefined,
9
+ /**
10
+ * The provider error, when one was thrown. It becomes the error's `cause`,
11
+ * which keeps its stack and any provider-specific fields.
12
+ */
13
+ options?: {
14
+ cause?: unknown;
15
+ });
16
+ }
17
+ //# sourceMappingURL=feature-flag-evaluation.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-flag-evaluation.error.d.ts","sourceRoot":"","sources":["../../src/errors/feature-flag-evaluation.error.ts"],"names":[],"mappings":"AAEA,iFAAiF;AACjF,qBAAa,0BAA2B,SAAQ,KAAK;aAIjC,IAAI,EAAE,MAAM;aACZ,WAAW,EAAE,MAAM;aACnB,SAAS,CAAC,EAAE,MAAM;aAClB,eAAe,CAAC,EAAE,MAAM;IAN1C,SAAkB,IAAI,gCAAgC;IAEtD,YACkB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,YAAA,EAClB,eAAe,CAAC,EAAE,MAAM,YAAA;IACxC;;;OAGG;IACH,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAQ9B;CACF"}
@@ -0,0 +1,24 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ /** Raised when feature evaluation is configured to surface provider failures. */
3
+ export class FeatureFlagEvaluationError extends Error {
4
+ flag;
5
+ requestName;
6
+ errorCode;
7
+ providerMessage;
8
+ name = 'FeatureFlagEvaluationError';
9
+ constructor(flag, requestName, errorCode, providerMessage,
10
+ /**
11
+ * The provider error, when one was thrown. It becomes the error's `cause`,
12
+ * which keeps its stack and any provider-specific fields.
13
+ */
14
+ options) {
15
+ super(`Feature flag "${flag}" could not be evaluated for ${requestName}` +
16
+ (errorCode ? ` (${errorCode})` : '') +
17
+ (providerMessage ? `: ${providerMessage}` : ''), options?.cause !== undefined ? { cause: options.cause } : undefined);
18
+ this.flag = flag;
19
+ this.requestName = requestName;
20
+ this.errorCode = errorCode;
21
+ this.providerMessage = providerMessage;
22
+ }
23
+ }
24
+ //# sourceMappingURL=feature-flag-evaluation.error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-flag-evaluation.error.js","sourceRoot":"","sources":["../../src/errors/feature-flag-evaluation.error.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,iFAAiF;AACjF,MAAM,OAAO,0BAA2B,SAAQ,KAAK;IAIjC,IAAI;IACJ,WAAW;IACX,SAAS;IACT,eAAe;IANf,IAAI,GAAG,4BAA4B,CAAC;IAEtD,YACkB,IAAY,EACZ,WAAmB,EACnB,SAAkB,EAClB,eAAwB;IACxC;;;OAGG;IACH,OAA6B;QAE7B,KAAK,CACH,iBAAiB,IAAI,gCAAgC,WAAW,EAAE;YAChE,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACpC,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EACjD,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CACpE,CAAC;oBAfc,IAAI;2BACJ,WAAW;yBACX,SAAS;+BACT,eAAe;IAajC,CAAC;CACF"}
@@ -0,0 +1,148 @@
1
+ import { type IPipelineBehavior, type IPipelineBehaviorContract, type IPipelineBehaviorOptionsResolver, type IPipelineContext, type NextDelegate, PIPELINE_BEHAVIOR_CONTRACT, type PipelineItemToken, type PipelineLogger } from '@cqrs-ddd/pipeline';
2
+ import type { Client, EvaluationContext } from '@openfeature/server-sdk';
3
+ import type { FeatureFlagBehaviorOptions, FeatureFlagDecision, TargetingKeyFactory } from './interfaces/feature-flags-options.interface.js';
4
+ /**
5
+ * Unique symbol key set on `context.items` recording the final boolean gate
6
+ * decision (`true` means the handler was allowed to execute).
7
+ *
8
+ * Use this when only the final gate result is needed. For provider variant,
9
+ * reason, error metadata, and targeting identity use
10
+ * {@link FEATURE_FLAG_DECISION_ITEM}.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const isEnabled = context.items.get(FEATURE_FLAG_ITEM) === true;
15
+ * ```
16
+ */
17
+ export declare const FEATURE_FLAG_ITEM: unique symbol;
18
+ /**
19
+ * Typed token for {@link FEATURE_FLAG_ITEM}: the feature flag gate decision. Reads and writes the same
20
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
21
+ */
22
+ export declare const FEATURE_FLAG_ITEM_TOKEN: PipelineItemToken<boolean>;
23
+ /**
24
+ * Unique symbol key set on `context.items` recording the evaluated feature flag
25
+ * key string.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const flagKey = context.items.get(FEATURE_FLAG_KEY_ITEM) as string | undefined;
30
+ * ```
31
+ */
32
+ export declare const FEATURE_FLAG_KEY_ITEM: unique symbol;
33
+ /**
34
+ * Typed token for {@link FEATURE_FLAG_KEY_ITEM}: the evaluated feature flag key. Reads and writes the same
35
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
36
+ */
37
+ export declare const FEATURE_FLAG_KEY_ITEM_TOKEN: PipelineItemToken<string>;
38
+ /**
39
+ * Unique symbol key set on `context.items` with the full
40
+ * {@link FeatureFlagDecision}: raw boolean value, final enabled decision,
41
+ * provider variant/reason/error metadata, and targeting key when available.
42
+ *
43
+ * This lets audit/logging/telemetry behaviors inspect the exact decision without
44
+ * evaluating the flag a second time.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const decision = context.items.get(FEATURE_FLAG_DECISION_ITEM) as
49
+ * | FeatureFlagDecision
50
+ * | undefined;
51
+ *
52
+ * if (decision?.variant === 'treatment-a') {
53
+ * // enrich telemetry/audit metadata
54
+ * }
55
+ * ```
56
+ */
57
+ export declare const FEATURE_FLAG_DECISION_ITEM: unique symbol;
58
+ /**
59
+ * Typed token for {@link FEATURE_FLAG_DECISION_ITEM}: the feature flag decision. Reads and writes the same
60
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
61
+ */
62
+ export declare const FEATURE_FLAG_DECISION_ITEM_TOKEN: PipelineItemToken<FeatureFlagDecision>;
63
+ /**
64
+ * Pipeline behavior that gates a handler behind an OpenFeature boolean flag.
65
+ *
66
+ * Provider-agnostic by design: it talks only to the OpenFeature {@link Client},
67
+ * so the backing provider (Unleash, Flagsmith, LaunchDarkly, a local file, …) is
68
+ * a drop-in swap of the client passed to the constructor (see `createFeatureFlagClient`).
69
+ * OpenFeature remains the feature-flag abstraction; this behavior adds only the
70
+ * pipeline-specific semantics that an application repeatedly needs.
71
+ *
72
+ * Resolution of the effective options for a handler:
73
+ * 1. Application-wide defaults, the second constructor argument.
74
+ * 2. Per-operation options from the `featureFlag({ ... })` entry,
75
+ * shallow-merged on top of the defaults (handler keys win).
76
+ *
77
+ * Behavior:
78
+ * - No `flag` configured → transparent pass-through.
79
+ * - Flag evaluates `true` and optional `allowedVariants` accepts the provider
80
+ * variant → run the handler.
81
+ * - Final gate disabled → return `fallback(context)` when provided, otherwise
82
+ * throw {@link FeatureDisabledError}.
83
+ * - Provider evaluation error → use the configured `defaultValue` by default,
84
+ * or throw {@link FeatureFlagEvaluationError} with `errorPolicy: 'throw'`.
85
+ *
86
+ * ### Stable rollout identity
87
+ *
88
+ * Percentage/gradual rollouts should use a stable user/account/device/tenant
89
+ * identity through `targetingKeyFactory`.
90
+ *
91
+ * @example Simple gate
92
+ * ```ts
93
+ * class NewCheckoutHandler {
94
+ * @pipeline.wrap({ kind: 'command' }, [FeatureFlagBehavior, { flag: 'new-checkout' }])
95
+ * async handle(command: NewCheckoutCommand) {}
96
+ * }
97
+ * ```
98
+ *
99
+ * @example Sticky user rollout with graceful fallback
100
+ * ```ts
101
+ * class GetRecommendationsHandler {
102
+ * @pipeline.wrap({ kind: 'query' }, [FeatureFlagBehavior, {
103
+ * flag: 'recommendations-v2',
104
+ * targetingKeyFactory: (ctx) =>
105
+ * ctx.items.get('currentUserId') as string | undefined,
106
+ * fallback: () => [],
107
+ * }])
108
+ * async handle(query: GetRecommendationsQuery) {}
109
+ * }
110
+ * ```
111
+ *
112
+ * @example Run only for one experiment variant
113
+ * ```ts
114
+ * @pipeline.wrap({ kind: 'command' }, [FeatureFlagBehavior, {
115
+ * flag: 'checkout-experiment',
116
+ * allowedVariants: ['treatment-a'],
117
+ * }])
118
+ * ```
119
+ *
120
+ * @example Make flag-provider failure visible instead of using the default
121
+ * ```ts
122
+ * @pipeline.wrap({ kind: 'command' }, [FeatureFlagBehavior, {
123
+ * flag: 'critical-kill-switch',
124
+ * errorPolicy: 'throw',
125
+ * }])
126
+ * ```
127
+ */
128
+ export declare class FeatureFlagBehavior implements IPipelineBehavior, IPipelineBehaviorOptionsResolver<FeatureFlagBehaviorOptions> {
129
+ private readonly client;
130
+ private readonly moduleContext?;
131
+ private readonly moduleTargetingKeyFactory?;
132
+ static readonly [PIPELINE_BEHAVIOR_CONTRACT]: IPipelineBehaviorContract;
133
+ private readonly logger;
134
+ private readonly defaults;
135
+ constructor(client: Client, defaults?: FeatureFlagBehaviorOptions, moduleContext?: EvaluationContext | undefined, logger?: PipelineLogger, moduleTargetingKeyFactory?: TargetingKeyFactory | undefined);
136
+ handle(context: IPipelineContext, next: NextDelegate): Promise<unknown>;
137
+ /**
138
+ * Resolves OpenFeature details without applying the handler's error policy.
139
+ *
140
+ * Provider failures are normalized into a decision plus a `failure`
141
+ * descriptor so the caller can publish telemetry/audit state before deciding
142
+ * whether to use the default value or throw.
143
+ */
144
+ private evaluate;
145
+ /** Shallow-merges pipeline-level options over the application defaults. */
146
+ resolveEffectiveOptions(options?: FeatureFlagBehaviorOptions): FeatureFlagBehaviorOptions;
147
+ }
148
+ //# sourceMappingURL=feature-flag.behavior.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-flag.behavior.d.ts","sourceRoot":"","sources":["../src/feature-flag.behavior.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,gCAAgC,EACrC,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,0BAA0B,EAG1B,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAEpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAIzE,OAAO,KAAK,EACV,0BAA0B,EAC1B,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,iDAAiD,CAAC;AAEzD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,eAA8B,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,iBAAiB,CAAC,OAAO,CACM,CAAC;AAEtE;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;GAGG;AACH,eAAO,MAAM,2BAA2B,EAAE,iBAAiB,CAAC,MAAM,CACU,CAAC;AAE7E;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,gCAAgC,EAAE,iBAAiB,CAAC,mBAAmB,CAIjF,CAAC;AAWJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,qBAAa,mBACX,YACE,iBAAiB,EACjB,gCAAgC,CAAC,0BAA0B,CAAC;IAiD5D,OAAO,CAAC,QAAQ,CAAC,MAAM;IAEvB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;IAE/B,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAC;IAnD7C,MAAM,CAAC,QAAQ,CAAC,CAAC,0BAA0B,CAAC,EAAE,yBAAyB,CAyCrE;IAEF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IAEtD,YACmB,MAAM,EAAE,MAAM,EAC/B,QAAQ,CAAC,EAAE,0BAA0B,EACpB,aAAa,CAAC,EAAE,iBAAiB,YAAA,EAClD,MAAM,CAAC,EAAE,cAAc,EACN,yBAAyB,CAAC,EAAE,mBAAmB,YAAA,EASjE;IAEK,MAAM,CACV,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,OAAO,CAAC,CA2ElB;IAED;;;;;;OAMG;YACW,QAAQ;IA2CtB,2EAA2E;IAC3E,uBAAuB,CACrB,OAAO,CAAC,EAAE,0BAA0B,GACnC,0BAA0B,CAE5B;CACF"}
@@ -0,0 +1,261 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { createPipelineItem, PIPELINE_BEHAVIOR_CONTRACT, setPipelineItem, } from '@cqrs-ddd/pipeline';
3
+ import { FeatureDisabledError } from './errors/feature-disabled.error.js';
4
+ import { FeatureFlagEvaluationError } from './errors/feature-flag-evaluation.error.js';
5
+ import { buildEvaluationContext } from './helpers/evaluation-context.js';
6
+ /**
7
+ * Unique symbol key set on `context.items` recording the final boolean gate
8
+ * decision (`true` means the handler was allowed to execute).
9
+ *
10
+ * Use this when only the final gate result is needed. For provider variant,
11
+ * reason, error metadata, and targeting identity use
12
+ * {@link FEATURE_FLAG_DECISION_ITEM}.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const isEnabled = context.items.get(FEATURE_FLAG_ITEM) === true;
17
+ * ```
18
+ */
19
+ export const FEATURE_FLAG_ITEM = Symbol('FEATURE_FLAG_ITEM');
20
+ /**
21
+ * Typed token for {@link FEATURE_FLAG_ITEM}: the feature flag gate decision. Reads and writes the same
22
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
23
+ */
24
+ export const FEATURE_FLAG_ITEM_TOKEN = createPipelineItem('FEATURE_FLAG_ITEM', FEATURE_FLAG_ITEM);
25
+ /**
26
+ * Unique symbol key set on `context.items` recording the evaluated feature flag
27
+ * key string.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const flagKey = context.items.get(FEATURE_FLAG_KEY_ITEM) as string | undefined;
32
+ * ```
33
+ */
34
+ export const FEATURE_FLAG_KEY_ITEM = Symbol('FEATURE_FLAG_KEY_ITEM');
35
+ /**
36
+ * Typed token for {@link FEATURE_FLAG_KEY_ITEM}: the evaluated feature flag key. Reads and writes the same
37
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
38
+ */
39
+ export const FEATURE_FLAG_KEY_ITEM_TOKEN = createPipelineItem('FEATURE_FLAG_KEY_ITEM', FEATURE_FLAG_KEY_ITEM);
40
+ /**
41
+ * Unique symbol key set on `context.items` with the full
42
+ * {@link FeatureFlagDecision}: raw boolean value, final enabled decision,
43
+ * provider variant/reason/error metadata, and targeting key when available.
44
+ *
45
+ * This lets audit/logging/telemetry behaviors inspect the exact decision without
46
+ * evaluating the flag a second time.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const decision = context.items.get(FEATURE_FLAG_DECISION_ITEM) as
51
+ * | FeatureFlagDecision
52
+ * | undefined;
53
+ *
54
+ * if (decision?.variant === 'treatment-a') {
55
+ * // enrich telemetry/audit metadata
56
+ * }
57
+ * ```
58
+ */
59
+ export const FEATURE_FLAG_DECISION_ITEM = Symbol('FEATURE_FLAG_DECISION_ITEM');
60
+ /**
61
+ * Typed token for {@link FEATURE_FLAG_DECISION_ITEM}: the feature flag decision. Reads and writes the same
62
+ * `context.items` entry through `getPipelineItem` / `requirePipelineItem`.
63
+ */
64
+ export const FEATURE_FLAG_DECISION_ITEM_TOKEN = createPipelineItem('FEATURE_FLAG_DECISION_ITEM', FEATURE_FLAG_DECISION_ITEM);
65
+ /**
66
+ * Pipeline behavior that gates a handler behind an OpenFeature boolean flag.
67
+ *
68
+ * Provider-agnostic by design: it talks only to the OpenFeature {@link Client},
69
+ * so the backing provider (Unleash, Flagsmith, LaunchDarkly, a local file, …) is
70
+ * a drop-in swap of the client passed to the constructor (see `createFeatureFlagClient`).
71
+ * OpenFeature remains the feature-flag abstraction; this behavior adds only the
72
+ * pipeline-specific semantics that an application repeatedly needs.
73
+ *
74
+ * Resolution of the effective options for a handler:
75
+ * 1. Application-wide defaults, the second constructor argument.
76
+ * 2. Per-operation options from the `featureFlag({ ... })` entry,
77
+ * shallow-merged on top of the defaults (handler keys win).
78
+ *
79
+ * Behavior:
80
+ * - No `flag` configured → transparent pass-through.
81
+ * - Flag evaluates `true` and optional `allowedVariants` accepts the provider
82
+ * variant → run the handler.
83
+ * - Final gate disabled → return `fallback(context)` when provided, otherwise
84
+ * throw {@link FeatureDisabledError}.
85
+ * - Provider evaluation error → use the configured `defaultValue` by default,
86
+ * or throw {@link FeatureFlagEvaluationError} with `errorPolicy: 'throw'`.
87
+ *
88
+ * ### Stable rollout identity
89
+ *
90
+ * Percentage/gradual rollouts should use a stable user/account/device/tenant
91
+ * identity through `targetingKeyFactory`.
92
+ *
93
+ * @example Simple gate
94
+ * ```ts
95
+ * class NewCheckoutHandler {
96
+ * @pipeline.wrap({ kind: 'command' }, [FeatureFlagBehavior, { flag: 'new-checkout' }])
97
+ * async handle(command: NewCheckoutCommand) {}
98
+ * }
99
+ * ```
100
+ *
101
+ * @example Sticky user rollout with graceful fallback
102
+ * ```ts
103
+ * class GetRecommendationsHandler {
104
+ * @pipeline.wrap({ kind: 'query' }, [FeatureFlagBehavior, {
105
+ * flag: 'recommendations-v2',
106
+ * targetingKeyFactory: (ctx) =>
107
+ * ctx.items.get('currentUserId') as string | undefined,
108
+ * fallback: () => [],
109
+ * }])
110
+ * async handle(query: GetRecommendationsQuery) {}
111
+ * }
112
+ * ```
113
+ *
114
+ * @example Run only for one experiment variant
115
+ * ```ts
116
+ * @pipeline.wrap({ kind: 'command' }, [FeatureFlagBehavior, {
117
+ * flag: 'checkout-experiment',
118
+ * allowedVariants: ['treatment-a'],
119
+ * }])
120
+ * ```
121
+ *
122
+ * @example Make flag-provider failure visible instead of using the default
123
+ * ```ts
124
+ * @pipeline.wrap({ kind: 'command' }, [FeatureFlagBehavior, {
125
+ * flag: 'critical-kill-switch',
126
+ * errorPolicy: 'throw',
127
+ * }])
128
+ * ```
129
+ */
130
+ export class FeatureFlagBehavior {
131
+ client;
132
+ moduleContext;
133
+ moduleTargetingKeyFactory;
134
+ static [PIPELINE_BEHAVIOR_CONTRACT] = {
135
+ validate: (context) => {
136
+ const options = context.effectiveOptions;
137
+ if (options?.flag !== undefined &&
138
+ (typeof options.flag !== 'string' || options.flag.trim() === '')) {
139
+ return [
140
+ {
141
+ handlerName: context.handlerName,
142
+ behaviorName: FeatureFlagBehavior.name,
143
+ message: 'FeatureFlagBehavior `flag` option must be a non-empty string',
144
+ fix: 'Provide a valid string flag name in options or omit options for pass-through.',
145
+ },
146
+ ];
147
+ }
148
+ if ((context.declarationSource === 'handler' ||
149
+ context.declarationSource === 'both') &&
150
+ options?.flag === undefined) {
151
+ return [
152
+ {
153
+ handlerName: context.handlerName,
154
+ behaviorName: FeatureFlagBehavior.name,
155
+ message: 'Explicit FeatureFlagBehavior intent requires a non-empty `flag` name',
156
+ fix: 'Provide flag in the featureFlag({ flag: "flag-name" }) entry.',
157
+ },
158
+ ];
159
+ }
160
+ return undefined;
161
+ },
162
+ };
163
+ logger;
164
+ defaults;
165
+ constructor(client, defaults, moduleContext, logger, moduleTargetingKeyFactory) {
166
+ this.client = client;
167
+ this.moduleContext = moduleContext;
168
+ this.moduleTargetingKeyFactory = moduleTargetingKeyFactory;
169
+ if (typeof client?.getBooleanDetails !== 'function') {
170
+ throw new TypeError('FeatureFlagBehavior requires an OpenFeature client, such as the one createFeatureFlagClient() resolves.');
171
+ }
172
+ this.defaults = defaults ?? {};
173
+ this.logger = logger ?? console;
174
+ }
175
+ async handle(context, next) {
176
+ const options = this.resolveEffectiveOptions(context.getBehaviorOptions(FeatureFlagBehavior));
177
+ if (!options.flag)
178
+ return next();
179
+ const targetingKeyFactory = options.targetingKeyFactory ?? this.moduleTargetingKeyFactory;
180
+ const evaluationContext = buildEvaluationContext(context, this.moduleContext, options.context, targetingKeyFactory);
181
+ const defaultValue = options.defaultValue ?? false;
182
+ const { details, failure } = await this.evaluate(options.flag, defaultValue, evaluationContext);
183
+ // A boolean true is necessary but, when allowedVariants is configured, not
184
+ // sufficient: the provider must also resolve one of the selected variants.
185
+ const variantAllowed = !options.allowedVariants?.length ||
186
+ (!!details.variant && options.allowedVariants.includes(details.variant));
187
+ const enabled = details.value && variantAllowed;
188
+ const decision = {
189
+ flagKey: options.flag,
190
+ value: details.value,
191
+ enabled,
192
+ ...(details.variant ? { variant: details.variant } : {}),
193
+ ...(details.reason ? { reason: details.reason } : {}),
194
+ ...(details.errorCode !== undefined
195
+ ? { errorCode: String(details.errorCode) }
196
+ : {}),
197
+ ...(details.errorMessage ? { errorMessage: details.errorMessage } : {}),
198
+ ...(evaluationContext.targetingKey
199
+ ? { targetingKey: evaluationContext.targetingKey }
200
+ : {}),
201
+ };
202
+ // Publish the decision before applying the throw policy so outer audit and
203
+ // telemetry behaviors can observe provider failures.
204
+ setPipelineItem(context, FEATURE_FLAG_KEY_ITEM_TOKEN, options.flag);
205
+ setPipelineItem(context, FEATURE_FLAG_ITEM_TOKEN, enabled);
206
+ setPipelineItem(context, FEATURE_FLAG_DECISION_ITEM_TOKEN, decision);
207
+ if (failure && (options.errorPolicy ?? 'use-default') === 'throw') {
208
+ throw new FeatureFlagEvaluationError(options.flag, context.requestName, failure.errorCode, failure.message, { cause: failure.cause });
209
+ }
210
+ const variantSuffix = details.variant ? ` variant=${details.variant}` : '';
211
+ const reasonSuffix = details.reason ? ` reason=${details.reason}` : '';
212
+ const state = enabled ? 'enabled' : 'disabled';
213
+ this.logger.debug?.(`Feature "${options.flag}" ${state} for ${context.requestName}${variantSuffix}${reasonSuffix}`, FeatureFlagBehavior.name);
214
+ if (enabled)
215
+ return next();
216
+ if (options.fallback)
217
+ return options.fallback(context);
218
+ throw new FeatureDisabledError(options.flag, context.requestName);
219
+ }
220
+ /**
221
+ * Resolves OpenFeature details without applying the handler's error policy.
222
+ *
223
+ * Provider failures are normalized into a decision plus a `failure`
224
+ * descriptor so the caller can publish telemetry/audit state before deciding
225
+ * whether to use the default value or throw.
226
+ */
227
+ async evaluate(flag, defaultValue, evaluationContext) {
228
+ let details;
229
+ try {
230
+ details = (await this.client.getBooleanDetails(flag, defaultValue, evaluationContext));
231
+ }
232
+ catch (error) {
233
+ const message = error instanceof Error ? error.message : String(error);
234
+ return {
235
+ details: {
236
+ value: defaultValue,
237
+ reason: 'ERROR',
238
+ errorMessage: message,
239
+ },
240
+ failure: { message, cause: error },
241
+ };
242
+ }
243
+ // A provider that reports an error in its details rather than throwing is
244
+ // the same outcome from the application's point of view.
245
+ if (details.errorCode !== undefined) {
246
+ return {
247
+ details,
248
+ failure: {
249
+ message: details.errorMessage ?? String(details.errorCode),
250
+ errorCode: String(details.errorCode),
251
+ },
252
+ };
253
+ }
254
+ return { details };
255
+ }
256
+ /** Shallow-merges pipeline-level options over the application defaults. */
257
+ resolveEffectiveOptions(options) {
258
+ return options ? { ...this.defaults, ...options } : this.defaults;
259
+ }
260
+ }
261
+ //# sourceMappingURL=feature-flag.behavior.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-flag.behavior.js","sourceRoot":"","sources":["../src/feature-flag.behavior.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { type IPipelineContext } from '@cqrs-ddd/pipeline';
2
+ /**
3
+ * The feature-flag decision of one execution as flat attributes, named after
4
+ * the OpenTelemetry feature-flag conventions: `feature_flag.key` and
5
+ * `feature_flag.enabled`, plus `feature_flag.variant` and `feature_flag.reason`
6
+ * when the provider reported them, and `feature_flag.error_code` only when
7
+ * evaluation failed. `{}` when `FeatureFlagBehavior` did not run. The targeting
8
+ * key and the provider's error message are never included.
9
+ *
10
+ * The result is plain data and needs no telemetry package: put it on a span, a
11
+ * log line, an audit record or a metric label (bounded values only).
12
+ *
13
+ * @param context - The pipeline execution, read after `FeatureFlagBehavior` ran.
14
+ * @returns The attributes; empty when the behavior did not run.
15
+ *
16
+ * @example Span attributes
17
+ * With `AttributesBehavior` of `@cqrs-ddd/pipeline-opentelemetry`:
18
+ * ```ts
19
+ * createPipeline({
20
+ * globalBehaviors: {
21
+ * before: [TraceBehavior, [AttributesBehavior, { factories: [buildFeatureFlagAttributes] }]],
22
+ * },
23
+ * });
24
+ * ```
25
+ *
26
+ * @example Audit metadata
27
+ * `audit()` is declared outside `featureFlag()`, so the record is built after its decision:
28
+ * ```ts
29
+ * @pipeline.wrap(
30
+ * { kind: 'command' },
31
+ * audit({ action: 'order.create', metadata: buildFeatureFlagAttributes }),
32
+ * featureFlag({ flag: 'checkout' }),
33
+ * )
34
+ * ```
35
+ *
36
+ * @example A log line from a custom behavior, after the chain ran
37
+ * ```ts
38
+ * async handle(context: IPipelineContext, next: NextDelegate) {
39
+ * try {
40
+ * return await next();
41
+ * } finally {
42
+ * this.logger.log({ request: context.requestName, ...buildFeatureFlagAttributes(context) });
43
+ * }
44
+ * }
45
+ * ```
46
+ */
47
+ export declare function buildFeatureFlagAttributes(context: IPipelineContext): Record<string, string | boolean>;
48
+ //# sourceMappingURL=build-attributes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-attributes.d.ts","sourceRoot":"","sources":["../../src/helpers/build-attributes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,gBAAgB,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAYlC"}