@mergedapp/feature-flags 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +651 -0
- package/dist/cjs/cli/audit.js +117 -0
- package/dist/cjs/cli/cleanup.js +105 -0
- package/dist/cjs/cli/config-loader.js +102 -0
- package/dist/cjs/cli/generate.js +194 -0
- package/dist/cjs/cli/parse-args.js +18 -0
- package/dist/cjs/cli.js +46 -0
- package/dist/cjs/client.js +505 -0
- package/dist/cjs/errors.js +24 -0
- package/dist/cjs/index.js +13 -0
- package/dist/cjs/jwt.js +85 -0
- package/dist/cjs/nestjs/bindings.js +36 -0
- package/dist/cjs/nestjs/constants.js +7 -0
- package/dist/cjs/nestjs/context.js +28 -0
- package/dist/cjs/nestjs/decorators.js +50 -0
- package/dist/cjs/nestjs/errors.js +25 -0
- package/dist/cjs/nestjs/evaluator.js +87 -0
- package/dist/cjs/nestjs/guard.js +67 -0
- package/dist/cjs/nestjs/interceptor.js +56 -0
- package/dist/cjs/nestjs/module.js +70 -0
- package/dist/cjs/nestjs/service.js +54 -0
- package/dist/cjs/nestjs/types.js +2 -0
- package/dist/cjs/nestjs.js +26 -0
- package/dist/cjs/openfeature/context.js +166 -0
- package/dist/cjs/openfeature/hooks.js +31 -0
- package/dist/cjs/openfeature/server-provider.js +107 -0
- package/dist/cjs/openfeature/server.js +13 -0
- package/dist/cjs/openfeature/shared.js +83 -0
- package/dist/cjs/openfeature/web-provider.js +156 -0
- package/dist/cjs/openfeature/web.js +13 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/persistence.js +249 -0
- package/dist/cjs/react/hooks.js +86 -0
- package/dist/cjs/react/provider.js +106 -0
- package/dist/cjs/react.js +7 -0
- package/dist/cjs/remote-evaluator.js +162 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cli/audit.d.ts +3 -0
- package/dist/cli/audit.js +114 -0
- package/dist/cli/cleanup.d.ts +3 -0
- package/dist/cli/cleanup.js +102 -0
- package/dist/cli/config-loader.d.ts +26 -0
- package/dist/cli/config-loader.js +66 -0
- package/dist/cli/generate.d.ts +3 -0
- package/dist/cli/generate.js +191 -0
- package/dist/cli/parse-args.d.ts +1 -0
- package/dist/cli/parse-args.js +15 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +45 -0
- package/dist/client.d.ts +67 -0
- package/dist/client.js +501 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +18 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/jwt.d.ts +20 -0
- package/dist/jwt.js +78 -0
- package/dist/nestjs/bindings.d.ts +5 -0
- package/dist/nestjs/bindings.js +33 -0
- package/dist/nestjs/constants.d.ts +4 -0
- package/dist/nestjs/constants.js +4 -0
- package/dist/nestjs/context.d.ts +12 -0
- package/dist/nestjs/context.js +24 -0
- package/dist/nestjs/decorators.d.ts +4 -0
- package/dist/nestjs/decorators.js +45 -0
- package/dist/nestjs/errors.d.ts +12 -0
- package/dist/nestjs/errors.js +20 -0
- package/dist/nestjs/evaluator.d.ts +17 -0
- package/dist/nestjs/evaluator.js +83 -0
- package/dist/nestjs/guard.d.ts +19 -0
- package/dist/nestjs/guard.js +63 -0
- package/dist/nestjs/interceptor.d.ts +10 -0
- package/dist/nestjs/interceptor.js +53 -0
- package/dist/nestjs/module.d.ts +6 -0
- package/dist/nestjs/module.js +67 -0
- package/dist/nestjs/service.d.ts +30 -0
- package/dist/nestjs/service.js +51 -0
- package/dist/nestjs/types.d.ts +100 -0
- package/dist/nestjs/types.js +1 -0
- package/dist/nestjs.cjs +1 -0
- package/dist/nestjs.d.ts +10 -0
- package/dist/nestjs.js +9 -0
- package/dist/openfeature/context.d.ts +10 -0
- package/dist/openfeature/context.js +160 -0
- package/dist/openfeature/hooks.d.ts +6 -0
- package/dist/openfeature/hooks.js +27 -0
- package/dist/openfeature/server-provider.d.ts +20 -0
- package/dist/openfeature/server-provider.js +102 -0
- package/dist/openfeature/server.cjs +1 -0
- package/dist/openfeature/server.d.ts +3 -0
- package/dist/openfeature/server.js +3 -0
- package/dist/openfeature/shared.d.ts +37 -0
- package/dist/openfeature/shared.js +74 -0
- package/dist/openfeature/web-provider.d.ts +27 -0
- package/dist/openfeature/web-provider.js +151 -0
- package/dist/openfeature/web.cjs +1 -0
- package/dist/openfeature/web.d.ts +3 -0
- package/dist/openfeature/web.js +3 -0
- package/dist/persistence.d.ts +39 -0
- package/dist/persistence.js +203 -0
- package/dist/react/hooks.d.ts +52 -0
- package/dist/react/hooks.js +78 -0
- package/dist/react/provider.d.ts +71 -0
- package/dist/react/provider.js +99 -0
- package/dist/react.cjs +1 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +2 -0
- package/dist/remote-evaluator.d.ts +28 -0
- package/dist/remote-evaluator.js +158 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +1 -0
- package/featureflags.config.schema.json +38 -0
- package/package.json +107 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.FeatureFlagsRequestContextStore = void 0;
|
|
10
|
+
exports.resolveFeatureFlagContext = resolveFeatureFlagContext;
|
|
11
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
12
|
+
const common_1 = require("@nestjs/common");
|
|
13
|
+
let FeatureFlagsRequestContextStore = class FeatureFlagsRequestContextStore {
|
|
14
|
+
storage = new node_async_hooks_1.AsyncLocalStorage();
|
|
15
|
+
getContext() {
|
|
16
|
+
return this.storage.getStore() ?? null;
|
|
17
|
+
}
|
|
18
|
+
run(context, callback) {
|
|
19
|
+
return this.storage.run(context ?? null, callback);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
exports.FeatureFlagsRequestContextStore = FeatureFlagsRequestContextStore;
|
|
23
|
+
exports.FeatureFlagsRequestContextStore = FeatureFlagsRequestContextStore = __decorate([
|
|
24
|
+
(0, common_1.Injectable)()
|
|
25
|
+
], FeatureFlagsRequestContextStore);
|
|
26
|
+
async function resolveFeatureFlagContext(params) {
|
|
27
|
+
return params.contextFactory ? params.contextFactory(params.context) : undefined;
|
|
28
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FeatureFlagContext = void 0;
|
|
4
|
+
exports.RequireFeatureFlag = RequireFeatureFlag;
|
|
5
|
+
exports.FeatureFlagGate = FeatureFlagGate;
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const constants_js_1 = require("./constants.js");
|
|
8
|
+
const errors_js_1 = require("./errors.js");
|
|
9
|
+
const guard_js_1 = require("./guard.js");
|
|
10
|
+
function RequireFeatureFlag(options) {
|
|
11
|
+
return (0, common_1.applyDecorators)((0, common_1.SetMetadata)(constants_js_1.REQUIRE_FEATURE_FLAG_KEY, options), (0, common_1.UseGuards)(guard_js_1.FeatureFlagGuard));
|
|
12
|
+
}
|
|
13
|
+
function FeatureFlagGate(options) {
|
|
14
|
+
return (_target, _propertyKey, descriptor) => {
|
|
15
|
+
const original = descriptor.value;
|
|
16
|
+
if (typeof original !== "function") {
|
|
17
|
+
return descriptor;
|
|
18
|
+
}
|
|
19
|
+
descriptor.value = async function wrappedFeatureFlagGate(...args) {
|
|
20
|
+
const serviceProperty = options.serviceProperty ?? "featureFlags";
|
|
21
|
+
const featureFlagsService = this[serviceProperty];
|
|
22
|
+
if (!isFeatureFlagsService(featureFlagsService)) {
|
|
23
|
+
throw new TypeError(`FeatureFlagGate expected a FeatureFlagsService instance on this.${serviceProperty}.`);
|
|
24
|
+
}
|
|
25
|
+
const context = options.resolveContext ? await options.resolveContext(this, args) : options.context;
|
|
26
|
+
const allowed = await (0, guard_js_1.evaluateGate)({
|
|
27
|
+
context,
|
|
28
|
+
defaultValue: options.defaultValue ?? false,
|
|
29
|
+
expectedValue: options.expectedValue,
|
|
30
|
+
featureFlagsService,
|
|
31
|
+
flagKey: options.flagKey,
|
|
32
|
+
});
|
|
33
|
+
if (!allowed) {
|
|
34
|
+
throw new errors_js_1.FeatureFlagGateError({ flagKey: options.flagKey });
|
|
35
|
+
}
|
|
36
|
+
return original.apply(this, args);
|
|
37
|
+
};
|
|
38
|
+
return descriptor;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
exports.FeatureFlagContext = (0, common_1.createParamDecorator)((_data, ctx) => {
|
|
42
|
+
const request = ctx.switchToHttp().getRequest();
|
|
43
|
+
return request?.[constants_js_1.FEATURE_FLAGS_REQUEST_CONTEXT] ?? null;
|
|
44
|
+
});
|
|
45
|
+
function isFeatureFlagsService(value) {
|
|
46
|
+
return (typeof value === "object" &&
|
|
47
|
+
value !== null &&
|
|
48
|
+
"getDetails" in value &&
|
|
49
|
+
typeof value.getDetails === "function");
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FeatureFlagGateError = exports.FeatureFlagDisabledHttpException = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
class FeatureFlagDisabledHttpException extends common_1.HttpException {
|
|
6
|
+
constructor({ flagKey, message }) {
|
|
7
|
+
super({
|
|
8
|
+
code: "FEATURE_FLAG_DISABLED",
|
|
9
|
+
error: "FEATURE_FLAG_DISABLED",
|
|
10
|
+
message: message ?? `Feature flag (${flagKey}) is disabled for this request.`,
|
|
11
|
+
statusCode: common_1.HttpStatus.FORBIDDEN,
|
|
12
|
+
}, common_1.HttpStatus.FORBIDDEN);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.FeatureFlagDisabledHttpException = FeatureFlagDisabledHttpException;
|
|
16
|
+
class FeatureFlagGateError extends FeatureFlagDisabledHttpException {
|
|
17
|
+
constructor({ flagKey }) {
|
|
18
|
+
super({
|
|
19
|
+
flagKey,
|
|
20
|
+
message: `Feature flag (${flagKey}) blocked this service method.`,
|
|
21
|
+
});
|
|
22
|
+
this.name = "FeatureFlagGateError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.FeatureFlagGateError = FeatureFlagGateError;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NativeNestFeatureFlagsEvaluator = void 0;
|
|
4
|
+
const remote_evaluator_js_1 = require("../remote-evaluator.js");
|
|
5
|
+
class NativeNestFeatureFlagsEvaluator {
|
|
6
|
+
options;
|
|
7
|
+
evaluator;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.evaluator = new remote_evaluator_js_1.RemoteFeatureFlagEvaluator(options);
|
|
11
|
+
}
|
|
12
|
+
async getDetails(request) {
|
|
13
|
+
const evaluation = await this.evaluator.evaluate(request.context ?? null);
|
|
14
|
+
const flag = findEvaluatedFlag({
|
|
15
|
+
flags: evaluation.flags,
|
|
16
|
+
flagIds: this.options.flagIds,
|
|
17
|
+
flagKey: request.flagKey,
|
|
18
|
+
});
|
|
19
|
+
if (!flag) {
|
|
20
|
+
return {
|
|
21
|
+
errorCode: "FLAG_NOT_FOUND",
|
|
22
|
+
errorMessage: `Feature flag (${request.flagKey}) was not found.`,
|
|
23
|
+
flagKey: request.flagKey,
|
|
24
|
+
flagMetadata: {},
|
|
25
|
+
reason: "ERROR",
|
|
26
|
+
value: request.defaultValue,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (!matchesExpectedType({
|
|
30
|
+
defaultValue: request.defaultValue,
|
|
31
|
+
flag,
|
|
32
|
+
})) {
|
|
33
|
+
return {
|
|
34
|
+
errorCode: "TYPE_MISMATCH",
|
|
35
|
+
errorMessage: `Feature flag (${flag.name}) resolved as ${flag.type}, which does not match the requested default value type.`,
|
|
36
|
+
flagKey: request.flagKey,
|
|
37
|
+
flagMetadata: createEvaluationMetadata(flag),
|
|
38
|
+
reason: "ERROR",
|
|
39
|
+
value: request.defaultValue,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
flagKey: request.flagKey,
|
|
44
|
+
flagMetadata: createEvaluationMetadata(flag),
|
|
45
|
+
reason: flag.enabled ? "TARGETING_MATCH" : "DISABLED",
|
|
46
|
+
value: flag.value,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.NativeNestFeatureFlagsEvaluator = NativeNestFeatureFlagsEvaluator;
|
|
51
|
+
function createEvaluationMetadata(flag) {
|
|
52
|
+
return {
|
|
53
|
+
enabled: flag.enabled,
|
|
54
|
+
id: flag.id,
|
|
55
|
+
name: flag.name,
|
|
56
|
+
teamId: flag.teamId ?? undefined,
|
|
57
|
+
type: flag.type,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function findEvaluatedFlag(params) {
|
|
61
|
+
const resolvedId = params.flagIds?.[params.flagKey];
|
|
62
|
+
if (resolvedId) {
|
|
63
|
+
const matchedByResolvedId = params.flags.find((flag) => flag.id === resolvedId);
|
|
64
|
+
if (matchedByResolvedId) {
|
|
65
|
+
return matchedByResolvedId;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return (params.flags.find((flag) => flag.id === params.flagKey) ?? params.flags.find((flag) => flag.name === params.flagKey));
|
|
69
|
+
}
|
|
70
|
+
function matchesExpectedType(params) {
|
|
71
|
+
if (typeof params.defaultValue === "boolean") {
|
|
72
|
+
return params.flag.type === "BOOLEAN";
|
|
73
|
+
}
|
|
74
|
+
if (typeof params.defaultValue === "string") {
|
|
75
|
+
return params.flag.type === "STRING";
|
|
76
|
+
}
|
|
77
|
+
if (typeof params.defaultValue === "number") {
|
|
78
|
+
return params.flag.type === "NUMBER";
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(params.defaultValue)) {
|
|
81
|
+
return params.flag.type === "ARRAY";
|
|
82
|
+
}
|
|
83
|
+
return params.flag.type === "JSON" && isObjectLikeValue(params.defaultValue);
|
|
84
|
+
}
|
|
85
|
+
function isObjectLikeValue(value) {
|
|
86
|
+
return value === null || typeof value === "object";
|
|
87
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.FeatureFlagGuard = void 0;
|
|
13
|
+
exports.evaluateGate = evaluateGate;
|
|
14
|
+
const common_1 = require("@nestjs/common");
|
|
15
|
+
const core_1 = require("@nestjs/core");
|
|
16
|
+
const constants_js_1 = require("./constants.js");
|
|
17
|
+
const context_js_1 = require("./context.js");
|
|
18
|
+
const errors_js_1 = require("./errors.js");
|
|
19
|
+
const service_js_1 = require("./service.js");
|
|
20
|
+
let FeatureFlagGuard = class FeatureFlagGuard {
|
|
21
|
+
reflector;
|
|
22
|
+
featureFlagsService;
|
|
23
|
+
constructor(reflector, featureFlagsService) {
|
|
24
|
+
this.reflector = reflector;
|
|
25
|
+
this.featureFlagsService = featureFlagsService;
|
|
26
|
+
}
|
|
27
|
+
async canActivate(context) {
|
|
28
|
+
const metadata = this.reflector.getAllAndOverride(constants_js_1.REQUIRE_FEATURE_FLAG_KEY, [context.getHandler(), context.getClass()]);
|
|
29
|
+
if (!metadata) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
const evaluationContext = metadata.contextFactory
|
|
33
|
+
? await (0, context_js_1.resolveFeatureFlagContext)({
|
|
34
|
+
context,
|
|
35
|
+
contextFactory: metadata.contextFactory,
|
|
36
|
+
})
|
|
37
|
+
: metadata.context;
|
|
38
|
+
const allowed = await evaluateGate({
|
|
39
|
+
context: evaluationContext,
|
|
40
|
+
defaultValue: metadata.defaultValue ?? false,
|
|
41
|
+
expectedValue: metadata.expectedValue,
|
|
42
|
+
featureFlagsService: this.featureFlagsService,
|
|
43
|
+
flagKey: metadata.flagKey,
|
|
44
|
+
});
|
|
45
|
+
if (!allowed) {
|
|
46
|
+
throw new errors_js_1.FeatureFlagDisabledHttpException({ flagKey: metadata.flagKey });
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
exports.FeatureFlagGuard = FeatureFlagGuard;
|
|
52
|
+
exports.FeatureFlagGuard = FeatureFlagGuard = __decorate([
|
|
53
|
+
(0, common_1.Injectable)(),
|
|
54
|
+
__metadata("design:paramtypes", [core_1.Reflector,
|
|
55
|
+
service_js_1.FeatureFlagsService])
|
|
56
|
+
], FeatureFlagGuard);
|
|
57
|
+
async function evaluateGate(params) {
|
|
58
|
+
const details = await params.featureFlagsService.getDetails({
|
|
59
|
+
context: params.context,
|
|
60
|
+
defaultValue: params.defaultValue,
|
|
61
|
+
flagKey: params.flagKey,
|
|
62
|
+
});
|
|
63
|
+
if (typeof params.expectedValue === "undefined") {
|
|
64
|
+
return Boolean(details.value);
|
|
65
|
+
}
|
|
66
|
+
return Object.is(details.value, params.expectedValue);
|
|
67
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.FeatureFlagContextInterceptor = void 0;
|
|
16
|
+
const rxjs_1 = require("rxjs");
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const constants_js_1 = require("./constants.js");
|
|
19
|
+
const context_js_1 = require("./context.js");
|
|
20
|
+
let FeatureFlagContextInterceptor = class FeatureFlagContextInterceptor {
|
|
21
|
+
options;
|
|
22
|
+
requestContextStore;
|
|
23
|
+
constructor(options, requestContextStore) {
|
|
24
|
+
this.options = options;
|
|
25
|
+
this.requestContextStore = requestContextStore;
|
|
26
|
+
}
|
|
27
|
+
async intercept(context, next) {
|
|
28
|
+
const evaluationContext = await (0, context_js_1.resolveFeatureFlagContext)({
|
|
29
|
+
context,
|
|
30
|
+
contextFactory: this.options.contextFactory,
|
|
31
|
+
});
|
|
32
|
+
const request = context.switchToHttp().getRequest();
|
|
33
|
+
if (request) {
|
|
34
|
+
request[constants_js_1.FEATURE_FLAGS_REQUEST_CONTEXT] = evaluationContext ?? null;
|
|
35
|
+
}
|
|
36
|
+
return new rxjs_1.Observable((subscriber) => {
|
|
37
|
+
let innerSubscription;
|
|
38
|
+
this.requestContextStore.run(evaluationContext ?? null, () => {
|
|
39
|
+
innerSubscription = next.handle().subscribe({
|
|
40
|
+
complete: () => subscriber.complete(),
|
|
41
|
+
error: (error) => subscriber.error(error),
|
|
42
|
+
next: (value) => subscriber.next(value),
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
return () => {
|
|
46
|
+
innerSubscription?.unsubscribe();
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
exports.FeatureFlagContextInterceptor = FeatureFlagContextInterceptor;
|
|
52
|
+
exports.FeatureFlagContextInterceptor = FeatureFlagContextInterceptor = __decorate([
|
|
53
|
+
(0, common_1.Injectable)(),
|
|
54
|
+
__param(0, (0, common_1.Inject)(constants_js_1.FEATURE_FLAGS_MODULE_OPTIONS)),
|
|
55
|
+
__metadata("design:paramtypes", [Object, context_js_1.FeatureFlagsRequestContextStore])
|
|
56
|
+
], FeatureFlagContextInterceptor);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.FeatureFlagsModule = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const core_1 = require("@nestjs/core");
|
|
12
|
+
const constants_js_1 = require("./constants.js");
|
|
13
|
+
const context_js_1 = require("./context.js");
|
|
14
|
+
const evaluator_js_1 = require("./evaluator.js");
|
|
15
|
+
const guard_js_1 = require("./guard.js");
|
|
16
|
+
const interceptor_js_1 = require("./interceptor.js");
|
|
17
|
+
const service_js_1 = require("./service.js");
|
|
18
|
+
let FeatureFlagsModule = class FeatureFlagsModule {
|
|
19
|
+
static forRoot(options) {
|
|
20
|
+
return createFeatureFlagsModule({
|
|
21
|
+
provideOptions: {
|
|
22
|
+
provide: constants_js_1.FEATURE_FLAGS_MODULE_OPTIONS,
|
|
23
|
+
useValue: options,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
static forRootAsync(options) {
|
|
28
|
+
return createFeatureFlagsModule({
|
|
29
|
+
imports: options.imports ?? [],
|
|
30
|
+
provideOptions: {
|
|
31
|
+
provide: constants_js_1.FEATURE_FLAGS_MODULE_OPTIONS,
|
|
32
|
+
inject: options.inject ?? [],
|
|
33
|
+
useFactory: options.useFactory,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
exports.FeatureFlagsModule = FeatureFlagsModule;
|
|
39
|
+
exports.FeatureFlagsModule = FeatureFlagsModule = __decorate([
|
|
40
|
+
(0, common_1.Module)({})
|
|
41
|
+
], FeatureFlagsModule);
|
|
42
|
+
function createFeatureFlagsModule(params) {
|
|
43
|
+
return {
|
|
44
|
+
module: FeatureFlagsModule,
|
|
45
|
+
imports: params.imports ?? [],
|
|
46
|
+
providers: [
|
|
47
|
+
params.provideOptions,
|
|
48
|
+
context_js_1.FeatureFlagsRequestContextStore,
|
|
49
|
+
service_js_1.FeatureFlagsService,
|
|
50
|
+
guard_js_1.FeatureFlagGuard,
|
|
51
|
+
interceptor_js_1.FeatureFlagContextInterceptor,
|
|
52
|
+
{
|
|
53
|
+
provide: constants_js_1.FEATURE_FLAGS_EVALUATOR,
|
|
54
|
+
inject: [constants_js_1.FEATURE_FLAGS_MODULE_OPTIONS],
|
|
55
|
+
useFactory: (options) => new evaluator_js_1.NativeNestFeatureFlagsEvaluator(options),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
provide: core_1.APP_INTERCEPTOR,
|
|
59
|
+
inject: [constants_js_1.FEATURE_FLAGS_MODULE_OPTIONS, interceptor_js_1.FeatureFlagContextInterceptor],
|
|
60
|
+
useFactory: (options, interceptor) => options.useGlobalInterceptor === false ? new PassthroughInterceptor() : interceptor,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
exports: [context_js_1.FeatureFlagsRequestContextStore, service_js_1.FeatureFlagsService, guard_js_1.FeatureFlagGuard, interceptor_js_1.FeatureFlagContextInterceptor],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
class PassthroughInterceptor {
|
|
67
|
+
intercept(_context, next) {
|
|
68
|
+
return next.handle();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.FeatureFlagsService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const constants_js_1 = require("./constants.js");
|
|
18
|
+
const context_js_1 = require("./context.js");
|
|
19
|
+
let FeatureFlagsService = class FeatureFlagsService {
|
|
20
|
+
evaluator;
|
|
21
|
+
requestContextStore;
|
|
22
|
+
constructor(evaluator, requestContextStore) {
|
|
23
|
+
this.evaluator = evaluator;
|
|
24
|
+
this.requestContextStore = requestContextStore;
|
|
25
|
+
}
|
|
26
|
+
getDetails(params) {
|
|
27
|
+
return this.evaluator.getDetails({
|
|
28
|
+
context: params.context ?? this.requestContextStore.getContext() ?? undefined,
|
|
29
|
+
defaultValue: params.defaultValue,
|
|
30
|
+
flagKey: params.flagKey,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async getValue(params) {
|
|
34
|
+
const details = await this.getDetails(params);
|
|
35
|
+
return details.value;
|
|
36
|
+
}
|
|
37
|
+
getBooleanDetails(params) {
|
|
38
|
+
return this.getDetails({
|
|
39
|
+
context: params.context,
|
|
40
|
+
defaultValue: (params.defaultValue ?? false),
|
|
41
|
+
flagKey: params.flagKey,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async isEnabled(params) {
|
|
45
|
+
const details = await this.getBooleanDetails(params);
|
|
46
|
+
return details.value;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
exports.FeatureFlagsService = FeatureFlagsService;
|
|
50
|
+
exports.FeatureFlagsService = FeatureFlagsService = __decorate([
|
|
51
|
+
(0, common_1.Injectable)(),
|
|
52
|
+
__param(0, (0, common_1.Inject)(constants_js_1.FEATURE_FLAGS_EVALUATOR)),
|
|
53
|
+
__metadata("design:paramtypes", [Object, context_js_1.FeatureFlagsRequestContextStore])
|
|
54
|
+
], FeatureFlagsService);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FeatureFlagsService = exports.FeatureFlagsModule = exports.FeatureFlagContextInterceptor = exports.evaluateGate = exports.FeatureFlagGuard = exports.NativeNestFeatureFlagsEvaluator = exports.FeatureFlagGateError = exports.FeatureFlagDisabledHttpException = exports.resolveFeatureFlagContext = exports.FeatureFlagsRequestContextStore = exports.RequireFeatureFlag = exports.FeatureFlagGate = exports.FeatureFlagContext = exports.createTypedNestjsBindings = void 0;
|
|
4
|
+
var bindings_js_1 = require("./nestjs/bindings.js");
|
|
5
|
+
Object.defineProperty(exports, "createTypedNestjsBindings", { enumerable: true, get: function () { return bindings_js_1.createTypedNestjsBindings; } });
|
|
6
|
+
var decorators_js_1 = require("./nestjs/decorators.js");
|
|
7
|
+
Object.defineProperty(exports, "FeatureFlagContext", { enumerable: true, get: function () { return decorators_js_1.FeatureFlagContext; } });
|
|
8
|
+
Object.defineProperty(exports, "FeatureFlagGate", { enumerable: true, get: function () { return decorators_js_1.FeatureFlagGate; } });
|
|
9
|
+
Object.defineProperty(exports, "RequireFeatureFlag", { enumerable: true, get: function () { return decorators_js_1.RequireFeatureFlag; } });
|
|
10
|
+
var context_js_1 = require("./nestjs/context.js");
|
|
11
|
+
Object.defineProperty(exports, "FeatureFlagsRequestContextStore", { enumerable: true, get: function () { return context_js_1.FeatureFlagsRequestContextStore; } });
|
|
12
|
+
Object.defineProperty(exports, "resolveFeatureFlagContext", { enumerable: true, get: function () { return context_js_1.resolveFeatureFlagContext; } });
|
|
13
|
+
var errors_js_1 = require("./nestjs/errors.js");
|
|
14
|
+
Object.defineProperty(exports, "FeatureFlagDisabledHttpException", { enumerable: true, get: function () { return errors_js_1.FeatureFlagDisabledHttpException; } });
|
|
15
|
+
Object.defineProperty(exports, "FeatureFlagGateError", { enumerable: true, get: function () { return errors_js_1.FeatureFlagGateError; } });
|
|
16
|
+
var evaluator_js_1 = require("./nestjs/evaluator.js");
|
|
17
|
+
Object.defineProperty(exports, "NativeNestFeatureFlagsEvaluator", { enumerable: true, get: function () { return evaluator_js_1.NativeNestFeatureFlagsEvaluator; } });
|
|
18
|
+
var guard_js_1 = require("./nestjs/guard.js");
|
|
19
|
+
Object.defineProperty(exports, "FeatureFlagGuard", { enumerable: true, get: function () { return guard_js_1.FeatureFlagGuard; } });
|
|
20
|
+
Object.defineProperty(exports, "evaluateGate", { enumerable: true, get: function () { return guard_js_1.evaluateGate; } });
|
|
21
|
+
var interceptor_js_1 = require("./nestjs/interceptor.js");
|
|
22
|
+
Object.defineProperty(exports, "FeatureFlagContextInterceptor", { enumerable: true, get: function () { return interceptor_js_1.FeatureFlagContextInterceptor; } });
|
|
23
|
+
var module_js_1 = require("./nestjs/module.js");
|
|
24
|
+
Object.defineProperty(exports, "FeatureFlagsModule", { enumerable: true, get: function () { return module_js_1.FeatureFlagsModule; } });
|
|
25
|
+
var service_js_1 = require("./nestjs/service.js");
|
|
26
|
+
Object.defineProperty(exports, "FeatureFlagsService", { enumerable: true, get: function () { return service_js_1.FeatureFlagsService; } });
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_TARGETING_KEY_ATTRIBUTE = void 0;
|
|
4
|
+
exports.mapOpenFeatureContextValue = mapOpenFeatureContextValue;
|
|
5
|
+
exports.createDefaultOpenFeatureContextMapper = createDefaultOpenFeatureContextMapper;
|
|
6
|
+
exports.mergeFeatureFlagEvaluationContexts = mergeFeatureFlagEvaluationContexts;
|
|
7
|
+
exports.DEFAULT_TARGETING_KEY_ATTRIBUTE = "subject.key";
|
|
8
|
+
const UNSAFE_OBJECT_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
9
|
+
const CYCLIC_CONTEXT_ERROR_MESSAGE = "Cyclic feature flag evaluation context is not allowed.";
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function isSafeObjectKey(key) {
|
|
14
|
+
return !UNSAFE_OBJECT_KEYS.has(key);
|
|
15
|
+
}
|
|
16
|
+
function createAttributeRecord() {
|
|
17
|
+
return Object.create(null);
|
|
18
|
+
}
|
|
19
|
+
function beginTraversal(value, visited) {
|
|
20
|
+
if (visited.has(value)) {
|
|
21
|
+
throw new TypeError(CYCLIC_CONTEXT_ERROR_MESSAGE);
|
|
22
|
+
}
|
|
23
|
+
visited.add(value);
|
|
24
|
+
}
|
|
25
|
+
function assertAcyclicFeatureFlagContextValue(value, visited = new WeakSet()) {
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
beginTraversal(value, visited);
|
|
28
|
+
try {
|
|
29
|
+
for (const entry of value) {
|
|
30
|
+
assertAcyclicFeatureFlagContextValue(entry, visited);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
visited.delete(value);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!isRecord(value)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
beginTraversal(value, visited);
|
|
42
|
+
try {
|
|
43
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
44
|
+
if (!isSafeObjectKey(key)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
assertAcyclicFeatureFlagContextValue(entry, visited);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
visited.delete(value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function setNestedAttribute(target, path, value) {
|
|
55
|
+
const segments = path.split(".").filter(Boolean);
|
|
56
|
+
const head = segments[0];
|
|
57
|
+
if (!head || !isSafeObjectKey(head)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const tail = segments.slice(1);
|
|
61
|
+
if (tail.length === 0) {
|
|
62
|
+
target[head] = value;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const nextTarget = isRecord(target[head]) ? target[head] : createAttributeRecord();
|
|
66
|
+
target[head] = nextTarget;
|
|
67
|
+
setNestedAttribute(nextTarget, tail.join("."), value);
|
|
68
|
+
}
|
|
69
|
+
function mergeAttributeValue(left, right, visited = new WeakSet()) {
|
|
70
|
+
if (isRecord(left) && isRecord(right)) {
|
|
71
|
+
beginTraversal(left, visited);
|
|
72
|
+
if (right !== left) {
|
|
73
|
+
beginTraversal(right, visited);
|
|
74
|
+
}
|
|
75
|
+
const merged = createAttributeRecord();
|
|
76
|
+
try {
|
|
77
|
+
for (const [key, value] of Object.entries(left)) {
|
|
78
|
+
if (!isSafeObjectKey(key)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
merged[key] = value;
|
|
82
|
+
}
|
|
83
|
+
for (const [key, value] of Object.entries(right)) {
|
|
84
|
+
if (!isSafeObjectKey(key)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
merged[key] = typeof merged[key] === "undefined" ? value : mergeAttributeValue(merged[key], value, visited);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
visited.delete(left);
|
|
92
|
+
if (right !== left) {
|
|
93
|
+
visited.delete(right);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return merged;
|
|
97
|
+
}
|
|
98
|
+
return right;
|
|
99
|
+
}
|
|
100
|
+
function mapOpenFeatureContextValue(value, visited = new WeakSet()) {
|
|
101
|
+
if (value instanceof Date) {
|
|
102
|
+
return value.toISOString();
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
beginTraversal(value, visited);
|
|
106
|
+
try {
|
|
107
|
+
return value.map((entry) => mapOpenFeatureContextValue(entry, visited));
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
visited.delete(value);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (value && typeof value === "object") {
|
|
114
|
+
beginTraversal(value, visited);
|
|
115
|
+
const mapped = createAttributeRecord();
|
|
116
|
+
try {
|
|
117
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
118
|
+
if (!isSafeObjectKey(key)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
mapped[key] = mapOpenFeatureContextValue(entry, visited);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
visited.delete(value);
|
|
126
|
+
}
|
|
127
|
+
return mapped;
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
function createDefaultOpenFeatureContextMapper(params) {
|
|
132
|
+
const targetingKeyAttribute = params?.targetingKeyAttribute ?? exports.DEFAULT_TARGETING_KEY_ATTRIBUTE;
|
|
133
|
+
return (context) => {
|
|
134
|
+
const attributes = createAttributeRecord();
|
|
135
|
+
if (context.targetingKey) {
|
|
136
|
+
setNestedAttribute(attributes, targetingKeyAttribute, context.targetingKey);
|
|
137
|
+
}
|
|
138
|
+
for (const [key, value] of Object.entries(context)) {
|
|
139
|
+
if (key === "targetingKey" || typeof value === "undefined") {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!isSafeObjectKey(key)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
attributes[key] = mapOpenFeatureContextValue(value);
|
|
146
|
+
}
|
|
147
|
+
return Object.keys(attributes).length > 0 ? { attributes } : null;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function mergeFeatureFlagEvaluationContexts(...contexts) {
|
|
151
|
+
const mergedAttributes = createAttributeRecord();
|
|
152
|
+
for (const context of contexts) {
|
|
153
|
+
if (!context?.attributes) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const [key, value] of Object.entries(context.attributes)) {
|
|
157
|
+
if (!isSafeObjectKey(key)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
assertAcyclicFeatureFlagContextValue(value);
|
|
161
|
+
mergedAttributes[key] =
|
|
162
|
+
typeof mergedAttributes[key] === "undefined" ? value : mergeAttributeValue(mergedAttributes[key], value);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return Object.keys(mergedAttributes).length > 0 ? { attributes: mergedAttributes } : null;
|
|
166
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createStaticContextHook = createStaticContextHook;
|
|
4
|
+
exports.createLoggingHook = createLoggingHook;
|
|
5
|
+
function createStaticContextHook(context) {
|
|
6
|
+
return {
|
|
7
|
+
before() {
|
|
8
|
+
return context;
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function createLoggingHook(params) {
|
|
13
|
+
const prefix = params.prefix ?? "feature-flags";
|
|
14
|
+
return {
|
|
15
|
+
after(hookContext, evaluationDetails, hookHints) {
|
|
16
|
+
params.logger.debug(`[${prefix}] evaluated flag (${hookContext.flagKey})`, {
|
|
17
|
+
defaultValue: hookContext.defaultValue,
|
|
18
|
+
errorCode: evaluationDetails.errorCode,
|
|
19
|
+
hookHints,
|
|
20
|
+
reason: evaluationDetails.reason,
|
|
21
|
+
value: evaluationDetails.value,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
error(hookContext, error, hookHints) {
|
|
25
|
+
params.logger.error(`[${prefix}] failed evaluating flag (${hookContext.flagKey})`, {
|
|
26
|
+
error,
|
|
27
|
+
hookHints,
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|