archetecture security fix

This commit is contained in:
root
2026-06-11 03:22:12 -04:00
parent 6def9993da
commit 9483750161
3126 changed files with 177194 additions and 37211 deletions
+9
View File
@@ -0,0 +1,9 @@
export declare function generateCompanyApiKey(): {
rawKey: string;
prefix: string;
keyHash: string;
};
export declare function getApiKeyPrefix(rawKey: string): string | null;
export declare function hashApiKey(rawKey: string): string;
export declare function timingSafeEqualHex(a: string, b: string): boolean;
//# sourceMappingURL=apiKeys.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"apiKeys.d.ts","sourceRoot":"","sources":["../../src/security/apiKeys.ts"],"names":[],"mappings":"AAEA,wBAAgB,qBAAqB;;;;EAKpC;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,iBAI7C;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,UAExC;AAED,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,WAKtD"}
+33
View File
@@ -0,0 +1,33 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateCompanyApiKey = generateCompanyApiKey;
exports.getApiKeyPrefix = getApiKeyPrefix;
exports.hashApiKey = hashApiKey;
exports.timingSafeEqualHex = timingSafeEqualHex;
const crypto_1 = __importDefault(require("crypto"));
function generateCompanyApiKey() {
const secret = crypto_1.default.randomBytes(32).toString('base64url');
const prefix = `rdg_${crypto_1.default.randomBytes(6).toString('hex')}`;
const rawKey = `${prefix}_${secret}`;
return { rawKey, prefix, keyHash: hashApiKey(rawKey) };
}
function getApiKeyPrefix(rawKey) {
const parts = rawKey.split('_');
if (parts.length < 3)
return null;
return `${parts[0]}_${parts[1]}`;
}
function hashApiKey(rawKey) {
return crypto_1.default.createHash('sha256').update(rawKey).digest('hex');
}
function timingSafeEqualHex(a, b) {
const left = Buffer.from(a, 'hex');
const right = Buffer.from(b, 'hex');
if (left.length !== right.length)
return false;
return crypto_1.default.timingSafeEqual(left, right);
}
//# sourceMappingURL=apiKeys.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"apiKeys.js","sourceRoot":"","sources":["../../src/security/apiKeys.ts"],"names":[],"mappings":";;;;;AAEA,sDAKC;AAED,0CAIC;AAED,gCAEC;AAED,gDAKC;AAxBD,oDAA2B;AAE3B,SAAgB,qBAAqB;IACnC,MAAM,MAAM,GAAG,gBAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;IAC3D,MAAM,MAAM,GAAG,OAAO,gBAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAA;IAC7D,MAAM,MAAM,GAAG,GAAG,MAAM,IAAI,MAAM,EAAE,CAAA;IACpC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,CAAA;AACxD,CAAC;AAED,SAAgB,eAAe,CAAC,MAAc;IAC5C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC/B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IACjC,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;AAClC,CAAC;AAED,SAAgB,UAAU,CAAC,MAAc;IACvC,OAAO,gBAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACjE,CAAC;AAED,SAAgB,kBAAkB,CAAC,CAAS,EAAE,CAAS;IACrD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;IAClC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;IACnC,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAC9C,OAAO,gBAAM,CAAC,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;AAC5C,CAAC"}
+10
View File
@@ -0,0 +1,10 @@
export declare const AdminPolicy: {
readonly impersonateCompany: readonly ["SUPER_ADMIN"];
readonly manageAdmins: readonly ["SUPER_ADMIN", "ADMIN"];
readonly managePricingPlans: readonly ["SUPER_ADMIN", "ADMIN"];
readonly manageSubscriptions: readonly ["SUPER_ADMIN", "ADMIN", "FINANCE"];
readonly performRefund: readonly ["SUPER_ADMIN", "ADMIN", "FINANCE"];
readonly viewSensitiveCredentials: readonly ["SUPER_ADMIN"];
};
export type AdminPolicyAction = keyof typeof AdminPolicy;
//# sourceMappingURL=adminPolicy.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"adminPolicy.d.ts","sourceRoot":"","sources":["../../../src/security/policies/adminPolicy.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW;;;;;;;CAOiC,CAAA;AAEzD,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,WAAW,CAAA"}
+12
View File
@@ -0,0 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AdminPolicy = void 0;
exports.AdminPolicy = {
impersonateCompany: ['SUPER_ADMIN'],
manageAdmins: ['SUPER_ADMIN', 'ADMIN'],
managePricingPlans: ['SUPER_ADMIN', 'ADMIN'],
manageSubscriptions: ['SUPER_ADMIN', 'ADMIN', 'FINANCE'],
performRefund: ['SUPER_ADMIN', 'ADMIN', 'FINANCE'],
viewSensitiveCredentials: ['SUPER_ADMIN'],
};
//# sourceMappingURL=adminPolicy.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"adminPolicy.js","sourceRoot":"","sources":["../../../src/security/policies/adminPolicy.ts"],"names":[],"mappings":";;;AAEa,QAAA,WAAW,GAAG;IACzB,kBAAkB,EAAE,CAAC,aAAa,CAAC;IACnC,YAAY,EAAE,CAAC,aAAa,EAAE,OAAO,CAAC;IACtC,kBAAkB,EAAE,CAAC,aAAa,EAAE,OAAO,CAAC;IAC5C,mBAAmB,EAAE,CAAC,aAAa,EAAE,OAAO,EAAE,SAAS,CAAC;IACxD,aAAa,EAAE,CAAC,aAAa,EAAE,OAAO,EAAE,SAAS,CAAC;IAClD,wBAAwB,EAAE,CAAC,aAAa,CAAC;CACc,CAAA"}
+12
View File
@@ -0,0 +1,12 @@
export declare const CompanyPolicy: {
readonly viewApiKey: readonly ["OWNER"];
readonly regenerateApiKey: readonly ["OWNER"];
readonly updateAccountingSettings: readonly ["OWNER", "MANAGER"];
readonly deletePricingRule: readonly ["OWNER", "MANAGER"];
readonly managePricingRules: readonly ["OWNER", "MANAGER"];
readonly manageTeam: readonly ["OWNER", "MANAGER"];
readonly viewBilling: readonly ["OWNER", "MANAGER"];
readonly manageBilling: readonly ["OWNER"];
};
export type CompanyPolicyAction = keyof typeof CompanyPolicy;
//# sourceMappingURL=companyPolicy.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"companyPolicy.d.ts","sourceRoot":"","sources":["../../../src/security/policies/companyPolicy.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa;;;;;;;;;CASkC,CAAA;AAE5D,MAAM,MAAM,mBAAmB,GAAG,MAAM,OAAO,aAAa,CAAA"}
+14
View File
@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CompanyPolicy = void 0;
exports.CompanyPolicy = {
viewApiKey: ['OWNER'],
regenerateApiKey: ['OWNER'],
updateAccountingSettings: ['OWNER', 'MANAGER'],
deletePricingRule: ['OWNER', 'MANAGER'],
managePricingRules: ['OWNER', 'MANAGER'],
manageTeam: ['OWNER', 'MANAGER'],
viewBilling: ['OWNER', 'MANAGER'],
manageBilling: ['OWNER'],
};
//# sourceMappingURL=companyPolicy.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"companyPolicy.js","sourceRoot":"","sources":["../../../src/security/policies/companyPolicy.ts"],"names":[],"mappings":";;;AAEa,QAAA,aAAa,GAAG;IAC3B,UAAU,EAAE,CAAC,OAAO,CAAC;IACrB,gBAAgB,EAAE,CAAC,OAAO,CAAC;IAC3B,wBAAwB,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC;IAC9C,iBAAiB,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC;IACvC,kBAAkB,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC;IACxC,UAAU,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC;IAChC,WAAW,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC;IACjC,aAAa,EAAE,CAAC,OAAO,CAAC;CACkC,CAAA"}
+7
View File
@@ -0,0 +1,7 @@
export declare function generatePublicAccessToken(): {
token: string;
tokenHash: string;
};
export declare function hashPublicAccessToken(token: string): string;
export declare function timingSafeEqualTokenHash(a: string, b: string): boolean;
//# sourceMappingURL=publicAccessTokens.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"publicAccessTokens.d.ts","sourceRoot":"","sources":["../../src/security/publicAccessTokens.ts"],"names":[],"mappings":"AAEA,wBAAgB,yBAAyB;;;EAGxC;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,UAElD;AAED,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,WAK5D"}
+24
View File
@@ -0,0 +1,24 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generatePublicAccessToken = generatePublicAccessToken;
exports.hashPublicAccessToken = hashPublicAccessToken;
exports.timingSafeEqualTokenHash = timingSafeEqualTokenHash;
const crypto_1 = __importDefault(require("crypto"));
function generatePublicAccessToken() {
const token = crypto_1.default.randomBytes(32).toString('base64url');
return { token, tokenHash: hashPublicAccessToken(token) };
}
function hashPublicAccessToken(token) {
return crypto_1.default.createHash('sha256').update(token).digest('hex');
}
function timingSafeEqualTokenHash(a, b) {
const left = Buffer.from(a, 'hex');
const right = Buffer.from(b, 'hex');
if (left.length !== right.length)
return false;
return crypto_1.default.timingSafeEqual(left, right);
}
//# sourceMappingURL=publicAccessTokens.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"publicAccessTokens.js","sourceRoot":"","sources":["../../src/security/publicAccessTokens.ts"],"names":[],"mappings":";;;;;AAEA,8DAGC;AAED,sDAEC;AAED,4DAKC;AAhBD,oDAA2B;AAE3B,SAAgB,yBAAyB;IACvC,MAAM,KAAK,GAAG,gBAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;IAC1D,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,qBAAqB,CAAC,KAAK,CAAC,EAAE,CAAA;AAC3D,CAAC;AAED,SAAgB,qBAAqB,CAAC,KAAa;IACjD,OAAO,gBAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAChE,CAAC;AAED,SAAgB,wBAAwB,CAAC,CAAS,EAAE,CAAS;IAC3D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;IAClC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;IACnC,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAC9C,OAAO,gBAAM,CAAC,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;AAC5C,CAAC"}
+6
View File
@@ -0,0 +1,6 @@
import type { Response } from 'express';
import type { ActorType } from './tokens';
export declare function getSessionCookieName(actorType: ActorType): string;
export declare function setSessionCookie(res: Response, actorType: ActorType, token: string, maxAgeMs?: number): void;
export declare function clearSessionCookie(res: Response, actorType: ActorType): void;
//# sourceMappingURL=sessionCookies.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"sessionCookies.d.ts","sourceRoot":"","sources":["../../src/security/sessionCookies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAQzC,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,UAExD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,QAQrG;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,QAOrE"}
+31
View File
@@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSessionCookieName = getSessionCookieName;
exports.setSessionCookie = setSessionCookie;
exports.clearSessionCookie = clearSessionCookie;
const COOKIE_NAMES = {
admin: 'admin_session',
employee: 'employee_session',
renter: 'renter_session',
};
function getSessionCookieName(actorType) {
return COOKIE_NAMES[actorType];
}
function setSessionCookie(res, actorType, token, maxAgeMs) {
res.cookie(getSessionCookieName(actorType), token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: actorType === 'admin' ? 'strict' : 'lax',
path: '/',
maxAge: maxAgeMs,
});
}
function clearSessionCookie(res, actorType) {
res.clearCookie(getSessionCookieName(actorType), {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: actorType === 'admin' ? 'strict' : 'lax',
path: '/',
});
}
//# sourceMappingURL=sessionCookies.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"sessionCookies.js","sourceRoot":"","sources":["../../src/security/sessionCookies.ts"],"names":[],"mappings":";;AASA,oDAEC;AAED,4CAQC;AAED,gDAOC;AA3BD,MAAM,YAAY,GAA8B;IAC9C,KAAK,EAAE,eAAe;IACtB,QAAQ,EAAE,kBAAkB;IAC5B,MAAM,EAAE,gBAAgB;CACzB,CAAA;AAED,SAAgB,oBAAoB,CAAC,SAAoB;IACvD,OAAO,YAAY,CAAC,SAAS,CAAC,CAAA;AAChC,CAAC;AAED,SAAgB,gBAAgB,CAAC,GAAa,EAAE,SAAoB,EAAE,KAAa,EAAE,QAAiB;IACpG,GAAG,CAAC,MAAM,CAAC,oBAAoB,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE;QACjD,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;QAC7C,QAAQ,EAAE,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK;QAClD,IAAI,EAAE,GAAG;QACT,MAAM,EAAE,QAAQ;KACjB,CAAC,CAAA;AACJ,CAAC;AAED,SAAgB,kBAAkB,CAAC,GAAa,EAAE,SAAoB;IACpE,GAAG,CAAC,WAAW,CAAC,oBAAoB,CAAC,SAAS,CAAC,EAAE;QAC/C,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;QAC7C,QAAQ,EAAE,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK;QAClD,IAAI,EAAE,GAAG;KACV,CAAC,CAAA;AACJ,CAAC"}
+15
View File
@@ -0,0 +1,15 @@
import jwt from 'jsonwebtoken';
export type ActorType = 'admin' | 'employee' | 'renter';
export type ActorTokenPayload = jwt.JwtPayload & {
sub: string;
type: ActorType;
sessionVersion?: number;
last2faAt?: number;
};
export declare function signActorToken(actorId: string, actorType: ActorType, options?: Pick<jwt.SignOptions, 'expiresIn'> & {
sessionVersion?: number;
last2faAt?: number;
}): string;
export declare function verifyActorToken(token: string, expectedType: ActorType): ActorTokenPayload;
export declare function verifyAnyActorToken(token: string): ActorTokenPayload;
//# sourceMappingURL=tokens.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/security/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,cAAc,CAAA;AAE9B,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAA;AAEvD,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,UAAU,GAAG;IAC/C,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,SAAS,CAAA;IACf,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAWD,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,WAAW,CAAC,GAAG;IAAE,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,UAkBnG;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,GAAG,iBAAiB,CAY1F;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CASpE"}
+51
View File
@@ -0,0 +1,51 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.signActorToken = signActorToken;
exports.verifyActorToken = verifyActorToken;
exports.verifyAnyActorToken = verifyAnyActorToken;
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
const ISSUER = 'rentaldrivego-api';
const ALG = ['HS256'];
function getJwtSecret() {
const secret = process.env.JWT_SECRET;
if (!secret)
throw new Error('JWT_SECRET is required');
return secret;
}
function signActorToken(actorId, actorType, options = {}) {
const { sessionVersion, last2faAt, ...signOptions } = options;
return jsonwebtoken_1.default.sign({
sub: actorId,
type: actorType,
...(sessionVersion === undefined ? {} : { sessionVersion }),
...(last2faAt === undefined ? {} : { last2faAt }),
}, getJwtSecret(), {
algorithm: 'HS256',
issuer: ISSUER,
audience: actorType,
expiresIn: signOptions.expiresIn ?? '8h',
});
}
function verifyActorToken(token, expectedType) {
const payload = jsonwebtoken_1.default.verify(token, getJwtSecret(), {
algorithms: ALG,
issuer: ISSUER,
audience: expectedType,
});
if (typeof payload.sub !== 'string' || payload.type !== expectedType) {
throw new Error('Invalid actor token');
}
return payload;
}
function verifyAnyActorToken(token) {
const decoded = jsonwebtoken_1.default.decode(token);
const actorType = decoded?.type;
if (actorType !== 'admin' && actorType !== 'employee' && actorType !== 'renter') {
throw new Error('Invalid actor token');
}
return verifyActorToken(token, actorType);
}
//# sourceMappingURL=tokens.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/security/tokens.ts"],"names":[],"mappings":";;;;;AAoBA,wCAqBC;AAED,4CAYC;AAED,kDASC;AAlED,gEAA8B;AAW9B,MAAM,MAAM,GAAG,mBAAmB,CAAA;AAClC,MAAM,GAAG,GAAoB,CAAC,OAAO,CAAC,CAAA;AAEtC,SAAS,YAAY;IACnB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAA;IACrC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAA;IACtD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAgB,cAAc,CAC5B,OAAe,EACf,SAAoB,EACpB,UAAgG,EAAE;IAElG,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,CAAA;IAC7D,OAAO,sBAAG,CAAC,IAAI,CACb;QACE,GAAG,EAAE,OAAO;QACZ,IAAI,EAAE,SAAS;QACf,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC;QAC3D,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC;KAClD,EACD,YAAY,EAAE,EACd;QACE,SAAS,EAAE,OAAO;QAClB,MAAM,EAAE,MAAM;QACd,QAAQ,EAAE,SAAS;QACnB,SAAS,EAAE,WAAW,CAAC,SAAS,IAAI,IAAI;KACzC,CACF,CAAA;AACH,CAAC;AAED,SAAgB,gBAAgB,CAAC,KAAa,EAAE,YAAuB;IACrE,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE;QAChD,UAAU,EAAE,GAAG;QACf,MAAM,EAAE,MAAM;QACd,QAAQ,EAAE,YAAY;KACvB,CAAmB,CAAA;IAEpB,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAA;IACxC,CAAC;IAED,OAAO,OAA4B,CAAA;AACrC,CAAC;AAED,SAAgB,mBAAmB,CAAC,KAAa;IAC/C,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,CAA0B,CAAA;IAC1D,MAAM,SAAS,GAAG,OAAO,EAAE,IAAI,CAAA;IAE/B,IAAI,SAAS,KAAK,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,SAAS,KAAK,QAAQ,EAAE,CAAC;QAChF,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAA;IACxC,CAAC;IAED,OAAO,gBAAgB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;AAC3C,CAAC"}
+14
View File
@@ -0,0 +1,14 @@
type WebhookProcessInput<T> = {
provider: string;
providerEventId: string;
eventType: string;
rawBody: string | Buffer;
handle: () => Promise<T>;
};
export declare function processWebhookOnce<T>({ provider, providerEventId, eventType, rawBody, handle, }: WebhookProcessInput<T>): Promise<{
duplicate: boolean;
result?: T;
}>;
export declare function getWebhookEventId(provider: 'amanpay' | 'paypal', payload: any): string;
export {};
//# sourceMappingURL=webhookIdempotency.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"webhookIdempotency.d.ts","sourceRoot":"","sources":["../../src/security/webhookIdempotency.ts"],"names":[],"mappings":"AAIA,KAAK,mBAAmB,CAAC,CAAC,IAAI;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;IACxB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAA;CACzB,CAAA;AAmCD,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,EAC1C,QAAQ,EACR,eAAe,EACf,SAAS,EACT,OAAO,EACP,MAAM,GACP,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,CAAC,CAAA;CAAE,CAAC,CAsCtE;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,GAAG,QAAQ,EAAE,OAAO,EAAE,GAAG,UAK7E"}
+87
View File
@@ -0,0 +1,87 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.processWebhookOnce = processWebhookOnce;
exports.getWebhookEventId = getWebhookEventId;
const crypto_1 = __importDefault(require("crypto"));
function payloadHash(rawBody) {
return crypto_1.default.createHash('sha256').update(rawBody).digest('hex');
}
function getWebhookEventModel() {
try {
// Lazy load keeps isolated unit tests from requiring a generated Prisma client
// before `prisma generate` has run in the local sandbox.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { prisma } = require('../lib/prisma');
return prisma.webhookEvent;
}
catch {
return undefined;
}
}
async function findExisting(model, provider, providerEventId) {
return model.findUnique({
where: { provider_providerEventId: { provider, providerEventId } },
});
}
async function mark(model, id, status, errorMessage) {
await model.update({
where: { id },
data: {
status,
processedAt: status === 'PROCESSED' ? new Date() : undefined,
errorMessage: errorMessage?.slice(0, 1000),
},
});
}
async function processWebhookOnce({ provider, providerEventId, eventType, rawBody, handle, }) {
const model = getWebhookEventModel();
if (!model) {
const result = await handle();
return { duplicate: false, result };
}
const hash = payloadHash(rawBody);
let eventRecord;
try {
eventRecord = await model.create({
data: {
provider,
providerEventId,
eventType,
status: 'PROCESSING',
payloadHash: hash,
},
});
}
catch (err) {
const existing = await findExisting(model, provider, providerEventId);
if (existing?.status === 'PROCESSED')
return { duplicate: true };
if (existing?.payloadHash && existing.payloadHash !== hash) {
throw new Error('Webhook event ID replayed with a different payload hash');
}
if (existing?.status === 'PROCESSING')
return { duplicate: true };
eventRecord = existing;
}
try {
const result = await handle();
if (eventRecord?.id)
await mark(model, eventRecord.id, 'PROCESSED');
return { duplicate: false, result };
}
catch (err) {
if (eventRecord?.id)
await mark(model, eventRecord.id, 'FAILED', err?.message);
throw err;
}
}
function getWebhookEventId(provider, payload) {
if (provider === 'paypal') {
return String(payload.id ?? payload.event_id ?? `${payload.event_type ?? 'unknown'}:${payload.resource?.id ?? 'missing'}`);
}
return String(payload.event_id ?? payload.id ?? payload.transaction_id ?? 'missing');
}
//# sourceMappingURL=webhookIdempotency.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"webhookIdempotency.js","sourceRoot":"","sources":["../../src/security/webhookIdempotency.ts"],"names":[],"mappings":"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"}