@fluyappgocore/commons-backend 1.0.214 → 1.0.216
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
|
/**
|
|
@@ -11,6 +11,12 @@ interface LicenseCache {
|
|
|
11
11
|
};
|
|
12
12
|
tier: string;
|
|
13
13
|
fetchedAt: number;
|
|
14
|
+
status?: string;
|
|
15
|
+
licenseStatus?: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
expiresAt?: string | null;
|
|
18
|
+
daysUntilExpiry?: number | null;
|
|
19
|
+
inGracePeriod?: boolean;
|
|
14
20
|
unreachable?: boolean;
|
|
15
21
|
unreachableSince?: number | null;
|
|
16
22
|
unreachableGraceDaysLeft?: number | null;
|
|
@@ -48,8 +54,17 @@ export declare function getLicenseStatus(): Promise<{
|
|
|
48
54
|
readOnly: boolean;
|
|
49
55
|
blocked: boolean;
|
|
50
56
|
tier: string;
|
|
57
|
+
status: string | null;
|
|
58
|
+
licenseStatus: string | null;
|
|
59
|
+
message: string | null;
|
|
60
|
+
expiresAt: string | null;
|
|
61
|
+
daysUntilExpiry: number | null;
|
|
62
|
+
inGracePeriod: boolean;
|
|
63
|
+
lastValidatedAt: number;
|
|
64
|
+
lastValidatedAtIso: string;
|
|
51
65
|
unreachable: boolean;
|
|
52
66
|
unreachableSince: number | null;
|
|
67
|
+
unreachableSinceIso: string | null;
|
|
53
68
|
unreachableGraceDaysLeft: number | null;
|
|
54
69
|
} | null>;
|
|
55
70
|
export {};
|
|
@@ -136,12 +136,13 @@ function writeGraceState(state) {
|
|
|
136
136
|
function fetchLicense() {
|
|
137
137
|
return __awaiter(this, void 0, void 0, function () {
|
|
138
138
|
function tryFetch(url, isProxy) {
|
|
139
|
+
var _a;
|
|
139
140
|
return __awaiter(this, void 0, void 0, function () {
|
|
140
|
-
var controller_1, timeout_1, headers, res, data,
|
|
141
|
-
return __generator(this, function (
|
|
142
|
-
switch (
|
|
141
|
+
var controller_1, timeout_1, headers, res, data, _b;
|
|
142
|
+
return __generator(this, function (_c) {
|
|
143
|
+
switch (_c.label) {
|
|
143
144
|
case 0:
|
|
144
|
-
|
|
145
|
+
_c.trys.push([0, 3, , 4]);
|
|
145
146
|
controller_1 = new AbortController();
|
|
146
147
|
timeout_1 = setTimeout(function () { return controller_1.abort(); }, 5000);
|
|
147
148
|
headers = {};
|
|
@@ -150,12 +151,12 @@ function fetchLicense() {
|
|
|
150
151
|
return [4 /*yield*/, fetch(url, { signal: controller_1.signal, headers: headers })
|
|
151
152
|
.finally(function () { return clearTimeout(timeout_1); })];
|
|
152
153
|
case 1:
|
|
153
|
-
res =
|
|
154
|
+
res = _c.sent();
|
|
154
155
|
if (!res.ok)
|
|
155
156
|
return [2 /*return*/, null];
|
|
156
157
|
return [4 /*yield*/, res.json()];
|
|
157
158
|
case 2:
|
|
158
|
-
data =
|
|
159
|
+
data = _c.sent();
|
|
159
160
|
return [2 /*return*/, {
|
|
160
161
|
valid: data.valid,
|
|
161
162
|
readOnly: data.readOnly || false,
|
|
@@ -164,9 +165,17 @@ function fetchLicense() {
|
|
|
164
165
|
limits: data.limits || {},
|
|
165
166
|
tier: data.tier || "BASIC",
|
|
166
167
|
fetchedAt: Date.now(),
|
|
168
|
+
// Capturamos el detalle del estado para que las respuestas 403
|
|
169
|
+
// tengan informacion accionable en lugar de un mensaje generico.
|
|
170
|
+
status: data.status,
|
|
171
|
+
licenseStatus: data.licenseStatus,
|
|
172
|
+
message: data.message,
|
|
173
|
+
expiresAt: (_a = data.expiresAt) !== null && _a !== void 0 ? _a : null,
|
|
174
|
+
daysUntilExpiry: typeof data.daysUntilExpiry === "number" ? data.daysUntilExpiry : null,
|
|
175
|
+
inGracePeriod: data.inGracePeriod === true,
|
|
167
176
|
}];
|
|
168
177
|
case 3:
|
|
169
|
-
|
|
178
|
+
_b = _c.sent();
|
|
170
179
|
return [2 /*return*/, null];
|
|
171
180
|
case 4: return [2 /*return*/];
|
|
172
181
|
}
|
|
@@ -334,8 +343,10 @@ function licenseGuard() {
|
|
|
334
343
|
// If license is not valid (expired/revoked), block
|
|
335
344
|
if (!license.valid) {
|
|
336
345
|
return [2 /*return*/, res.status(403).json({
|
|
337
|
-
error: "Licencia no valida",
|
|
346
|
+
error: license.message || "Licencia no valida",
|
|
338
347
|
code: "LICENSE_INVALID",
|
|
348
|
+
// Detalle accionable para que el cliente sepa el motivo exacto.
|
|
349
|
+
details: buildLicenseDetails(license),
|
|
339
350
|
})];
|
|
340
351
|
}
|
|
341
352
|
// If no features defined in license, allow everything
|
|
@@ -350,6 +361,7 @@ function licenseGuard() {
|
|
|
350
361
|
code: "FEATURE_DISABLED",
|
|
351
362
|
feature: feature,
|
|
352
363
|
tier: license.tier,
|
|
364
|
+
details: buildLicenseDetails(license),
|
|
353
365
|
})];
|
|
354
366
|
}
|
|
355
367
|
}
|
|
@@ -360,6 +372,42 @@ function licenseGuard() {
|
|
|
360
372
|
}); };
|
|
361
373
|
}
|
|
362
374
|
exports.licenseGuard = licenseGuard;
|
|
375
|
+
/**
|
|
376
|
+
* Helper compartido por los tres guards para enriquecer la respuesta 403
|
|
377
|
+
* con la informacion completa del estado de la licencia.
|
|
378
|
+
*
|
|
379
|
+
* Incluye:
|
|
380
|
+
* - status / licenseStatus / message del servidor licensing
|
|
381
|
+
* - expiresAt y daysUntilExpiry para que el cliente sepa cuanto tiempo paso
|
|
382
|
+
* - inGracePeriod para mostrar el banner correcto
|
|
383
|
+
* - tier
|
|
384
|
+
* - lastValidatedAt (timestamp en ms y formato ISO) — cuando fue la
|
|
385
|
+
* ultima vez que se consulto exitosamente el servidor licensing.
|
|
386
|
+
* Critico para distinguir "el server esta caido hace 3 dias" vs
|
|
387
|
+
* "el server respondio recien que la licencia esta revocada".
|
|
388
|
+
* - unreachable / unreachableSince / unreachableGraceDaysLeft cuando el
|
|
389
|
+
* servidor licensing no responde y estamos dentro/fuera del grace.
|
|
390
|
+
*/
|
|
391
|
+
function buildLicenseDetails(license) {
|
|
392
|
+
var _a, _b, _c, _d;
|
|
393
|
+
return {
|
|
394
|
+
status: license.status || null,
|
|
395
|
+
licenseStatus: license.licenseStatus || null,
|
|
396
|
+
message: license.message || null,
|
|
397
|
+
tier: license.tier,
|
|
398
|
+
expiresAt: (_a = license.expiresAt) !== null && _a !== void 0 ? _a : null,
|
|
399
|
+
daysUntilExpiry: (_b = license.daysUntilExpiry) !== null && _b !== void 0 ? _b : null,
|
|
400
|
+
inGracePeriod: license.inGracePeriod === true,
|
|
401
|
+
readOnly: license.readOnly,
|
|
402
|
+
blocked: license.blocked,
|
|
403
|
+
lastValidatedAt: license.fetchedAt,
|
|
404
|
+
lastValidatedAtIso: new Date(license.fetchedAt).toISOString(),
|
|
405
|
+
unreachable: license.unreachable === true,
|
|
406
|
+
unreachableSince: (_c = license.unreachableSince) !== null && _c !== void 0 ? _c : null,
|
|
407
|
+
unreachableSinceIso: license.unreachableSince ? new Date(license.unreachableSince).toISOString() : null,
|
|
408
|
+
unreachableGraceDaysLeft: (_d = license.unreachableGraceDaysLeft) !== null && _d !== void 0 ? _d : null,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
363
411
|
/**
|
|
364
412
|
* Blocks write operations (POST/PUT/DELETE) when license is in READONLY mode.
|
|
365
413
|
* Use on all mutation routes. GET requests are always allowed in readonly.
|
|
@@ -381,14 +429,16 @@ function licenseWriteGuard() {
|
|
|
381
429
|
return [2 /*return*/, next()]; // Can't check → allow
|
|
382
430
|
if (license.blocked) {
|
|
383
431
|
return [2 /*return*/, res.status(403).json({
|
|
384
|
-
error: "Licencia expirada. Acceso bloqueado.",
|
|
432
|
+
error: license.message || "Licencia expirada. Acceso bloqueado.",
|
|
385
433
|
code: "LICENSE_BLOCKED",
|
|
434
|
+
details: buildLicenseDetails(license),
|
|
386
435
|
})];
|
|
387
436
|
}
|
|
388
437
|
if (license.readOnly) {
|
|
389
438
|
return [2 /*return*/, res.status(403).json({
|
|
390
|
-
error: "Licencia en modo solo lectura. No se permiten cambios.",
|
|
439
|
+
error: license.message || "Licencia en modo solo lectura. No se permiten cambios.",
|
|
391
440
|
code: "LICENSE_READONLY",
|
|
441
|
+
details: buildLicenseDetails(license),
|
|
392
442
|
})];
|
|
393
443
|
}
|
|
394
444
|
next();
|
|
@@ -415,8 +465,9 @@ function licenseLoginGuard() {
|
|
|
415
465
|
return [2 /*return*/, next()]; // Can't check → allow
|
|
416
466
|
if (license.blocked) {
|
|
417
467
|
return [2 /*return*/, res.status(403).json({
|
|
418
|
-
error: "Licencia expirada. El acceso ha sido suspendido. Contacte al administrador.",
|
|
468
|
+
error: license.message || "Licencia expirada. El acceso ha sido suspendido. Contacte al administrador.",
|
|
419
469
|
code: "LICENSE_BLOCKED",
|
|
470
|
+
details: buildLicenseDetails(license),
|
|
420
471
|
})];
|
|
421
472
|
}
|
|
422
473
|
next();
|
|
@@ -432,14 +483,14 @@ exports.licenseLoginGuard = licenseLoginGuard;
|
|
|
432
483
|
* "licensing server unreachable, X days left" banner.
|
|
433
484
|
*/
|
|
434
485
|
function getLicenseStatus() {
|
|
435
|
-
var _a, _b;
|
|
486
|
+
var _a, _b, _c, _d;
|
|
436
487
|
return __awaiter(this, void 0, void 0, function () {
|
|
437
488
|
var lic;
|
|
438
|
-
return __generator(this, function (
|
|
439
|
-
switch (
|
|
489
|
+
return __generator(this, function (_e) {
|
|
490
|
+
switch (_e.label) {
|
|
440
491
|
case 0: return [4 /*yield*/, getCachedLicense()];
|
|
441
492
|
case 1:
|
|
442
|
-
lic =
|
|
493
|
+
lic = _e.sent();
|
|
443
494
|
if (!lic)
|
|
444
495
|
return [2 /*return*/, null];
|
|
445
496
|
return [2 /*return*/, {
|
|
@@ -447,9 +498,18 @@ function getLicenseStatus() {
|
|
|
447
498
|
readOnly: lic.readOnly,
|
|
448
499
|
blocked: lic.blocked,
|
|
449
500
|
tier: lic.tier,
|
|
501
|
+
status: lic.status || null,
|
|
502
|
+
licenseStatus: lic.licenseStatus || null,
|
|
503
|
+
message: lic.message || null,
|
|
504
|
+
expiresAt: (_a = lic.expiresAt) !== null && _a !== void 0 ? _a : null,
|
|
505
|
+
daysUntilExpiry: (_b = lic.daysUntilExpiry) !== null && _b !== void 0 ? _b : null,
|
|
506
|
+
inGracePeriod: lic.inGracePeriod === true,
|
|
507
|
+
lastValidatedAt: lic.fetchedAt,
|
|
508
|
+
lastValidatedAtIso: new Date(lic.fetchedAt).toISOString(),
|
|
450
509
|
unreachable: lic.unreachable === true,
|
|
451
|
-
unreachableSince: (
|
|
452
|
-
|
|
510
|
+
unreachableSince: (_c = lic.unreachableSince) !== null && _c !== void 0 ? _c : null,
|
|
511
|
+
unreachableSinceIso: lic.unreachableSince ? new Date(lic.unreachableSince).toISOString() : null,
|
|
512
|
+
unreachableGraceDaysLeft: (_d = lic.unreachableGraceDaysLeft) !== null && _d !== void 0 ? _d : null,
|
|
453
513
|
}];
|
|
454
514
|
}
|
|
455
515
|
});
|