@bernierllc/email-manager 0.2.0 → 0.4.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/README.md +653 -0
- package/dist/capabilities/degradation-logger.d.ts +59 -0
- package/dist/capabilities/degradation-logger.d.ts.map +1 -0
- package/dist/capabilities/degradation-logger.js +106 -0
- package/dist/capabilities/degradation-logger.js.map +1 -0
- package/dist/capabilities/feature-router.d.ts +55 -0
- package/dist/capabilities/feature-router.d.ts.map +1 -0
- package/dist/capabilities/feature-router.js +143 -0
- package/dist/capabilities/feature-router.js.map +1 -0
- package/dist/capabilities/in-memory-resolver.d.ts +27 -0
- package/dist/capabilities/in-memory-resolver.d.ts.map +1 -0
- package/dist/capabilities/in-memory-resolver.js +79 -0
- package/dist/capabilities/in-memory-resolver.js.map +1 -0
- package/dist/capabilities/index.d.ts +13 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +21 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/matrix.d.ts +66 -0
- package/dist/capabilities/matrix.d.ts.map +1 -0
- package/dist/capabilities/matrix.js +247 -0
- package/dist/capabilities/matrix.js.map +1 -0
- package/dist/capabilities/redis-resolver.d.ts +95 -0
- package/dist/capabilities/redis-resolver.d.ts.map +1 -0
- package/dist/capabilities/redis-resolver.js +227 -0
- package/dist/capabilities/redis-resolver.js.map +1 -0
- package/dist/capabilities/resolver-factory.d.ts +30 -0
- package/dist/capabilities/resolver-factory.d.ts.map +1 -0
- package/dist/capabilities/resolver-factory.js +70 -0
- package/dist/capabilities/resolver-factory.js.map +1 -0
- package/dist/capabilities/resolver.d.ts +40 -0
- package/dist/capabilities/resolver.d.ts.map +1 -0
- package/dist/capabilities/resolver.js +18 -0
- package/dist/capabilities/resolver.js.map +1 -0
- package/dist/capabilities/routing-metadata.d.ts +16 -0
- package/dist/capabilities/routing-metadata.d.ts.map +1 -0
- package/dist/capabilities/routing-metadata.js +17 -0
- package/dist/capabilities/routing-metadata.js.map +1 -0
- package/dist/capabilities/safe-resolver.d.ts +24 -0
- package/dist/capabilities/safe-resolver.d.ts.map +1 -0
- package/dist/capabilities/safe-resolver.js +48 -0
- package/dist/capabilities/safe-resolver.js.map +1 -0
- package/dist/config/schema.d.ts +99 -4
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +17 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/email-manager.d.ts.map +1 -1
- package/dist/email-manager.js +28 -1
- package/dist/email-manager.js.map +1 -1
- package/dist/enhanced-email-manager.d.ts +163 -1
- package/dist/enhanced-email-manager.d.ts.map +1 -1
- package/dist/enhanced-email-manager.js +412 -8
- package/dist/enhanced-email-manager.js.map +1 -1
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/managers/provider-manager.d.ts +43 -0
- package/dist/managers/provider-manager.d.ts.map +1 -1
- package/dist/managers/provider-manager.js +102 -4
- package/dist/managers/provider-manager.js.map +1 -1
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +35 -22
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type LogLevel = 'silent' | 'info' | 'warn' | 'error';
|
|
2
|
+
export interface DegradationLoggerConfig {
|
|
3
|
+
logger: {
|
|
4
|
+
warn: Function;
|
|
5
|
+
info: Function;
|
|
6
|
+
error: Function;
|
|
7
|
+
};
|
|
8
|
+
logLevel: LogLevel;
|
|
9
|
+
includeDocLinks: boolean;
|
|
10
|
+
docsBaseUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DegradationEvent {
|
|
13
|
+
provider: string;
|
|
14
|
+
feature: string;
|
|
15
|
+
strategy: string;
|
|
16
|
+
description: string;
|
|
17
|
+
docUrl: string;
|
|
18
|
+
}
|
|
19
|
+
export interface UnsupportedEvent {
|
|
20
|
+
provider: string;
|
|
21
|
+
feature: string;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* DegradationLogger provides structured logging for feature degradation
|
|
26
|
+
* and unsupported feature events across email providers. It respects
|
|
27
|
+
* configurable log levels and optionally includes documentation links.
|
|
28
|
+
*/
|
|
29
|
+
export declare class DegradationLogger {
|
|
30
|
+
private readonly config;
|
|
31
|
+
constructor(config: DegradationLoggerConfig);
|
|
32
|
+
/**
|
|
33
|
+
* Log a degradation event when a provider does not natively support a feature
|
|
34
|
+
* and a fallback strategy is being used.
|
|
35
|
+
*/
|
|
36
|
+
logDegradation(event: DegradationEvent): void;
|
|
37
|
+
/**
|
|
38
|
+
* Log an unsupported feature event. These are always logged at error level
|
|
39
|
+
* (unless log level is silent).
|
|
40
|
+
*/
|
|
41
|
+
logUnsupported(event: UnsupportedEvent): void;
|
|
42
|
+
/**
|
|
43
|
+
* Check whether a message at the given severity should be logged
|
|
44
|
+
* based on the configured log level.
|
|
45
|
+
*
|
|
46
|
+
* The configured logLevel sets the minimum severity that gets logged:
|
|
47
|
+
* - 'silent': nothing is logged
|
|
48
|
+
* - 'error': only error-level messages
|
|
49
|
+
* - 'warn': error + warn messages
|
|
50
|
+
* - 'info': everything (error + warn + info)
|
|
51
|
+
*/
|
|
52
|
+
private shouldLog;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a doc URL, prepending the docsBaseUrl if the path is relative
|
|
55
|
+
* and includeDocLinks is enabled.
|
|
56
|
+
*/
|
|
57
|
+
private resolveDocUrl;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=degradation-logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"degradation-logger.d.ts","sourceRoot":"","sources":["../../src/capabilities/degradation-logger.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE5D,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,QAAQ,CAAA;KAAE,CAAC;IAC5D,QAAQ,EAAE,QAAQ,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAaD;;;;GAIG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;gBAErC,MAAM,EAAE,uBAAuB;IAI3C;;;OAGG;IACH,cAAc,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAwB7C;;;OAGG;IACH,cAAc,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAgB7C;;;;;;;;;OASG;IACH,OAAO,CAAC,SAAS;IAUjB;;;OAGG;IACH,OAAO,CAAC,aAAa;CAqBtB"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Severity ranking for log levels, used for filtering.
|
|
10
|
+
* Higher numbers mean more severe / less filtered.
|
|
11
|
+
*/
|
|
12
|
+
const LOG_LEVEL_SEVERITY = {
|
|
13
|
+
silent: -1,
|
|
14
|
+
info: 0,
|
|
15
|
+
warn: 1,
|
|
16
|
+
error: 2,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* DegradationLogger provides structured logging for feature degradation
|
|
20
|
+
* and unsupported feature events across email providers. It respects
|
|
21
|
+
* configurable log levels and optionally includes documentation links.
|
|
22
|
+
*/
|
|
23
|
+
export class DegradationLogger {
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Log a degradation event when a provider does not natively support a feature
|
|
29
|
+
* and a fallback strategy is being used.
|
|
30
|
+
*/
|
|
31
|
+
logDegradation(event) {
|
|
32
|
+
if (!this.shouldLog('warn')) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const fullDocUrl = this.resolveDocUrl(event.docUrl);
|
|
36
|
+
const message = `${event.provider.toUpperCase()} does not support native ${event.feature}. ` +
|
|
37
|
+
`Using ${event.strategy}: ${event.description}. ` +
|
|
38
|
+
`Docs: ${fullDocUrl}. ` +
|
|
39
|
+
`Override: set featureOverrides.${event.feature}.behavior = 'error' to disable degradation.`;
|
|
40
|
+
const metadata = {
|
|
41
|
+
provider: event.provider,
|
|
42
|
+
feature: event.feature,
|
|
43
|
+
strategy: event.strategy,
|
|
44
|
+
description: event.description,
|
|
45
|
+
docUrl: fullDocUrl,
|
|
46
|
+
};
|
|
47
|
+
this.config.logger.warn(message, metadata);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Log an unsupported feature event. These are always logged at error level
|
|
51
|
+
* (unless log level is silent).
|
|
52
|
+
*/
|
|
53
|
+
logUnsupported(event) {
|
|
54
|
+
if (!this.shouldLog('error')) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const message = `${event.provider.toUpperCase()} does not support ${event.feature}: ${event.message}`;
|
|
58
|
+
const metadata = {
|
|
59
|
+
provider: event.provider,
|
|
60
|
+
feature: event.feature,
|
|
61
|
+
};
|
|
62
|
+
this.config.logger.error(message, metadata);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check whether a message at the given severity should be logged
|
|
66
|
+
* based on the configured log level.
|
|
67
|
+
*
|
|
68
|
+
* The configured logLevel sets the minimum severity that gets logged:
|
|
69
|
+
* - 'silent': nothing is logged
|
|
70
|
+
* - 'error': only error-level messages
|
|
71
|
+
* - 'warn': error + warn messages
|
|
72
|
+
* - 'info': everything (error + warn + info)
|
|
73
|
+
*/
|
|
74
|
+
shouldLog(messageSeverity) {
|
|
75
|
+
const configuredLevel = this.config.logLevel;
|
|
76
|
+
if (configuredLevel === 'silent') {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
// The configured level is the minimum severity we show.
|
|
80
|
+
// A message is shown if its severity >= the configured threshold.
|
|
81
|
+
return LOG_LEVEL_SEVERITY[messageSeverity] >= LOG_LEVEL_SEVERITY[configuredLevel];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolve a doc URL, prepending the docsBaseUrl if the path is relative
|
|
85
|
+
* and includeDocLinks is enabled.
|
|
86
|
+
*/
|
|
87
|
+
resolveDocUrl(docUrl) {
|
|
88
|
+
if (!this.config.includeDocLinks) {
|
|
89
|
+
return docUrl;
|
|
90
|
+
}
|
|
91
|
+
// If it's already an absolute URL, return as-is
|
|
92
|
+
if (docUrl.startsWith('http://') || docUrl.startsWith('https://')) {
|
|
93
|
+
return docUrl;
|
|
94
|
+
}
|
|
95
|
+
// Prepend docsBaseUrl for relative paths
|
|
96
|
+
if (this.config.docsBaseUrl) {
|
|
97
|
+
const base = this.config.docsBaseUrl.endsWith('/')
|
|
98
|
+
? this.config.docsBaseUrl.slice(0, -1)
|
|
99
|
+
: this.config.docsBaseUrl;
|
|
100
|
+
const path = docUrl.startsWith('/') ? docUrl : `/${docUrl}`;
|
|
101
|
+
return `${base}${path}`;
|
|
102
|
+
}
|
|
103
|
+
return docUrl;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=degradation-logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"degradation-logger.js","sourceRoot":"","sources":["../../src/capabilities/degradation-logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CapabilityResolver } from './resolver.js';
|
|
2
|
+
export interface ProviderInfo {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
type: 'sendgrid' | 'mailgun' | 'postmark' | 'ses' | 'smtp';
|
|
6
|
+
isActive: boolean;
|
|
7
|
+
priority: number;
|
|
8
|
+
}
|
|
9
|
+
export interface RoutingResult {
|
|
10
|
+
provider: ProviderInfo;
|
|
11
|
+
source: 'provider' | 'platform' | 'enhanced' | 'unsupported';
|
|
12
|
+
degraded: boolean;
|
|
13
|
+
degradation: {
|
|
14
|
+
strategy: string;
|
|
15
|
+
description: string;
|
|
16
|
+
docUrl: string;
|
|
17
|
+
} | null;
|
|
18
|
+
attemptedProviders: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface FeatureOverride {
|
|
21
|
+
behavior: 'native-first' | 'platform-always' | 'error';
|
|
22
|
+
}
|
|
23
|
+
export interface FeatureRouterConfig {
|
|
24
|
+
resolver: CapabilityResolver;
|
|
25
|
+
fallbackResolver?: CapabilityResolver;
|
|
26
|
+
featureOverrides?: Record<string, FeatureOverride>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Smart routing engine that decides which provider handles a feature request.
|
|
30
|
+
*
|
|
31
|
+
* The router inspects the capability matrix (via a {@link CapabilityResolver})
|
|
32
|
+
* to find the best active provider for a given feature, respecting priority
|
|
33
|
+
* ordering and feature overrides.
|
|
34
|
+
*/
|
|
35
|
+
export declare class FeatureRouter {
|
|
36
|
+
private readonly resolver;
|
|
37
|
+
private readonly fallbackResolver;
|
|
38
|
+
private readonly featureOverrides;
|
|
39
|
+
constructor(config: FeatureRouterConfig);
|
|
40
|
+
/**
|
|
41
|
+
* Find the best provider for a given feature.
|
|
42
|
+
*
|
|
43
|
+
* @throws {FeatureUnsupportedError} when no provider can handle the feature
|
|
44
|
+
*/
|
|
45
|
+
findProviderForFeature(feature: string, providers: ProviderInfo[]): Promise<RoutingResult>;
|
|
46
|
+
private handlePlatformAlways;
|
|
47
|
+
private handleErrorOverride;
|
|
48
|
+
private handleNativeFirst;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a capability entry, falling back to the fallback resolver on error.
|
|
51
|
+
* Returns `undefined` when the capability cannot be resolved at all.
|
|
52
|
+
*/
|
|
53
|
+
private resolveCapability;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=feature-router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-router.d.ts","sourceRoot":"","sources":["../../src/capabilities/feature-router.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAMxD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,GAAG,MAAM,CAAC;IAC3D,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,YAAY,CAAC;IACvB,MAAM,EAAE,UAAU,GAAG,UAAU,GAAG,UAAU,GAAG,aAAa,CAAC;IAC7D,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9E,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,cAAc,GAAG,iBAAiB,GAAG,OAAO,CAAC;CACxD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,gBAAgB,CAAC,EAAE,kBAAkB,CAAC;IACtC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACpD;AAMD;;;;;;GAMG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAiC;IAClE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkC;gBAEvD,MAAM,EAAE,mBAAmB;IAMvC;;;;OAIG;IACG,sBAAsB,CAC1B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,YAAY,EAAE,GACxB,OAAO,CAAC,aAAa,CAAC;YAiCX,oBAAoB;YAgBpB,mBAAmB;YA4BnB,iBAAiB;IAuD/B;;;OAGG;YACW,iBAAiB;CAmBhC"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
import { FeatureUnsupportedError } from '../errors.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// FeatureRouter
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/**
|
|
13
|
+
* Smart routing engine that decides which provider handles a feature request.
|
|
14
|
+
*
|
|
15
|
+
* The router inspects the capability matrix (via a {@link CapabilityResolver})
|
|
16
|
+
* to find the best active provider for a given feature, respecting priority
|
|
17
|
+
* ordering and feature overrides.
|
|
18
|
+
*/
|
|
19
|
+
export class FeatureRouter {
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.resolver = config.resolver;
|
|
22
|
+
this.fallbackResolver = config.fallbackResolver;
|
|
23
|
+
this.featureOverrides = config.featureOverrides ?? {};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Find the best provider for a given feature.
|
|
27
|
+
*
|
|
28
|
+
* @throws {FeatureUnsupportedError} when no provider can handle the feature
|
|
29
|
+
*/
|
|
30
|
+
async findProviderForFeature(feature, providers) {
|
|
31
|
+
// 1. Filter to active providers only
|
|
32
|
+
const activeProviders = providers.filter((p) => p.isActive);
|
|
33
|
+
if (activeProviders.length === 0) {
|
|
34
|
+
throw new FeatureUnsupportedError(feature, providers.map((p) => p.id));
|
|
35
|
+
}
|
|
36
|
+
// 2. Sort by priority (lowest number = highest priority)
|
|
37
|
+
const sorted = [...activeProviders].sort((a, b) => a.priority - b.priority);
|
|
38
|
+
// 3. Check for feature override
|
|
39
|
+
const override = this.featureOverrides[feature];
|
|
40
|
+
if (override?.behavior === 'platform-always') {
|
|
41
|
+
return this.handlePlatformAlways(feature, sorted);
|
|
42
|
+
}
|
|
43
|
+
if (override?.behavior === 'error') {
|
|
44
|
+
return this.handleErrorOverride(feature, sorted, providers);
|
|
45
|
+
}
|
|
46
|
+
// 4. Default: native-first -- iterate providers in priority order
|
|
47
|
+
return this.handleNativeFirst(feature, sorted, providers);
|
|
48
|
+
}
|
|
49
|
+
// -------------------------------------------------------------------------
|
|
50
|
+
// Override handlers
|
|
51
|
+
// -------------------------------------------------------------------------
|
|
52
|
+
async handlePlatformAlways(feature, sorted) {
|
|
53
|
+
const first = sorted[0];
|
|
54
|
+
const capability = await this.resolveCapability(first.type, feature);
|
|
55
|
+
return {
|
|
56
|
+
provider: first,
|
|
57
|
+
source: 'platform',
|
|
58
|
+
degraded: true,
|
|
59
|
+
degradation: capability?.degradation ?? null,
|
|
60
|
+
attemptedProviders: [first.id],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async handleErrorOverride(feature, sorted, allProviders) {
|
|
64
|
+
const attemptedProviders = [];
|
|
65
|
+
for (const provider of sorted) {
|
|
66
|
+
attemptedProviders.push(provider.id);
|
|
67
|
+
const capability = await this.resolveCapability(provider.type, feature);
|
|
68
|
+
if (capability && (capability.source === 'provider' || capability.source === 'enhanced')) {
|
|
69
|
+
return {
|
|
70
|
+
provider,
|
|
71
|
+
source: capability.source,
|
|
72
|
+
degraded: false,
|
|
73
|
+
degradation: null,
|
|
74
|
+
attemptedProviders,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new FeatureUnsupportedError(feature, allProviders.map((p) => p.id));
|
|
79
|
+
}
|
|
80
|
+
async handleNativeFirst(feature, sorted, allProviders) {
|
|
81
|
+
const attemptedProviders = [];
|
|
82
|
+
let platformFallback;
|
|
83
|
+
for (const provider of sorted) {
|
|
84
|
+
attemptedProviders.push(provider.id);
|
|
85
|
+
const capability = await this.resolveCapability(provider.type, feature);
|
|
86
|
+
if (!capability) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// Native or enhanced -- return immediately
|
|
90
|
+
if (capability.source === 'provider' || capability.source === 'enhanced') {
|
|
91
|
+
return {
|
|
92
|
+
provider,
|
|
93
|
+
source: capability.source,
|
|
94
|
+
degraded: false,
|
|
95
|
+
degradation: null,
|
|
96
|
+
attemptedProviders,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Track first platform fallback
|
|
100
|
+
if (capability.source === 'platform' && !platformFallback) {
|
|
101
|
+
platformFallback = { provider, capability };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// 5. No native/enhanced found -- try platform degradation
|
|
105
|
+
if (platformFallback) {
|
|
106
|
+
return {
|
|
107
|
+
provider: platformFallback.provider,
|
|
108
|
+
source: 'platform',
|
|
109
|
+
degraded: true,
|
|
110
|
+
degradation: platformFallback.capability.degradation ?? null,
|
|
111
|
+
attemptedProviders,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// 6. Completely unsupported
|
|
115
|
+
throw new FeatureUnsupportedError(feature, allProviders.map((p) => p.id));
|
|
116
|
+
}
|
|
117
|
+
// -------------------------------------------------------------------------
|
|
118
|
+
// Resolver helper
|
|
119
|
+
// -------------------------------------------------------------------------
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a capability entry, falling back to the fallback resolver on error.
|
|
122
|
+
* Returns `undefined` when the capability cannot be resolved at all.
|
|
123
|
+
*/
|
|
124
|
+
async resolveCapability(providerType, feature) {
|
|
125
|
+
try {
|
|
126
|
+
return await this.resolver.getCapability(providerType, feature);
|
|
127
|
+
}
|
|
128
|
+
catch (primaryError) {
|
|
129
|
+
if (this.fallbackResolver) {
|
|
130
|
+
try {
|
|
131
|
+
return await this.fallbackResolver.getCapability(providerType, feature);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Both resolvers failed -- treat as unresolvable
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// No fallback -- propagate error
|
|
139
|
+
throw primaryError;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=feature-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-router.js","sourceRoot":"","sources":["../../src/capabilities/feature-router.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CapabilityEntry } from '@bernierllc/email-sender';
|
|
2
|
+
import type { CapabilityResolverAdmin } from './resolver.js';
|
|
3
|
+
/**
|
|
4
|
+
* In-memory implementation of {@link CapabilityResolverAdmin}.
|
|
5
|
+
*
|
|
6
|
+
* Initialised with a deep clone of the canonical {@link CAPABILITY_MATRIX} so
|
|
7
|
+
* that mutations made through the admin interface never affect the original
|
|
8
|
+
* static data.
|
|
9
|
+
*/
|
|
10
|
+
export declare class InMemoryCapabilityResolver implements CapabilityResolverAdmin {
|
|
11
|
+
private readonly data;
|
|
12
|
+
constructor();
|
|
13
|
+
getCapability(provider: string, feature: string): Promise<CapabilityEntry>;
|
|
14
|
+
getProviderCapabilities(provider: string): Promise<Record<string, CapabilityEntry>>;
|
|
15
|
+
setCapability(provider: string, feature: string, entry: CapabilityEntry): Promise<void>;
|
|
16
|
+
setProviderCapabilities(provider: string, capabilities: Record<string, CapabilityEntry>): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the capability map for a known provider or throws.
|
|
19
|
+
*/
|
|
20
|
+
private resolveProvider;
|
|
21
|
+
/**
|
|
22
|
+
* Validates a capability entry against the Zod schema, throwing a
|
|
23
|
+
* {@link CapabilityResolverError} with code `INVALID_ENTRY` on failure.
|
|
24
|
+
*/
|
|
25
|
+
private validateEntry;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=in-memory-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-resolver.d.ts","sourceRoot":"","sources":["../../src/capabilities/in-memory-resolver.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAKhE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D;;;;;;GAMG;AACH,qBAAa,0BAA2B,YAAW,uBAAuB;IACxE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;;IAUtC,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAY1E,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAQnF,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvF,uBAAuB,CAC3B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAC5C,OAAO,CAAC,IAAI,CAAC;IAahB;;OAEG;IACH,OAAO,CAAC,eAAe;IAWvB;;;OAGG;IACH,OAAO,CAAC,aAAa;CAUtB"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
import { capabilityEntrySchema } from '@bernierllc/email-sender';
|
|
9
|
+
import { CAPABILITY_MATRIX } from './matrix.js';
|
|
10
|
+
import { CapabilityResolverError } from './resolver.js';
|
|
11
|
+
/**
|
|
12
|
+
* In-memory implementation of {@link CapabilityResolverAdmin}.
|
|
13
|
+
*
|
|
14
|
+
* Initialised with a deep clone of the canonical {@link CAPABILITY_MATRIX} so
|
|
15
|
+
* that mutations made through the admin interface never affect the original
|
|
16
|
+
* static data.
|
|
17
|
+
*/
|
|
18
|
+
export class InMemoryCapabilityResolver {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.data = structuredClone(CAPABILITY_MATRIX);
|
|
21
|
+
}
|
|
22
|
+
// -----------------------------------------------------------------------
|
|
23
|
+
// Read operations
|
|
24
|
+
// -----------------------------------------------------------------------
|
|
25
|
+
async getCapability(provider, feature) {
|
|
26
|
+
const providerCaps = this.resolveProvider(provider);
|
|
27
|
+
const entry = providerCaps[feature];
|
|
28
|
+
if (!entry) {
|
|
29
|
+
throw new CapabilityResolverError(`Unknown feature "${feature}" for provider "${provider}"`, 'UNKNOWN_FEATURE');
|
|
30
|
+
}
|
|
31
|
+
return entry;
|
|
32
|
+
}
|
|
33
|
+
async getProviderCapabilities(provider) {
|
|
34
|
+
return { ...this.resolveProvider(provider) };
|
|
35
|
+
}
|
|
36
|
+
// -----------------------------------------------------------------------
|
|
37
|
+
// Write operations
|
|
38
|
+
// -----------------------------------------------------------------------
|
|
39
|
+
async setCapability(provider, feature, entry) {
|
|
40
|
+
this.validateEntry(entry);
|
|
41
|
+
// Ensure provider bucket exists (create on demand for admin use-cases)
|
|
42
|
+
if (!this.data.providers[provider]) {
|
|
43
|
+
this.data.providers[provider] = {};
|
|
44
|
+
}
|
|
45
|
+
this.data.providers[provider][feature] = entry;
|
|
46
|
+
}
|
|
47
|
+
async setProviderCapabilities(provider, capabilities) {
|
|
48
|
+
// Validate every entry before committing any of them
|
|
49
|
+
for (const [feature, entry] of Object.entries(capabilities)) {
|
|
50
|
+
this.validateEntry(entry, feature);
|
|
51
|
+
}
|
|
52
|
+
this.data.providers[provider] = { ...capabilities };
|
|
53
|
+
}
|
|
54
|
+
// -----------------------------------------------------------------------
|
|
55
|
+
// Internal helpers
|
|
56
|
+
// -----------------------------------------------------------------------
|
|
57
|
+
/**
|
|
58
|
+
* Resolves the capability map for a known provider or throws.
|
|
59
|
+
*/
|
|
60
|
+
resolveProvider(provider) {
|
|
61
|
+
const caps = this.data.providers[provider];
|
|
62
|
+
if (!caps) {
|
|
63
|
+
throw new CapabilityResolverError(`Unknown provider "${provider}"`, 'UNKNOWN_PROVIDER');
|
|
64
|
+
}
|
|
65
|
+
return caps;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Validates a capability entry against the Zod schema, throwing a
|
|
69
|
+
* {@link CapabilityResolverError} with code `INVALID_ENTRY` on failure.
|
|
70
|
+
*/
|
|
71
|
+
validateEntry(entry, label) {
|
|
72
|
+
const result = capabilityEntrySchema.safeParse(entry);
|
|
73
|
+
if (!result.success) {
|
|
74
|
+
const prefix = label ? `Invalid entry for "${label}": ` : 'Invalid capability entry: ';
|
|
75
|
+
throw new CapabilityResolverError(prefix + result.error.issues.map((i) => i.message).join('; '), 'INVALID_ENTRY');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=in-memory-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-resolver.js","sourceRoot":"","sources":["../../src/capabilities/in-memory-resolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { FEATURE_DEFINITIONS, FEATURE_NAMES, PROVIDER_NAMES, CAPABILITY_MATRIX, getCapabilityMatrix, } from './matrix.js';
|
|
2
|
+
export type { FeatureName, ProviderName, ProviderCapabilityMap, CapabilityMatrixData, } from './matrix.js';
|
|
3
|
+
export { CapabilityResolverError } from './resolver.js';
|
|
4
|
+
export type { CapabilityResolver, CapabilityResolverAdmin } from './resolver.js';
|
|
5
|
+
export { InMemoryCapabilityResolver } from './in-memory-resolver.js';
|
|
6
|
+
export { RedisCapabilityResolver } from './redis-resolver.js';
|
|
7
|
+
export type { RedisLike, RedisPipelineLike, RedisCapabilityResolverOptions, } from './redis-resolver.js';
|
|
8
|
+
export { SafeCapabilityResolver } from './safe-resolver.js';
|
|
9
|
+
export { createCapabilityResolver } from './resolver-factory.js';
|
|
10
|
+
export type { ResolverFactoryOptions, ResolverFactoryLogger } from './resolver-factory.js';
|
|
11
|
+
export { FeatureRouter } from './feature-router.js';
|
|
12
|
+
export type { ProviderInfo, RoutingResult, FeatureOverride, FeatureRouterConfig, } from './feature-router.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/capabilities/index.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,WAAW,EACX,YAAY,EACZ,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACxD,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAGjF,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAGrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,8BAA8B,GAC/B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAG3F,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EACV,YAAY,EACZ,aAAa,EACb,eAAe,EACf,mBAAmB,GACpB,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
export { FEATURE_DEFINITIONS, FEATURE_NAMES, PROVIDER_NAMES, CAPABILITY_MATRIX, getCapabilityMatrix, } from './matrix.js';
|
|
9
|
+
// Capability resolver interfaces and error class
|
|
10
|
+
export { CapabilityResolverError } from './resolver.js';
|
|
11
|
+
// In-memory resolver implementation
|
|
12
|
+
export { InMemoryCapabilityResolver } from './in-memory-resolver.js';
|
|
13
|
+
// Redis-backed resolver implementation
|
|
14
|
+
export { RedisCapabilityResolver } from './redis-resolver.js';
|
|
15
|
+
// Safe fallback wrapper
|
|
16
|
+
export { SafeCapabilityResolver } from './safe-resolver.js';
|
|
17
|
+
// Factory function for auto-detecting resolver
|
|
18
|
+
export { createCapabilityResolver } from './resolver-factory.js';
|
|
19
|
+
// Feature router
|
|
20
|
+
export { FeatureRouter } from './feature-router.js';
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/capabilities/index.ts"],"names":[],"mappings":"AAAA;;;;;;EAME;AAEF,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AASrB,iDAAiD;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAGxD,oCAAoC;AACpC,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAErE,uCAAuC;AACvC,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAO9D,wBAAwB;AACxB,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,+CAA+C;AAC/C,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAGjE,iBAAiB;AACjB,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { CapabilityEntry } from '@bernierllc/email-sender';
|
|
2
|
+
export declare const FEATURE_DEFINITIONS: {
|
|
3
|
+
readonly sendEmail: "Send a single email through a provider";
|
|
4
|
+
readonly batchSend: "Send multiple emails in a single operation";
|
|
5
|
+
readonly providerTemplates: "Use templates stored on the provider (server-side)";
|
|
6
|
+
readonly localTemplates: "Render templates locally before sending";
|
|
7
|
+
readonly calendarInvites: "Attach iCalendar (.ics) invites to outgoing emails";
|
|
8
|
+
readonly calendarEventMgmt: "Create, update, and cancel calendar events programmatically";
|
|
9
|
+
readonly webhooksReceive: "Receive webhook callbacks from the provider for email events";
|
|
10
|
+
readonly webhookNormalization: "Normalize provider-specific webhook payloads into a common format";
|
|
11
|
+
readonly deliveryTracking: "Track delivery status of sent emails (delivered, bounced, etc.)";
|
|
12
|
+
readonly inboundEmailParsing: "Parse inbound emails forwarded by the provider";
|
|
13
|
+
/**
|
|
14
|
+
* subscriptionMgmt -- Manage subscriber lists, preference centres, and
|
|
15
|
+
* opt-in / opt-out lifecycle. This is distinct from suppressionLists which
|
|
16
|
+
* deals with bounce/complaint suppression at the provider level.
|
|
17
|
+
*/
|
|
18
|
+
readonly subscriptionMgmt: "Manage subscriber lists, preferences, and opt-in/opt-out lifecycle";
|
|
19
|
+
/**
|
|
20
|
+
* suppressionLists -- Provider-level bounce and complaint suppression.
|
|
21
|
+
* Automatically maintained by the provider to prevent sending to addresses
|
|
22
|
+
* that have bounced or filed complaints. Distinct from subscriptionMgmt
|
|
23
|
+
* which handles user-facing subscription preferences.
|
|
24
|
+
*/
|
|
25
|
+
readonly suppressionLists: "Provider-level bounce and complaint suppression lists";
|
|
26
|
+
readonly unsubscribeUrlGeneration: "Generate one-click unsubscribe URLs for email headers";
|
|
27
|
+
readonly scheduledSend: "Schedule emails for future delivery";
|
|
28
|
+
readonly openClickTracking: "Track email opens and link clicks";
|
|
29
|
+
readonly emailContentParsing: "Parse and transform email content (HTML/text)";
|
|
30
|
+
readonly emailHeaderMgmt: "Set and manage custom email headers";
|
|
31
|
+
readonly attachments: "Attach files to outgoing emails";
|
|
32
|
+
readonly advancedAttachmentProcessing: "Process, validate, and transform attachments (resize, compress, etc.)";
|
|
33
|
+
readonly dkimSpf: "Programmatic DKIM/SPF authentication configuration";
|
|
34
|
+
readonly linkBranding: "Custom branded tracking domains for links";
|
|
35
|
+
readonly retryResilience: "Automatic retry with backoff on transient failures";
|
|
36
|
+
readonly multiProviderFailover: "Automatic failover to alternate providers on failure";
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Union of every recognised feature name, derived from FEATURE_DEFINITIONS.
|
|
40
|
+
*/
|
|
41
|
+
export type FeatureName = keyof typeof FEATURE_DEFINITIONS;
|
|
42
|
+
/**
|
|
43
|
+
* Ordered array of every feature name (derived from FEATURE_DEFINITIONS keys).
|
|
44
|
+
*/
|
|
45
|
+
export declare const FEATURE_NAMES: readonly FeatureName[];
|
|
46
|
+
export type ProviderName = 'sendgrid' | 'mailgun' | 'postmark' | 'ses' | 'smtp';
|
|
47
|
+
export declare const PROVIDER_NAMES: readonly ProviderName[];
|
|
48
|
+
/**
|
|
49
|
+
* A complete capability map for a single provider -- one entry per feature.
|
|
50
|
+
*/
|
|
51
|
+
export type ProviderCapabilityMap = Record<FeatureName, CapabilityEntry>;
|
|
52
|
+
/**
|
|
53
|
+
* Top-level structure for the capability matrix.
|
|
54
|
+
*/
|
|
55
|
+
export interface CapabilityMatrixData {
|
|
56
|
+
version: string;
|
|
57
|
+
lastUpdated: string;
|
|
58
|
+
providers: Record<ProviderName, ProviderCapabilityMap>;
|
|
59
|
+
}
|
|
60
|
+
export declare const CAPABILITY_MATRIX: CapabilityMatrixData;
|
|
61
|
+
/**
|
|
62
|
+
* Returns a deep clone of the capability matrix to prevent mutation of the
|
|
63
|
+
* canonical data.
|
|
64
|
+
*/
|
|
65
|
+
export declare function getCapabilityMatrix(): CapabilityMatrixData;
|
|
66
|
+
//# sourceMappingURL=matrix.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matrix.d.ts","sourceRoot":"","sources":["../../src/capabilities/matrix.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAUhE,eAAO,MAAM,mBAAmB;;;;;;;;;;;IAW9B;;;;OAIG;;IAEH;;;;;OAKG;;;;;;;;;;;;;CAaK,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,mBAAmB,CAAC;AAE3D;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,WAAW,EAE9B,CAAC;AAMnB,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,GAAG,MAAM,CAAC;AAEhF,eAAO,MAAM,cAAc,EAAE,SAAS,YAAY,EAMxC,CAAC;AAMX;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC,YAAY,EAAE,qBAAqB,CAAC,CAAC;CACxD;AAuND,eAAO,MAAM,iBAAiB,EAAE,oBAU/B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,oBAAoB,CAE1D"}
|