@fluyappgocore/commons-backend 1.0.212 → 1.0.214
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.
|
@@ -1,14 +1,4 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from "express";
|
|
2
|
-
/**
|
|
3
|
-
* License feature guard middleware for microservices.
|
|
4
|
-
* Caches license data from the licensing server and checks feature flags.
|
|
5
|
-
*
|
|
6
|
-
* Usage in any MS route:
|
|
7
|
-
* router.get("/analytics", authFBMiddleware, licenseGuard("analytics-summary"), controller)
|
|
8
|
-
*
|
|
9
|
-
* Requires LICENSE_URL and INSTALLATION_UUID env vars.
|
|
10
|
-
* Falls back to allowing access if license server is unreachable.
|
|
11
|
-
*/
|
|
12
2
|
interface LicenseCache {
|
|
13
3
|
valid: boolean;
|
|
14
4
|
readOnly: boolean;
|
|
@@ -21,6 +11,9 @@ interface LicenseCache {
|
|
|
21
11
|
};
|
|
22
12
|
tier: string;
|
|
23
13
|
fetchedAt: number;
|
|
14
|
+
unreachable?: boolean;
|
|
15
|
+
unreachableSince?: number | null;
|
|
16
|
+
unreachableGraceDaysLeft?: number | null;
|
|
24
17
|
}
|
|
25
18
|
/**
|
|
26
19
|
* Check if a license feature is enabled.
|
|
@@ -47,10 +40,16 @@ export declare function licenseWriteGuard(): (req: Request, res: Response, next:
|
|
|
47
40
|
export declare function licenseLoginGuard(): (_req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
|
48
41
|
/**
|
|
49
42
|
* Get current license status (for use in controllers).
|
|
43
|
+
* Includes the unreachable-grace fields so the frontend can render a
|
|
44
|
+
* "licensing server unreachable, X days left" banner.
|
|
50
45
|
*/
|
|
51
46
|
export declare function getLicenseStatus(): Promise<{
|
|
52
47
|
valid: boolean;
|
|
53
48
|
readOnly: boolean;
|
|
54
49
|
blocked: boolean;
|
|
50
|
+
tier: string;
|
|
51
|
+
unreachable: boolean;
|
|
52
|
+
unreachableSince: number | null;
|
|
53
|
+
unreachableGraceDaysLeft: number | null;
|
|
55
54
|
} | null>;
|
|
56
55
|
export {};
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
2
13
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
14
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
15
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -37,61 +48,226 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
37
48
|
};
|
|
38
49
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
50
|
exports.getLicenseStatus = exports.licenseLoginGuard = exports.licenseWriteGuard = exports.licenseGuard = exports.getLicenseLimits = exports.isLicenseFeatureEnabled = void 0;
|
|
51
|
+
var fs_1 = require("fs");
|
|
52
|
+
/**
|
|
53
|
+
* License feature guard middleware for microservices.
|
|
54
|
+
* Caches license data from the licensing server and checks feature flags.
|
|
55
|
+
*
|
|
56
|
+
* Usage in any MS route:
|
|
57
|
+
* router.get("/analytics", authFBMiddleware, licenseGuard("analytics-summary"), controller)
|
|
58
|
+
*
|
|
59
|
+
* Requires LICENSE_URL and INSTALLATION_UUID env vars.
|
|
60
|
+
*
|
|
61
|
+
* Unreachable behaviour (PaaS policy):
|
|
62
|
+
* - If the licensing server can't be reached, the tenant keeps
|
|
63
|
+
* working for UNREACHABLE_GRACE_DAYS days using the last cached
|
|
64
|
+
* features (or open if there was none). After that, /license-status
|
|
65
|
+
* reports `blocked: true` and the standard guards reject traffic.
|
|
66
|
+
* - The client app is expected to read `unreachable` +
|
|
67
|
+
* `unreachableGraceDaysLeft` from `getLicenseStatus()` and show a
|
|
68
|
+
* warning banner.
|
|
69
|
+
*/
|
|
70
|
+
var UNREACHABLE_GRACE_DAYS = 7;
|
|
71
|
+
var UNREACHABLE_GRACE_MS = UNREACHABLE_GRACE_DAYS * 24 * 60 * 60 * 1000;
|
|
72
|
+
var GRACE_STATE_FILE = "/tmp/fluyapp-license-grace.json";
|
|
40
73
|
var cache = null;
|
|
41
74
|
var CACHE_TTL = 1000 * 60 * 60; // 1 hour
|
|
42
|
-
function
|
|
75
|
+
function readGraceState() {
|
|
43
76
|
return __awaiter(this, void 0, void 0, function () {
|
|
44
|
-
var
|
|
77
|
+
var raw, _a;
|
|
45
78
|
return __generator(this, function (_b) {
|
|
46
79
|
switch (_b.label) {
|
|
47
80
|
case 0:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!licenseUrl || !installationUuid)
|
|
51
|
-
return [2 /*return*/, null];
|
|
52
|
-
_b.label = 1;
|
|
81
|
+
_b.trys.push([0, 2, , 3]);
|
|
82
|
+
return [4 /*yield*/, fs_1.promises.readFile(GRACE_STATE_FILE, "utf8")];
|
|
53
83
|
case 1:
|
|
54
|
-
_b.
|
|
55
|
-
|
|
56
|
-
timeout_1 = setTimeout(function () { return controller_1.abort(); }, 5000);
|
|
57
|
-
return [4 /*yield*/, fetch(licenseUrl + "/api/license/validate/" + installationUuid, { signal: controller_1.signal }).finally(function () { return clearTimeout(timeout_1); })];
|
|
84
|
+
raw = _b.sent();
|
|
85
|
+
return [2 /*return*/, JSON.parse(raw)];
|
|
58
86
|
case 2:
|
|
59
|
-
res = _b.sent();
|
|
60
|
-
if (!res.ok)
|
|
61
|
-
return [2 /*return*/, null];
|
|
62
|
-
return [4 /*yield*/, res.json()];
|
|
63
|
-
case 3:
|
|
64
|
-
data = _b.sent();
|
|
65
|
-
return [2 /*return*/, {
|
|
66
|
-
valid: data.valid,
|
|
67
|
-
readOnly: data.readOnly || false,
|
|
68
|
-
blocked: data.blocked || false,
|
|
69
|
-
features: data.features || {},
|
|
70
|
-
limits: data.limits || {},
|
|
71
|
-
tier: data.tier || "BASIC",
|
|
72
|
-
fetchedAt: Date.now(),
|
|
73
|
-
}];
|
|
74
|
-
case 4:
|
|
75
87
|
_a = _b.sent();
|
|
76
88
|
return [2 /*return*/, null];
|
|
77
|
-
case
|
|
89
|
+
case 3: return [2 /*return*/];
|
|
78
90
|
}
|
|
79
91
|
});
|
|
80
92
|
});
|
|
81
93
|
}
|
|
82
|
-
function
|
|
94
|
+
function writeGraceState(state) {
|
|
95
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
96
|
+
var _a;
|
|
97
|
+
return __generator(this, function (_b) {
|
|
98
|
+
switch (_b.label) {
|
|
99
|
+
case 0:
|
|
100
|
+
_b.trys.push([0, 5, , 6]);
|
|
101
|
+
if (!state) return [3 /*break*/, 2];
|
|
102
|
+
return [4 /*yield*/, fs_1.promises.writeFile(GRACE_STATE_FILE, JSON.stringify(state))];
|
|
103
|
+
case 1:
|
|
104
|
+
_b.sent();
|
|
105
|
+
return [3 /*break*/, 4];
|
|
106
|
+
case 2: return [4 /*yield*/, fs_1.promises.unlink(GRACE_STATE_FILE).catch(function () { })];
|
|
107
|
+
case 3:
|
|
108
|
+
_b.sent();
|
|
109
|
+
_b.label = 4;
|
|
110
|
+
case 4: return [3 /*break*/, 6];
|
|
111
|
+
case 5:
|
|
112
|
+
_a = _b.sent();
|
|
113
|
+
return [3 /*break*/, 6];
|
|
114
|
+
case 6: return [2 /*return*/];
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Where to fetch license data from. Two modes:
|
|
121
|
+
*
|
|
122
|
+
* - LICENSE_PROXY_URL set → in-cluster proxy (recommended).
|
|
123
|
+
* All MS except ms-entity hit `${LICENSE_PROXY_URL}` which resolves to
|
|
124
|
+
* `http://msentities-clusterip-srv:8092/api_entities/internal/license-status`.
|
|
125
|
+
* Only ms-entity itself actually talks to licensing.fluyapp.io. Dramatic
|
|
126
|
+
* reduction in outbound traffic (N × M → 1 × M).
|
|
127
|
+
*
|
|
128
|
+
* - LICENSE_URL set → direct fetch from the licensing server.
|
|
129
|
+
* Used by ms-entity (which IS the aggregator) and as fallback for any MS
|
|
130
|
+
* where the proxy is unreachable.
|
|
131
|
+
*
|
|
132
|
+
* If both are set, the proxy is tried first; on failure we fall back to the
|
|
133
|
+
* direct URL so a network issue between MS doesn't break licensing for
|
|
134
|
+
* everyone.
|
|
135
|
+
*/
|
|
136
|
+
function fetchLicense() {
|
|
83
137
|
return __awaiter(this, void 0, void 0, function () {
|
|
84
|
-
|
|
138
|
+
function tryFetch(url, isProxy) {
|
|
139
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
140
|
+
var controller_1, timeout_1, headers, res, data, _a;
|
|
141
|
+
return __generator(this, function (_b) {
|
|
142
|
+
switch (_b.label) {
|
|
143
|
+
case 0:
|
|
144
|
+
_b.trys.push([0, 3, , 4]);
|
|
145
|
+
controller_1 = new AbortController();
|
|
146
|
+
timeout_1 = setTimeout(function () { return controller_1.abort(); }, 5000);
|
|
147
|
+
headers = {};
|
|
148
|
+
if (isProxy && internalSecret)
|
|
149
|
+
headers["x-internal-secret"] = internalSecret;
|
|
150
|
+
return [4 /*yield*/, fetch(url, { signal: controller_1.signal, headers: headers })
|
|
151
|
+
.finally(function () { return clearTimeout(timeout_1); })];
|
|
152
|
+
case 1:
|
|
153
|
+
res = _b.sent();
|
|
154
|
+
if (!res.ok)
|
|
155
|
+
return [2 /*return*/, null];
|
|
156
|
+
return [4 /*yield*/, res.json()];
|
|
157
|
+
case 2:
|
|
158
|
+
data = _b.sent();
|
|
159
|
+
return [2 /*return*/, {
|
|
160
|
+
valid: data.valid,
|
|
161
|
+
readOnly: data.readOnly || false,
|
|
162
|
+
blocked: data.blocked || false,
|
|
163
|
+
features: data.features || {},
|
|
164
|
+
limits: data.limits || {},
|
|
165
|
+
tier: data.tier || "BASIC",
|
|
166
|
+
fetchedAt: Date.now(),
|
|
167
|
+
}];
|
|
168
|
+
case 3:
|
|
169
|
+
_a = _b.sent();
|
|
170
|
+
return [2 /*return*/, null];
|
|
171
|
+
case 4: return [2 /*return*/];
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
var proxyUrl, licenseUrl, installationUuid, internalSecret, cached, cached;
|
|
85
177
|
return __generator(this, function (_a) {
|
|
86
178
|
switch (_a.label) {
|
|
179
|
+
case 0:
|
|
180
|
+
proxyUrl = process.env.LICENSE_PROXY_URL || "";
|
|
181
|
+
licenseUrl = process.env.LICENSE_URL || "";
|
|
182
|
+
installationUuid = process.env.INSTALLATION_UUID || process.env.ENTITY_UUID || "";
|
|
183
|
+
internalSecret = process.env.INTERNAL_SECRET || "";
|
|
184
|
+
if (!proxyUrl) return [3 /*break*/, 2];
|
|
185
|
+
return [4 /*yield*/, tryFetch(proxyUrl, true)];
|
|
186
|
+
case 1:
|
|
187
|
+
cached = _a.sent();
|
|
188
|
+
if (cached)
|
|
189
|
+
return [2 /*return*/, cached];
|
|
190
|
+
_a.label = 2;
|
|
191
|
+
case 2:
|
|
192
|
+
if (!(licenseUrl && installationUuid)) return [3 /*break*/, 4];
|
|
193
|
+
return [4 /*yield*/, tryFetch(licenseUrl + "/api/license/validate/" + installationUuid, false)];
|
|
194
|
+
case 3:
|
|
195
|
+
cached = _a.sent();
|
|
196
|
+
if (cached)
|
|
197
|
+
return [2 /*return*/, cached];
|
|
198
|
+
_a.label = 4;
|
|
199
|
+
case 4: return [2 /*return*/, null];
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function getCachedLicense() {
|
|
205
|
+
var _a;
|
|
206
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
207
|
+
var fresh, now, stored, since, elapsed, expired, daysLeft;
|
|
208
|
+
return __generator(this, function (_b) {
|
|
209
|
+
switch (_b.label) {
|
|
87
210
|
case 0:
|
|
88
211
|
if (cache && (Date.now() - cache.fetchedAt) < CACHE_TTL)
|
|
89
212
|
return [2 /*return*/, cache];
|
|
90
213
|
return [4 /*yield*/, fetchLicense()];
|
|
91
214
|
case 1:
|
|
92
|
-
fresh =
|
|
93
|
-
if (fresh)
|
|
94
|
-
|
|
215
|
+
fresh = _b.sent();
|
|
216
|
+
if (!fresh) return [3 /*break*/, 3];
|
|
217
|
+
// Successful refresh: clear any pending grace state.
|
|
218
|
+
return [4 /*yield*/, writeGraceState(null)];
|
|
219
|
+
case 2:
|
|
220
|
+
// Successful refresh: clear any pending grace state.
|
|
221
|
+
_b.sent();
|
|
222
|
+
cache = __assign(__assign({}, fresh), { unreachable: false, unreachableSince: null, unreachableGraceDaysLeft: null });
|
|
223
|
+
return [2 /*return*/, cache];
|
|
224
|
+
case 3:
|
|
225
|
+
now = Date.now();
|
|
226
|
+
return [4 /*yield*/, readGraceState()];
|
|
227
|
+
case 4:
|
|
228
|
+
stored = _b.sent();
|
|
229
|
+
since = (_a = stored === null || stored === void 0 ? void 0 : stored.unreachableSince) !== null && _a !== void 0 ? _a : now;
|
|
230
|
+
if (!!stored) return [3 /*break*/, 6];
|
|
231
|
+
return [4 /*yield*/, writeGraceState({ unreachableSince: now })];
|
|
232
|
+
case 5:
|
|
233
|
+
_b.sent();
|
|
234
|
+
_b.label = 6;
|
|
235
|
+
case 6:
|
|
236
|
+
elapsed = now - since;
|
|
237
|
+
expired = elapsed >= UNREACHABLE_GRACE_MS;
|
|
238
|
+
daysLeft = expired ? 0 : Math.ceil((UNREACHABLE_GRACE_MS - elapsed) / (24 * 60 * 60 * 1000));
|
|
239
|
+
if (expired) {
|
|
240
|
+
// Past 7 days without a successful fetch — treat as blocked.
|
|
241
|
+
cache = {
|
|
242
|
+
valid: false,
|
|
243
|
+
readOnly: false,
|
|
244
|
+
blocked: true,
|
|
245
|
+
features: {},
|
|
246
|
+
limits: { maxBranches: 0, maxAgents: 0, maxServices: 0 },
|
|
247
|
+
tier: "BASIC",
|
|
248
|
+
fetchedAt: now,
|
|
249
|
+
unreachable: true,
|
|
250
|
+
unreachableSince: since,
|
|
251
|
+
unreachableGraceDaysLeft: 0,
|
|
252
|
+
};
|
|
253
|
+
return [2 /*return*/, cache];
|
|
254
|
+
}
|
|
255
|
+
// Inside grace: keep last known features if we had any, otherwise open.
|
|
256
|
+
if (cache && cache.valid !== false) {
|
|
257
|
+
return [2 /*return*/, __assign(__assign({}, cache), { unreachable: true, unreachableSince: since, unreachableGraceDaysLeft: daysLeft, fetchedAt: now })];
|
|
258
|
+
}
|
|
259
|
+
cache = {
|
|
260
|
+
valid: true,
|
|
261
|
+
readOnly: false,
|
|
262
|
+
blocked: false,
|
|
263
|
+
features: {},
|
|
264
|
+
limits: { maxBranches: 0, maxAgents: 0, maxServices: 0 },
|
|
265
|
+
tier: "GRACE",
|
|
266
|
+
fetchedAt: now,
|
|
267
|
+
unreachable: true,
|
|
268
|
+
unreachableSince: since,
|
|
269
|
+
unreachableGraceDaysLeft: daysLeft,
|
|
270
|
+
};
|
|
95
271
|
return [2 /*return*/, cache];
|
|
96
272
|
}
|
|
97
273
|
});
|
|
@@ -252,11 +428,30 @@ function licenseLoginGuard() {
|
|
|
252
428
|
exports.licenseLoginGuard = licenseLoginGuard;
|
|
253
429
|
/**
|
|
254
430
|
* Get current license status (for use in controllers).
|
|
431
|
+
* Includes the unreachable-grace fields so the frontend can render a
|
|
432
|
+
* "licensing server unreachable, X days left" banner.
|
|
255
433
|
*/
|
|
256
434
|
function getLicenseStatus() {
|
|
435
|
+
var _a, _b;
|
|
257
436
|
return __awaiter(this, void 0, void 0, function () {
|
|
258
|
-
|
|
259
|
-
|
|
437
|
+
var lic;
|
|
438
|
+
return __generator(this, function (_c) {
|
|
439
|
+
switch (_c.label) {
|
|
440
|
+
case 0: return [4 /*yield*/, getCachedLicense()];
|
|
441
|
+
case 1:
|
|
442
|
+
lic = _c.sent();
|
|
443
|
+
if (!lic)
|
|
444
|
+
return [2 /*return*/, null];
|
|
445
|
+
return [2 /*return*/, {
|
|
446
|
+
valid: lic.valid,
|
|
447
|
+
readOnly: lic.readOnly,
|
|
448
|
+
blocked: lic.blocked,
|
|
449
|
+
tier: lic.tier,
|
|
450
|
+
unreachable: lic.unreachable === true,
|
|
451
|
+
unreachableSince: (_a = lic.unreachableSince) !== null && _a !== void 0 ? _a : null,
|
|
452
|
+
unreachableGraceDaysLeft: (_b = lic.unreachableGraceDaysLeft) !== null && _b !== void 0 ? _b : null,
|
|
453
|
+
}];
|
|
454
|
+
}
|
|
260
455
|
});
|
|
261
456
|
});
|
|
262
457
|
}
|