@fluyappgocore/commons-backend 1.0.211 → 1.0.213

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.
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Guest-JWT auth for the customer-facing portal.
3
+ *
4
+ * Three token levels, each signed with the shared JWT_KEY env var so
5
+ * every microservice (moving, video, agents) can verify them without a
6
+ * network round-trip:
7
+ *
8
+ * - guest → "I'm a visitor at entity X, branch Y" (30 min, no ticket)
9
+ * - ticket → "I'm the holder of ticket T at entity X, branch Y" (2 h)
10
+ * - (livekit tokens are minted separately by livekit-server-sdk and
11
+ * are NOT part of this module — they live inside msvideo)
12
+ *
13
+ * Usage in any express app:
14
+ *
15
+ * import { verifyPortalToken, requireScope } from "@fluyappgocore/commons-backend";
16
+ *
17
+ * router.get("/queue",
18
+ * verifyPortalToken,
19
+ * requireScope("guest"), // accepts guest OR ticket
20
+ * handler,
21
+ * );
22
+ *
23
+ * router.get("/tickets/:uuid",
24
+ * verifyPortalToken,
25
+ * requireScope("ticket"),
26
+ * requireTicketMatch("uuid"),
27
+ * handler,
28
+ * );
29
+ */
30
+ import { NextFunction, Request, Response } from "express";
31
+ export declare type PortalScope = "guest" | "ticket";
32
+ export interface PortalClaims {
33
+ /** "guest:<uuid>" or "ticket:<ticketUuid>" — useful for audit/rate-limit */
34
+ sub: string;
35
+ /** JWT audience, always "portal" so we don't cross-verify with user tokens */
36
+ aud: "portal";
37
+ scope: PortalScope;
38
+ entityUuid: string;
39
+ branchUuid: string;
40
+ /** Present iff scope === "ticket" */
41
+ ticketUuid?: string;
42
+ iat: number;
43
+ exp: number;
44
+ }
45
+ export interface RequestWithPortal extends Request {
46
+ portalAuth?: PortalClaims;
47
+ }
48
+ /** Issue a fresh guest token for a visitor who just landed on a branch URL. */
49
+ export declare function issueGuestToken(input: {
50
+ entityUuid: string;
51
+ branchUuid: string;
52
+ }): {
53
+ token: string;
54
+ expiresAt: number;
55
+ };
56
+ /** Upgrade a guest into a ticket holder after POST /tickets succeeds. */
57
+ export declare function issueTicketToken(input: {
58
+ entityUuid: string;
59
+ branchUuid: string;
60
+ ticketUuid: string;
61
+ }): {
62
+ token: string;
63
+ expiresAt: number;
64
+ };
65
+ /**
66
+ * Express middleware: reads `Authorization: Bearer <token>`, verifies the
67
+ * signature, decodes the claims, attaches them as `req.portalAuth`.
68
+ *
69
+ * Rejects anything that isn't a portal-scoped token (aud !== "portal")
70
+ * so a regular user/agent JWT can NEVER accidentally pass this gate.
71
+ */
72
+ export declare function verifyPortalToken(req: RequestWithPortal, _res: Response, next: NextFunction): void;
73
+ /**
74
+ * Gate a route by scope.
75
+ *
76
+ * `requireScope("guest")` accepts BOTH guest and ticket tokens (ticket
77
+ * implies guest, since a ticket holder is also a visitor at that branch).
78
+ *
79
+ * `requireScope("ticket")` accepts only ticket tokens.
80
+ */
81
+ export declare function requireScope(minimum: PortalScope): (req: RequestWithPortal, _res: Response, next: NextFunction) => void;
82
+ /**
83
+ * Guard that the `ticketUuid` claim inside the JWT matches the URL param.
84
+ * Prevents a ticket holder from reading someone else's ticket by tampering
85
+ * with the path.
86
+ */
87
+ export declare function requireTicketMatch(paramName?: string): (req: RequestWithPortal, _res: Response, next: NextFunction) => void;
88
+ /**
89
+ * Guard that the `branchUuid` claim inside the JWT matches the URL param
90
+ * or body field. Same idea, one layer up.
91
+ */
92
+ export declare function requireBranchMatch(source: "params" | "body", fieldName: string): (req: RequestWithPortal, _res: Response, next: NextFunction) => void;
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
+ }) : (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ o[k2] = m[k];
8
+ }));
9
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
11
+ }) : function(o, v) {
12
+ o["default"] = v;
13
+ });
14
+ var __importStar = (this && this.__importStar) || function (mod) {
15
+ if (mod && mod.__esModule) return mod;
16
+ var result = {};
17
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
+ __setModuleDefault(result, mod);
19
+ return result;
20
+ };
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.requireBranchMatch = exports.requireTicketMatch = exports.requireScope = exports.verifyPortalToken = exports.issueTicketToken = exports.issueGuestToken = void 0;
23
+ var jwt = __importStar(require("jsonwebtoken"));
24
+ var exceptions_1 = require("../exceptions");
25
+ var SECRET = process.env.JWT_KEY || "";
26
+ var GUEST_TTL_SEC = 30 * 60; // 30 minutes
27
+ var TICKET_TTL_SEC = 2 * 60 * 60; // 2 hours
28
+ function randomId() {
29
+ return (Date.now().toString(36) +
30
+ Math.random().toString(36).slice(2, 10));
31
+ }
32
+ /** Issue a fresh guest token for a visitor who just landed on a branch URL. */
33
+ function issueGuestToken(input) {
34
+ var now = Math.floor(Date.now() / 1000);
35
+ var exp = now + GUEST_TTL_SEC;
36
+ var payload = {
37
+ sub: "guest:" + randomId(),
38
+ aud: "portal",
39
+ scope: "guest",
40
+ entityUuid: input.entityUuid,
41
+ branchUuid: input.branchUuid,
42
+ iat: now,
43
+ exp: exp,
44
+ };
45
+ var token = jwt.sign(payload, SECRET, { algorithm: "HS256" });
46
+ return { token: token, expiresAt: exp * 1000 };
47
+ }
48
+ exports.issueGuestToken = issueGuestToken;
49
+ /** Upgrade a guest into a ticket holder after POST /tickets succeeds. */
50
+ function issueTicketToken(input) {
51
+ var now = Math.floor(Date.now() / 1000);
52
+ var exp = now + TICKET_TTL_SEC;
53
+ var payload = {
54
+ sub: "ticket:" + input.ticketUuid,
55
+ aud: "portal",
56
+ scope: "ticket",
57
+ entityUuid: input.entityUuid,
58
+ branchUuid: input.branchUuid,
59
+ ticketUuid: input.ticketUuid,
60
+ iat: now,
61
+ exp: exp,
62
+ };
63
+ var token = jwt.sign(payload, SECRET, { algorithm: "HS256" });
64
+ return { token: token, expiresAt: exp * 1000 };
65
+ }
66
+ exports.issueTicketToken = issueTicketToken;
67
+ /**
68
+ * Express middleware: reads `Authorization: Bearer <token>`, verifies the
69
+ * signature, decodes the claims, attaches them as `req.portalAuth`.
70
+ *
71
+ * Rejects anything that isn't a portal-scoped token (aud !== "portal")
72
+ * so a regular user/agent JWT can NEVER accidentally pass this gate.
73
+ */
74
+ function verifyPortalToken(req, _res, next) {
75
+ var _a;
76
+ var auth = (_a = req.headers) === null || _a === void 0 ? void 0 : _a.authorization;
77
+ if (!auth || !auth.startsWith("Bearer ")) {
78
+ return next(new exceptions_1.HttpException(401, "Missing portal token"));
79
+ }
80
+ var token = auth.slice("Bearer ".length).trim();
81
+ try {
82
+ var decoded = jwt.verify(token, SECRET, {
83
+ algorithms: ["HS256"],
84
+ });
85
+ if (decoded.aud !== "portal") {
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
+ }
96
+ }
97
+ exports.verifyPortalToken = verifyPortalToken;
98
+ /**
99
+ * Gate a route by scope.
100
+ *
101
+ * `requireScope("guest")` accepts BOTH guest and ticket tokens (ticket
102
+ * implies guest, since a ticket holder is also a visitor at that branch).
103
+ *
104
+ * `requireScope("ticket")` accepts only ticket tokens.
105
+ */
106
+ function requireScope(minimum) {
107
+ return function (req, _res, next) {
108
+ if (!req.portalAuth) {
109
+ return next(new exceptions_1.HttpException(401, "Portal token not verified"));
110
+ }
111
+ if (minimum === "ticket" && req.portalAuth.scope !== "ticket") {
112
+ return next(new exceptions_1.HttpException(403, "Ticket-scoped token required"));
113
+ }
114
+ return next();
115
+ };
116
+ }
117
+ exports.requireScope = requireScope;
118
+ /**
119
+ * Guard that the `ticketUuid` claim inside the JWT matches the URL param.
120
+ * Prevents a ticket holder from reading someone else's ticket by tampering
121
+ * with the path.
122
+ */
123
+ function requireTicketMatch(paramName) {
124
+ if (paramName === void 0) { paramName = "uuid"; }
125
+ return function (req, _res, next) {
126
+ var _a, _b;
127
+ var claimed = (_a = req.portalAuth) === null || _a === void 0 ? void 0 : _a.ticketUuid;
128
+ var requested = (_b = req.params) === null || _b === void 0 ? void 0 : _b[paramName];
129
+ if (!claimed || !requested || claimed !== requested) {
130
+ return next(new exceptions_1.HttpException(403, "Ticket id mismatch"));
131
+ }
132
+ return next();
133
+ };
134
+ }
135
+ exports.requireTicketMatch = requireTicketMatch;
136
+ /**
137
+ * Guard that the `branchUuid` claim inside the JWT matches the URL param
138
+ * or body field. Same idea, one layer up.
139
+ */
140
+ function requireBranchMatch(source, fieldName) {
141
+ return function (req, _res, next) {
142
+ var _a, _b, _c;
143
+ var claimed = (_a = req.portalAuth) === null || _a === void 0 ? void 0 : _a.branchUuid;
144
+ var requested = source === "params" ? (_b = req.params) === null || _b === void 0 ? void 0 : _b[fieldName] : (_c = req.body) === null || _c === void 0 ? void 0 : _c[fieldName];
145
+ if (!claimed || !requested || claimed !== requested) {
146
+ return next(new exceptions_1.HttpException(403, "Branch id mismatch"));
147
+ }
148
+ return next();
149
+ };
150
+ }
151
+ exports.requireBranchMatch = requireBranchMatch;
@@ -2,3 +2,4 @@ export * from './auth.middleware';
2
2
  export * from './error.middleware';
3
3
  export * from './validation.middleware';
4
4
  export * from './licenseGuard.middleware';
5
+ export * from './guestAuth.middleware';
@@ -14,3 +14,4 @@ __exportStar(require("./auth.middleware"), exports);
14
14
  __exportStar(require("./error.middleware"), exports);
15
15
  __exportStar(require("./validation.middleware"), exports);
16
16
  __exportStar(require("./licenseGuard.middleware"), exports);
17
+ __exportStar(require("./guestAuth.middleware"), exports);
@@ -39,42 +39,87 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.getLicenseStatus = exports.licenseLoginGuard = exports.licenseWriteGuard = exports.licenseGuard = exports.getLicenseLimits = exports.isLicenseFeatureEnabled = void 0;
40
40
  var cache = null;
41
41
  var CACHE_TTL = 1000 * 60 * 60; // 1 hour
42
+ /**
43
+ * Where to fetch license data from. Two modes:
44
+ *
45
+ * - LICENSE_PROXY_URL set → in-cluster proxy (recommended).
46
+ * All MS except ms-entity hit `${LICENSE_PROXY_URL}` which resolves to
47
+ * `http://msentities-clusterip-srv:8092/api_entities/internal/license-status`.
48
+ * Only ms-entity itself actually talks to licensing.fluyapp.io. Dramatic
49
+ * reduction in outbound traffic (N × M → 1 × M).
50
+ *
51
+ * - LICENSE_URL set → direct fetch from the licensing server.
52
+ * Used by ms-entity (which IS the aggregator) and as fallback for any MS
53
+ * where the proxy is unreachable.
54
+ *
55
+ * If both are set, the proxy is tried first; on failure we fall back to the
56
+ * direct URL so a network issue between MS doesn't break licensing for
57
+ * everyone.
58
+ */
42
59
  function fetchLicense() {
43
60
  return __awaiter(this, void 0, void 0, function () {
44
- var licenseUrl, installationUuid, controller_1, timeout_1, res, data, _a;
45
- return __generator(this, function (_b) {
46
- switch (_b.label) {
61
+ function tryFetch(url, isProxy) {
62
+ return __awaiter(this, void 0, void 0, function () {
63
+ var controller_1, timeout_1, headers, res, data, _a;
64
+ return __generator(this, function (_b) {
65
+ switch (_b.label) {
66
+ case 0:
67
+ _b.trys.push([0, 3, , 4]);
68
+ controller_1 = new AbortController();
69
+ timeout_1 = setTimeout(function () { return controller_1.abort(); }, 5000);
70
+ headers = {};
71
+ if (isProxy && internalSecret)
72
+ headers["x-internal-secret"] = internalSecret;
73
+ return [4 /*yield*/, fetch(url, { signal: controller_1.signal, headers: headers })
74
+ .finally(function () { return clearTimeout(timeout_1); })];
75
+ case 1:
76
+ res = _b.sent();
77
+ if (!res.ok)
78
+ return [2 /*return*/, null];
79
+ return [4 /*yield*/, res.json()];
80
+ case 2:
81
+ data = _b.sent();
82
+ return [2 /*return*/, {
83
+ valid: data.valid,
84
+ readOnly: data.readOnly || false,
85
+ blocked: data.blocked || false,
86
+ features: data.features || {},
87
+ limits: data.limits || {},
88
+ tier: data.tier || "BASIC",
89
+ fetchedAt: Date.now(),
90
+ }];
91
+ case 3:
92
+ _a = _b.sent();
93
+ return [2 /*return*/, null];
94
+ case 4: return [2 /*return*/];
95
+ }
96
+ });
97
+ });
98
+ }
99
+ var proxyUrl, licenseUrl, installationUuid, internalSecret, cached, cached;
100
+ return __generator(this, function (_a) {
101
+ switch (_a.label) {
47
102
  case 0:
103
+ proxyUrl = process.env.LICENSE_PROXY_URL || "";
48
104
  licenseUrl = process.env.LICENSE_URL || "";
49
105
  installationUuid = process.env.INSTALLATION_UUID || process.env.ENTITY_UUID || "";
50
- if (!licenseUrl || !installationUuid)
51
- return [2 /*return*/, null];
52
- _b.label = 1;
106
+ internalSecret = process.env.INTERNAL_SECRET || "";
107
+ if (!proxyUrl) return [3 /*break*/, 2];
108
+ return [4 /*yield*/, tryFetch(proxyUrl, true)];
53
109
  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); })];
110
+ cached = _a.sent();
111
+ if (cached)
112
+ return [2 /*return*/, cached];
113
+ _a.label = 2;
58
114
  case 2:
59
- res = _b.sent();
60
- if (!res.ok)
61
- return [2 /*return*/, null];
62
- return [4 /*yield*/, res.json()];
115
+ if (!(licenseUrl && installationUuid)) return [3 /*break*/, 4];
116
+ return [4 /*yield*/, tryFetch(licenseUrl + "/api/license/validate/" + installationUuid, false)];
63
117
  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
- _a = _b.sent();
76
- return [2 /*return*/, null];
77
- case 5: return [2 /*return*/];
118
+ cached = _a.sent();
119
+ if (cached)
120
+ return [2 /*return*/, cached];
121
+ _a.label = 4;
122
+ case 4: return [2 /*return*/, null];
78
123
  }
79
124
  });
80
125
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluyappgocore/commons-backend",
3
- "version": "1.0.211",
3
+ "version": "1.0.213",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",