@voltro/plugin-flags 0.80.0 → 0.82.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/CHANGELOG.md +1610 -6
- package/dist/index.js +137 -131
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
9
9
|
let n = 2166136261, r = `${e}:${t}`;
|
|
10
10
|
for (let e = 0; e < r.length; e++) n ^= r.charCodeAt(e), n = Math.imul(n, 16777619);
|
|
11
11
|
return n >>> 0;
|
|
12
|
-
}, E = (e, t) => T(e, t) % 100,
|
|
12
|
+
}, E = (e, t) => T(e, t) % 100, ee = (e, t) => T(`${e}:variant`, t) % 1e4, D = (e, t) => {
|
|
13
13
|
if (e.subjectIds && !(t.id != null && e.subjectIds.includes(t.id)) || e.tenantIds && !(t.tenantId != null && e.tenantIds.includes(t.tenantId)) || e.subjectTypes && !(t.type != null && e.subjectTypes.includes(t.type))) return !1;
|
|
14
14
|
if (e.metadata) {
|
|
15
15
|
let n = t.metadata ?? {};
|
|
@@ -44,43 +44,43 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
44
44
|
if (!e.active) return !1;
|
|
45
45
|
a = e.rollout;
|
|
46
46
|
}
|
|
47
|
-
if (i.targeting && i.targeting.length > 0 && !i.targeting.some((e) =>
|
|
47
|
+
if (i.targeting && i.targeting.length > 0 && !i.targeting.some((e) => D(e, n))) return !1;
|
|
48
48
|
let o = a ?? i.rollout ?? 100;
|
|
49
49
|
if (o < 100 && o > 0) {
|
|
50
50
|
if (!(E(e, k(i, n)) < o)) return !1;
|
|
51
51
|
} else if (o <= 0) return !1;
|
|
52
|
-
return i.variants && i.variants.length > 0 && i.offVariant != null ?
|
|
53
|
-
},
|
|
52
|
+
return i.variants && i.variants.length > 0 && i.offVariant != null ? N(e, i, n).name !== i.offVariant : !0;
|
|
53
|
+
}, M = (e) => {
|
|
54
54
|
let t = e.reduce((e, t) => e + (t.weight ?? 1), 0) || e.length, n = 0;
|
|
55
55
|
return e.map((r, i) => (n += r.weight ?? 1, i === e.length - 1 ? 1e4 : Math.round(n / t * 1e4)));
|
|
56
|
-
},
|
|
56
|
+
}, N = (e, t, n) => {
|
|
57
57
|
let r = t.variants ?? [];
|
|
58
58
|
if (r.length === 0) throw Error(`flag "${e}" has no variants`);
|
|
59
|
-
let i =
|
|
59
|
+
let i = ee(e, k(t, n)), a = M(r);
|
|
60
60
|
for (let e = 0; e < r.length; e++) if (i < a[e]) return r[e];
|
|
61
61
|
return r[r.length - 1];
|
|
62
|
-
},
|
|
62
|
+
}, P = (e, t, n, r = Date.now()) => {
|
|
63
63
|
let i = w(t), a = j(e, t, n, r);
|
|
64
64
|
if (!i.variants || i.variants.length === 0) return {
|
|
65
65
|
name: a ? "on" : "off",
|
|
66
66
|
value: a,
|
|
67
67
|
enabled: a
|
|
68
68
|
};
|
|
69
|
-
let o =
|
|
69
|
+
let o = N(e, i, n);
|
|
70
70
|
return {
|
|
71
71
|
name: o.name,
|
|
72
72
|
value: o.value,
|
|
73
73
|
enabled: a
|
|
74
74
|
};
|
|
75
|
-
},
|
|
75
|
+
}, te = (e, t, n = Date.now()) => {
|
|
76
76
|
let r = {};
|
|
77
77
|
for (let [i, a] of Object.entries(e)) r[i] = j(i, a, t, n);
|
|
78
78
|
return r;
|
|
79
|
-
},
|
|
79
|
+
}, ne = (e, t, n = Date.now()) => {
|
|
80
80
|
let r = {};
|
|
81
|
-
for (let [i, a] of Object.entries(e)) r[i] =
|
|
81
|
+
for (let [i, a] of Object.entries(e)) r[i] = P(i, a, t, n);
|
|
82
82
|
return r;
|
|
83
|
-
},
|
|
83
|
+
}, F = "_voltro_feature_flag_audit", I = "_voltro_feature_flags", L = x(I, {
|
|
84
84
|
id: h({ prefix: "flag" }),
|
|
85
85
|
key: S().unique(),
|
|
86
86
|
enabled: f().default(!0),
|
|
@@ -92,14 +92,14 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
92
92
|
schedule: _().nullable(),
|
|
93
93
|
description: S().nullable(),
|
|
94
94
|
updatedAt: C().default("now").onUpdate("now")
|
|
95
|
-
}),
|
|
95
|
+
}), R = x(F, {
|
|
96
96
|
id: h({ prefix: "flagaud" }),
|
|
97
97
|
flag: S(),
|
|
98
98
|
enabled: f(),
|
|
99
99
|
previousEnabled: f().nullable(),
|
|
100
100
|
actorId: S().nullable(),
|
|
101
101
|
at: C().default("now")
|
|
102
|
-
}).index("byFlagAuditFlag", ["flag"]).index("byFlagAuditAt", ["at"]),
|
|
102
|
+
}).index("byFlagAuditFlag", ["flag"]).index("byFlagAuditAt", ["at"]), re = (e, t) => {
|
|
103
103
|
let n = { ...e };
|
|
104
104
|
for (let e of t) {
|
|
105
105
|
let t = {
|
|
@@ -115,7 +115,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
115
115
|
n[e.key] = t;
|
|
116
116
|
}
|
|
117
117
|
return n;
|
|
118
|
-
},
|
|
118
|
+
}, z = new class {
|
|
119
119
|
flags = {};
|
|
120
120
|
set(e) {
|
|
121
121
|
this.flags = e;
|
|
@@ -126,18 +126,18 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
126
126
|
get(e) {
|
|
127
127
|
return this.flags[e];
|
|
128
128
|
}
|
|
129
|
-
}(),
|
|
130
|
-
let n =
|
|
129
|
+
}(), B = (e, t) => V(e, t) !== null, V = (e, t) => {
|
|
130
|
+
let n = z.get(e);
|
|
131
131
|
if (n === void 0) return null;
|
|
132
132
|
let r = w(n), i = r.enabled !== !1;
|
|
133
|
-
return
|
|
134
|
-
...
|
|
133
|
+
return z.set({
|
|
134
|
+
...z.all(),
|
|
135
135
|
[e]: {
|
|
136
136
|
...r,
|
|
137
137
|
enabled: t
|
|
138
138
|
}
|
|
139
139
|
}), i;
|
|
140
|
-
},
|
|
140
|
+
}, H = (e) => Object.entries(e).map(([e, t]) => {
|
|
141
141
|
let n = w(t);
|
|
142
142
|
return {
|
|
143
143
|
key: e,
|
|
@@ -147,7 +147,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
147
147
|
scheduled: n.schedule != null,
|
|
148
148
|
description: n.description
|
|
149
149
|
};
|
|
150
|
-
}),
|
|
150
|
+
}), ie = (e, t) => {
|
|
151
151
|
let n = w(t);
|
|
152
152
|
return {
|
|
153
153
|
key: e,
|
|
@@ -160,8 +160,8 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
160
160
|
schedule: n.schedule ?? null,
|
|
161
161
|
description: n.description ?? null
|
|
162
162
|
};
|
|
163
|
-
},
|
|
164
|
-
loadOverrides: async () => (await e.query(v(
|
|
163
|
+
}, ae = (e) => ({
|
|
164
|
+
loadOverrides: async () => (await e.query(v(L).descriptor)).map((e) => ({
|
|
165
165
|
key: String(e.key),
|
|
166
166
|
enabled: e.enabled === !0 || e.enabled === 1,
|
|
167
167
|
rollout: e.rollout == null ? null : Number(e.rollout),
|
|
@@ -173,10 +173,10 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
173
173
|
description: e.description ?? null
|
|
174
174
|
})),
|
|
175
175
|
upsertOverride: async (t) => {
|
|
176
|
-
let n =
|
|
177
|
-
await e.upsert(
|
|
176
|
+
let n = U(t);
|
|
177
|
+
await e.upsert(I, n, { conflictColumns: ["key"] });
|
|
178
178
|
}
|
|
179
|
-
}),
|
|
179
|
+
}), U = (e) => ({
|
|
180
180
|
id: p("flag", e.key),
|
|
181
181
|
key: e.key,
|
|
182
182
|
enabled: e.enabled,
|
|
@@ -188,15 +188,15 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
188
188
|
schedule: e.schedule,
|
|
189
189
|
description: e.description,
|
|
190
190
|
updatedAt: /* @__PURE__ */ new Date()
|
|
191
|
-
}),
|
|
191
|
+
}), oe = (e) => ({
|
|
192
192
|
flag: String(e.flag),
|
|
193
193
|
enabled: e.enabled === !0 || e.enabled === 1,
|
|
194
194
|
previousEnabled: e.previousEnabled == null ? null : e.previousEnabled === !0 || e.previousEnabled === 1,
|
|
195
195
|
actorId: e.actorId ?? null,
|
|
196
196
|
at: e.at instanceof Date ? e.at : new Date(String(e.at))
|
|
197
|
-
}),
|
|
197
|
+
}), se = (e) => ({
|
|
198
198
|
record: async (t) => {
|
|
199
|
-
await e.insert(
|
|
199
|
+
await e.insert(F, {
|
|
200
200
|
id: `flagaud_${t.at.getTime()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
201
201
|
flag: t.flag,
|
|
202
202
|
enabled: t.enabled,
|
|
@@ -206,21 +206,21 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
206
206
|
});
|
|
207
207
|
},
|
|
208
208
|
list: async (t) => {
|
|
209
|
-
let n = v(
|
|
210
|
-
return t?.flag != null && (n = n.where(m("flag", t.flag))), t?.limit != null && (n = n.limit(t.limit)), (await e.query(n.descriptor)).map(
|
|
209
|
+
let n = v(R).orderBy("at", "desc");
|
|
210
|
+
return t?.flag != null && (n = n.where(m("flag", t.flag))), t?.limit != null && (n = n.limit(t.limit)), (await e.query(n.descriptor)).map(oe);
|
|
211
211
|
}
|
|
212
|
-
}),
|
|
212
|
+
}), ce = async (e, t, n, r, i) => {
|
|
213
213
|
await e.transactional(async (e) => {
|
|
214
|
-
await e.insertIgnore(
|
|
215
|
-
...
|
|
214
|
+
await e.insertIgnore(I, {
|
|
215
|
+
...U(ie(t, n)),
|
|
216
216
|
id: p("flag", crypto.randomUUID())
|
|
217
217
|
}, { conflictColumns: ["key"] });
|
|
218
|
-
let a = (await e.query(v(
|
|
218
|
+
let a = (await e.query(v(L).where(m("key", t)).forUpdate().descriptor))[0];
|
|
219
219
|
if (!a) throw Error("flag override disappeared during toggle");
|
|
220
|
-
await e.update(
|
|
220
|
+
await e.update(I, String(a.id), {
|
|
221
221
|
enabled: r,
|
|
222
222
|
updatedAt: /* @__PURE__ */ new Date()
|
|
223
|
-
}), await
|
|
223
|
+
}), await se(e).record({
|
|
224
224
|
flag: t,
|
|
225
225
|
enabled: r,
|
|
226
226
|
previousEnabled: a.enabled === !0 || a.enabled === 1,
|
|
@@ -228,7 +228,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
228
228
|
at: /* @__PURE__ */ new Date()
|
|
229
229
|
});
|
|
230
230
|
});
|
|
231
|
-
},
|
|
231
|
+
}, W = (e, t) => {
|
|
232
232
|
let n = new Map(t.map((e) => [e.name, e])), r = [];
|
|
233
233
|
for (let t of e) {
|
|
234
234
|
let e = t.experiment;
|
|
@@ -267,12 +267,12 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
return r;
|
|
270
|
-
},
|
|
271
|
-
let n =
|
|
270
|
+
}, le = (e, t) => {
|
|
271
|
+
let n = W(e, t);
|
|
272
272
|
if (n.length === 0) return;
|
|
273
273
|
let r = n.map((e) => ` · flag "${e.flag}" → experiment "${e.experiment}": ${e.problem}`);
|
|
274
274
|
throw Error(`@voltro/plugin-flags: ${n.length} flag→experiment link problem(s):\n${r.join("\n")}`);
|
|
275
|
-
},
|
|
275
|
+
}, ue = 864e5, G = (e) => typeof e == "number" ? e : Date.parse(e), K = (e, t) => {
|
|
276
276
|
let n = w(e);
|
|
277
277
|
if (n.enabled === !1) return {
|
|
278
278
|
shape: "constantOff",
|
|
@@ -280,11 +280,11 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
280
280
|
};
|
|
281
281
|
if (n.schedule) {
|
|
282
282
|
let { activateAt: e, deactivateAt: r } = n.schedule;
|
|
283
|
-
if (r != null && t >=
|
|
283
|
+
if (r != null && t >= G(r)) return {
|
|
284
284
|
shape: "expired",
|
|
285
285
|
reason: `schedule.deactivateAt (${String(r)}) has passed — the flag can never be on again`
|
|
286
286
|
};
|
|
287
|
-
if (e != null && t <
|
|
287
|
+
if (e != null && t < G(e)) return {
|
|
288
288
|
shape: "notYetActive",
|
|
289
289
|
reason: `schedule.activateAt (${String(e)}) is in the future — pending, not dead`
|
|
290
290
|
};
|
|
@@ -294,7 +294,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
294
294
|
shape: "constantOff",
|
|
295
295
|
reason: `rollout: ${r} — nobody is in the rollout`
|
|
296
296
|
};
|
|
297
|
-
let i = (n.targeting?.length ?? 0) > 0, a = (n.variants?.length ?? 0) > 1, o = n.schedule !== void 0 && (n.schedule.ramp !== void 0 || n.schedule.deactivateAt != null && t <
|
|
297
|
+
let i = (n.targeting?.length ?? 0) > 0, a = (n.variants?.length ?? 0) > 1, o = n.schedule !== void 0 && (n.schedule.ramp !== void 0 || n.schedule.deactivateAt != null && t < G(n.schedule.deactivateAt));
|
|
298
298
|
return r >= 100 && !i && !a && !o ? {
|
|
299
299
|
shape: "constantOn",
|
|
300
300
|
reason: "rollout 100 with no targeting, no variants and no live schedule — resolves true for every caller"
|
|
@@ -314,7 +314,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
314
314
|
"The window is bounded by retention (see coverage.retentionDays). Nothing older is evidence: a flag last read BEFORE the window has no observation at all and reads \"neverObserved\", which is exactly what a flag declared this morning reads.",
|
|
315
315
|
"\"neverObserved\" is not a removal candidate. A flag declared yesterday and a flag abandoned last year produce the identical row; only a flag that WAS observed and then stopped (\"stale\") is proven unused."
|
|
316
316
|
], fe = (e) => {
|
|
317
|
-
let t = e.now ?? Date.now(), n = e.staleAfterDays, r = t - n *
|
|
317
|
+
let t = e.now ?? Date.now(), n = e.staleAfterDays, r = t - n * ue, i = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map(), o;
|
|
318
318
|
for (let t of e.usage) {
|
|
319
319
|
let e = t.lastAt.getTime();
|
|
320
320
|
if (!Number.isFinite(e)) continue;
|
|
@@ -322,7 +322,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
322
322
|
let n = t.source === "bulk" ? a : i, r = n.get(t.flag);
|
|
323
323
|
(r === void 0 || e > r) && n.set(t.flag, e);
|
|
324
324
|
}
|
|
325
|
-
let s = o === void 0 ? 0 : Math.floor((t - o) /
|
|
325
|
+
let s = o === void 0 ? 0 : Math.floor((t - o) / ue);
|
|
326
326
|
return {
|
|
327
327
|
coverage: {
|
|
328
328
|
tracking: e.tracking,
|
|
@@ -332,7 +332,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
332
332
|
staleAfterDays: n
|
|
333
333
|
},
|
|
334
334
|
findings: Object.entries(e.flags).map(([o, c]) => {
|
|
335
|
-
let { shape: l, reason: u } =
|
|
335
|
+
let { shape: l, reason: u } = K(c, t), d = i.get(o), f = a.get(o), p = [u], m;
|
|
336
336
|
e.tracking ? d !== void 0 && d >= r ? (m = "evaluated", p.push(`last server-side read ${new Date(d).toISOString()}`)) : d === void 0 ? (m = "neverObserved", p.push(s > 0 ? `no server-side read in the ${s}-day observation window` : "no server-side read observed yet (the observation window is empty)"), f !== void 0 && p.push("the flag WAS delivered to a client in a bulk flags.evaluate response — which does not prove it was read")) : (m = "stale", p.push(`last server-side read ${new Date(d).toISOString()}, more than ${n} day(s) ago, over an observation window of ${s} day(s)`)) : (m = "untracked", p.push("evaluation tracking is off — no usage evidence exists for this deployment"));
|
|
337
337
|
let h = l === "constantOn" || l === "constantOff" || l === "expired";
|
|
338
338
|
return h && p.push("the definition alone makes this flag a constant — it selects nothing"), {
|
|
@@ -458,14 +458,14 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
458
458
|
envVar: "VOLTRO_FLAG_USAGE_TTL_HOURS"
|
|
459
459
|
}), n;
|
|
460
460
|
}, Z = (e) => "request" in e && e.request?.subject || "subject" in e && e.subject || {}, Q = (e, t) => {
|
|
461
|
-
let n =
|
|
461
|
+
let n = z.get(t);
|
|
462
462
|
return X(t, "targeted"), n !== void 0 && j(t, n, Z(e));
|
|
463
463
|
}, xe = (e, t) => Q(e, t) ? l.void : l.fail(new c({ flag: t })), Se = (e, t) => {
|
|
464
|
-
let n =
|
|
465
|
-
return X(t.key, "targeted"), n === void 0 ? t.defaultValue : i(t,
|
|
464
|
+
let n = z.get(t.key);
|
|
465
|
+
return X(t.key, "targeted"), n === void 0 ? t.defaultValue : i(t, P(t.key, n, Z(e)));
|
|
466
466
|
}, Ce = (e, t) => {
|
|
467
|
-
let n =
|
|
468
|
-
return X(t.key, "targeted"), n === void 0 ? null :
|
|
467
|
+
let n = z.get(t.key);
|
|
468
|
+
return X(t.key, "targeted"), n === void 0 ? null : P(t.key, n, Z(e)).name;
|
|
469
469
|
}, we = (e, t) => e === t || e.startsWith("/") && e.endsWith("/") && new RegExp(e.slice(1, -1)).test(t), $ = e, Te = (e = {}) => {
|
|
470
470
|
let i = e.gatedBy ?? {}, o = d({
|
|
471
471
|
base: $,
|
|
@@ -477,71 +477,87 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
477
477
|
if (e.flags?.[t.key] !== void 0) throw Error(`${$}: ${JSON.stringify(t.key)} is declared BOTH in \`flags\` and in \`typedFlags\`. One key, one declaration — the typed one would otherwise be silently overwritten by the untyped baseline.`);
|
|
478
478
|
f.set(t.key, t);
|
|
479
479
|
}
|
|
480
|
-
s.some((e) => e.experiment !== void 0) &&
|
|
480
|
+
s.some((e) => e.experiment !== void 0) && le(s, e.experiments ?? []);
|
|
481
481
|
let p = { ...e.flags ?? {} };
|
|
482
482
|
for (let e of s) p[e.key] = e.definition;
|
|
483
|
-
let m = e.store === "postgres", h, g, _, v, y, b = [],
|
|
484
|
-
if (
|
|
485
|
-
let
|
|
486
|
-
if (
|
|
487
|
-
let
|
|
488
|
-
let t =
|
|
483
|
+
let m = e.store === "postgres", h, g, _, v, y, b, x, S = [], C = e.usage ?? {}, w = C.track ?? m;
|
|
484
|
+
if (w && !m) throw Error(`${$}: usage.track requires store: 'postgres' — there is nowhere to record an evaluation on the memory tier, and a dead-flag report that silently observed nothing is exactly the defect it exists to find.`);
|
|
485
|
+
let T = C.staleAfterDays ?? 30, E = C.retentionDays ?? 90;
|
|
486
|
+
if (T > E) throw Error(`${$}: usage.staleAfterDays (${T}) exceeds usage.retentionDays (${E}), so the observation window can never be long enough to prove a flag stale and every verdict would be withheld.`);
|
|
487
|
+
let ee = (e) => {
|
|
488
|
+
let t = re(p, e), n = [];
|
|
489
489
|
for (let [e, r] of f) {
|
|
490
490
|
let i = t[e];
|
|
491
491
|
if (i === void 0) continue;
|
|
492
492
|
let o = a(r, typeof i == "boolean" ? { enabled: i } : i);
|
|
493
493
|
o.ok || (n.push(o.refusal), t[e] = r.definition);
|
|
494
494
|
}
|
|
495
|
-
|
|
496
|
-
},
|
|
495
|
+
S = n, z.set(t);
|
|
496
|
+
}, D = 0, O = async () => {
|
|
497
497
|
if (!g) return;
|
|
498
|
-
let e = ++
|
|
499
|
-
e ===
|
|
500
|
-
},
|
|
498
|
+
let e = ++D, t = await g.loadOverrides();
|
|
499
|
+
e === D && ee(t);
|
|
500
|
+
}, k = async () => {
|
|
501
|
+
if (b && g) {
|
|
502
|
+
if (await O(), w && v && !y) {
|
|
503
|
+
x = be(process.env, E), Y.enable();
|
|
504
|
+
let e = b.logger;
|
|
505
|
+
y = ye({
|
|
506
|
+
store: v,
|
|
507
|
+
intervalMs: C.flushIntervalMs ?? 3e5,
|
|
508
|
+
log: { warn: (t, n) => e.warn(t, n) }
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
for (let e of S) b.logger.warn("feature flag override REFUSED — code-declared definition stands", {
|
|
512
|
+
flag: e.key,
|
|
513
|
+
reason: e.reason
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}, A = (e) => {
|
|
501
517
|
if (i[e] !== void 0) return i[e];
|
|
502
518
|
for (let [t, n] of Object.entries(i)) if (t !== e && we(t, e)) return n;
|
|
503
|
-
},
|
|
504
|
-
let n =
|
|
519
|
+
}, j = (e, t) => {
|
|
520
|
+
let n = A(t.tag);
|
|
505
521
|
return n === void 0 || Q({ subject: t.subject }, n) ? e : l.fail(new c({ flag: n }));
|
|
506
|
-
},
|
|
522
|
+
}, M = Object.keys(i).length > 0, N = (e) => {
|
|
507
523
|
for (let t of e) X(t, "bulk");
|
|
508
|
-
},
|
|
524
|
+
}, P = {
|
|
509
525
|
...t,
|
|
510
526
|
description: "Resolve every feature flag for the calling subject.",
|
|
511
527
|
execute: (e, t) => l.sync(() => {
|
|
512
|
-
let e =
|
|
513
|
-
return
|
|
528
|
+
let e = z.all();
|
|
529
|
+
return N(Object.keys(e)), te(e, t.request.subject ?? {});
|
|
514
530
|
})
|
|
515
|
-
},
|
|
531
|
+
}, F = {
|
|
516
532
|
...n,
|
|
517
533
|
description: "Resolve every feature flag to its served variant for the calling subject.",
|
|
518
534
|
execute: (e, t) => l.sync(() => {
|
|
519
|
-
let e =
|
|
520
|
-
return
|
|
535
|
+
let e = z.all();
|
|
536
|
+
return N(Object.keys(e)), ne(e, t.request.subject ?? {});
|
|
521
537
|
})
|
|
522
|
-
},
|
|
538
|
+
}, I = (e) => ({
|
|
523
539
|
kind: "json",
|
|
524
540
|
data: e
|
|
525
|
-
}),
|
|
541
|
+
}), B = (e, t) => ({
|
|
526
542
|
kind: "json",
|
|
527
543
|
status: e,
|
|
528
544
|
data: { error: t }
|
|
529
|
-
}),
|
|
545
|
+
}), ie = async () => {
|
|
530
546
|
let e = [];
|
|
531
|
-
if (
|
|
547
|
+
if (w && v) try {
|
|
532
548
|
e = await v.load();
|
|
533
549
|
} catch {
|
|
534
550
|
e = [];
|
|
535
551
|
}
|
|
536
552
|
return fe({
|
|
537
|
-
flags:
|
|
553
|
+
flags: z.all(),
|
|
538
554
|
usage: e,
|
|
539
|
-
tracking:
|
|
555
|
+
tracking: w,
|
|
540
556
|
liveCounts: Y.liveTargetedCounts(),
|
|
541
|
-
staleAfterDays:
|
|
542
|
-
retentionDays:
|
|
557
|
+
staleAfterDays: T,
|
|
558
|
+
retentionDays: E
|
|
543
559
|
});
|
|
544
|
-
},
|
|
560
|
+
}, U = () => {
|
|
545
561
|
let e = {};
|
|
546
562
|
for (let [t, n] of f) e[t] = {
|
|
547
563
|
valueType: n.valueType,
|
|
@@ -549,19 +565,20 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
549
565
|
experiment: n.experiment ?? null
|
|
550
566
|
};
|
|
551
567
|
return e;
|
|
552
|
-
},
|
|
568
|
+
}, oe = [
|
|
553
569
|
{
|
|
554
570
|
method: "GET",
|
|
555
571
|
path: "/list",
|
|
556
572
|
scope: "process",
|
|
557
573
|
description: "Every resolved flag (key, enabled, rollout, description) plus the dead-flag lifecycle report: per-flag shape + usage verdict, the observation coverage window, and the limits of what the report can prove.",
|
|
558
574
|
handler: () => l.promise(async () => {
|
|
559
|
-
let e = await
|
|
560
|
-
return
|
|
561
|
-
flags:
|
|
575
|
+
let e = await ie();
|
|
576
|
+
return I({
|
|
577
|
+
flags: H(z.all()),
|
|
578
|
+
writeScope: m ? "shared-store" : "process",
|
|
562
579
|
lifecycle: e,
|
|
563
|
-
typed:
|
|
564
|
-
overrideRefusals:
|
|
580
|
+
typed: U(),
|
|
581
|
+
overrideRefusals: S
|
|
565
582
|
});
|
|
566
583
|
})
|
|
567
584
|
},
|
|
@@ -574,21 +591,21 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
574
591
|
try {
|
|
575
592
|
t = JSON.parse(e.body || "{}");
|
|
576
593
|
} catch {
|
|
577
|
-
return
|
|
594
|
+
return B(400, "invalid JSON body");
|
|
578
595
|
}
|
|
579
|
-
if (!t || typeof t.key != "string" || !t.key || typeof t.enabled != "boolean") return
|
|
580
|
-
let n = t.key, r = t.enabled, i =
|
|
581
|
-
if (i === void 0) return
|
|
596
|
+
if (!t || typeof t.key != "string" || !t.key || typeof t.enabled != "boolean") return B(400, "key (string) + enabled (boolean) required");
|
|
597
|
+
let n = t.key, r = t.enabled, i = z.get(n);
|
|
598
|
+
if (i === void 0) return B(404, `unknown flag "${n}"`);
|
|
582
599
|
if (m) {
|
|
583
600
|
let e = h;
|
|
584
|
-
if (!e) return
|
|
601
|
+
if (!e) return B(503, "flag store is not ready");
|
|
585
602
|
if ((yield* l.tryPromise(async () => {
|
|
586
|
-
await
|
|
587
|
-
}).pipe(l.either))._tag === "Left") return yield* l.tryPromise(
|
|
588
|
-
} else
|
|
589
|
-
return
|
|
603
|
+
await ce(e, n, i, r, typeof t.actor == "string" ? t.actor : null), await O();
|
|
604
|
+
}).pipe(l.either))._tag === "Left") return yield* l.tryPromise(O).pipe(l.ignore), B(503, "flag update could not be confirmed; reload the flag state before retrying");
|
|
605
|
+
} else V(n, r);
|
|
606
|
+
return I({
|
|
590
607
|
ok: !0,
|
|
591
|
-
flags:
|
|
608
|
+
flags: H(z.all())
|
|
592
609
|
});
|
|
593
610
|
})
|
|
594
611
|
},
|
|
@@ -598,7 +615,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
598
615
|
scope: m ? "shared-store" : "declaration",
|
|
599
616
|
description: "The kill-switch flip audit trail, newest-first. `?flag=<key>` filters; `?limit=<n>` caps (postgres tier only).",
|
|
600
617
|
handler: (e) => l.gen(function* () {
|
|
601
|
-
if (!_) return
|
|
618
|
+
if (!_) return I({
|
|
602
619
|
audit: [],
|
|
603
620
|
note: "audit trail requires store: \"postgres\""
|
|
604
621
|
});
|
|
@@ -606,7 +623,7 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
606
623
|
...n == null ? {} : { flag: n },
|
|
607
624
|
limit: i
|
|
608
625
|
}));
|
|
609
|
-
return
|
|
626
|
+
return I({ audit: a.map((e) => ({
|
|
610
627
|
flag: e.flag,
|
|
611
628
|
enabled: e.enabled,
|
|
612
629
|
previousEnabled: e.previousEnabled,
|
|
@@ -615,8 +632,8 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
615
632
|
})) });
|
|
616
633
|
})
|
|
617
634
|
}
|
|
618
|
-
],
|
|
619
|
-
...
|
|
635
|
+
], W = [
|
|
636
|
+
...M ? [
|
|
620
637
|
"rpc:intercept:mutation",
|
|
621
638
|
"rpc:intercept:query",
|
|
622
639
|
"rpc:intercept:action",
|
|
@@ -630,10 +647,10 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
630
647
|
name: o,
|
|
631
648
|
baseName: $,
|
|
632
649
|
description: "Feature flags — targeting, % rollout, kill-switch, declarative gating + client evaluation.",
|
|
633
|
-
permissions:
|
|
634
|
-
routes: [
|
|
650
|
+
permissions: W,
|
|
651
|
+
routes: [P, F],
|
|
635
652
|
rpcClientDescriptors: r,
|
|
636
|
-
inspectEndpoints:
|
|
653
|
+
inspectEndpoints: oe,
|
|
637
654
|
errorSchemas: [{
|
|
638
655
|
schema: c,
|
|
639
656
|
import: {
|
|
@@ -641,45 +658,34 @@ var w = (e) => typeof e == "boolean" ? { enabled: e } : e, T = (e, t) => {
|
|
|
641
658
|
name: "FlagDisabled"
|
|
642
659
|
}
|
|
643
660
|
}],
|
|
644
|
-
...
|
|
645
|
-
interceptMutation:
|
|
646
|
-
interceptQuery:
|
|
647
|
-
interceptAction:
|
|
648
|
-
interceptWorkflow:
|
|
661
|
+
...M ? {
|
|
662
|
+
interceptMutation: j,
|
|
663
|
+
interceptQuery: j,
|
|
664
|
+
interceptAction: j,
|
|
665
|
+
interceptWorkflow: j
|
|
649
666
|
} : {},
|
|
650
|
-
...m ? { extendSchema: { tables:
|
|
651
|
-
I,
|
|
667
|
+
...m ? { extendSchema: { tables: w ? [
|
|
652
668
|
L,
|
|
669
|
+
R,
|
|
653
670
|
J
|
|
654
|
-
] : [
|
|
655
|
-
...m ? { bindDataStore: (e) => {
|
|
656
|
-
h = e, g =
|
|
671
|
+
] : [L, R] } } : {},
|
|
672
|
+
...m ? { bindDataStore: async (e) => {
|
|
673
|
+
h = e, g = ae(h), _ = se(e), w && (v = _e(e)), await k();
|
|
657
674
|
} } : {},
|
|
658
|
-
...m ? { onChangeEvent: (e) => e.table === "_voltro_feature_flags" ? l.tryPromise(() =>
|
|
675
|
+
...m ? { onChangeEvent: (e) => e.table === "_voltro_feature_flags" ? l.tryPromise(() => O()) : l.void } : {},
|
|
659
676
|
onActivate: (t) => l.gen(function* () {
|
|
660
|
-
|
|
661
|
-
let n;
|
|
662
|
-
S && v && (n = be(process.env, w), Y.enable(), y = ye({
|
|
663
|
-
store: v,
|
|
664
|
-
intervalMs: x.flushIntervalMs ?? 3e5,
|
|
665
|
-
log: { warn: (e, n) => t.logger.warn(e, n) }
|
|
666
|
-
}));
|
|
667
|
-
for (let e of b) t.logger.warn("feature flag override REFUSED — the code-declared definition stands", {
|
|
668
|
-
flag: e.key,
|
|
669
|
-
reason: e.reason
|
|
670
|
-
});
|
|
671
|
-
t.logger.info("feature flags active", {
|
|
677
|
+
z.set(p), b = t, g && (yield* l.promise(k)), t.logger.info("feature flags active", {
|
|
672
678
|
count: Object.keys(p).length,
|
|
673
679
|
typed: f.size,
|
|
674
680
|
gated: Object.keys(i).length,
|
|
675
681
|
store: e.store ?? "memory",
|
|
676
|
-
usage:
|
|
682
|
+
usage: w ? y ? `tracking (stale after ${T}d, observations kept ${Math.round((x ?? 0) / 864e5)}d)` : "waiting for store binding" : "off — no dead-flag evidence is collected"
|
|
677
683
|
});
|
|
678
684
|
}),
|
|
679
685
|
onDeactivate: () => l.promise(async () => {
|
|
680
|
-
await y?.stop(), y = void 0, Y.disable(),
|
|
686
|
+
b = void 0, await y?.stop(), y = void 0, Y.disable(), z.set({});
|
|
681
687
|
})
|
|
682
688
|
});
|
|
683
689
|
};
|
|
684
690
|
//#endregion
|
|
685
|
-
export { ve as DEFAULT_FLAG_USAGE_FLUSH_MS, de as FLAG_LIFECYCLE_LIMITS, q as FLAG_USAGE_TABLE, c as FlagDisabled,
|
|
691
|
+
export { ve as DEFAULT_FLAG_USAGE_FLUSH_MS, de as FLAG_LIFECYCLE_LIMITS, q as FLAG_USAGE_TABLE, c as FlagDisabled, le as assertFlagExperimentLinks, W as checkFlagExperimentLinks, K as classifyShape, _e as dataStoreFlagUsageStore, s as defineFlag, H as describeFlags, te as evaluateAll, ne as evaluateAllVariants, j as evaluateFlag, A as evaluateSchedule, P as evaluateVariant, L as featureFlagsTable, R as flagAuditTable, fe as flagLifecycleReport, z as flagRegistry, Y as flagUsageBuffer, he as flagUsageRowId, J as flagUsageTable, Se as flagValue, Ce as flagVariant, Te as flagsPlugin, V as flipFlagEnabled, a as gateOverride, Q as isFlagEnabled, o as isTypedFlag, re as mergeOverrides, X as recordFlagEvaluation, be as registerFlagUsageRetention, xe as requireFlag, N as resolveVariant, B as setFlagEnabled, ye as startFlagUsageFlush, w as toDefinition, i as typedValueOf, pe as utcDay };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-flags",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.0",
|
|
4
4
|
"description": "Feature flags — per-subject / per-tenant targeting, deterministic % rollouts, kill-switch. Gate mutations/queries/actions declaratively or guard in-handler; evaluate flags client-side for UI gating. Config-as-code (memory) or runtime-toggleable (postgres).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"node": ">=24.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@voltro/client": "0.
|
|
52
|
-
"@voltro/database": "0.
|
|
53
|
-
"@voltro/logger": "0.
|
|
54
|
-
"@voltro/protocol": "0.
|
|
51
|
+
"@voltro/client": "0.82.0",
|
|
52
|
+
"@voltro/database": "0.82.0",
|
|
53
|
+
"@voltro/logger": "0.82.0",
|
|
54
|
+
"@voltro/protocol": "0.82.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"effect": "^3.22.2"
|