@fluyappgocore/commons-backend 1.0.213 → 1.0.215
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.
|
@@ -39,12 +39,28 @@ export interface PortalClaims {
|
|
|
39
39
|
branchUuid: string;
|
|
40
40
|
/** Present iff scope === "ticket" */
|
|
41
41
|
ticketUuid?: string;
|
|
42
|
+
/** Stable token id — used for revocation by jti, kept in claims so
|
|
43
|
+
* the verify path can blacklist a single emission without nuking
|
|
44
|
+
* every token ever issued for the same ticket. */
|
|
45
|
+
jti?: string;
|
|
42
46
|
iat: number;
|
|
43
47
|
exp: number;
|
|
44
48
|
}
|
|
45
49
|
export interface RequestWithPortal extends Request {
|
|
46
50
|
portalAuth?: PortalClaims;
|
|
47
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Revokes every ticket-scoped token currently in flight for one
|
|
54
|
+
* ticket. Use this when the customer reports a stolen cookie or
|
|
55
|
+
* when an admin manually closes a session. TTL of the revocation
|
|
56
|
+
* key matches the longest possible JWT lifetime so it cleans itself.
|
|
57
|
+
*/
|
|
58
|
+
export declare function revokeTicketTokens(ticketUuid: string, ttlSec?: number): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Revokes a specific token emission by its `jti`. Useful when only
|
|
61
|
+
* one device should be cut off, not the whole ticket session.
|
|
62
|
+
*/
|
|
63
|
+
export declare function revokeTokenByJti(jti: string, ttlSec?: number): Promise<void>;
|
|
48
64
|
/** Issue a fresh guest token for a visitor who just landed on a branch URL. */
|
|
49
65
|
export declare function issueGuestToken(input: {
|
|
50
66
|
entityUuid: string;
|
|
@@ -53,11 +69,22 @@ export declare function issueGuestToken(input: {
|
|
|
53
69
|
token: string;
|
|
54
70
|
expiresAt: number;
|
|
55
71
|
};
|
|
56
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Upgrade a guest into a ticket holder. Optional `ttlSec` lets the
|
|
74
|
+
* caller dial the lifetime per ticket phase:
|
|
75
|
+
*
|
|
76
|
+
* - WAITING / CALLING / ATTENDING → 2h (default)
|
|
77
|
+
* - FINISHED → 7d (NPS, share, print receipt)
|
|
78
|
+
* - CANCELLED / ABANDONED → 30 min (de-prioritised)
|
|
79
|
+
*
|
|
80
|
+
* Renewing on every poll keeps the cookie warm without forcing a
|
|
81
|
+
* "still here" re-auth dance.
|
|
82
|
+
*/
|
|
57
83
|
export declare function issueTicketToken(input: {
|
|
58
84
|
entityUuid: string;
|
|
59
85
|
branchUuid: string;
|
|
60
86
|
ticketUuid: string;
|
|
87
|
+
ttlSec?: number;
|
|
61
88
|
}): {
|
|
62
89
|
token: string;
|
|
63
90
|
expiresAt: number;
|
|
@@ -69,7 +96,7 @@ export declare function issueTicketToken(input: {
|
|
|
69
96
|
* Rejects anything that isn't a portal-scoped token (aud !== "portal")
|
|
70
97
|
* so a regular user/agent JWT can NEVER accidentally pass this gate.
|
|
71
98
|
*/
|
|
72
|
-
export declare function verifyPortalToken(req: RequestWithPortal, _res: Response, next: NextFunction): void
|
|
99
|
+
export declare function verifyPortalToken(req: RequestWithPortal, _res: Response, next: NextFunction): Promise<void>;
|
|
73
100
|
/**
|
|
74
101
|
* Gate a route by scope.
|
|
75
102
|
*
|
|
@@ -18,17 +18,160 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
18
18
|
__setModuleDefault(result, mod);
|
|
19
19
|
return result;
|
|
20
20
|
};
|
|
21
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
22
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
23
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
24
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
25
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
26
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
27
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
31
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
32
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
33
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
34
|
+
function step(op) {
|
|
35
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
36
|
+
while (_) try {
|
|
37
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
38
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
39
|
+
switch (op[0]) {
|
|
40
|
+
case 0: case 1: t = op; break;
|
|
41
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
42
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
43
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
44
|
+
default:
|
|
45
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
46
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
47
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
48
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
49
|
+
if (t[2]) _.ops.pop();
|
|
50
|
+
_.trys.pop(); continue;
|
|
51
|
+
}
|
|
52
|
+
op = body.call(thisArg, _);
|
|
53
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
54
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
55
|
+
}
|
|
56
|
+
};
|
|
21
57
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.requireBranchMatch = exports.requireTicketMatch = exports.requireScope = exports.verifyPortalToken = exports.issueTicketToken = exports.issueGuestToken = void 0;
|
|
58
|
+
exports.requireBranchMatch = exports.requireTicketMatch = exports.requireScope = exports.verifyPortalToken = exports.issueTicketToken = exports.issueGuestToken = exports.revokeTokenByJti = exports.revokeTicketTokens = void 0;
|
|
23
59
|
var jwt = __importStar(require("jsonwebtoken"));
|
|
60
|
+
var redis = __importStar(require("redis"));
|
|
24
61
|
var exceptions_1 = require("../exceptions");
|
|
62
|
+
var promisify = require("util").promisify;
|
|
25
63
|
var SECRET = process.env.JWT_KEY || "";
|
|
26
64
|
var GUEST_TTL_SEC = 30 * 60; // 30 minutes
|
|
27
65
|
var TICKET_TTL_SEC = 2 * 60 * 60; // 2 hours
|
|
66
|
+
// ─── Revocation backbone ────────────────────────────────────────────
|
|
67
|
+
//
|
|
68
|
+
// JWT is stateless by design — the only way to invalidate a still-fresh
|
|
69
|
+
// token is to keep a server-side blacklist and check it on every verify.
|
|
70
|
+
// We keep two key namespaces in Redis:
|
|
71
|
+
//
|
|
72
|
+
// portal_revoked_jti:<jti> → revoke a single token emission
|
|
73
|
+
// portal_revoked_ticket:<ticketUuid> → revoke ALL ticket-scoped tokens
|
|
74
|
+
// for a given ticket (admin "kill
|
|
75
|
+
// the session" action)
|
|
76
|
+
//
|
|
77
|
+
// The key value is irrelevant; presence == revoked. We TTL the keys so
|
|
78
|
+
// we don't accumulate forever — once the token's exp is in the past
|
|
79
|
+
// the JWT verify alone rejects it, blacklist becomes redundant.
|
|
80
|
+
//
|
|
81
|
+
// Best-effort: if Redis is unreachable, we FAIL OPEN (allow the JWT)
|
|
82
|
+
// instead of locking everyone out during an outage. The signed JWT
|
|
83
|
+
// itself is still authentic. Trade-off documented for security audit.
|
|
84
|
+
var RedisClient = null;
|
|
85
|
+
var existsAsync = null;
|
|
86
|
+
var setAsync = null;
|
|
87
|
+
function getRedis() {
|
|
88
|
+
if (RedisClient)
|
|
89
|
+
return { existsAsync: existsAsync, setAsync: setAsync };
|
|
90
|
+
try {
|
|
91
|
+
RedisClient = redis.createClient({
|
|
92
|
+
host: process.env.REDIS_HOST || "redis",
|
|
93
|
+
port: Number(process.env.REDIS_PORT) || 6379,
|
|
94
|
+
});
|
|
95
|
+
RedisClient.on("error", function (e) {
|
|
96
|
+
// Silent — we fail open. Logging too loud across every microservice.
|
|
97
|
+
if (process.env.DEBUG_PORTAL_AUTH) {
|
|
98
|
+
console.warn("[portalAuth.redis]", e.message);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
existsAsync = promisify(RedisClient.exists).bind(RedisClient);
|
|
102
|
+
setAsync = promisify(RedisClient.set).bind(RedisClient);
|
|
103
|
+
}
|
|
104
|
+
catch (_a) {
|
|
105
|
+
// Redis client init failed → leave nulls, fail open everywhere.
|
|
106
|
+
}
|
|
107
|
+
return { existsAsync: existsAsync, setAsync: setAsync };
|
|
108
|
+
}
|
|
28
109
|
function randomId() {
|
|
29
110
|
return (Date.now().toString(36) +
|
|
30
111
|
Math.random().toString(36).slice(2, 10));
|
|
31
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Revokes every ticket-scoped token currently in flight for one
|
|
115
|
+
* ticket. Use this when the customer reports a stolen cookie or
|
|
116
|
+
* when an admin manually closes a session. TTL of the revocation
|
|
117
|
+
* key matches the longest possible JWT lifetime so it cleans itself.
|
|
118
|
+
*/
|
|
119
|
+
function revokeTicketTokens(ticketUuid, ttlSec) {
|
|
120
|
+
if (ttlSec === void 0) { ttlSec = 7 * 24 * 60 * 60; }
|
|
121
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
122
|
+
var setAsync, _a;
|
|
123
|
+
return __generator(this, function (_b) {
|
|
124
|
+
switch (_b.label) {
|
|
125
|
+
case 0:
|
|
126
|
+
setAsync = getRedis().setAsync;
|
|
127
|
+
if (!setAsync || !ticketUuid)
|
|
128
|
+
return [2 /*return*/];
|
|
129
|
+
_b.label = 1;
|
|
130
|
+
case 1:
|
|
131
|
+
_b.trys.push([1, 3, , 4]);
|
|
132
|
+
return [4 /*yield*/, setAsync("portal_revoked_ticket:" + ticketUuid, "1", "EX", ttlSec)];
|
|
133
|
+
case 2:
|
|
134
|
+
_b.sent();
|
|
135
|
+
return [3 /*break*/, 4];
|
|
136
|
+
case 3:
|
|
137
|
+
_a = _b.sent();
|
|
138
|
+
return [3 /*break*/, 4];
|
|
139
|
+
case 4: return [2 /*return*/];
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
exports.revokeTicketTokens = revokeTicketTokens;
|
|
145
|
+
/**
|
|
146
|
+
* Revokes a specific token emission by its `jti`. Useful when only
|
|
147
|
+
* one device should be cut off, not the whole ticket session.
|
|
148
|
+
*/
|
|
149
|
+
function revokeTokenByJti(jti, ttlSec) {
|
|
150
|
+
if (ttlSec === void 0) { ttlSec = 7 * 24 * 60 * 60; }
|
|
151
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
152
|
+
var setAsync, _a;
|
|
153
|
+
return __generator(this, function (_b) {
|
|
154
|
+
switch (_b.label) {
|
|
155
|
+
case 0:
|
|
156
|
+
setAsync = getRedis().setAsync;
|
|
157
|
+
if (!setAsync || !jti)
|
|
158
|
+
return [2 /*return*/];
|
|
159
|
+
_b.label = 1;
|
|
160
|
+
case 1:
|
|
161
|
+
_b.trys.push([1, 3, , 4]);
|
|
162
|
+
return [4 /*yield*/, setAsync("portal_revoked_jti:" + jti, "1", "EX", ttlSec)];
|
|
163
|
+
case 2:
|
|
164
|
+
_b.sent();
|
|
165
|
+
return [3 /*break*/, 4];
|
|
166
|
+
case 3:
|
|
167
|
+
_a = _b.sent();
|
|
168
|
+
return [3 /*break*/, 4];
|
|
169
|
+
case 4: return [2 /*return*/];
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
exports.revokeTokenByJti = revokeTokenByJti;
|
|
32
175
|
/** Issue a fresh guest token for a visitor who just landed on a branch URL. */
|
|
33
176
|
function issueGuestToken(input) {
|
|
34
177
|
var now = Math.floor(Date.now() / 1000);
|
|
@@ -39,6 +182,7 @@ function issueGuestToken(input) {
|
|
|
39
182
|
scope: "guest",
|
|
40
183
|
entityUuid: input.entityUuid,
|
|
41
184
|
branchUuid: input.branchUuid,
|
|
185
|
+
jti: randomId(),
|
|
42
186
|
iat: now,
|
|
43
187
|
exp: exp,
|
|
44
188
|
};
|
|
@@ -46,10 +190,21 @@ function issueGuestToken(input) {
|
|
|
46
190
|
return { token: token, expiresAt: exp * 1000 };
|
|
47
191
|
}
|
|
48
192
|
exports.issueGuestToken = issueGuestToken;
|
|
49
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Upgrade a guest into a ticket holder. Optional `ttlSec` lets the
|
|
195
|
+
* caller dial the lifetime per ticket phase:
|
|
196
|
+
*
|
|
197
|
+
* - WAITING / CALLING / ATTENDING → 2h (default)
|
|
198
|
+
* - FINISHED → 7d (NPS, share, print receipt)
|
|
199
|
+
* - CANCELLED / ABANDONED → 30 min (de-prioritised)
|
|
200
|
+
*
|
|
201
|
+
* Renewing on every poll keeps the cookie warm without forcing a
|
|
202
|
+
* "still here" re-auth dance.
|
|
203
|
+
*/
|
|
50
204
|
function issueTicketToken(input) {
|
|
51
205
|
var now = Math.floor(Date.now() / 1000);
|
|
52
|
-
var
|
|
206
|
+
var ttl = input.ttlSec && input.ttlSec > 0 ? input.ttlSec : TICKET_TTL_SEC;
|
|
207
|
+
var exp = now + ttl;
|
|
53
208
|
var payload = {
|
|
54
209
|
sub: "ticket:" + input.ticketUuid,
|
|
55
210
|
aud: "portal",
|
|
@@ -57,6 +212,7 @@ function issueTicketToken(input) {
|
|
|
57
212
|
entityUuid: input.entityUuid,
|
|
58
213
|
branchUuid: input.branchUuid,
|
|
59
214
|
ticketUuid: input.ticketUuid,
|
|
215
|
+
jti: randomId(),
|
|
60
216
|
iat: now,
|
|
61
217
|
exp: exp,
|
|
62
218
|
};
|
|
@@ -73,26 +229,59 @@ exports.issueTicketToken = issueTicketToken;
|
|
|
73
229
|
*/
|
|
74
230
|
function verifyPortalToken(req, _res, next) {
|
|
75
231
|
var _a;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
232
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
233
|
+
var auth, token, decoded, existsAsync, checks, results, _b;
|
|
234
|
+
return __generator(this, function (_c) {
|
|
235
|
+
switch (_c.label) {
|
|
236
|
+
case 0:
|
|
237
|
+
auth = (_a = req.headers) === null || _a === void 0 ? void 0 : _a.authorization;
|
|
238
|
+
if (!auth || !auth.startsWith("Bearer ")) {
|
|
239
|
+
return [2 /*return*/, next(new exceptions_1.HttpException(401, "Missing portal token"))];
|
|
240
|
+
}
|
|
241
|
+
token = auth.slice("Bearer ".length).trim();
|
|
242
|
+
try {
|
|
243
|
+
decoded = jwt.verify(token, SECRET, {
|
|
244
|
+
algorithms: ["HS256"],
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
return [2 /*return*/, next(new exceptions_1.HttpException(401, (err === null || err === void 0 ? void 0 : err.name) === "TokenExpiredError"
|
|
249
|
+
? "Portal token expired"
|
|
250
|
+
: "Invalid portal token"))];
|
|
251
|
+
}
|
|
252
|
+
if (decoded.aud !== "portal") {
|
|
253
|
+
return [2 /*return*/, next(new exceptions_1.HttpException(401, "Invalid token audience"))];
|
|
254
|
+
}
|
|
255
|
+
existsAsync = getRedis().existsAsync;
|
|
256
|
+
if (!existsAsync) return [3 /*break*/, 5];
|
|
257
|
+
_c.label = 1;
|
|
258
|
+
case 1:
|
|
259
|
+
_c.trys.push([1, 4, , 5]);
|
|
260
|
+
checks = [];
|
|
261
|
+
if (decoded.jti) {
|
|
262
|
+
checks.push(existsAsync("portal_revoked_jti:" + decoded.jti));
|
|
263
|
+
}
|
|
264
|
+
if (decoded.ticketUuid) {
|
|
265
|
+
checks.push(existsAsync("portal_revoked_ticket:" + decoded.ticketUuid));
|
|
266
|
+
}
|
|
267
|
+
if (!(checks.length > 0)) return [3 /*break*/, 3];
|
|
268
|
+
return [4 /*yield*/, Promise.all(checks)];
|
|
269
|
+
case 2:
|
|
270
|
+
results = _c.sent();
|
|
271
|
+
if (results.some(function (r) { return r === 1; })) {
|
|
272
|
+
return [2 /*return*/, next(new exceptions_1.HttpException(401, "Portal token revoked"))];
|
|
273
|
+
}
|
|
274
|
+
_c.label = 3;
|
|
275
|
+
case 3: return [3 /*break*/, 5];
|
|
276
|
+
case 4:
|
|
277
|
+
_b = _c.sent();
|
|
278
|
+
return [3 /*break*/, 5];
|
|
279
|
+
case 5:
|
|
280
|
+
req.portalAuth = decoded;
|
|
281
|
+
return [2 /*return*/, next()];
|
|
282
|
+
}
|
|
84
283
|
});
|
|
85
|
-
|
|
86
|
-
return next(new exceptions_1.HttpException(401, "Invalid token audience"));
|
|
87
|
-
}
|
|
88
|
-
req.portalAuth = decoded;
|
|
89
|
-
return next();
|
|
90
|
-
}
|
|
91
|
-
catch (err) {
|
|
92
|
-
return next(new exceptions_1.HttpException(401, (err === null || err === void 0 ? void 0 : err.name) === "TokenExpiredError"
|
|
93
|
-
? "Portal token expired"
|
|
94
|
-
: "Invalid portal token"));
|
|
95
|
-
}
|
|
284
|
+
});
|
|
96
285
|
}
|
|
97
286
|
exports.verifyPortalToken = verifyPortalToken;
|
|
98
287
|
/**
|
|
@@ -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,8 +48,74 @@ 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
|
|
75
|
+
function readGraceState() {
|
|
76
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
77
|
+
var raw, _a;
|
|
78
|
+
return __generator(this, function (_b) {
|
|
79
|
+
switch (_b.label) {
|
|
80
|
+
case 0:
|
|
81
|
+
_b.trys.push([0, 2, , 3]);
|
|
82
|
+
return [4 /*yield*/, fs_1.promises.readFile(GRACE_STATE_FILE, "utf8")];
|
|
83
|
+
case 1:
|
|
84
|
+
raw = _b.sent();
|
|
85
|
+
return [2 /*return*/, JSON.parse(raw)];
|
|
86
|
+
case 2:
|
|
87
|
+
_a = _b.sent();
|
|
88
|
+
return [2 /*return*/, null];
|
|
89
|
+
case 3: return [2 /*return*/];
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
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
|
+
}
|
|
42
119
|
/**
|
|
43
120
|
* Where to fetch license data from. Two modes:
|
|
44
121
|
*
|
|
@@ -125,18 +202,72 @@ function fetchLicense() {
|
|
|
125
202
|
});
|
|
126
203
|
}
|
|
127
204
|
function getCachedLicense() {
|
|
205
|
+
var _a;
|
|
128
206
|
return __awaiter(this, void 0, void 0, function () {
|
|
129
|
-
var fresh;
|
|
130
|
-
return __generator(this, function (
|
|
131
|
-
switch (
|
|
207
|
+
var fresh, now, stored, since, elapsed, expired, daysLeft;
|
|
208
|
+
return __generator(this, function (_b) {
|
|
209
|
+
switch (_b.label) {
|
|
132
210
|
case 0:
|
|
133
211
|
if (cache && (Date.now() - cache.fetchedAt) < CACHE_TTL)
|
|
134
212
|
return [2 /*return*/, cache];
|
|
135
213
|
return [4 /*yield*/, fetchLicense()];
|
|
136
214
|
case 1:
|
|
137
|
-
fresh =
|
|
138
|
-
if (fresh)
|
|
139
|
-
|
|
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
|
+
};
|
|
140
271
|
return [2 /*return*/, cache];
|
|
141
272
|
}
|
|
142
273
|
});
|
|
@@ -297,11 +428,30 @@ function licenseLoginGuard() {
|
|
|
297
428
|
exports.licenseLoginGuard = licenseLoginGuard;
|
|
298
429
|
/**
|
|
299
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.
|
|
300
433
|
*/
|
|
301
434
|
function getLicenseStatus() {
|
|
435
|
+
var _a, _b;
|
|
302
436
|
return __awaiter(this, void 0, void 0, function () {
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
}
|
|
305
455
|
});
|
|
306
456
|
});
|
|
307
457
|
}
|