@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 fetchLicense() {
75
+ function readGraceState() {
43
76
  return __awaiter(this, void 0, void 0, function () {
44
- var licenseUrl, installationUuid, controller_1, timeout_1, res, data, _a;
77
+ var raw, _a;
45
78
  return __generator(this, function (_b) {
46
79
  switch (_b.label) {
47
80
  case 0:
48
- licenseUrl = process.env.LICENSE_URL || "";
49
- installationUuid = process.env.INSTALLATION_UUID || process.env.ENTITY_UUID || "";
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.trys.push([1, 4, , 5]);
55
- controller_1 = new AbortController();
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 5: return [2 /*return*/];
89
+ case 3: return [2 /*return*/];
78
90
  }
79
91
  });
80
92
  });
81
93
  }
82
- function getCachedLicense() {
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
- var fresh;
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 = _a.sent();
93
- if (fresh)
94
- cache = fresh;
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
- return __generator(this, function (_a) {
259
- return [2 /*return*/, getCachedLicense()];
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluyappgocore/commons-backend",
3
- "version": "1.0.212",
3
+ "version": "1.0.214",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",