paygate-mcp 10.13.0 → 10.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit-trail.d.ts +115 -0
- package/dist/audit-trail.d.ts.map +1 -0
- package/dist/audit-trail.js +198 -0
- package/dist/audit-trail.js.map +1 -0
- package/dist/feature-flags.d.ts +111 -0
- package/dist/feature-flags.d.ts.map +1 -0
- package/dist/feature-flags.js +228 -0
- package/dist/feature-flags.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/request-pipeline.d.ts +143 -0
- package/dist/request-pipeline.d.ts.map +1 -0
- package/dist/request-pipeline.js +197 -0
- package/dist/request-pipeline.js.map +1 -0
- package/dist/usage-trends.d.ts +114 -0
- package/dist/usage-trends.d.ts.map +1 -0
- package/dist/usage-trends.js +261 -0
- package/dist/usage-trends.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuditTrailManager — Compliance-ready audit trail with tamper detection.
|
|
3
|
+
*
|
|
4
|
+
* Records every significant action with actor, target, and metadata.
|
|
5
|
+
* Uses hash chains for tamper detection — each entry's hash includes
|
|
6
|
+
* the previous entry's hash, creating a verifiable chain of custody.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const audit = new AuditTrailManager();
|
|
11
|
+
*
|
|
12
|
+
* audit.record({
|
|
13
|
+
* action: 'key.created',
|
|
14
|
+
* actor: 'admin_1',
|
|
15
|
+
* target: 'key_abc',
|
|
16
|
+
* details: { credits: 1000 },
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* const entries = audit.query({ actor: 'admin_1' });
|
|
20
|
+
* const valid = audit.verifyChain(); // true if no tampering
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export interface AuditEntry {
|
|
24
|
+
id: string;
|
|
25
|
+
sequence: number;
|
|
26
|
+
action: string;
|
|
27
|
+
actor: string;
|
|
28
|
+
actorType?: string;
|
|
29
|
+
target: string;
|
|
30
|
+
targetType?: string;
|
|
31
|
+
details: Record<string, unknown>;
|
|
32
|
+
timestamp: number;
|
|
33
|
+
/** IP address or origin. */
|
|
34
|
+
source?: string;
|
|
35
|
+
/** Hash of this entry (includes previous hash for chain). */
|
|
36
|
+
hash: string;
|
|
37
|
+
/** Hash of the previous entry. */
|
|
38
|
+
previousHash: string;
|
|
39
|
+
}
|
|
40
|
+
export interface AuditRecordParams {
|
|
41
|
+
action: string;
|
|
42
|
+
actor: string;
|
|
43
|
+
actorType?: string;
|
|
44
|
+
target: string;
|
|
45
|
+
targetType?: string;
|
|
46
|
+
details?: Record<string, unknown>;
|
|
47
|
+
source?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface AuditQuery {
|
|
50
|
+
action?: string;
|
|
51
|
+
actions?: string[];
|
|
52
|
+
actor?: string;
|
|
53
|
+
actorType?: string;
|
|
54
|
+
target?: string;
|
|
55
|
+
targetType?: string;
|
|
56
|
+
startTime?: number;
|
|
57
|
+
endTime?: number;
|
|
58
|
+
limit?: number;
|
|
59
|
+
offset?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface AuditQueryResult {
|
|
62
|
+
entries: AuditEntry[];
|
|
63
|
+
total: number;
|
|
64
|
+
hasMore: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface ChainVerification {
|
|
67
|
+
valid: boolean;
|
|
68
|
+
totalEntries: number;
|
|
69
|
+
firstBrokenAt?: number;
|
|
70
|
+
brokenEntry?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface AuditTrailConfig {
|
|
73
|
+
maxEntries?: number;
|
|
74
|
+
}
|
|
75
|
+
export interface AuditTrailStats {
|
|
76
|
+
totalEntries: number;
|
|
77
|
+
totalActors: number;
|
|
78
|
+
totalActions: number;
|
|
79
|
+
chainValid: boolean;
|
|
80
|
+
oldestEntry: number | null;
|
|
81
|
+
newestEntry: number | null;
|
|
82
|
+
}
|
|
83
|
+
export declare class AuditTrailManager {
|
|
84
|
+
private entries;
|
|
85
|
+
private sequence;
|
|
86
|
+
private maxEntries;
|
|
87
|
+
private lastHash;
|
|
88
|
+
constructor(config?: AuditTrailConfig);
|
|
89
|
+
/** Record an audit entry. Returns the entry ID. */
|
|
90
|
+
record(params: AuditRecordParams): string;
|
|
91
|
+
/** Record multiple entries. */
|
|
92
|
+
recordBatch(params: AuditRecordParams[]): string[];
|
|
93
|
+
/** Query audit entries with filters. */
|
|
94
|
+
query(q?: AuditQuery): AuditQueryResult;
|
|
95
|
+
/** Get a single entry by ID. */
|
|
96
|
+
getEntry(id: string): AuditEntry | null;
|
|
97
|
+
/** Get all entries for a specific target. */
|
|
98
|
+
getTargetHistory(target: string): AuditEntry[];
|
|
99
|
+
/** Get all entries by a specific actor. */
|
|
100
|
+
getActorHistory(actor: string): AuditEntry[];
|
|
101
|
+
/** Verify the integrity of the audit chain. */
|
|
102
|
+
verifyChain(): ChainVerification;
|
|
103
|
+
/** Get action frequency counts. */
|
|
104
|
+
getActionCounts(): Map<string, number>;
|
|
105
|
+
/** Get unique actors. */
|
|
106
|
+
getActors(): string[];
|
|
107
|
+
/** Get unique targets. */
|
|
108
|
+
getTargets(): string[];
|
|
109
|
+
getStats(): AuditTrailStats;
|
|
110
|
+
/** Clear all data. */
|
|
111
|
+
destroy(): void;
|
|
112
|
+
/** Simple hash function for chain integrity. Production should use crypto. */
|
|
113
|
+
private simpleHash;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=audit-trail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-trail.d.ts","sourceRoot":"","sources":["../src/audit-trail.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AuditTrailManager — Compliance-ready audit trail with tamper detection.
|
|
4
|
+
*
|
|
5
|
+
* Records every significant action with actor, target, and metadata.
|
|
6
|
+
* Uses hash chains for tamper detection — each entry's hash includes
|
|
7
|
+
* the previous entry's hash, creating a verifiable chain of custody.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const audit = new AuditTrailManager();
|
|
12
|
+
*
|
|
13
|
+
* audit.record({
|
|
14
|
+
* action: 'key.created',
|
|
15
|
+
* actor: 'admin_1',
|
|
16
|
+
* target: 'key_abc',
|
|
17
|
+
* details: { credits: 1000 },
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* const entries = audit.query({ actor: 'admin_1' });
|
|
21
|
+
* const valid = audit.verifyChain(); // true if no tampering
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.AuditTrailManager = void 0;
|
|
26
|
+
// ── Implementation ───────────────────────────────────────────────────
|
|
27
|
+
class AuditTrailManager {
|
|
28
|
+
entries = [];
|
|
29
|
+
sequence = 0;
|
|
30
|
+
maxEntries;
|
|
31
|
+
lastHash = '0'; // Genesis hash
|
|
32
|
+
constructor(config = {}) {
|
|
33
|
+
this.maxEntries = config.maxEntries ?? 100_000;
|
|
34
|
+
}
|
|
35
|
+
// ── Recording ─────────────────────────────────────────────────────
|
|
36
|
+
/** Record an audit entry. Returns the entry ID. */
|
|
37
|
+
record(params) {
|
|
38
|
+
if (!params.action)
|
|
39
|
+
throw new Error('Action is required');
|
|
40
|
+
if (!params.actor)
|
|
41
|
+
throw new Error('Actor is required');
|
|
42
|
+
if (!params.target)
|
|
43
|
+
throw new Error('Target is required');
|
|
44
|
+
const id = `audit_${++this.sequence}`;
|
|
45
|
+
const timestamp = Date.now();
|
|
46
|
+
const previousHash = this.lastHash;
|
|
47
|
+
// Compute hash of this entry (simplified — production would use crypto)
|
|
48
|
+
const hashInput = `${id}|${params.action}|${params.actor}|${params.target}|${timestamp}|${previousHash}`;
|
|
49
|
+
const hash = this.simpleHash(hashInput);
|
|
50
|
+
const entry = {
|
|
51
|
+
id,
|
|
52
|
+
sequence: this.sequence,
|
|
53
|
+
action: params.action,
|
|
54
|
+
actor: params.actor,
|
|
55
|
+
actorType: params.actorType,
|
|
56
|
+
target: params.target,
|
|
57
|
+
targetType: params.targetType,
|
|
58
|
+
details: { ...(params.details ?? {}) },
|
|
59
|
+
timestamp,
|
|
60
|
+
source: params.source,
|
|
61
|
+
hash,
|
|
62
|
+
previousHash,
|
|
63
|
+
};
|
|
64
|
+
this.entries.push(entry);
|
|
65
|
+
this.lastHash = hash;
|
|
66
|
+
// Evict oldest if over limit
|
|
67
|
+
if (this.entries.length > this.maxEntries) {
|
|
68
|
+
this.entries.splice(0, this.entries.length - this.maxEntries);
|
|
69
|
+
}
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
/** Record multiple entries. */
|
|
73
|
+
recordBatch(params) {
|
|
74
|
+
return params.map(p => this.record(p));
|
|
75
|
+
}
|
|
76
|
+
// ── Query ─────────────────────────────────────────────────────────
|
|
77
|
+
/** Query audit entries with filters. */
|
|
78
|
+
query(q = {}) {
|
|
79
|
+
let filtered = this.entries;
|
|
80
|
+
if (q.action)
|
|
81
|
+
filtered = filtered.filter(e => e.action === q.action);
|
|
82
|
+
if (q.actions && q.actions.length > 0)
|
|
83
|
+
filtered = filtered.filter(e => q.actions.includes(e.action));
|
|
84
|
+
if (q.actor)
|
|
85
|
+
filtered = filtered.filter(e => e.actor === q.actor);
|
|
86
|
+
if (q.actorType)
|
|
87
|
+
filtered = filtered.filter(e => e.actorType === q.actorType);
|
|
88
|
+
if (q.target)
|
|
89
|
+
filtered = filtered.filter(e => e.target === q.target);
|
|
90
|
+
if (q.targetType)
|
|
91
|
+
filtered = filtered.filter(e => e.targetType === q.targetType);
|
|
92
|
+
if (q.startTime)
|
|
93
|
+
filtered = filtered.filter(e => e.timestamp >= q.startTime);
|
|
94
|
+
if (q.endTime)
|
|
95
|
+
filtered = filtered.filter(e => e.timestamp <= q.endTime);
|
|
96
|
+
const total = filtered.length;
|
|
97
|
+
const limit = q.limit ?? 100;
|
|
98
|
+
const offset = q.offset ?? 0;
|
|
99
|
+
filtered = filtered.slice(offset, offset + limit);
|
|
100
|
+
return {
|
|
101
|
+
entries: filtered,
|
|
102
|
+
total,
|
|
103
|
+
hasMore: offset + limit < total,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Get a single entry by ID. */
|
|
107
|
+
getEntry(id) {
|
|
108
|
+
return this.entries.find(e => e.id === id) ?? null;
|
|
109
|
+
}
|
|
110
|
+
/** Get all entries for a specific target. */
|
|
111
|
+
getTargetHistory(target) {
|
|
112
|
+
return this.entries.filter(e => e.target === target);
|
|
113
|
+
}
|
|
114
|
+
/** Get all entries by a specific actor. */
|
|
115
|
+
getActorHistory(actor) {
|
|
116
|
+
return this.entries.filter(e => e.actor === actor);
|
|
117
|
+
}
|
|
118
|
+
// ── Chain Verification ────────────────────────────────────────────
|
|
119
|
+
/** Verify the integrity of the audit chain. */
|
|
120
|
+
verifyChain() {
|
|
121
|
+
if (this.entries.length === 0) {
|
|
122
|
+
return { valid: true, totalEntries: 0 };
|
|
123
|
+
}
|
|
124
|
+
for (let i = 0; i < this.entries.length; i++) {
|
|
125
|
+
const entry = this.entries[i];
|
|
126
|
+
// Verify hash
|
|
127
|
+
const hashInput = `${entry.id}|${entry.action}|${entry.actor}|${entry.target}|${entry.timestamp}|${entry.previousHash}`;
|
|
128
|
+
const expectedHash = this.simpleHash(hashInput);
|
|
129
|
+
if (entry.hash !== expectedHash) {
|
|
130
|
+
return {
|
|
131
|
+
valid: false,
|
|
132
|
+
totalEntries: this.entries.length,
|
|
133
|
+
firstBrokenAt: i,
|
|
134
|
+
brokenEntry: entry.id,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Verify chain link (except first entry in current window — may have been evicted)
|
|
138
|
+
if (i > 0 && entry.previousHash !== this.entries[i - 1].hash) {
|
|
139
|
+
return {
|
|
140
|
+
valid: false,
|
|
141
|
+
totalEntries: this.entries.length,
|
|
142
|
+
firstBrokenAt: i,
|
|
143
|
+
brokenEntry: entry.id,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { valid: true, totalEntries: this.entries.length };
|
|
148
|
+
}
|
|
149
|
+
// ── Analytics ─────────────────────────────────────────────────────
|
|
150
|
+
/** Get action frequency counts. */
|
|
151
|
+
getActionCounts() {
|
|
152
|
+
const counts = new Map();
|
|
153
|
+
for (const e of this.entries) {
|
|
154
|
+
counts.set(e.action, (counts.get(e.action) ?? 0) + 1);
|
|
155
|
+
}
|
|
156
|
+
return counts;
|
|
157
|
+
}
|
|
158
|
+
/** Get unique actors. */
|
|
159
|
+
getActors() {
|
|
160
|
+
return [...new Set(this.entries.map(e => e.actor))];
|
|
161
|
+
}
|
|
162
|
+
/** Get unique targets. */
|
|
163
|
+
getTargets() {
|
|
164
|
+
return [...new Set(this.entries.map(e => e.target))];
|
|
165
|
+
}
|
|
166
|
+
// ── Stats ─────────────────────────────────────────────────────────
|
|
167
|
+
getStats() {
|
|
168
|
+
const actors = new Set(this.entries.map(e => e.actor));
|
|
169
|
+
const actions = new Set(this.entries.map(e => e.action));
|
|
170
|
+
return {
|
|
171
|
+
totalEntries: this.entries.length,
|
|
172
|
+
totalActors: actors.size,
|
|
173
|
+
totalActions: actions.size,
|
|
174
|
+
chainValid: this.verifyChain().valid,
|
|
175
|
+
oldestEntry: this.entries.length > 0 ? this.entries[0].timestamp : null,
|
|
176
|
+
newestEntry: this.entries.length > 0 ? this.entries[this.entries.length - 1].timestamp : null,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Clear all data. */
|
|
180
|
+
destroy() {
|
|
181
|
+
this.entries = [];
|
|
182
|
+
this.sequence = 0;
|
|
183
|
+
this.lastHash = '0';
|
|
184
|
+
}
|
|
185
|
+
// ── Private ───────────────────────────────────────────────────────
|
|
186
|
+
/** Simple hash function for chain integrity. Production should use crypto. */
|
|
187
|
+
simpleHash(input) {
|
|
188
|
+
let hash = 0;
|
|
189
|
+
for (let i = 0; i < input.length; i++) {
|
|
190
|
+
const char = input.charCodeAt(i);
|
|
191
|
+
hash = ((hash << 5) - hash) + char;
|
|
192
|
+
hash = hash & hash;
|
|
193
|
+
}
|
|
194
|
+
return `h_${Math.abs(hash).toString(36)}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
exports.AuditTrailManager = AuditTrailManager;
|
|
198
|
+
//# sourceMappingURL=audit-trail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-trail.js","sourceRoot":"","sources":["../src/audit-trail.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;AAuEH,wEAAwE;AAExE,MAAa,iBAAiB;IACpB,OAAO,GAAiB,EAAE,CAAC;IAC3B,QAAQ,GAAG,CAAC,CAAC;IACb,UAAU,CAAS;IACnB,QAAQ,GAAG,GAAG,CAAC,CAAC,eAAe;IAEvC,YAAY,SAA2B,EAAE;QACvC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,OAAO,CAAC;IACjD,CAAC;IAED,qEAAqE;IAErE,mDAAmD;IACnD,MAAM,CAAC,MAAyB;QAC9B,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;QAC1D,IAAI,CAAC,MAAM,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;QAE1D,MAAM,EAAE,GAAG,SAAS,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACtC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC;QAEnC,wEAAwE;QACxE,MAAM,SAAS,GAAG,GAAG,EAAE,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,IAAI,SAAS,IAAI,YAAY,EAAE,CAAC;QACzG,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAExC,MAAM,KAAK,GAAe;YACxB,EAAE;YACF,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,OAAO,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE;YACtC,SAAS;YACT,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,IAAI;YACJ,YAAY;SACb,CAAC;QAEF,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QAErB,6BAA6B;QAC7B,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YAC1C,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;QAChE,CAAC;QAED,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,+BAA+B;IAC/B,WAAW,CAAC,MAA2B;QACrC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC;IAED,qEAAqE;IAErE,wCAAwC;IACxC,KAAK,CAAC,IAAgB,EAAE;QACtB,IAAI,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC;QAE5B,IAAI,CAAC,CAAC,MAAM;YAAE,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;QACrE,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QACtG,IAAI,CAAC,CAAC,KAAK;YAAE,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;QAClE,IAAI,CAAC,CAAC,SAAS;YAAE,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC;QAC9E,IAAI,CAAC,CAAC,MAAM;YAAE,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;QACrE,IAAI,CAAC,CAAC,UAAU;YAAE,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC;QACjF,IAAI,CAAC,CAAC,SAAS;YAAE,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAU,CAAC,CAAC;QAC9E,IAAI,CAAC,CAAC,OAAO;YAAE,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,OAAQ,CAAC,CAAC;QAE1E,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC;QAC9B,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC;QAC7B,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;QAC7B,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC;QAElD,OAAO;YACL,OAAO,EAAE,QAAQ;YACjB,KAAK;YACL,OAAO,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK;SAChC,CAAC;IACJ,CAAC;IAED,gCAAgC;IAChC,QAAQ,CAAC,EAAU;QACjB,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,IAAI,CAAC;IACrD,CAAC;IAED,6CAA6C;IAC7C,gBAAgB,CAAC,MAAc;QAC7B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC;IACvD,CAAC;IAED,2CAA2C;IAC3C,eAAe,CAAC,KAAa;QAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;IACrD,CAAC;IAED,qEAAqE;IAErE,+CAA+C;IAC/C,WAAW;QACT,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9B,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QAC1C,CAAC;QAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAE9B,cAAc;YACd,MAAM,SAAS,GAAG,GAAG,KAAK,CAAC,EAAE,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;YACxH,MAAM,YAAY,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAEhD,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAChC,OAAO;oBACL,KAAK,EAAE,KAAK;oBACZ,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM;oBACjC,aAAa,EAAE,CAAC;oBAChB,WAAW,EAAE,KAAK,CAAC,EAAE;iBACtB,CAAC;YACJ,CAAC;YAED,mFAAmF;YACnF,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,YAAY,KAAK,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBAC7D,OAAO;oBACL,KAAK,EAAE,KAAK;oBACZ,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM;oBACjC,aAAa,EAAE,CAAC;oBAChB,WAAW,EAAE,KAAK,CAAC,EAAE;iBACtB,CAAC;YACJ,CAAC;QACH,CAAC;QAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;IAC5D,CAAC;IAED,qEAAqE;IAErE,mCAAmC;IACnC,eAAe;QACb,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;QACzC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAC7B,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACxD,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,yBAAyB;IACzB,SAAS;QACP,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACtD,CAAC;IAED,0BAA0B;IAC1B,UAAU;QACR,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,qEAAqE;IAErE,QAAQ;QACN,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QACvD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QAEzD,OAAO;YACL,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM;YACjC,WAAW,EAAE,MAAM,CAAC,IAAI;YACxB,YAAY,EAAE,OAAO,CAAC,IAAI;YAC1B,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK;YACpC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;YACvE,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;SAC9F,CAAC;IACJ,CAAC;IAED,sBAAsB;IACtB,OAAO;QACL,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAClB,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;QAClB,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC;IACtB,CAAC;IAED,qEAAqE;IAErE,8EAA8E;IACtE,UAAU,CAAC,KAAa;QAC9B,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YACjC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;YACnC,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,OAAO,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;IAC5C,CAAC;CACF;AAjMD,8CAiMC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeatureFlagManager — Toggle features per key with rollouts and scheduling.
|
|
3
|
+
*
|
|
4
|
+
* Control feature visibility per API key with percentage-based rollouts,
|
|
5
|
+
* A/B groups, and time-based scheduling. Useful for gradual feature
|
|
6
|
+
* launches, beta testing, and kill switches.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const flags = new FeatureFlagManager();
|
|
11
|
+
*
|
|
12
|
+
* flags.createFlag({
|
|
13
|
+
* name: 'new_search_v2',
|
|
14
|
+
* rolloutPercent: 50, // 50% of keys get this
|
|
15
|
+
* enabledKeys: ['key_beta'],
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* flags.isEnabled('new_search_v2', 'key_abc'); // depends on hash
|
|
19
|
+
* flags.isEnabled('new_search_v2', 'key_beta'); // always true
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export interface FeatureFlag {
|
|
23
|
+
name: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
/** Global enable/disable. If false, flag is off for everyone. */
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
/** Percentage of keys that get this feature (0-100). */
|
|
28
|
+
rolloutPercent: number;
|
|
29
|
+
/** Keys that always have this feature, regardless of rollout. */
|
|
30
|
+
enabledKeys: Set<string>;
|
|
31
|
+
/** Keys that never have this feature, regardless of rollout. */
|
|
32
|
+
disabledKeys: Set<string>;
|
|
33
|
+
/** A/B group name. Keys are consistently assigned to the same group. */
|
|
34
|
+
group?: string;
|
|
35
|
+
/** Schedule: only active between these times (ISO strings). */
|
|
36
|
+
activeFrom?: number;
|
|
37
|
+
activeUntil?: number;
|
|
38
|
+
createdAt: number;
|
|
39
|
+
updatedAt: number;
|
|
40
|
+
}
|
|
41
|
+
export interface FlagCreateParams {
|
|
42
|
+
name: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
enabled?: boolean;
|
|
45
|
+
rolloutPercent?: number;
|
|
46
|
+
enabledKeys?: string[];
|
|
47
|
+
disabledKeys?: string[];
|
|
48
|
+
group?: string;
|
|
49
|
+
activeFrom?: string;
|
|
50
|
+
activeUntil?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface FlagEvaluation {
|
|
53
|
+
flag: string;
|
|
54
|
+
key: string;
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
reason: 'flag_disabled' | 'key_allowlist' | 'key_blocklist' | 'schedule_inactive' | 'rollout_included' | 'rollout_excluded' | 'flag_not_found';
|
|
57
|
+
}
|
|
58
|
+
export interface FeatureFlagConfig {
|
|
59
|
+
/** Default rollout percent for new flags. Default 100 (enabled for all). */
|
|
60
|
+
defaultRolloutPercent?: number;
|
|
61
|
+
}
|
|
62
|
+
export interface FeatureFlagStats {
|
|
63
|
+
totalFlags: number;
|
|
64
|
+
enabledFlags: number;
|
|
65
|
+
disabledFlags: number;
|
|
66
|
+
totalEvaluations: number;
|
|
67
|
+
totalEnabled: number;
|
|
68
|
+
totalDisabled: number;
|
|
69
|
+
}
|
|
70
|
+
export declare class FeatureFlagManager {
|
|
71
|
+
private flags;
|
|
72
|
+
private defaultRolloutPercent;
|
|
73
|
+
private totalEvaluations;
|
|
74
|
+
private totalEnabled;
|
|
75
|
+
private totalDisabled;
|
|
76
|
+
constructor(config?: FeatureFlagConfig);
|
|
77
|
+
/** Create a new feature flag. */
|
|
78
|
+
createFlag(params: FlagCreateParams): FeatureFlag;
|
|
79
|
+
/** Remove a flag. */
|
|
80
|
+
removeFlag(name: string): boolean;
|
|
81
|
+
/** Get a flag definition. */
|
|
82
|
+
getFlag(name: string): FeatureFlag | null;
|
|
83
|
+
/** List all flags. */
|
|
84
|
+
listFlags(): FeatureFlag[];
|
|
85
|
+
/** Enable or disable a flag globally. */
|
|
86
|
+
setEnabled(name: string, enabled: boolean): boolean;
|
|
87
|
+
/** Update rollout percentage. */
|
|
88
|
+
setRolloutPercent(name: string, percent: number): boolean;
|
|
89
|
+
/** Add a key to the allowlist. */
|
|
90
|
+
addToAllowlist(name: string, key: string): boolean;
|
|
91
|
+
/** Add a key to the blocklist. */
|
|
92
|
+
addToBlocklist(name: string, key: string): boolean;
|
|
93
|
+
/** Remove a key from both lists. */
|
|
94
|
+
removeFromLists(name: string, key: string): boolean;
|
|
95
|
+
/** Set schedule window. */
|
|
96
|
+
setSchedule(name: string, activeFrom?: string, activeUntil?: string): boolean;
|
|
97
|
+
/** Check if a flag is enabled for a specific key. */
|
|
98
|
+
isEnabled(name: string, key: string): boolean;
|
|
99
|
+
/** Evaluate a flag for a key with full details. */
|
|
100
|
+
evaluate(name: string, key: string): FlagEvaluation;
|
|
101
|
+
/** Evaluate all flags for a key. Returns map of flag name → enabled. */
|
|
102
|
+
evaluateAll(key: string): Map<string, boolean>;
|
|
103
|
+
/** Get all keys that would have a flag enabled (from allowlist + rollout sampling). */
|
|
104
|
+
getEnabledKeys(name: string, sampleKeys: string[]): string[];
|
|
105
|
+
getStats(): FeatureFlagStats;
|
|
106
|
+
/** Clear all data. */
|
|
107
|
+
destroy(): void;
|
|
108
|
+
/** Simple deterministic hash for consistent rollout bucketing. */
|
|
109
|
+
private hashKey;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=feature-flags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-flags.d.ts","sourceRoot":"","sources":["../src/feature-flags.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FeatureFlagManager — Toggle features per key with rollouts and scheduling.
|
|
4
|
+
*
|
|
5
|
+
* Control feature visibility per API key with percentage-based rollouts,
|
|
6
|
+
* A/B groups, and time-based scheduling. Useful for gradual feature
|
|
7
|
+
* launches, beta testing, and kill switches.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const flags = new FeatureFlagManager();
|
|
12
|
+
*
|
|
13
|
+
* flags.createFlag({
|
|
14
|
+
* name: 'new_search_v2',
|
|
15
|
+
* rolloutPercent: 50, // 50% of keys get this
|
|
16
|
+
* enabledKeys: ['key_beta'],
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* flags.isEnabled('new_search_v2', 'key_abc'); // depends on hash
|
|
20
|
+
* flags.isEnabled('new_search_v2', 'key_beta'); // always true
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.FeatureFlagManager = void 0;
|
|
25
|
+
// ── Implementation ───────────────────────────────────────────────────
|
|
26
|
+
class FeatureFlagManager {
|
|
27
|
+
flags = new Map();
|
|
28
|
+
defaultRolloutPercent;
|
|
29
|
+
// Stats
|
|
30
|
+
totalEvaluations = 0;
|
|
31
|
+
totalEnabled = 0;
|
|
32
|
+
totalDisabled = 0;
|
|
33
|
+
constructor(config = {}) {
|
|
34
|
+
this.defaultRolloutPercent = config.defaultRolloutPercent ?? 100;
|
|
35
|
+
}
|
|
36
|
+
// ── Flag Management ───────────────────────────────────────────────
|
|
37
|
+
/** Create a new feature flag. */
|
|
38
|
+
createFlag(params) {
|
|
39
|
+
if (this.flags.has(params.name)) {
|
|
40
|
+
throw new Error(`Flag already exists: ${params.name}`);
|
|
41
|
+
}
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const flag = {
|
|
44
|
+
name: params.name,
|
|
45
|
+
description: params.description,
|
|
46
|
+
enabled: params.enabled ?? true,
|
|
47
|
+
rolloutPercent: Math.min(100, Math.max(0, params.rolloutPercent ?? this.defaultRolloutPercent)),
|
|
48
|
+
enabledKeys: new Set(params.enabledKeys ?? []),
|
|
49
|
+
disabledKeys: new Set(params.disabledKeys ?? []),
|
|
50
|
+
group: params.group,
|
|
51
|
+
activeFrom: params.activeFrom ? new Date(params.activeFrom).getTime() : undefined,
|
|
52
|
+
activeUntil: params.activeUntil ? new Date(params.activeUntil).getTime() : undefined,
|
|
53
|
+
createdAt: now,
|
|
54
|
+
updatedAt: now,
|
|
55
|
+
};
|
|
56
|
+
this.flags.set(params.name, flag);
|
|
57
|
+
return flag;
|
|
58
|
+
}
|
|
59
|
+
/** Remove a flag. */
|
|
60
|
+
removeFlag(name) {
|
|
61
|
+
return this.flags.delete(name);
|
|
62
|
+
}
|
|
63
|
+
/** Get a flag definition. */
|
|
64
|
+
getFlag(name) {
|
|
65
|
+
return this.flags.get(name) ?? null;
|
|
66
|
+
}
|
|
67
|
+
/** List all flags. */
|
|
68
|
+
listFlags() {
|
|
69
|
+
return [...this.flags.values()];
|
|
70
|
+
}
|
|
71
|
+
/** Enable or disable a flag globally. */
|
|
72
|
+
setEnabled(name, enabled) {
|
|
73
|
+
const flag = this.flags.get(name);
|
|
74
|
+
if (!flag)
|
|
75
|
+
return false;
|
|
76
|
+
flag.enabled = enabled;
|
|
77
|
+
flag.updatedAt = Date.now();
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/** Update rollout percentage. */
|
|
81
|
+
setRolloutPercent(name, percent) {
|
|
82
|
+
const flag = this.flags.get(name);
|
|
83
|
+
if (!flag)
|
|
84
|
+
return false;
|
|
85
|
+
flag.rolloutPercent = Math.min(100, Math.max(0, percent));
|
|
86
|
+
flag.updatedAt = Date.now();
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/** Add a key to the allowlist. */
|
|
90
|
+
addToAllowlist(name, key) {
|
|
91
|
+
const flag = this.flags.get(name);
|
|
92
|
+
if (!flag)
|
|
93
|
+
return false;
|
|
94
|
+
flag.enabledKeys.add(key);
|
|
95
|
+
flag.disabledKeys.delete(key);
|
|
96
|
+
flag.updatedAt = Date.now();
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/** Add a key to the blocklist. */
|
|
100
|
+
addToBlocklist(name, key) {
|
|
101
|
+
const flag = this.flags.get(name);
|
|
102
|
+
if (!flag)
|
|
103
|
+
return false;
|
|
104
|
+
flag.disabledKeys.add(key);
|
|
105
|
+
flag.enabledKeys.delete(key);
|
|
106
|
+
flag.updatedAt = Date.now();
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/** Remove a key from both lists. */
|
|
110
|
+
removeFromLists(name, key) {
|
|
111
|
+
const flag = this.flags.get(name);
|
|
112
|
+
if (!flag)
|
|
113
|
+
return false;
|
|
114
|
+
flag.enabledKeys.delete(key);
|
|
115
|
+
flag.disabledKeys.delete(key);
|
|
116
|
+
flag.updatedAt = Date.now();
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
/** Set schedule window. */
|
|
120
|
+
setSchedule(name, activeFrom, activeUntil) {
|
|
121
|
+
const flag = this.flags.get(name);
|
|
122
|
+
if (!flag)
|
|
123
|
+
return false;
|
|
124
|
+
flag.activeFrom = activeFrom ? new Date(activeFrom).getTime() : undefined;
|
|
125
|
+
flag.activeUntil = activeUntil ? new Date(activeUntil).getTime() : undefined;
|
|
126
|
+
flag.updatedAt = Date.now();
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
// ── Evaluation ────────────────────────────────────────────────────
|
|
130
|
+
/** Check if a flag is enabled for a specific key. */
|
|
131
|
+
isEnabled(name, key) {
|
|
132
|
+
return this.evaluate(name, key).enabled;
|
|
133
|
+
}
|
|
134
|
+
/** Evaluate a flag for a key with full details. */
|
|
135
|
+
evaluate(name, key) {
|
|
136
|
+
this.totalEvaluations++;
|
|
137
|
+
const flag = this.flags.get(name);
|
|
138
|
+
if (!flag) {
|
|
139
|
+
this.totalDisabled++;
|
|
140
|
+
return { flag: name, key, enabled: false, reason: 'flag_not_found' };
|
|
141
|
+
}
|
|
142
|
+
// Global disable
|
|
143
|
+
if (!flag.enabled) {
|
|
144
|
+
this.totalDisabled++;
|
|
145
|
+
return { flag: name, key, enabled: false, reason: 'flag_disabled' };
|
|
146
|
+
}
|
|
147
|
+
// Blocklist takes precedence
|
|
148
|
+
if (flag.disabledKeys.has(key)) {
|
|
149
|
+
this.totalDisabled++;
|
|
150
|
+
return { flag: name, key, enabled: false, reason: 'key_blocklist' };
|
|
151
|
+
}
|
|
152
|
+
// Allowlist
|
|
153
|
+
if (flag.enabledKeys.has(key)) {
|
|
154
|
+
this.totalEnabled++;
|
|
155
|
+
return { flag: name, key, enabled: true, reason: 'key_allowlist' };
|
|
156
|
+
}
|
|
157
|
+
// Schedule check
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
if (flag.activeFrom && now < flag.activeFrom) {
|
|
160
|
+
this.totalDisabled++;
|
|
161
|
+
return { flag: name, key, enabled: false, reason: 'schedule_inactive' };
|
|
162
|
+
}
|
|
163
|
+
if (flag.activeUntil && now > flag.activeUntil) {
|
|
164
|
+
this.totalDisabled++;
|
|
165
|
+
return { flag: name, key, enabled: false, reason: 'schedule_inactive' };
|
|
166
|
+
}
|
|
167
|
+
// Rollout: deterministic hash-based assignment
|
|
168
|
+
const hash = this.hashKey(key, flag.group ?? name);
|
|
169
|
+
const bucket = hash % 100;
|
|
170
|
+
if (bucket < flag.rolloutPercent) {
|
|
171
|
+
this.totalEnabled++;
|
|
172
|
+
return { flag: name, key, enabled: true, reason: 'rollout_included' };
|
|
173
|
+
}
|
|
174
|
+
this.totalDisabled++;
|
|
175
|
+
return { flag: name, key, enabled: false, reason: 'rollout_excluded' };
|
|
176
|
+
}
|
|
177
|
+
/** Evaluate all flags for a key. Returns map of flag name → enabled. */
|
|
178
|
+
evaluateAll(key) {
|
|
179
|
+
const result = new Map();
|
|
180
|
+
for (const flag of this.flags.values()) {
|
|
181
|
+
result.set(flag.name, this.isEnabled(flag.name, key));
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
/** Get all keys that would have a flag enabled (from allowlist + rollout sampling). */
|
|
186
|
+
getEnabledKeys(name, sampleKeys) {
|
|
187
|
+
return sampleKeys.filter(key => this.isEnabled(name, key));
|
|
188
|
+
}
|
|
189
|
+
// ── Stats ─────────────────────────────────────────────────────────
|
|
190
|
+
getStats() {
|
|
191
|
+
let enabled = 0, disabled = 0;
|
|
192
|
+
for (const flag of this.flags.values()) {
|
|
193
|
+
if (flag.enabled)
|
|
194
|
+
enabled++;
|
|
195
|
+
else
|
|
196
|
+
disabled++;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
totalFlags: this.flags.size,
|
|
200
|
+
enabledFlags: enabled,
|
|
201
|
+
disabledFlags: disabled,
|
|
202
|
+
totalEvaluations: this.totalEvaluations,
|
|
203
|
+
totalEnabled: this.totalEnabled,
|
|
204
|
+
totalDisabled: this.totalDisabled,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** Clear all data. */
|
|
208
|
+
destroy() {
|
|
209
|
+
this.flags.clear();
|
|
210
|
+
this.totalEvaluations = 0;
|
|
211
|
+
this.totalEnabled = 0;
|
|
212
|
+
this.totalDisabled = 0;
|
|
213
|
+
}
|
|
214
|
+
// ── Private ───────────────────────────────────────────────────────
|
|
215
|
+
/** Simple deterministic hash for consistent rollout bucketing. */
|
|
216
|
+
hashKey(key, salt) {
|
|
217
|
+
const str = `${key}:${salt}`;
|
|
218
|
+
let hash = 0;
|
|
219
|
+
for (let i = 0; i < str.length; i++) {
|
|
220
|
+
const char = str.charCodeAt(i);
|
|
221
|
+
hash = ((hash << 5) - hash) + char;
|
|
222
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
223
|
+
}
|
|
224
|
+
return Math.abs(hash);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
exports.FeatureFlagManager = FeatureFlagManager;
|
|
228
|
+
//# sourceMappingURL=feature-flags.js.map
|