@platform-modules/civil-aviation-authority 2.3.301 → 2.3.303

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,19 @@
1
+ import type { UserSessionReader } from './user-session-validation';
2
+ export type JwtAuthFailure = {
3
+ ok: false;
4
+ statusCode: 401;
5
+ message: string;
6
+ };
7
+ export type JwtAuthSuccess = {
8
+ ok: true;
9
+ claims: Record<string, unknown>;
10
+ userId: number;
11
+ sessionId: number | null;
12
+ };
13
+ export type JwtAuthResult = JwtAuthSuccess | JwtAuthFailure;
14
+ /**
15
+ * After jwt.verify(), enforce server-side session binding (logout revocation).
16
+ */
17
+ export declare function authorizeVerifiedJwtSession(verified: Record<string, unknown>, sessionReader: UserSessionReader, options?: {
18
+ requireActiveSession?: boolean;
19
+ }): Promise<JwtAuthResult>;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authorizeVerifiedJwtSession = authorizeVerifiedJwtSession;
4
+ const jwt_token_helper_1 = require("./jwt-token.helper");
5
+ const user_session_validation_1 = require("./user-session-validation");
6
+ /**
7
+ * After jwt.verify(), enforce server-side session binding (logout revocation).
8
+ */
9
+ async function authorizeVerifiedJwtSession(verified, sessionReader, options) {
10
+ const parsed = (0, jwt_token_helper_1.parseVerifiedJwtClaims)(verified);
11
+ if (!parsed) {
12
+ return { ok: false, statusCode: 401, message: 'Invalid Token' };
13
+ }
14
+ const requireActiveSession = options?.requireActiveSession !== false;
15
+ if (requireActiveSession) {
16
+ const sessionValid = await (0, user_session_validation_1.validateUserSessionBinding)(parsed.userId, parsed.sessionId, sessionReader);
17
+ if (!sessionValid) {
18
+ return { ok: false, statusCode: 401, message: 'Session Expired please login again' };
19
+ }
20
+ }
21
+ return {
22
+ ok: true,
23
+ claims: parsed.raw,
24
+ userId: parsed.userId,
25
+ sessionId: parsed.sessionId,
26
+ };
27
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Extract CAA API JWT from standard request headers.
3
+ * Supports `jwt` header (legacy) and `Authorization: Bearer <token>`.
4
+ */
5
+ export declare function extractJwtToken(headers: Record<string, string | string[] | undefined> | undefined): string | null;
6
+ export type JwtTokenRequest = {
7
+ headers: Record<string, string | string[] | undefined>;
8
+ body?: unknown;
9
+ };
10
+ /** JWT from headers (`jwt`, `Authorization: Bearer`) or JSON body (`token`, `jwt`, `access_token`). */
11
+ export declare function extractJwtTokenFromRequest(request: JwtTokenRequest): string | null;
12
+ export type VerifiedJwtClaims = {
13
+ userId: number;
14
+ sessionId: number | null;
15
+ raw: Record<string, unknown>;
16
+ };
17
+ /** Parse verified JWT payload into userId + sessionId. */
18
+ export declare function parseVerifiedJwtClaims(verified: Record<string, unknown>): VerifiedJwtClaims | null;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractJwtToken = extractJwtToken;
4
+ exports.extractJwtTokenFromRequest = extractJwtTokenFromRequest;
5
+ exports.parseVerifiedJwtClaims = parseVerifiedJwtClaims;
6
+ /**
7
+ * Extract CAA API JWT from standard request headers.
8
+ * Supports `jwt` header (legacy) and `Authorization: Bearer <token>`.
9
+ */
10
+ function extractJwtToken(headers) {
11
+ if (!headers)
12
+ return null;
13
+ const pick = (value) => {
14
+ const raw = Array.isArray(value) ? value[0] : value;
15
+ const trimmed = raw?.trim();
16
+ return trimmed ? trimmed : null;
17
+ };
18
+ const fromJwtHeader = pick(headers.jwt ?? headers.JWT);
19
+ if (fromJwtHeader)
20
+ return fromJwtHeader;
21
+ const authorization = pick(headers.authorization ?? headers.Authorization);
22
+ if (!authorization)
23
+ return null;
24
+ const bearerMatch = /^Bearer\s+(.+)$/i.exec(authorization);
25
+ if (bearerMatch?.[1]?.trim())
26
+ return bearerMatch[1].trim();
27
+ return authorization;
28
+ }
29
+ /** JWT from headers (`jwt`, `Authorization: Bearer`) or JSON body (`token`, `jwt`, `access_token`). */
30
+ function extractJwtTokenFromRequest(request) {
31
+ const fromHeaders = extractJwtToken(request.headers);
32
+ if (fromHeaders)
33
+ return fromHeaders;
34
+ const body = request.body;
35
+ if (!body || typeof body !== 'object')
36
+ return null;
37
+ for (const key of ['token', 'jwt', 'access_token']) {
38
+ const value = body[key];
39
+ if (typeof value === 'string' && value.trim())
40
+ return value.trim();
41
+ }
42
+ return null;
43
+ }
44
+ /** Parse verified JWT payload into userId + sessionId. */
45
+ function parseVerifiedJwtClaims(verified) {
46
+ const userId = Number(verified.userId);
47
+ if (!Number.isFinite(userId) || userId <= 0)
48
+ return null;
49
+ const sessionIdRaw = verified.sessionId;
50
+ const parsedSessionId = sessionIdRaw == null || sessionIdRaw === '' ? null : Number(sessionIdRaw);
51
+ return {
52
+ userId,
53
+ sessionId: parsedSessionId != null && Number.isFinite(parsedSessionId) && parsedSessionId > 0
54
+ ? parsedSessionId
55
+ : null,
56
+ raw: verified,
57
+ };
58
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Server-side JWT session binding (CAA VAPT M003/M004).
3
+ * JWT remains valid cryptographically until exp; access is allowed only while
4
+ * the bound user_sessions row is active and not expired.
5
+ */
6
+ export type UserSessionReader = {
7
+ isSessionActiveById(sessionId: number, userId: number): Promise<boolean>;
8
+ /** Legacy fallback when REQUIRE_JWT_SESSION_ID=false and token has no sessionId. */
9
+ isSessionActive?(userId: number): Promise<unknown>;
10
+ };
11
+ /** When true (default), JWT must include sessionId and that session must be active. */
12
+ export declare function isRequireJwtSessionId(): boolean;
13
+ export declare function resolveJwtSessionId(sessionId: unknown): number | null;
14
+ /**
15
+ * Validate that a verified JWT is still authorized (not logged out / expired server-side).
16
+ */
17
+ export declare function validateUserSessionBinding(userId: number, sessionId: number | null | undefined, reader: UserSessionReader): Promise<boolean>;
18
+ /** Shared TypeORM filters: active, not deleted, not past expires_at. */
19
+ export declare function applyActiveUserSessionFilters(qb: {
20
+ andWhere: (clause: string, params?: Record<string, unknown>) => unknown;
21
+ }, alias?: string): void;
22
+ /** @deprecated Use applyActiveUserSessionFilters — kept for backward compatibility. */
23
+ export declare function activeUserSessionSql(alias?: string): {
24
+ clause: string;
25
+ params: Record<string, unknown>;
26
+ };
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * Server-side JWT session binding (CAA VAPT M003/M004).
4
+ * JWT remains valid cryptographically until exp; access is allowed only while
5
+ * the bound user_sessions row is active and not expired.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.isRequireJwtSessionId = isRequireJwtSessionId;
9
+ exports.resolveJwtSessionId = resolveJwtSessionId;
10
+ exports.validateUserSessionBinding = validateUserSessionBinding;
11
+ exports.applyActiveUserSessionFilters = applyActiveUserSessionFilters;
12
+ exports.activeUserSessionSql = activeUserSessionSql;
13
+ /** When true (default), JWT must include sessionId and that session must be active. */
14
+ function isRequireJwtSessionId() {
15
+ const raw = (process.env.REQUIRE_JWT_SESSION_ID ?? 'true').trim().toLowerCase();
16
+ return raw !== 'false' && raw !== '0' && raw !== 'no';
17
+ }
18
+ function resolveJwtSessionId(sessionId) {
19
+ if (sessionId == null || sessionId === '')
20
+ return null;
21
+ const sid = Number(sessionId);
22
+ if (!Number.isFinite(sid) || sid <= 0)
23
+ return null;
24
+ return sid;
25
+ }
26
+ /**
27
+ * Validate that a verified JWT is still authorized (not logged out / expired server-side).
28
+ */
29
+ async function validateUserSessionBinding(userId, sessionId, reader) {
30
+ if (!Number.isFinite(userId) || userId <= 0)
31
+ return false;
32
+ const sid = resolveJwtSessionId(sessionId);
33
+ if (sid != null) {
34
+ return reader.isSessionActiveById(sid, userId);
35
+ }
36
+ if (isRequireJwtSessionId()) {
37
+ return false;
38
+ }
39
+ if (!reader.isSessionActive)
40
+ return false;
41
+ const legacy = await reader.isSessionActive(userId);
42
+ return !!legacy;
43
+ }
44
+ /** Shared TypeORM filters: active, not deleted, not past expires_at. */
45
+ function applyActiveUserSessionFilters(qb, alias = 'user_sessions') {
46
+ qb.andWhere(`${alias}.is_active = :is_active`, { is_active: true });
47
+ qb.andWhere(`${alias}.is_deleted = :is_deleted`, { is_deleted: false });
48
+ qb.andWhere(`(${alias}.expires_at IS NULL OR ${alias}.expires_at > :session_now)`, { session_now: new Date() });
49
+ }
50
+ /** @deprecated Use applyActiveUserSessionFilters — kept for backward compatibility. */
51
+ function activeUserSessionSql(alias = 'user_sessions') {
52
+ return {
53
+ clause: `${alias}.is_active = :is_active AND ${alias}.is_deleted = :is_deleted AND (${alias}.expires_at IS NULL OR ${alias}.expires_at > :session_now)`,
54
+ params: {
55
+ is_active: true,
56
+ is_deleted: false,
57
+ session_now: new Date(),
58
+ },
59
+ };
60
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export * from './models/user';
2
2
  export * from './models/role';
3
3
  export * from './models/user-sessions';
4
+ export * from './auth/user-session-validation';
5
+ export * from './auth/jwt-token.helper';
6
+ export * from './auth/authenticate-jwt-session';
4
7
  export * from './models/ITHelpDeskModel';
5
8
  export * from './models/ServiceTypeModel';
6
9
  export * from './models/CAAServices';
package/dist/index.js CHANGED
@@ -20,6 +20,9 @@ exports.RespondToEnquiriesRequestAttachment = exports.RespondToEnquiriesWorkFlow
20
20
  __exportStar(require("./models/user"), exports);
21
21
  __exportStar(require("./models/role"), exports);
22
22
  __exportStar(require("./models/user-sessions"), exports);
23
+ __exportStar(require("./auth/user-session-validation"), exports);
24
+ __exportStar(require("./auth/jwt-token.helper"), exports);
25
+ __exportStar(require("./auth/authenticate-jwt-session"), exports);
23
26
  __exportStar(require("./models/ITHelpDeskModel"), exports);
24
27
  // export * from './models/ITServicesTypesSalalahModel'; // REMOVED - table no longer used
25
28
  // export * from './models/ITServicesTypesMuscatModel'; // REMOVED - table no longer used
@@ -23,6 +23,7 @@ export declare class ResidentialUnitRentalRequest extends BaseModel {
23
23
  family_size: number;
24
24
  phone_number: string;
25
25
  location_of_unit: string;
26
+ number_of_apartments: string | null;
26
27
  comment: string;
27
28
  status: string;
28
29
  user_id: number;
@@ -69,6 +69,10 @@ __decorate([
69
69
  (0, typeorm_1.Column)({ type: 'varchar', length: 255, nullable: true }),
70
70
  __metadata("design:type", String)
71
71
  ], ResidentialUnitRentalRequest.prototype, "location_of_unit", void 0);
72
+ __decorate([
73
+ (0, typeorm_1.Column)({ type: 'varchar', length: 255, nullable: true }),
74
+ __metadata("design:type", Object)
75
+ ], ResidentialUnitRentalRequest.prototype, "number_of_apartments", void 0);
72
76
  __decorate([
73
77
  (0, typeorm_1.Column)({ type: 'text', nullable: true }),
74
78
  __metadata("design:type", String)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platform-modules/civil-aviation-authority",
3
- "version": "2.3.301",
3
+ "version": "2.3.303",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
@@ -9,6 +9,9 @@ ALTER TYPE asset_maintenance_category_en ADD VALUE IF NOT EXISTS 'Other';
9
9
  ALTER TABLE residential_unit_rental_requests
10
10
  ALTER COLUMN preferred_start_date DROP NOT NULL;
11
11
 
12
+ ALTER TABLE residential_unit_rental_requests
13
+ ADD COLUMN IF NOT EXISTS number_of_apartments VARCHAR(255);
14
+
12
15
  -- 3. Appeal against administrative decision: optional / removed payload fields
13
16
  ALTER TABLE appeal_against_administrative_decision_requests
14
17
  ALTER COLUMN description DROP NOT NULL,
@@ -0,0 +1,51 @@
1
+ import { parseVerifiedJwtClaims } from './jwt-token.helper';
2
+ import { validateUserSessionBinding } from './user-session-validation';
3
+ import type { UserSessionReader } from './user-session-validation';
4
+
5
+ export type JwtAuthFailure = {
6
+ ok: false;
7
+ statusCode: 401;
8
+ message: string;
9
+ };
10
+
11
+ export type JwtAuthSuccess = {
12
+ ok: true;
13
+ claims: Record<string, unknown>;
14
+ userId: number;
15
+ sessionId: number | null;
16
+ };
17
+
18
+ export type JwtAuthResult = JwtAuthSuccess | JwtAuthFailure;
19
+
20
+ /**
21
+ * After jwt.verify(), enforce server-side session binding (logout revocation).
22
+ */
23
+ export async function authorizeVerifiedJwtSession(
24
+ verified: Record<string, unknown>,
25
+ sessionReader: UserSessionReader,
26
+ options?: { requireActiveSession?: boolean },
27
+ ): Promise<JwtAuthResult> {
28
+ const parsed = parseVerifiedJwtClaims(verified);
29
+ if (!parsed) {
30
+ return { ok: false, statusCode: 401, message: 'Invalid Token' };
31
+ }
32
+
33
+ const requireActiveSession = options?.requireActiveSession !== false;
34
+ if (requireActiveSession) {
35
+ const sessionValid = await validateUserSessionBinding(
36
+ parsed.userId,
37
+ parsed.sessionId,
38
+ sessionReader,
39
+ );
40
+ if (!sessionValid) {
41
+ return { ok: false, statusCode: 401, message: 'Session Expired please login again' };
42
+ }
43
+ }
44
+
45
+ return {
46
+ ok: true,
47
+ claims: parsed.raw,
48
+ userId: parsed.userId,
49
+ sessionId: parsed.sessionId,
50
+ };
51
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Extract CAA API JWT from standard request headers.
3
+ * Supports `jwt` header (legacy) and `Authorization: Bearer <token>`.
4
+ */
5
+ export function extractJwtToken(
6
+ headers: Record<string, string | string[] | undefined> | undefined,
7
+ ): string | null {
8
+ if (!headers) return null;
9
+
10
+ const pick = (value: string | string[] | undefined): string | null => {
11
+ const raw = Array.isArray(value) ? value[0] : value;
12
+ const trimmed = raw?.trim();
13
+ return trimmed ? trimmed : null;
14
+ };
15
+
16
+ const fromJwtHeader = pick(headers.jwt ?? headers.JWT);
17
+ if (fromJwtHeader) return fromJwtHeader;
18
+
19
+ const authorization = pick(headers.authorization ?? headers.Authorization);
20
+ if (!authorization) return null;
21
+
22
+ const bearerMatch = /^Bearer\s+(.+)$/i.exec(authorization);
23
+ if (bearerMatch?.[1]?.trim()) return bearerMatch[1].trim();
24
+
25
+ return authorization;
26
+ }
27
+
28
+ export type JwtTokenRequest = {
29
+ headers: Record<string, string | string[] | undefined>;
30
+ body?: unknown;
31
+ };
32
+
33
+ /** JWT from headers (`jwt`, `Authorization: Bearer`) or JSON body (`token`, `jwt`, `access_token`). */
34
+ export function extractJwtTokenFromRequest(request: JwtTokenRequest): string | null {
35
+ const fromHeaders = extractJwtToken(request.headers);
36
+ if (fromHeaders) return fromHeaders;
37
+
38
+ const body = request.body as Record<string, unknown> | undefined;
39
+ if (!body || typeof body !== 'object') return null;
40
+
41
+ for (const key of ['token', 'jwt', 'access_token'] as const) {
42
+ const value = body[key];
43
+ if (typeof value === 'string' && value.trim()) return value.trim();
44
+ }
45
+ return null;
46
+ }
47
+
48
+ export type VerifiedJwtClaims = {
49
+ userId: number;
50
+ sessionId: number | null;
51
+ raw: Record<string, unknown>;
52
+ };
53
+
54
+ /** Parse verified JWT payload into userId + sessionId. */
55
+ export function parseVerifiedJwtClaims(verified: Record<string, unknown>): VerifiedJwtClaims | null {
56
+ const userId = Number(verified.userId);
57
+ if (!Number.isFinite(userId) || userId <= 0) return null;
58
+
59
+ const sessionIdRaw = verified.sessionId;
60
+ const parsedSessionId =
61
+ sessionIdRaw == null || sessionIdRaw === '' ? null : Number(sessionIdRaw);
62
+
63
+ return {
64
+ userId,
65
+ sessionId:
66
+ parsedSessionId != null && Number.isFinite(parsedSessionId) && parsedSessionId > 0
67
+ ? parsedSessionId
68
+ : null,
69
+ raw: verified,
70
+ };
71
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Server-side JWT session binding (CAA VAPT M003/M004).
3
+ * JWT remains valid cryptographically until exp; access is allowed only while
4
+ * the bound user_sessions row is active and not expired.
5
+ */
6
+
7
+ export type UserSessionReader = {
8
+ isSessionActiveById(sessionId: number, userId: number): Promise<boolean>;
9
+ /** Legacy fallback when REQUIRE_JWT_SESSION_ID=false and token has no sessionId. */
10
+ isSessionActive?(userId: number): Promise<unknown>;
11
+ };
12
+
13
+ /** When true (default), JWT must include sessionId and that session must be active. */
14
+ export function isRequireJwtSessionId(): boolean {
15
+ const raw = (process.env.REQUIRE_JWT_SESSION_ID ?? 'true').trim().toLowerCase();
16
+ return raw !== 'false' && raw !== '0' && raw !== 'no';
17
+ }
18
+
19
+ export function resolveJwtSessionId(sessionId: unknown): number | null {
20
+ if (sessionId == null || sessionId === '') return null;
21
+ const sid = Number(sessionId);
22
+ if (!Number.isFinite(sid) || sid <= 0) return null;
23
+ return sid;
24
+ }
25
+
26
+ /**
27
+ * Validate that a verified JWT is still authorized (not logged out / expired server-side).
28
+ */
29
+ export async function validateUserSessionBinding(
30
+ userId: number,
31
+ sessionId: number | null | undefined,
32
+ reader: UserSessionReader,
33
+ ): Promise<boolean> {
34
+ if (!Number.isFinite(userId) || userId <= 0) return false;
35
+
36
+ const sid = resolveJwtSessionId(sessionId);
37
+ if (sid != null) {
38
+ return reader.isSessionActiveById(sid, userId);
39
+ }
40
+
41
+ if (isRequireJwtSessionId()) {
42
+ return false;
43
+ }
44
+
45
+ if (!reader.isSessionActive) return false;
46
+ const legacy = await reader.isSessionActive(userId);
47
+ return !!legacy;
48
+ }
49
+
50
+ /** Shared TypeORM filters: active, not deleted, not past expires_at. */
51
+ export function applyActiveUserSessionFilters(
52
+ qb: {
53
+ andWhere: (clause: string, params?: Record<string, unknown>) => unknown;
54
+ },
55
+ alias = 'user_sessions',
56
+ ): void {
57
+ qb.andWhere(`${alias}.is_active = :is_active`, { is_active: true });
58
+ qb.andWhere(`${alias}.is_deleted = :is_deleted`, { is_deleted: false });
59
+ qb.andWhere(
60
+ `(${alias}.expires_at IS NULL OR ${alias}.expires_at > :session_now)`,
61
+ { session_now: new Date() },
62
+ );
63
+ }
64
+
65
+ /** @deprecated Use applyActiveUserSessionFilters — kept for backward compatibility. */
66
+ export function activeUserSessionSql(alias = 'user_sessions'): {
67
+ clause: string;
68
+ params: Record<string, unknown>;
69
+ } {
70
+ return {
71
+ clause: `${alias}.is_active = :is_active AND ${alias}.is_deleted = :is_deleted AND (${alias}.expires_at IS NULL OR ${alias}.expires_at > :session_now)`,
72
+ params: {
73
+ is_active: true,
74
+ is_deleted: false,
75
+ session_now: new Date(),
76
+ },
77
+ };
78
+ }
package/src/index.ts CHANGED
@@ -3,6 +3,9 @@ import { JobTransferRequestStatus } from './models/JobTransferRequestModel';
3
3
  export * from './models/user';
4
4
  export * from './models/role';
5
5
  export * from './models/user-sessions';
6
+ export * from './auth/user-session-validation';
7
+ export * from './auth/jwt-token.helper';
8
+ export * from './auth/authenticate-jwt-session';
6
9
  export * from './models/ITHelpDeskModel';
7
10
  // export * from './models/ITServicesTypesSalalahModel'; // REMOVED - table no longer used
8
11
  // export * from './models/ITServicesTypesMuscatModel'; // REMOVED - table no longer used
@@ -113,8 +113,8 @@ export class ResidentialUnitRentalRequest extends BaseModel {
113
113
 
114
114
  location_of_unit: string;
115
115
 
116
-
117
-
116
+ @Column({ type: 'varchar', length: 255, nullable: true })
117
+ number_of_apartments: string | null;
118
118
 
119
119
  @Column({ type: 'text', nullable: true })
120
120