@frontmcp/plugin-feature-flags 1.8.7 → 1.9.1-rc.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/esm/index.mjs CHANGED
@@ -1,3 +1,6 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
1
4
  var __defProp = Object.defineProperty;
2
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -356,6 +359,7 @@ var FeatureFlagAccessor = class {
356
359
  adapter;
357
360
  ctx;
358
361
  config;
362
+ /** The adapter's answers by flag key: `undefined` when it has none (an unknown flag). */
359
363
  cache = /* @__PURE__ */ new Map();
360
364
  constructor(adapter, ctx, config) {
361
365
  this.adapter = adapter;
@@ -364,27 +368,41 @@ var FeatureFlagAccessor = class {
364
368
  }
365
369
  /**
366
370
  * Check if a feature flag is enabled.
371
+ *
372
+ * `defaultValue` (else the plugin's `defaultValue`, else `false`) is the answer when the adapter
373
+ * throws or has no answer for the flag: a key the static adapter was not given, or one a custom
374
+ * adapter's `evaluateFlags()` omits. A flag the adapter answers keeps its answer, `false` included.
375
+ * It used to apply only when the adapter threw, so `isEnabled('unknown', true)` was `false` (#678).
367
376
  */
368
377
  async isEnabled(flagKey, defaultValue) {
378
+ const answer = await this.evaluate(flagKey);
379
+ return answer ?? defaultValue ?? this.config.defaultValue ?? false;
380
+ }
381
+ /**
382
+ * The adapter's answer for one flag, or `undefined` when it has none or throws. Asked through
383
+ * `evaluateFlags()`, as the execution gates ask, which is how an adapter says it does not know a
384
+ * flag; `isEnabled()` answers `false` for it. A successful answer is cached per `cacheStrategy`.
385
+ */
386
+ async evaluate(flagKey) {
369
387
  const cacheStrategy = this.config.cacheStrategy ?? "none";
370
388
  const cacheTtlMs = this.config.cacheTtlMs ?? 3e4;
371
389
  if (cacheStrategy !== "none") {
372
390
  const cached = this.cache.get(flagKey);
373
391
  if (cached && Date.now() < cached.expiresAt) {
374
- return cached.value;
392
+ return cached.answer;
375
393
  }
376
394
  }
377
- const context = this.buildContext();
378
- let result;
395
+ let answer;
379
396
  try {
380
- result = await this.adapter.isEnabled(flagKey, context);
397
+ const results = await this.adapter.evaluateFlags([flagKey], this.buildContext());
398
+ answer = results.has(flagKey) ? results.get(flagKey) === true : void 0;
381
399
  } catch {
382
- result = defaultValue ?? this.config.defaultValue ?? false;
400
+ return void 0;
383
401
  }
384
402
  if (cacheStrategy !== "none") {
385
- this.cache.set(flagKey, { value: result, expiresAt: Date.now() + cacheTtlMs });
403
+ this.cache.set(flagKey, { answer, expiresAt: Date.now() + cacheTtlMs });
386
404
  }
387
- return result;
405
+ return answer;
388
406
  }
389
407
  /**
390
408
  * Get a feature flag variant (for multi-variate flags).
@@ -434,6 +452,21 @@ var GetPromptHook = FlowHooksOf("prompts:get-prompt");
434
452
  var CompleteHook = FlowHooksOf("completion:complete");
435
453
  var FilterSkillsHook = FlowHooksOf("skills:filter");
436
454
  var SUPPORTED_ADAPTERS = ["static", "splitio", "launchdarkly", "unleash", "custom"];
455
+ var REQUIRED_ADAPTER_METHODS = ["isEnabled", "getVariant", "evaluateFlags"];
456
+ function assertCustomAdapter(adapterInstance) {
457
+ let problem;
458
+ if (typeof adapterInstance !== "object" || adapterInstance === null) {
459
+ problem = `got ${adapterInstance === void 0 ? "undefined" : JSON.stringify(adapterInstance)}`;
460
+ } else {
461
+ const instance = adapterInstance;
462
+ const missing = REQUIRED_ADAPTER_METHODS.filter((method) => typeof instance[method] !== "function");
463
+ if (missing.length > 0) problem = `missing ${missing.map((m) => `${m}()`).join(", ")}`;
464
+ }
465
+ if (problem === void 0) return;
466
+ throw new FeatureFlagConfigurationError(
467
+ `FeatureFlagPlugin.init({ adapter: 'custom' }) requires an \`adapterInstance\` option: an object implementing FeatureFlagAdapter (${REQUIRED_ADAPTER_METHODS.map((m) => `${m}()`).join(", ")}); ${problem}.`
468
+ );
469
+ }
437
470
  var FeatureFlagPlugin = class extends DynamicPlugin {
438
471
  options;
439
472
  constructor(options) {
@@ -689,6 +722,7 @@ __publicField(FeatureFlagPlugin, "dynamicProviders", (options) => {
689
722
  });
690
723
  break;
691
724
  case "custom":
725
+ assertCustomAdapter(options.adapterInstance);
692
726
  providers.push({
693
727
  name: "feature-flags:adapter:custom",
694
728
  provide: FeatureFlagAdapterToken,
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/plugin-feature-flags",
3
- "version": "1.8.7",
3
+ "version": "1.9.1-rc.1",
4
4
  "description": "Feature flag plugin for FrontMCP - dynamically gate MCP capabilities behind feature flags",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "license": "Apache-2.0",
@@ -49,7 +49,7 @@
49
49
  }
50
50
  },
51
51
  "dependencies": {
52
- "@frontmcp/sdk": "1.8.7"
52
+ "@frontmcp/sdk": "1.9.1-rc.1"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@launchdarkly/node-server-sdk": "^9.0.0 || ^10.0.0",
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag.plugin.d.ts","sourceRoot":"","sources":["../src/feature-flag.plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAYb,KAAK,SAAS,EAGd,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAOvB,OAAO,KAAK,EAEV,wBAAwB,EACxB,6BAA6B,EAE9B,MAAM,sBAAsB,CAAC;AAe9B;;;;;;;;;;;;;;;;;;GAkBG;AAeH,MAAM,CAAC,OAAO,OAAO,iBAAkB,SAAQ,aAAa,CAAC,wBAAwB,EAAE,6BAA6B,CAAC;IACnH,OAAO,EAAE,wBAAwB,CAAC;gBAEtB,OAAO,EAAE,6BAA6B;IAKlD;;OAEG;IACH,OAAgB,gBAAgB,GAAI,SAAS,6BAA6B,KAAG,YAAY,EAAE,CAiGzF;IAMF;;;OAGG;IAEG,eAAe,CAAC,OAAO,EAAE,SAAS,CAAC,kBAAkB,CAAC;IAqB5D;;OAEG;IAEG,mBAAmB,CAAC,OAAO,EAAE,SAAS,CAAC,0BAA0B,CAAC;IAoBxE;;;;OAIG;IAEG,2BAA2B,CAAC,OAAO,EAAE,SAAS,CAAC,mCAAmC,CAAC;IAoBzF;;OAEG;IAEG,iBAAiB,CAAC,OAAO,EAAE,GAAG;IAsBpC;;;;;;;;OAQG;IAEG,YAAY,CAAC,OAAO,EAAE,SAAS,CAAC,eAAe,CAAC;IAsBtD;;;OAGG;IAEG,iBAAiB,CAAC,OAAO,EAAE,SAAS,CAAC,iBAAiB,CAAC;IAI7D;;;;;;OAMG;IAEG,gBAAgB,CAAC,OAAO,EAAE,GAAG;IAInC;;;;;OAKG;IAEG,aAAa,CAAC,OAAO,EAAE,GAAG;IAIhC;;;;;OAKG;IAEG,cAAc,CAAC,OAAO,EAAE,GAAG;IAMjC;;;;;OAKG;YACW,kBAAkB;IA+BhC;;;;;;;;OAQG;IACH,OAAO,CAAC,WAAW;IAYnB;;OAEG;IACH,OAAO,CAAC,eAAe;IAiBvB;;OAEG;YACW,iBAAiB;IAQ/B;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;;;OAIG;IACH,OAAO,CAAC,YAAY;CAWrB"}
1
+ {"version":3,"file":"feature-flag.plugin.d.ts","sourceRoot":"","sources":["../src/feature-flag.plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAYb,KAAK,SAAS,EAGd,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAOvB,OAAO,KAAK,EAEV,wBAAwB,EACxB,6BAA6B,EAE9B,MAAM,sBAAsB,CAAC;AAwC9B;;;;;;;;;;;;;;;;;;GAkBG;AAeH,MAAM,CAAC,OAAO,OAAO,iBAAkB,SAAQ,aAAa,CAAC,wBAAwB,EAAE,6BAA6B,CAAC;IACnH,OAAO,EAAE,wBAAwB,CAAC;gBAEtB,OAAO,EAAE,6BAA6B;IAKlD;;OAEG;IACH,OAAgB,gBAAgB,GAAI,SAAS,6BAA6B,KAAG,YAAY,EAAE,CAkGzF;IAMF;;;OAGG;IAEG,eAAe,CAAC,OAAO,EAAE,SAAS,CAAC,kBAAkB,CAAC;IAqB5D;;OAEG;IAEG,mBAAmB,CAAC,OAAO,EAAE,SAAS,CAAC,0BAA0B,CAAC;IAoBxE;;;;OAIG;IAEG,2BAA2B,CAAC,OAAO,EAAE,SAAS,CAAC,mCAAmC,CAAC;IAoBzF;;OAEG;IAEG,iBAAiB,CAAC,OAAO,EAAE,GAAG;IAsBpC;;;;;;;;OAQG;IAEG,YAAY,CAAC,OAAO,EAAE,SAAS,CAAC,eAAe,CAAC;IAsBtD;;;OAGG;IAEG,iBAAiB,CAAC,OAAO,EAAE,SAAS,CAAC,iBAAiB,CAAC;IAI7D;;;;;;OAMG;IAEG,gBAAgB,CAAC,OAAO,EAAE,GAAG;IAInC;;;;;OAKG;IAEG,aAAa,CAAC,OAAO,EAAE,GAAG;IAIhC;;;;;OAKG;IAEG,cAAc,CAAC,OAAO,EAAE,GAAG;IAMjC;;;;;OAKG;YACW,kBAAkB;IA+BhC;;;;;;;;OAQG;IACH,OAAO,CAAC,WAAW;IAYnB;;OAEG;IACH,OAAO,CAAC,eAAe;IAiBvB;;OAEG;YACW,iBAAiB;IAQ/B;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;;;OAIG;IACH,OAAO,CAAC,YAAY;CAWrB"}
@@ -27,6 +27,11 @@ export type UserIdResolver = (ctx: FrontMcpContext) => string | undefined;
27
27
  export type AttributesResolver = (ctx: FrontMcpContext) => Record<string, unknown>;
28
28
  /** Shared base options for all adapter configurations. */
29
29
  interface FeatureFlagBaseOptions {
30
+ /**
31
+ * What `this.featureFlags.isEnabled()` answers when the adapter throws or has no answer for the
32
+ * flag and the call passes no `defaultValue` of its own. Gates use their ref's `defaultValue`.
33
+ * @default false
34
+ */
30
35
  defaultValue?: boolean;
31
36
  cacheStrategy?: FeatureFlagCacheStrategy;
32
37
  cacheTtlMs?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag.types.d.ts","sourceRoot":"","sources":["../src/feature-flag.types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"feature-flag.types.d.ts","sourceRoot":"","sources":["../src/feature-flag.types.ts"],"names":[],"mappings":"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"}
package/index.js CHANGED
@@ -358,6 +358,7 @@ var FeatureFlagAccessor = class {
358
358
  adapter;
359
359
  ctx;
360
360
  config;
361
+ /** The adapter's answers by flag key: `undefined` when it has none (an unknown flag). */
361
362
  cache = /* @__PURE__ */ new Map();
362
363
  constructor(adapter, ctx, config) {
363
364
  this.adapter = adapter;
@@ -366,27 +367,41 @@ var FeatureFlagAccessor = class {
366
367
  }
367
368
  /**
368
369
  * Check if a feature flag is enabled.
370
+ *
371
+ * `defaultValue` (else the plugin's `defaultValue`, else `false`) is the answer when the adapter
372
+ * throws or has no answer for the flag: a key the static adapter was not given, or one a custom
373
+ * adapter's `evaluateFlags()` omits. A flag the adapter answers keeps its answer, `false` included.
374
+ * It used to apply only when the adapter threw, so `isEnabled('unknown', true)` was `false` (#678).
369
375
  */
370
376
  async isEnabled(flagKey, defaultValue) {
377
+ const answer = await this.evaluate(flagKey);
378
+ return answer ?? defaultValue ?? this.config.defaultValue ?? false;
379
+ }
380
+ /**
381
+ * The adapter's answer for one flag, or `undefined` when it has none or throws. Asked through
382
+ * `evaluateFlags()`, as the execution gates ask, which is how an adapter says it does not know a
383
+ * flag; `isEnabled()` answers `false` for it. A successful answer is cached per `cacheStrategy`.
384
+ */
385
+ async evaluate(flagKey) {
371
386
  const cacheStrategy = this.config.cacheStrategy ?? "none";
372
387
  const cacheTtlMs = this.config.cacheTtlMs ?? 3e4;
373
388
  if (cacheStrategy !== "none") {
374
389
  const cached = this.cache.get(flagKey);
375
390
  if (cached && Date.now() < cached.expiresAt) {
376
- return cached.value;
391
+ return cached.answer;
377
392
  }
378
393
  }
379
- const context = this.buildContext();
380
- let result;
394
+ let answer;
381
395
  try {
382
- result = await this.adapter.isEnabled(flagKey, context);
396
+ const results = await this.adapter.evaluateFlags([flagKey], this.buildContext());
397
+ answer = results.has(flagKey) ? results.get(flagKey) === true : void 0;
383
398
  } catch {
384
- result = defaultValue ?? this.config.defaultValue ?? false;
399
+ return void 0;
385
400
  }
386
401
  if (cacheStrategy !== "none") {
387
- this.cache.set(flagKey, { value: result, expiresAt: Date.now() + cacheTtlMs });
402
+ this.cache.set(flagKey, { answer, expiresAt: Date.now() + cacheTtlMs });
388
403
  }
389
- return result;
404
+ return answer;
390
405
  }
391
406
  /**
392
407
  * Get a feature flag variant (for multi-variate flags).
@@ -436,6 +451,21 @@ var GetPromptHook = (0, import_sdk4.FlowHooksOf)("prompts:get-prompt");
436
451
  var CompleteHook = (0, import_sdk4.FlowHooksOf)("completion:complete");
437
452
  var FilterSkillsHook = (0, import_sdk4.FlowHooksOf)("skills:filter");
438
453
  var SUPPORTED_ADAPTERS = ["static", "splitio", "launchdarkly", "unleash", "custom"];
454
+ var REQUIRED_ADAPTER_METHODS = ["isEnabled", "getVariant", "evaluateFlags"];
455
+ function assertCustomAdapter(adapterInstance) {
456
+ let problem;
457
+ if (typeof adapterInstance !== "object" || adapterInstance === null) {
458
+ problem = `got ${adapterInstance === void 0 ? "undefined" : JSON.stringify(adapterInstance)}`;
459
+ } else {
460
+ const instance = adapterInstance;
461
+ const missing = REQUIRED_ADAPTER_METHODS.filter((method) => typeof instance[method] !== "function");
462
+ if (missing.length > 0) problem = `missing ${missing.map((m) => `${m}()`).join(", ")}`;
463
+ }
464
+ if (problem === void 0) return;
465
+ throw new FeatureFlagConfigurationError(
466
+ `FeatureFlagPlugin.init({ adapter: 'custom' }) requires an \`adapterInstance\` option: an object implementing FeatureFlagAdapter (${REQUIRED_ADAPTER_METHODS.map((m) => `${m}()`).join(", ")}); ${problem}.`
467
+ );
468
+ }
439
469
  var FeatureFlagPlugin = class extends import_sdk4.DynamicPlugin {
440
470
  options;
441
471
  constructor(options) {
@@ -691,6 +721,7 @@ __publicField(FeatureFlagPlugin, "dynamicProviders", (options) => {
691
721
  });
692
722
  break;
693
723
  case "custom":
724
+ assertCustomAdapter(options.adapterInstance);
694
725
  providers.push({
695
726
  name: "feature-flags:adapter:custom",
696
727
  provide: FeatureFlagAdapterToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/plugin-feature-flags",
3
- "version": "1.8.7",
3
+ "version": "1.9.1-rc.1",
4
4
  "description": "Feature flag plugin for FrontMCP - dynamically gate MCP capabilities behind feature flags",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "license": "Apache-2.0",
@@ -49,7 +49,7 @@
49
49
  }
50
50
  },
51
51
  "dependencies": {
52
- "@frontmcp/sdk": "1.8.7"
52
+ "@frontmcp/sdk": "1.9.1-rc.1"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@launchdarkly/node-server-sdk": "^9.0.0 || ^10.0.0",
@@ -9,12 +9,24 @@ export declare class FeatureFlagAccessor {
9
9
  private readonly adapter;
10
10
  private readonly ctx;
11
11
  private readonly config;
12
+ /** The adapter's answers by flag key: `undefined` when it has none (an unknown flag). */
12
13
  private readonly cache;
13
14
  constructor(adapter: FeatureFlagAdapter, ctx: FrontMcpContext, config: FeatureFlagPluginOptions);
14
15
  /**
15
16
  * Check if a feature flag is enabled.
17
+ *
18
+ * `defaultValue` (else the plugin's `defaultValue`, else `false`) is the answer when the adapter
19
+ * throws or has no answer for the flag: a key the static adapter was not given, or one a custom
20
+ * adapter's `evaluateFlags()` omits. A flag the adapter answers keeps its answer, `false` included.
21
+ * It used to apply only when the adapter threw, so `isEnabled('unknown', true)` was `false` (#678).
16
22
  */
17
23
  isEnabled(flagKey: string, defaultValue?: boolean): Promise<boolean>;
24
+ /**
25
+ * The adapter's answer for one flag, or `undefined` when it has none or throws. Asked through
26
+ * `evaluateFlags()`, as the execution gates ask, which is how an adapter says it does not know a
27
+ * flag; `isEnabled()` answers `false` for it. A successful answer is cached per `cacheStrategy`.
28
+ */
29
+ private evaluate;
18
30
  /**
19
31
  * Get a feature flag variant (for multi-variate flags).
20
32
  */
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag-accessor.provider.d.ts","sourceRoot":"","sources":["../../src/providers/feature-flag-accessor.provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAA2B,MAAM,eAAe,CAAC;AAEzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4CAA4C,CAAC;AAErF,OAAO,KAAK,EAEV,wBAAwB,EACxB,cAAc,EACd,kBAAkB,EACnB,MAAM,uBAAuB,CAAC;AAE/B;;;GAGG;AACH,qBAKa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAClD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4D;gBAEtE,OAAO,EAAE,kBAAkB,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,wBAAwB;IAM/F;;OAEG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IA4B1E;;OAEG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAK9D;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAKtE;;OAEG;IACG,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;IAOvD;;OAEG;IACH,OAAO,CAAC,YAAY;CAGrB;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,kBAAkB,EAC3B,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,wBAAwB,GAC/B,mBAAmB,CAErB"}
1
+ {"version":3,"file":"feature-flag-accessor.provider.d.ts","sourceRoot":"","sources":["../../src/providers/feature-flag-accessor.provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAA2B,MAAM,eAAe,CAAC;AAEzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4CAA4C,CAAC;AAErF,OAAO,KAAK,EAEV,wBAAwB,EACxB,cAAc,EACd,kBAAkB,EACnB,MAAM,uBAAuB,CAAC;AAE/B;;;GAGG;AACH,qBAKa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAClD,yFAAyF;IACzF,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyE;gBAEnF,OAAO,EAAE,kBAAkB,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,wBAAwB;IAM/F;;;;;;;OAOG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAK1E;;;;OAIG;YACW,QAAQ;IA0BtB;;OAEG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAK9D;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAKtE;;OAEG;IACG,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;IAOvD;;OAEG;IACH,OAAO,CAAC,YAAY;CAGrB;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,kBAAkB,EAC3B,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,wBAAwB,GAC/B,mBAAmB,CAErB"}