@fluyappgocore/commons-backend 1.0.211 → 1.0.212

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);
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.212",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",