@velajs/feature-flags 0.1.0 → 0.1.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/dist/index.js CHANGED
@@ -1,15 +1,114 @@
1
- // Public surface of @velajs/feature-flags (the `.` entry). Server-only: no
2
- // React, no client hooks. Runtime-specific drivers (Cloudflare Flagship / KV)
3
- // ship from @velajs/cloudflare against the FeatureFlagDriver contract exported
4
- // here.
5
- // --- DI: module / service / tokens ---------------------------------------
6
- export { FeatureFlagsModule } from "./feature-flags.module.js";
7
- export { FeatureFlagsService } from "./feature-flags.service.js";
8
- export { FEATURE_FLAG_TOKENS } from "./feature-flags.tokens.js";
9
- export { MemoryFlagDriver, memoryFlagDriver } from "./drivers/memory.driver.js";
10
- export { FeatureFlagDriverRegistry, buildDriverRegistry } from "./drivers/registry.js";
11
- // --- Guard + decorator ----------------------------------------------------
12
- export { FeatureFlagGuard } from "./guards/feature-flag.guard.js";
13
- export { FeatureFlag, FEATURE_FLAG_METADATA } from "./decorators/feature-flag.decorator.js";
14
- // --- Errors ---------------------------------------------------------------
15
- export { FeatureFlagError } from "./feature-flags.error.js";
1
+ import { a as FEATURE_FLAG_TOKENS, c as MemoryFlagDriver, i as __decorateMetadata, l as memoryFlagDriver, n as __decorate, o as FeatureFlagDriverRegistry, r as __decorateParam, s as buildDriverRegistry, t as FeatureFlagsService, u as FeatureFlagError } from "./feature-flags.service-CNOZ1DhZ.js";
2
+ import { APP_GUARD, ForbiddenException, Inject, Injectable, NotFoundException, REQUEST_CONTEXT, Reflector, Scope, SetMetadata, defineModule } from "@velajs/vela";
3
+ //#region src/decorators/feature-flag.decorator.ts
4
+ const FEATURE_FLAG_METADATA = "vela:feature-flags:flag";
5
+ /**
6
+ * Gate a route (handler) or controller behind a boolean feature flag. Pair
7
+ * with {@link FeatureFlagGuard} (via `@UseGuards` or the module's `isGlobal`
8
+ * app-wide registration).
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * @UseGuards(FeatureFlagGuard)
13
+ * @Controller('/checkout')
14
+ * class CheckoutController {
15
+ * @FeatureFlag('new-checkout') // 404 when off
16
+ * @Get('/v2') v2() { ... }
17
+ *
18
+ * @FeatureFlag('beta', { onDisabled: 'forbidden' }) // 403 when off
19
+ * @Get('/beta') beta() { ... }
20
+ * }
21
+ * ```
22
+ */
23
+ function FeatureFlag(key, options = {}) {
24
+ const meta = {
25
+ key,
26
+ onDisabled: options.onDisabled ?? "notFound"
27
+ };
28
+ return SetMetadata(FEATURE_FLAG_METADATA, meta);
29
+ }
30
+ //#endregion
31
+ //#region src/guards/feature-flag.guard.ts
32
+ let FeatureFlagGuard = class FeatureFlagGuard {
33
+ flags;
34
+ reflector = new Reflector();
35
+ constructor(flags) {
36
+ this.flags = flags;
37
+ }
38
+ async canActivate(context) {
39
+ const meta = this.reflector.getAllAndOverride(FEATURE_FLAG_METADATA, context);
40
+ if (!meta) return true;
41
+ const requestContext = this.requestContext(context);
42
+ if (await (requestContext ? this.flags.forRequest(requestContext) : this.flags).getBooleanValue(meta.key)) return true;
43
+ if (meta.onDisabled === "forbidden") throw new ForbiddenException(`Feature "${meta.key}" is not enabled.`);
44
+ throw new NotFoundException();
45
+ }
46
+ /** The current request context, via the child container on the Hono context. */
47
+ requestContext(context) {
48
+ try {
49
+ return context.getContext().get("container")?.resolve(REQUEST_CONTEXT);
50
+ } catch {
51
+ return;
52
+ }
53
+ }
54
+ };
55
+ FeatureFlagGuard = __decorate([
56
+ Injectable(),
57
+ __decorateParam(0, Inject(FEATURE_FLAG_TOKENS.Service)),
58
+ __decorateMetadata("design:paramtypes", [Object])
59
+ ], FeatureFlagGuard);
60
+ //#endregion
61
+ //#region src/feature-flags.module.ts
62
+ /**
63
+ * The feature-flags module. Authored on vela's public `defineModule`, so
64
+ * `forRoot({ drivers, default?, manifest?, context?, isGlobal? })` and the
65
+ * matching `forRootAsync({ inject, useFactory, ... })` come for free.
66
+ *
67
+ * `lazy: true` is valid here: every provider is sync-constructible (a factory
68
+ * for the driver registry, a sync-constructor service, a guard) and there are
69
+ * no async lifecycle hooks — so the module defers to first use without
70
+ * violating the sync-seam rule (see vela `MODULE_AUTHORING.md`).
71
+ *
72
+ * `isGlobal: true` makes the module globally visible AND registers
73
+ * {@link FeatureFlagGuard} app-wide (`APP_GUARD`) so every `@FeatureFlag()`
74
+ * route is gated without a per-controller `@UseGuards`.
75
+ */
76
+ const { ConfigurableModuleClass } = defineModule({
77
+ name: "FeatureFlags",
78
+ optionsToken: FEATURE_FLAG_TOKENS.Options,
79
+ lazy: true,
80
+ transform: (definition, extras) => extras.isGlobal ? {
81
+ ...definition,
82
+ global: true,
83
+ providers: [...definition.providers ?? [], {
84
+ provide: APP_GUARD,
85
+ useExisting: FeatureFlagGuard
86
+ }]
87
+ } : definition,
88
+ setup: ({ OPTIONS }) => ({
89
+ providers: [
90
+ {
91
+ provide: FEATURE_FLAG_TOKENS.DriverRegistry,
92
+ useFactory: (options) => buildDriverRegistry(options),
93
+ inject: [OPTIONS]
94
+ },
95
+ {
96
+ provide: FEATURE_FLAG_TOKENS.Service,
97
+ useClass: FeatureFlagsService,
98
+ scope: Scope.TRANSIENT
99
+ },
100
+ FeatureFlagGuard
101
+ ],
102
+ exports: [
103
+ FEATURE_FLAG_TOKENS.Service,
104
+ FEATURE_FLAG_TOKENS.DriverRegistry,
105
+ FeatureFlagGuard,
106
+ OPTIONS
107
+ ]
108
+ })
109
+ });
110
+ var FeatureFlagsModule = class extends ConfigurableModuleClass {};
111
+ //#endregion
112
+ export { FEATURE_FLAG_METADATA, FEATURE_FLAG_TOKENS, FeatureFlag, FeatureFlagDriverRegistry, FeatureFlagError, FeatureFlagGuard, FeatureFlagsModule, FeatureFlagsService, MemoryFlagDriver, buildDriverRegistry, memoryFlagDriver };
113
+
114
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/decorators/feature-flag.decorator.ts","../src/guards/feature-flag.guard.ts","../src/feature-flags.module.ts"],"sourcesContent":["import { SetMetadata } from '@velajs/vela';\nimport type { FlagKey } from '../feature-flags.types';\n\n/** What the {@link FeatureFlagGuard} does when a gated flag is off. */\nexport type FeatureFlagDisabledBehavior = 'notFound' | 'forbidden';\n\nexport interface FeatureFlagOptions {\n /**\n * Response when the flag is off. `'notFound'` (default) hides the route\n * entirely (404); `'forbidden'` reveals it exists but denies access (403).\n */\n onDisabled?: FeatureFlagDisabledBehavior;\n}\n\n/** The metadata `@FeatureFlag()` attaches, read by {@link FeatureFlagGuard}. */\nexport interface FeatureFlagMetadata {\n key: string;\n onDisabled: FeatureFlagDisabledBehavior;\n}\n\nexport const FEATURE_FLAG_METADATA = 'vela:feature-flags:flag';\n\n/**\n * Gate a route (handler) or controller behind a boolean feature flag. Pair\n * with {@link FeatureFlagGuard} (via `@UseGuards` or the module's `isGlobal`\n * app-wide registration).\n *\n * @example\n * ```ts\n * @UseGuards(FeatureFlagGuard)\n * @Controller('/checkout')\n * class CheckoutController {\n * @FeatureFlag('new-checkout') // 404 when off\n * @Get('/v2') v2() { ... }\n *\n * @FeatureFlag('beta', { onDisabled: 'forbidden' }) // 403 when off\n * @Get('/beta') beta() { ... }\n * }\n * ```\n */\nexport function FeatureFlag(key: FlagKey, options: FeatureFlagOptions = {}) {\n const meta: FeatureFlagMetadata = { key, onDisabled: options.onDisabled ?? 'notFound' };\n return SetMetadata(FEATURE_FLAG_METADATA, meta);\n}\n","import {\n Container,\n ForbiddenException,\n Inject,\n Injectable,\n NotFoundException,\n REQUEST_CONTEXT,\n Reflector,\n type CanActivate,\n type ExecutionContext,\n type RequestContext,\n} from '@velajs/vela';\nimport {\n FEATURE_FLAG_METADATA,\n type FeatureFlagMetadata,\n} from '../decorators/feature-flag.decorator';\nimport type { FeatureFlagsService } from '../feature-flags.service';\nimport { FEATURE_FLAG_TOKENS } from '../feature-flags.tokens';\n\n/**\n * Route gate for `@FeatureFlag()`. Reads the handler/controller metadata, then\n * `getBooleanValue(key)`; when the flag is off it throws `NotFoundException`\n * (the route appears hidden) or `ForbiddenException` per the decorator option.\n * Handlers with no `@FeatureFlag()` metadata pass through untouched, so the\n * guard is safe to register app-wide (`FeatureFlagsModule.forRoot({ isGlobal:\n * true })`) or per-route via `@UseGuards(FeatureFlagGuard)`.\n *\n * It does NOT inject `REQUEST_CONTEXT` — that would make the guard request-\n * scoped and break lazy-module materialization (which constructs every provider\n * once, at first use, possibly outside a request). Instead it reads the request\n * context off the per-request child container carried on the Hono context, so\n * the module's `context` resolver still runs for the gate decision.\n */\n@Injectable()\nexport class FeatureFlagGuard implements CanActivate {\n private readonly reflector = new Reflector();\n\n constructor(@Inject(FEATURE_FLAG_TOKENS.Service) private readonly flags: FeatureFlagsService) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const meta = this.reflector.getAllAndOverride<FeatureFlagMetadata>(\n FEATURE_FLAG_METADATA,\n context,\n );\n if (!meta) return true;\n\n const requestContext = this.requestContext(context);\n const flags = requestContext ? this.flags.forRequest(requestContext) : this.flags;\n const enabled = await flags.getBooleanValue(meta.key);\n if (enabled) return true;\n\n if (meta.onDisabled === 'forbidden') {\n throw new ForbiddenException(`Feature \"${meta.key}\" is not enabled.`);\n }\n throw new NotFoundException();\n }\n\n /** The current request context, via the child container on the Hono context. */\n private requestContext(context: ExecutionContext): RequestContext | undefined {\n try {\n const container = context.getContext().get('container') as Container | undefined;\n return container?.resolve(REQUEST_CONTEXT);\n } catch {\n return undefined;\n }\n }\n}\n","import { APP_GUARD, Scope, defineModule } from '@velajs/vela';\nimport { buildDriverRegistry } from './drivers/registry';\nimport { FeatureFlagGuard } from './guards/feature-flag.guard';\nimport { FeatureFlagsService } from './feature-flags.service';\nimport { FEATURE_FLAG_TOKENS } from './feature-flags.tokens';\nimport type { FeatureFlagsOptions } from './feature-flags.types';\n\n/**\n * The feature-flags module. Authored on vela's public `defineModule`, so\n * `forRoot({ drivers, default?, manifest?, context?, isGlobal? })` and the\n * matching `forRootAsync({ inject, useFactory, ... })` come for free.\n *\n * `lazy: true` is valid here: every provider is sync-constructible (a factory\n * for the driver registry, a sync-constructor service, a guard) and there are\n * no async lifecycle hooks — so the module defers to first use without\n * violating the sync-seam rule (see vela `MODULE_AUTHORING.md`).\n *\n * `isGlobal: true` makes the module globally visible AND registers\n * {@link FeatureFlagGuard} app-wide (`APP_GUARD`) so every `@FeatureFlag()`\n * route is gated without a per-controller `@UseGuards`.\n */\nconst { ConfigurableModuleClass } = defineModule<FeatureFlagsOptions>({\n name: 'FeatureFlags',\n optionsToken: FEATURE_FLAG_TOKENS.Options,\n lazy: true,\n transform: (definition, extras) =>\n (extras as { isGlobal?: boolean }).isGlobal\n ? {\n ...definition,\n global: true,\n providers: [\n ...(definition.providers ?? []),\n { provide: APP_GUARD, useExisting: FeatureFlagGuard },\n ],\n }\n : definition,\n setup: ({ OPTIONS }) => ({\n providers: [\n {\n provide: FEATURE_FLAG_TOKENS.DriverRegistry,\n useFactory: (options: FeatureFlagsOptions) => buildDriverRegistry(options),\n inject: [OPTIONS],\n },\n // Transient: a fresh service per injection, so `use()`/`forRequest()`\n // clones and per-request context stay isolated. The service never injects\n // REQUEST_CONTEXT, so it does not bubble to request scope and remains\n // resolvable in queue / scheduled / global scope.\n {\n provide: FEATURE_FLAG_TOKENS.Service,\n useClass: FeatureFlagsService,\n scope: Scope.TRANSIENT,\n },\n FeatureFlagGuard,\n ],\n exports: [\n FEATURE_FLAG_TOKENS.Service,\n FEATURE_FLAG_TOKENS.DriverRegistry,\n FeatureFlagGuard,\n OPTIONS,\n ],\n }),\n});\n\nexport class FeatureFlagsModule extends ConfigurableModuleClass {}\n"],"mappings":";;;AAoBA,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;AAoBrC,SAAgB,YAAY,KAAc,UAA8B,CAAC,GAAG;CAC1E,MAAM,OAA4B;EAAE;EAAK,YAAY,QAAQ,cAAc;CAAW;CACtF,OAAO,YAAY,uBAAuB,IAAI;AAChD;;;ACTO,IAAA,mBAAA,MAAM,iBAAwC;CAGe;CAFlE,YAA6B,IAAI,UAAU;CAE3C,YAAY,OAAkF;EAA5B,KAAA,QAAA;CAA6B;CAE/F,MAAM,YAAY,SAA6C;EAC7D,MAAM,OAAO,KAAK,UAAU,kBAC1B,uBACA,OACF;EACA,IAAI,CAAC,MAAM,OAAO;EAElB,MAAM,iBAAiB,KAAK,eAAe,OAAO;EAGlD,IAAI,OAFU,iBAAiB,KAAK,MAAM,WAAW,cAAc,IAAI,KAAK,MAAA,CAChD,gBAAgB,KAAK,GAAG,GACvC,OAAO;EAEpB,IAAI,KAAK,eAAe,aACtB,MAAM,IAAI,mBAAmB,YAAY,KAAK,IAAI,kBAAkB;EAEtE,MAAM,IAAI,kBAAkB;CAC9B;;CAGA,eAAuB,SAAuD;EAC5E,IAAI;GAEF,OADkB,QAAQ,WAAW,CAAC,CAAC,IAAI,WAC5B,CAAC,EAAE,QAAQ,eAAe;EAC3C,QAAQ;GACN;EACF;CACF;AACF;;CAjCC,WAAW;oBAIG,OAAO,oBAAoB,OAAO,CAAA;;;;;;;;;;;;;;;;;;;AChBjD,MAAM,EAAE,4BAA4B,aAAkC;CACpE,MAAM;CACN,cAAc,oBAAoB;CAClC,MAAM;CACN,YAAY,YAAY,WACrB,OAAkC,WAC/B;EACE,GAAG;EACH,QAAQ;EACR,WAAW,CACT,GAAI,WAAW,aAAa,CAAC,GAC7B;GAAE,SAAS;GAAW,aAAa;EAAiB,CACtD;CACF,IACA;CACN,QAAQ,EAAE,eAAe;EACvB,WAAW;GACT;IACE,SAAS,oBAAoB;IAC7B,aAAa,YAAiC,oBAAoB,OAAO;IACzE,QAAQ,CAAC,OAAO;GAClB;GAKA;IACE,SAAS,oBAAoB;IAC7B,UAAU;IACV,OAAO,MAAM;GACf;GACA;EACF;EACA,SAAS;GACP,oBAAoB;GACpB,oBAAoB;GACpB;GACA;EACF;CACF;AACF,CAAC;AAED,IAAa,qBAAb,cAAwC,wBAAwB,CAAC"}
@@ -0,0 +1,235 @@
1
+ import { LoggerService, RequestContext } from "@velajs/vela";
2
+ //#region src/drivers/driver.d.ts
3
+ /**
4
+ * The cross-package contract every feature-flag backend implements.
5
+ *
6
+ * This is the seam that platform packages plug into: `@velajs/cloudflare`
7
+ * ships a Flagship-binding driver and a KV-backed driver against this exact
8
+ * interface, `@velajs/feature-flags` ships {@link MemoryFlagDriver}. A driver
9
+ * maps a key + fallback (+ optional targeting context) onto a typed value and
10
+ * MUST return the `fallback` — never throw — when it cannot resolve the key.
11
+ *
12
+ * Evaluation *details* (`FlagEvaluationDetails`) are synthesized by
13
+ * `FeatureFlagsService` around these four value methods; drivers may
14
+ * optionally override that synthesis, but the value methods are the contract.
15
+ */
16
+ interface FeatureFlagDriver {
17
+ readonly name: string;
18
+ getBoolean(key: string, fallback: boolean, ctx?: FlagContext): Promise<boolean>;
19
+ getString(key: string, fallback: string, ctx?: FlagContext): Promise<string>;
20
+ getNumber(key: string, fallback: number, ctx?: FlagContext): Promise<number>;
21
+ getObject<T extends object>(key: string, fallback: T, ctx?: FlagContext): Promise<T>;
22
+ }
23
+ //#endregion
24
+ //#region src/feature-flags.types.d.ts
25
+ /**
26
+ * A value a feature flag can resolve to. Mirrors the four evaluation methods
27
+ * on {@link FeatureFlagDriver}.
28
+ */
29
+ type FlagValue = boolean | string | number | object;
30
+ /**
31
+ * A free-form evaluation context handed to a driver (for targeting: user id,
32
+ * plan, country, …). Drivers that don't do targeting ignore it.
33
+ */
34
+ type FlagContext = Record<string, unknown>;
35
+ /**
36
+ * Augment this interface to get typed flag keys on the service and the
37
+ * `@FeatureFlag()` decorator.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * declare module '@velajs/feature-flags' {
42
+ * interface FeatureFlagRegistry {
43
+ * 'new-checkout': boolean;
44
+ * 'checkout-flow': string;
45
+ * }
46
+ * }
47
+ * ```
48
+ */
49
+ interface FeatureFlagRegistry {}
50
+ /**
51
+ * A flag key. Narrows to the declared {@link FeatureFlagRegistry} keys when the
52
+ * app augments it; falls back to `string` otherwise.
53
+ */
54
+ type FlagKey = keyof FeatureFlagRegistry extends never ? string : keyof FeatureFlagRegistry & string;
55
+ /**
56
+ * A declared set of flags and their default values.
57
+ *
58
+ * Drivers have no enumeration API, so the flags you intend to evaluate as a
59
+ * batch (via {@link FeatureFlagsService.all}) must be declared once here. Each
60
+ * default also doubles as the type hint used to pick the evaluation method.
61
+ */
62
+ type FlagManifest = Record<string, FlagValue>;
63
+ /** Why an evaluation returned the value it did. */
64
+ type FlagEvaluationReason = 'STATIC' | 'DEFAULT' | 'ERROR';
65
+ /** A flag value plus the metadata the service synthesizes around a driver read. */
66
+ interface FlagEvaluationDetails<T extends FlagValue = FlagValue> {
67
+ flagKey: string;
68
+ value: T;
69
+ reason: FlagEvaluationReason;
70
+ errorMessage?: string;
71
+ }
72
+ /**
73
+ * Feature-flags module configuration.
74
+ */
75
+ interface FeatureFlagsOptions {
76
+ /**
77
+ * The drivers this app can evaluate against. When omitted, a single
78
+ * in-memory {@link MemoryFlagDriver} named `"memory"` is used (all flags
79
+ * resolve to their declared defaults).
80
+ */
81
+ drivers?: FeatureFlagDriver[];
82
+ /** Name of the driver the injected service targets. Defaults to `drivers[0].name`. */
83
+ default?: string;
84
+ /** Declared flags + defaults. Powers manifest defaults and {@link FeatureFlagsService.all}. */
85
+ manifest?: FlagManifest;
86
+ /**
87
+ * Resolves a per-request evaluation context (for example `{ userId }`) merged
88
+ * into every evaluation. Per-call context passed to a method overrides these.
89
+ * Receives the current request context; skipped outside request scope.
90
+ */
91
+ context?: (ctx: RequestContext) => FlagContext | Promise<FlagContext>;
92
+ }
93
+ //#endregion
94
+ //#region src/drivers/registry.d.ts
95
+ /**
96
+ * The set of registered {@link FeatureFlagDriver}s plus the default selection.
97
+ * Built once per module instance ({@link buildDriverRegistry}) and injected
98
+ * into {@link FeatureFlagsService}; `use(name)` looks a driver up here.
99
+ */
100
+ declare class FeatureFlagDriverRegistry {
101
+ private readonly drivers;
102
+ readonly defaultName: string;
103
+ constructor(drivers: readonly FeatureFlagDriver[], defaultName?: string);
104
+ /** Look a driver up by name; throws {@link FeatureFlagError} when unknown. */
105
+ get(name: string): FeatureFlagDriver;
106
+ /** Resolve a named driver, or the default when `name` is omitted. */
107
+ resolve(name?: string): FeatureFlagDriver;
108
+ /** All registered driver names. */
109
+ names(): string[];
110
+ }
111
+ /**
112
+ * Build the driver registry from module options. Falls back to a single
113
+ * in-memory driver when none are configured, so `FeatureFlagsModule.forRoot({})`
114
+ * yields a working (all-defaults) flags service for local development.
115
+ */
116
+ declare function buildDriverRegistry(options: FeatureFlagsOptions): FeatureFlagDriverRegistry;
117
+ //#endregion
118
+ //#region src/feature-flags.service.d.ts
119
+ /**
120
+ * The injectable consumers reach for. A thin, type-safe, never-throw wrapper
121
+ * over a {@link FeatureFlagDriver}, with two ergonomic additions:
122
+ *
123
+ * - **Manifest defaults** — omit a default and the value declared in the app's
124
+ * `manifest` is used (an explicit argument always wins).
125
+ * - **Default context** — the module's `context` resolver is merged into every
126
+ * evaluation (per-call context overrides it), resolved from the current
127
+ * request and skipped automatically outside request scope.
128
+ *
129
+ * Switch drivers with {@link use}; bind a request with {@link forRequest} (the
130
+ * guard does this). Evaluation NEVER throws — the driver returns the fallback
131
+ * on evaluation errors, and the service additionally absorbs any thrown error
132
+ * into the same fallback with a logged warning.
133
+ *
134
+ * `@Transient`: constructed synchronously (lazy-module-safe), a fresh instance
135
+ * per injection, and resolvable in and out of request scope — it never
136
+ * DI-injects `REQUEST_CONTEXT`, so it does not bubble to request scope.
137
+ */
138
+ declare class FeatureFlagsService {
139
+ private readonly registry;
140
+ private readonly options;
141
+ private readonly boundContext?;
142
+ private readonly logger;
143
+ private readonly manifest;
144
+ private readonly driver;
145
+ constructor(registry: FeatureFlagDriverRegistry, options: FeatureFlagsOptions, logger?: LoggerService, boundContext?: RequestContext | undefined, boundDriver?: FeatureFlagDriver);
146
+ /** The name of the driver this instance targets. */
147
+ get driverName(): string;
148
+ /**
149
+ * Switch to a different registered driver. Returns a NEW immutable instance
150
+ * bound to `name`; the original is unchanged. Throws if `name` is unknown.
151
+ */
152
+ use(name: string): FeatureFlagsService;
153
+ /**
154
+ * Bind a request context. Returns a NEW immutable instance whose evaluations
155
+ * merge `options.context(ctx)`. Used by {@link FeatureFlagGuard}; consumers
156
+ * resolved in request scope can also call it explicitly.
157
+ */
158
+ forRequest(ctx: RequestContext): FeatureFlagsService;
159
+ /** Evaluate a flag as a `boolean`. */
160
+ getBooleanValue(flagKey: FlagKey, defaultValue?: boolean, context?: FlagContext): Promise<boolean>;
161
+ /** Evaluate a flag as a `string`. */
162
+ getStringValue(flagKey: FlagKey, defaultValue?: string, context?: FlagContext): Promise<string>;
163
+ /** Evaluate a flag as a `number`. */
164
+ getNumberValue(flagKey: FlagKey, defaultValue?: number, context?: FlagContext): Promise<number>;
165
+ /** Evaluate a flag as a typed object. */
166
+ getObjectValue<T extends object>(flagKey: FlagKey, defaultValue?: T, context?: FlagContext): Promise<T>;
167
+ /** Evaluate a `boolean` flag with synthesized evaluation metadata. */
168
+ getBooleanDetails(flagKey: FlagKey, defaultValue?: boolean, context?: FlagContext): Promise<FlagEvaluationDetails<boolean>>;
169
+ /** Evaluate a `string` flag with synthesized evaluation metadata. */
170
+ getStringDetails(flagKey: FlagKey, defaultValue?: string, context?: FlagContext): Promise<FlagEvaluationDetails<string>>;
171
+ /** Evaluate a `number` flag with synthesized evaluation metadata. */
172
+ getNumberDetails(flagKey: FlagKey, defaultValue?: number, context?: FlagContext): Promise<FlagEvaluationDetails<number>>;
173
+ /** Evaluate a typed object flag with synthesized evaluation metadata. */
174
+ getObjectDetails<T extends object>(flagKey: FlagKey, defaultValue?: T, context?: FlagContext): Promise<FlagEvaluationDetails<T>>;
175
+ /**
176
+ * Evaluate every flag declared in the manifest and return a `{ key: value }`
177
+ * map. The evaluation method is chosen from each declared default's type.
178
+ * A throwing context resolver falls back to the manifest defaults rather than
179
+ * taking the batch down.
180
+ */
181
+ all(context?: FlagContext): Promise<Record<string, FlagValue>>;
182
+ /** Immutable clone with a different driver and/or bound context. */
183
+ private clone;
184
+ /** Resolve the merged evaluation context (default context + per-call override). */
185
+ private context;
186
+ /** Pick the default: explicit arg, then manifest, then the type's zero value. */
187
+ private fallback;
188
+ /** Evaluate a single flag, choosing the method from the declared default's type. */
189
+ private evaluate;
190
+ /**
191
+ * Run an evaluation and absorb any failure into the fallback. A flag lookup
192
+ * must never take the caller down with it.
193
+ */
194
+ private safe;
195
+ private details;
196
+ private errorDetails;
197
+ }
198
+ //#endregion
199
+ //#region src/drivers/memory.driver.d.ts
200
+ interface MemoryFlagDriverOptions {
201
+ /** Driver name used for `use(name)` / the default-driver selection. Default `"memory"`. */
202
+ name?: string;
203
+ /** Initial flag values. */
204
+ values?: FlagManifest;
205
+ }
206
+ /**
207
+ * In-memory {@link FeatureFlagDriver}: the default driver shipped in-package
208
+ * and the test fake in one. Reads from an internal `Map` seeded from options
209
+ * or mutated with {@link set}/{@link reset} — the memory driver ignores the
210
+ * evaluation context (it does no targeting). An unknown key returns the
211
+ * caller's `fallback`, exactly like a remote driver that can't resolve it.
212
+ */
213
+ declare class MemoryFlagDriver implements FeatureFlagDriver {
214
+ readonly name: string;
215
+ private readonly store;
216
+ constructor(options?: MemoryFlagDriverOptions);
217
+ /** Set a flag value. Chainable. */
218
+ set(key: string, value: FlagValue): this;
219
+ /** Remove a flag (subsequent reads return the caller's fallback). Chainable. */
220
+ delete(key: string): this;
221
+ /** True when `key` has a stored value. */
222
+ has(key: string): boolean;
223
+ /** Clear all flags, then optionally seed a fresh set. Chainable. */
224
+ reset(values?: FlagManifest): this;
225
+ getBoolean(key: string, fallback: boolean, _ctx?: FlagContext): Promise<boolean>;
226
+ getString(key: string, fallback: string, _ctx?: FlagContext): Promise<string>;
227
+ getNumber(key: string, fallback: number, _ctx?: FlagContext): Promise<number>;
228
+ getObject<T extends object>(key: string, fallback: T, _ctx?: FlagContext): Promise<T>;
229
+ private read;
230
+ }
231
+ /** Convenience factory for {@link MemoryFlagDriver}. */
232
+ declare function memoryFlagDriver(options?: MemoryFlagDriverOptions): MemoryFlagDriver;
233
+ //#endregion
234
+ export { FeatureFlagDriverRegistry as a, FeatureFlagsOptions as c, FlagEvaluationReason as d, FlagKey as f, FeatureFlagDriver as h, FeatureFlagsService as i, FlagContext as l, FlagValue as m, MemoryFlagDriverOptions as n, buildDriverRegistry as o, FlagManifest as p, memoryFlagDriver as r, FeatureFlagRegistry as s, MemoryFlagDriver as t, FlagEvaluationDetails as u };
235
+ //# sourceMappingURL=memory.driver-C-5Y2b7_.d.ts.map
@@ -1,13 +1,10 @@
1
- import { MemoryFlagDriver } from '../drivers/memory.driver';
2
- import { FeatureFlagsService } from '../feature-flags.service';
3
- import type { FeatureFlagsOptions, FlagManifest } from '../feature-flags.types';
4
- export { MemoryFlagDriver, memoryFlagDriver } from '../drivers/memory.driver';
5
- export type { MemoryFlagDriverOptions } from '../drivers/memory.driver';
6
- export interface TestFeatureFlags {
7
- /** A real service bound to the memory driver below. */
8
- service: FeatureFlagsService;
9
- /** The backing memory driver — mutate flags with `.set()`/`.reset()`. */
10
- driver: MemoryFlagDriver;
1
+ import { c as FeatureFlagsOptions, i as FeatureFlagsService, n as MemoryFlagDriverOptions, p as FlagManifest, r as memoryFlagDriver, t as MemoryFlagDriver } from "../memory.driver-C-5Y2b7_.js";
2
+ //#region src/testing/index.d.ts
3
+ interface TestFeatureFlags {
4
+ /** A real service bound to the memory driver below. */
5
+ service: FeatureFlagsService;
6
+ /** The backing memory driver — mutate flags with `.set()`/`.reset()`. */
7
+ driver: MemoryFlagDriver;
11
8
  }
12
9
  /**
13
10
  * Build a real {@link FeatureFlagsService} over an in-memory driver, no DI /
@@ -22,4 +19,7 @@ export interface TestFeatureFlags {
22
19
  * expect(await service.getBooleanValue('new-checkout')).toBe(false);
23
20
  * ```
24
21
  */
25
- export declare function createTestFeatureFlags(values?: FlagManifest, options?: Pick<FeatureFlagsOptions, 'manifest' | 'context'>): TestFeatureFlags;
22
+ declare function createTestFeatureFlags(values?: FlagManifest, options?: Pick<FeatureFlagsOptions, 'manifest' | 'context'>): TestFeatureFlags;
23
+ //#endregion
24
+ export { MemoryFlagDriver, type MemoryFlagDriverOptions, TestFeatureFlags, createTestFeatureFlags, memoryFlagDriver };
25
+ //# sourceMappingURL=index.d.ts.map
@@ -1,36 +1,30 @@
1
- // @velajs/feature-flags/testing the memory driver IS the fake. This subpath
2
- // re-exports it plus a zero-DI helper for unit tests that want a real
3
- // FeatureFlagsService over an in-memory driver without bootstrapping a module.
4
- import { FeatureFlagDriverRegistry } from "../drivers/registry.js";
5
- import { MemoryFlagDriver } from "../drivers/memory.driver.js";
6
- import { FeatureFlagsService } from "../feature-flags.service.js";
7
- export { MemoryFlagDriver, memoryFlagDriver } from "../drivers/memory.driver.js";
1
+ import { c as MemoryFlagDriver, l as memoryFlagDriver, o as FeatureFlagDriverRegistry, t as FeatureFlagsService } from "../feature-flags.service-CNOZ1DhZ.js";
2
+ //#region src/testing/index.ts
8
3
  /**
9
- * Build a real {@link FeatureFlagsService} over an in-memory driver, no DI /
10
- * app bootstrap required. Flip flags on the returned `driver` and assert on the
11
- * `service`.
12
- *
13
- * @example
14
- * ```ts
15
- * const { service, driver } = createTestFeatureFlags({ 'new-checkout': true });
16
- * expect(await service.getBooleanValue('new-checkout')).toBe(true);
17
- * driver.set('new-checkout', false);
18
- * expect(await service.getBooleanValue('new-checkout')).toBe(false);
19
- * ```
20
- */ export function createTestFeatureFlags(values = {}, options = {}) {
21
- const driver = new MemoryFlagDriver({
22
- values
23
- });
24
- const registry = new FeatureFlagDriverRegistry([
25
- driver
26
- ], driver.name);
27
- const service = new FeatureFlagsService(registry, {
28
- default: driver.name,
29
- manifest: options.manifest,
30
- context: options.context
31
- });
32
- return {
33
- service,
34
- driver
35
- };
4
+ * Build a real {@link FeatureFlagsService} over an in-memory driver, no DI /
5
+ * app bootstrap required. Flip flags on the returned `driver` and assert on the
6
+ * `service`.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const { service, driver } = createTestFeatureFlags({ 'new-checkout': true });
11
+ * expect(await service.getBooleanValue('new-checkout')).toBe(true);
12
+ * driver.set('new-checkout', false);
13
+ * expect(await service.getBooleanValue('new-checkout')).toBe(false);
14
+ * ```
15
+ */
16
+ function createTestFeatureFlags(values = {}, options = {}) {
17
+ const driver = new MemoryFlagDriver({ values });
18
+ return {
19
+ service: new FeatureFlagsService(new FeatureFlagDriverRegistry([driver], driver.name), {
20
+ default: driver.name,
21
+ manifest: options.manifest,
22
+ context: options.context
23
+ }),
24
+ driver
25
+ };
36
26
  }
27
+ //#endregion
28
+ export { MemoryFlagDriver, createTestFeatureFlags, memoryFlagDriver };
29
+
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/testing/index.ts"],"sourcesContent":["// @velajs/feature-flags/testing — the memory driver IS the fake. This subpath\n// re-exports it plus a zero-DI helper for unit tests that want a real\n// FeatureFlagsService over an in-memory driver without bootstrapping a module.\n\nimport { FeatureFlagDriverRegistry } from '../drivers/registry';\nimport { MemoryFlagDriver } from '../drivers/memory.driver';\nimport { FeatureFlagsService } from '../feature-flags.service';\nimport type { FeatureFlagsOptions, FlagManifest } from '../feature-flags.types';\n\nexport { MemoryFlagDriver, memoryFlagDriver } from '../drivers/memory.driver';\nexport type { MemoryFlagDriverOptions } from '../drivers/memory.driver';\n\nexport interface TestFeatureFlags {\n /** A real service bound to the memory driver below. */\n service: FeatureFlagsService;\n /** The backing memory driver — mutate flags with `.set()`/`.reset()`. */\n driver: MemoryFlagDriver;\n}\n\n/**\n * Build a real {@link FeatureFlagsService} over an in-memory driver, no DI /\n * app bootstrap required. Flip flags on the returned `driver` and assert on the\n * `service`.\n *\n * @example\n * ```ts\n * const { service, driver } = createTestFeatureFlags({ 'new-checkout': true });\n * expect(await service.getBooleanValue('new-checkout')).toBe(true);\n * driver.set('new-checkout', false);\n * expect(await service.getBooleanValue('new-checkout')).toBe(false);\n * ```\n */\nexport function createTestFeatureFlags(\n values: FlagManifest = {},\n options: Pick<FeatureFlagsOptions, 'manifest' | 'context'> = {},\n): TestFeatureFlags {\n const driver = new MemoryFlagDriver({ values });\n const registry = new FeatureFlagDriverRegistry([driver], driver.name);\n const service = new FeatureFlagsService(registry, {\n default: driver.name,\n manifest: options.manifest,\n context: options.context,\n });\n return { service, driver };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgCA,SAAgB,uBACd,SAAuB,CAAC,GACxB,UAA6D,CAAC,GAC5C;CAClB,MAAM,SAAS,IAAI,iBAAiB,EAAE,OAAO,CAAC;CAO9C,OAAO;EAAE,SAAA,IALW,oBAAoB,IADnB,0BAA0B,CAAC,MAAM,GAAG,OAAO,IACjB,GAAG;GAChD,SAAS,OAAO;GAChB,UAAU,QAAQ;GAClB,SAAS,QAAQ;EACnB,CACe;EAAG;CAAO;AAC3B"}
package/package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "name": "@velajs/feature-flags",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Edge-first, driver-based feature flags for the Vela framework",
5
+ "keywords": [
6
+ "cloudflare-workers",
7
+ "driver",
8
+ "edge",
9
+ "feature-flags",
10
+ "feature-toggles",
11
+ "framework",
12
+ "vela"
13
+ ],
14
+ "homepage": "https://github.com/velajs/feature-flags#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/velajs/feature-flags/issues"
17
+ },
18
+ "license": "MIT",
19
+ "author": "ksh",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/velajs/feature-flags.git"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "LICENSE",
28
+ "CHANGELOG.md"
29
+ ],
5
30
  "type": "module",
31
+ "sideEffects": false,
6
32
  "main": "./dist/index.js",
7
33
  "types": "./dist/index.d.ts",
8
34
  "exports": {
@@ -15,52 +41,40 @@
15
41
  "import": "./dist/testing/index.js"
16
42
  }
17
43
  },
18
- "files": [
19
- "dist",
20
- "README.md",
21
- "LICENSE",
22
- "CHANGELOG.md"
23
- ],
24
- "sideEffects": false,
25
- "keywords": [
26
- "vela",
27
- "feature-flags",
28
- "feature-toggles",
29
- "driver",
30
- "edge",
31
- "cloudflare-workers",
32
- "framework"
33
- ],
34
- "author": "ksh",
35
- "license": "MIT",
36
- "repository": {
37
- "type": "git",
38
- "url": "git+https://github.com/velajs/feature-flags.git"
39
- },
40
- "homepage": "https://github.com/velajs/feature-flags#readme",
41
- "bugs": {
42
- "url": "https://github.com/velajs/feature-flags/issues"
43
- },
44
- "engines": {
45
- "node": ">=20"
46
- },
47
- "peerDependencies": {
48
- "@velajs/vela": ">=1.11.0",
49
- "hono": ">=4"
50
- },
51
44
  "devDependencies": {
52
- "@swc/cli": "^0.8.1",
45
+ "@arethetypeswrong/cli": "^0.18.5",
46
+ "@changesets/cli": "^2.31.0",
53
47
  "@swc/core": "^1.15.43",
54
48
  "@velajs/testing": "^0.4.0",
55
49
  "@velajs/vela": "^1.12.0",
56
50
  "hono": "^4.12.27",
57
- "typescript": "^6.0.3",
51
+ "oxfmt": "^0.58.0",
52
+ "oxlint": "^1.73.0",
53
+ "publint": "^0.3.21",
54
+ "tsdown": "^0.22.4",
55
+ "typescript": "^7.0.2",
58
56
  "unplugin-swc": "^1.5.9",
59
- "vitest": "^4.1.9"
57
+ "vitest": "^4.1.10"
58
+ },
59
+ "peerDependencies": {
60
+ "@velajs/vela": ">=1.11.0",
61
+ "hono": ">=4"
62
+ },
63
+ "engines": {
64
+ "node": ">=24"
60
65
  },
61
66
  "scripts": {
62
- "build": "rm -rf dist && swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
67
+ "build": "tsdown",
63
68
  "test": "vitest run",
64
- "typecheck": "tsc --noEmit"
69
+ "typecheck": "tsc --noEmit",
70
+ "lint": "oxlint .",
71
+ "format": "oxfmt .",
72
+ "format:check": "oxfmt --check .",
73
+ "publint": "publint",
74
+ "attw": "attw --pack . --profile esm-only",
75
+ "changeset": "changeset",
76
+ "version-packages": "changeset version",
77
+ "release": "pnpm build && changeset publish",
78
+ "verify": "pnpm lint && pnpm format:check && pnpm build && pnpm typecheck && pnpm test && pnpm publint && pnpm attw"
65
79
  }
66
80
  }
@@ -1,35 +0,0 @@
1
- import type { FlagKey } from '../feature-flags.types';
2
- /** What the {@link FeatureFlagGuard} does when a gated flag is off. */
3
- export type FeatureFlagDisabledBehavior = 'notFound' | 'forbidden';
4
- export interface FeatureFlagOptions {
5
- /**
6
- * Response when the flag is off. `'notFound'` (default) hides the route
7
- * entirely (404); `'forbidden'` reveals it exists but denies access (403).
8
- */
9
- onDisabled?: FeatureFlagDisabledBehavior;
10
- }
11
- /** The metadata `@FeatureFlag()` attaches, read by {@link FeatureFlagGuard}. */
12
- export interface FeatureFlagMetadata {
13
- key: string;
14
- onDisabled: FeatureFlagDisabledBehavior;
15
- }
16
- export declare const FEATURE_FLAG_METADATA = "vela:feature-flags:flag";
17
- /**
18
- * Gate a route (handler) or controller behind a boolean feature flag. Pair
19
- * with {@link FeatureFlagGuard} (via `@UseGuards` or the module's `isGlobal`
20
- * app-wide registration).
21
- *
22
- * @example
23
- * ```ts
24
- * @UseGuards(FeatureFlagGuard)
25
- * @Controller('/checkout')
26
- * class CheckoutController {
27
- * @FeatureFlag('new-checkout') // 404 when off
28
- * @Get('/v2') v2() { ... }
29
- *
30
- * @FeatureFlag('beta', { onDisabled: 'forbidden' }) // 403 when off
31
- * @Get('/beta') beta() { ... }
32
- * }
33
- * ```
34
- */
35
- export declare function FeatureFlag(key: FlagKey, options?: FeatureFlagOptions): (target: object, propertyKey?: string | symbol) => void;