feat: changed backend-pg to backend

This commit is contained in:
Akhil Mohan
2024-01-27 19:14:33 +05:30
parent d13eafcef7
commit 66d258f02b
792 changed files with 40298 additions and 40607 deletions
+44
View File
@@ -0,0 +1,44 @@
import { buildMongoQueryMatcher, MongoAbility } from "@casl/ability";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch";
const $glob: FieldInstruction<string> = {
type: "field",
validate(instruction, value) {
if (typeof value !== "string") {
throw new Error(`"${instruction.name}" expects value to be a string`);
}
}
};
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const secretPath = context.get(object, node.field);
const permissionSecretGlobPath = node.value;
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
};
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
/**
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
*/
const extractPermissions = (ability: MongoAbility) =>
ability.rules.map((permission) => `${permission.action}_${permission.subject}`);
/**
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
*
*/
export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2: MongoAbility) => {
const set1 = new Set(extractPermissions(permissions1));
const set2 = new Set(extractPermissions(permissions2));
for (const perm of set2) {
if (!set1.has(perm)) {
return false;
}
}
return set1.size >= set2.size;
};
+138
View File
@@ -0,0 +1,138 @@
import { Logger } from "pino";
import { z } from "zod";
import { zpStr } from "../zod";
export const GITLAB_URL = "https://gitlab.com";
const zodStrBool = z
.enum(["true", "false"])
.optional()
.transform((val) => val === "true");
const envSchema = z
.object({
PORT: z.coerce.number().default(4000),
REDIS_URL: zpStr(z.string()),
HOST: zpStr(z.string().default("localhost")),
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")),
NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
SALT_ROUNDS: z.coerce.number().default(10),
// TODO(akhilmhdh): will be changed to one
ENCRYPTION_KEY: zpStr(z.string().optional()),
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
HTTPS_ENABLED: zodStrBool,
// smtp options
SMTP_HOST: zpStr(z.string().optional()),
SMTP_SECURE: zodStrBool,
SMTP_PORT: z.coerce.number().default(587),
SMTP_USERNAME: zpStr(z.string().optional()),
SMTP_PASSWORD: zpStr(z.string().optional()),
SMTP_FROM_ADDRESS: zpStr(z.string().optional()),
SMTP_FROM_NAME: zpStr(z.string().optional().default("Infisical")),
COOKIE_SECRET_SIGN_KEY: z
.string()
.min(32)
.default("#5VihU%rbXHcHwWwCot5L3vyPsx$7dWYw^iGk!EJg2bC*f$PD$%KCqx^R@#^LSEf"),
SITE_URL: zpStr(z.string().optional()),
// Telemetry
TELEMETRY_ENABLED: zodStrBool.default("true"),
POSTHOG_HOST: zpStr(z.string().optional().default("https://app.posthog.com")),
POSTHOG_PROJECT_API_KEY: zpStr(
z.string().optional().default("phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE")
),
LOOPS_API_KEY: zpStr(z.string().optional()),
// jwt options
AUTH_SECRET: zpStr(z.string()).default(process.env.JWT_AUTH_SECRET), // for those still using old JWT_AUTH_SECRET
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
JWT_MFA_LIFETIME: zpStr(z.string().default("5m")),
JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")),
// Oauth
CLIENT_ID_GOOGLE_LOGIN: zpStr(z.string().optional()),
CLIENT_SECRET_GOOGLE_LOGIN: zpStr(z.string().optional()),
CLIENT_ID_GITHUB_LOGIN: zpStr(z.string().optional()),
CLIENT_SECRET_GITHUB_LOGIN: zpStr(z.string().optional()),
CLIENT_ID_GITLAB_LOGIN: zpStr(z.string().optional()),
CLIENT_SECRET_GITLAB_LOGIN: zpStr(z.string().optional()),
CLIENT_GITLAB_LOGIN_URL: zpStr(z.string().optional().default(process.env.URL_GITLAB_LOGIN ?? GITLAB_URL)), // fallback since URL_GITLAB_LOGIN has been renamed
// integration client secrets
// heroku
CLIENT_ID_HEROKU: zpStr(z.string().optional()),
CLIENT_SECRET_HEROKU: zpStr(z.string().optional()),
// vercel
CLIENT_ID_VERCEL: zpStr(z.string().optional()),
CLIENT_SECRET_VERCEL: zpStr(z.string().optional()),
CLIENT_SLUG_VERCEL: zpStr(z.string().optional()),
// netlify
CLIENT_ID_NETLIFY: zpStr(z.string().optional()),
CLIENT_SECRET_NETLIFY: zpStr(z.string().optional()),
// bit bucket
CLIENT_ID_BITBUCKET: zpStr(z.string().optional()),
CLIENT_SECRET_BITBUCKET: zpStr(z.string().optional()),
// gcp secret manager
CLIENT_ID_GCP_SECRET_MANAGER: zpStr(z.string().optional()),
CLIENT_SECRET_GCP_SECRET_MANAGER: zpStr(z.string().optional()),
// github
CLIENT_ID_GITHUB: zpStr(z.string().optional()),
CLIENT_SECRET_GITHUB: zpStr(z.string().optional()),
// azure
CLIENT_ID_AZURE: zpStr(z.string().optional()),
CLIENT_SECRET_AZURE: zpStr(z.string().optional()),
// gitlab
CLIENT_ID_GITLAB: zpStr(z.string().optional()),
CLIENT_SECRET_GITLAB: zpStr(z.string().optional()),
URL_GITLAB_URL: zpStr(z.string().optional().default(GITLAB_URL)),
// SECRET-SCANNING
SECRET_SCANNING_WEBHOOK_PROXY: zpStr(z.string().optional()),
SECRET_SCANNING_WEBHOOK_SECRET: zpStr(z.string().optional()),
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
// LICENCE
LICENSE_SERVER_URL: zpStr(z.string().optional()),
LICENSE_SERVER_KEY: zpStr(z.string().optional()),
LICENSE_KEY: zpStr(z.string().optional()),
STANDALONE_MODE: z
.enum(["true", "false"])
.transform((val) => val === "true")
.optional()
})
.transform((data) => ({
...data,
isSmtpConfigured: Boolean(data.SMTP_HOST),
isRedisConfigured: Boolean(data.REDIS_URL),
isDevelopmentMode: data.NODE_ENV === "development",
isProductionMode: data.NODE_ENV === "production",
isSecretScanningConfigured:
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET)
}));
let envCfg: Readonly<z.infer<typeof envSchema>>;
export const getConfig = () => envCfg;
// cannot import singleton logger directly as it needs config to load various transport
export const initEnvConfig = (logger: Logger) => {
const parsedEnv = envSchema.safeParse(process.env);
if (!parsedEnv.success) {
logger.error("Invalid environment variables. Check the error below");
logger.error(parsedEnv.error.issues);
process.exit(-1);
}
envCfg = Object.freeze(parsedEnv.data);
return envCfg;
};
export const formatSmtpConfig = () => ({
host: envCfg.SMTP_HOST,
port: envCfg.SMTP_PORT,
auth:
envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD
? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD }
: undefined,
secure: envCfg.SMTP_SECURE,
from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`
});
+10
View File
@@ -0,0 +1,10 @@
import axios from "axios";
import axiosRetry from "axios-retry";
export const request = axios.create();
axiosRetry(request, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (err) => axiosRetry.isNetworkError(err) || axiosRetry.isRetryableError(err)
});
+246
View File
@@ -0,0 +1,246 @@
import crypto from "node:crypto";
import * as argon2 from "argon2";
import nacl from "tweetnacl";
import naclUtils from "tweetnacl-util";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "../config/env";
export type TDecryptSymmetricInput = {
ciphertext: string;
iv: string;
tag: string;
key: string;
};
export const IV_BYTES_SIZE = 12;
export const BLOCK_SIZE_BYTES_16 = 16;
export const decryptSymmetric = ({ ciphertext, iv, tag, key }: TDecryptSymmetricInput): string => {
const secretKey = crypto.createSecretKey(key, "base64");
const decipher = crypto.createDecipheriv(
SecretEncryptionAlgo.AES_256_GCM,
secretKey,
Buffer.from(iv, "base64")
);
decipher.setAuthTag(Buffer.from(tag, "base64"));
let cleartext = decipher.update(ciphertext, "base64", "utf8");
cleartext += decipher.final("utf8");
return cleartext;
};
export const encryptSymmetric = (plaintext: string, key: string) => {
const iv = crypto.randomBytes(IV_BYTES_SIZE);
const secretKey = crypto.createSecretKey(key, "base64");
const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, secretKey, iv);
let ciphertext = cipher.update(plaintext, "utf8", "base64");
ciphertext += cipher.final("base64");
return {
ciphertext,
iv: iv.toString("base64"),
tag: cipher.getAuthTag().toString("base64")
};
};
export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string) => {
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16);
const cipher = crypto.createCipheriv(SecretEncryptionAlgo.AES_256_GCM, key, iv);
let ciphertext = cipher.update(plaintext, "utf8", "base64");
ciphertext += cipher.final("base64");
return {
ciphertext,
iv: iv.toString("base64"),
tag: cipher.getAuthTag().toString("base64")
};
};
export const decryptSymmetric128BitHexKeyUTF8 = ({
ciphertext,
iv,
tag,
key
}: TDecryptSymmetricInput): string => {
const decipher = crypto.createDecipheriv(
SecretEncryptionAlgo.AES_256_GCM,
key,
Buffer.from(iv, "base64")
);
decipher.setAuthTag(Buffer.from(tag, "base64"));
let cleartext = decipher.update(ciphertext, "base64", "utf8");
cleartext += decipher.final("utf8");
return cleartext;
};
export const encryptAsymmetric = (plaintext: string, publicKey: string, privateKey: string) => {
const nonce = nacl.randomBytes(24);
const ciphertext = nacl.box(
naclUtils.decodeUTF8(plaintext),
nonce,
naclUtils.decodeBase64(publicKey),
naclUtils.decodeBase64(privateKey)
);
return {
ciphertext: naclUtils.encodeBase64(ciphertext),
nonce: naclUtils.encodeBase64(nonce)
};
};
export type TDecryptAsymmetricInput = {
ciphertext: string;
nonce: string;
publicKey: string;
privateKey: string;
};
export const decryptAsymmetric = ({
ciphertext,
nonce,
publicKey,
privateKey
}: TDecryptAsymmetricInput) => {
const plaintext: Uint8Array | null = nacl.box.open(
naclUtils.decodeBase64(ciphertext),
naclUtils.decodeBase64(nonce),
naclUtils.decodeBase64(publicKey),
naclUtils.decodeBase64(privateKey)
);
if (plaintext == null) throw Error("Invalid ciphertext or keys");
return naclUtils.encodeUTF8(plaintext);
};
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
export const generateAsymmetricKeyPair = () => {
const pair = nacl.box.keyPair();
return {
publicKey: naclUtils.encodeBase64(pair.publicKey),
privateKey: naclUtils.encodeBase64(pair.secretKey)
};
};
export type TGenSecretBlindIndex = {
secretName: string;
keyEncoding: SecretKeyEncoding;
rootEncryptionKey?: string;
encryptionKey?: string;
iv: string;
tag: string;
ciphertext: string;
};
export const buildSecretBlindIndexFromName = async ({
secretName,
ciphertext,
keyEncoding,
iv,
tag,
encryptionKey,
rootEncryptionKey
}: TGenSecretBlindIndex) => {
if (!encryptionKey && !rootEncryptionKey) throw new Error("Missing secret blind index key");
let salt = "";
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
salt = decryptSymmetric({ iv, ciphertext, key: rootEncryptionKey, tag });
} else if (encryptionKey && keyEncoding === SecretKeyEncoding.UTF8) {
salt = decryptSymmetric128BitHexKeyUTF8({ iv, ciphertext, key: encryptionKey, tag });
}
if (!salt) throw new Error("Missing secret blind index key");
const secretBlindIndex = await argon2.hash(secretName, {
type: argon2.argon2id,
salt: Buffer.from(salt, "base64"),
saltLength: 16, // default 16 bytes
memoryCost: 65536, // default pool of 64 MiB per thread.
hashLength: 32,
parallelism: 1,
raw: true
});
return secretBlindIndex.toString("base64");
};
export const createSecretBlindIndex = (rootEncryptionKey?: string, encryptionKey?: string) => {
if (!encryptionKey && !rootEncryptionKey) throw new Error("Atleast one encryption key needed");
const salt = crypto.randomBytes(16).toString("base64");
if (rootEncryptionKey) {
const data = encryptSymmetric(salt, rootEncryptionKey);
return {
...data,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.BASE64
};
}
if (encryptionKey) {
const data = encryptSymmetric128BitHexKeyUTF8(salt, encryptionKey);
return {
...data,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
};
}
throw new Error("Failed to generate blind index due to encryption key missing");
};
export const infisicalSymmetricEncypt = (data: string) => {
const appCfg = getConfig();
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
const encryptionKey = appCfg.ENCRYPTION_KEY;
if (rootEncryptionKey) {
const { iv, tag, ciphertext } = encryptSymmetric(data, rootEncryptionKey);
return {
iv,
tag,
ciphertext,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
encoding: SecretKeyEncoding.BASE64
};
}
if (encryptionKey) {
const { iv, tag, ciphertext } = encryptSymmetric128BitHexKeyUTF8(data, encryptionKey);
return {
iv,
tag,
ciphertext,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
encoding: SecretKeyEncoding.UTF8
};
}
throw new Error("Missing both encryption keys");
};
export const infisicalSymmetricDecrypt = <T extends any = string>({
keyEncoding,
ciphertext,
tag,
iv
}: Omit<TDecryptSymmetricInput, "key"> & {
keyEncoding: SecretKeyEncoding;
}) => {
const appCfg = getConfig();
const rootEncryptionKey = appCfg.ROOT_ENCRYPTION_KEY;
const encryptionKey = appCfg.ENCRYPTION_KEY;
if (rootEncryptionKey && keyEncoding === SecretKeyEncoding.BASE64) {
const data = decryptSymmetric({ key: rootEncryptionKey, iv, tag, ciphertext });
return data as T;
}
if (encryptionKey && keyEncoding === SecretKeyEncoding.UTF8) {
const data = decryptSymmetric128BitHexKeyUTF8({ key: encryptionKey, iv, tag, ciphertext });
return data as T;
}
throw new Error("Missing both encryption keys");
};
+12
View File
@@ -0,0 +1,12 @@
export {
buildSecretBlindIndexFromName,
createSecretBlindIndex,
decryptAsymmetric,
decryptSymmetric,
decryptSymmetric128BitHexKeyUTF8,
encryptAsymmetric,
encryptSymmetric,
encryptSymmetric128BitHexKeyUTF8,
generateAsymmetricKeyPair
} from "./encryption";
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
+26
View File
@@ -0,0 +1,26 @@
import jsrp from "jsrp";
export const generateSrpServerKey = async (salt: string, verifier: string) => {
// eslint-disable-next-line new-cap
const server = new jsrp.server();
await new Promise((resolve) => {
server.init({ salt, verifier }, () => resolve(null));
});
return { pubKey: server.getPublicKey(), privateKey: server.getPrivateKey() };
};
export const srpCheckClientProof = async (
salt: string,
verifier: string,
serverPrivateKey: string,
clientPublicKey: string,
clientProof: string
) => {
// eslint-disable-next-line new-cap
const server = new jsrp.server();
await new Promise((resolve) => {
server.init({ salt, verifier, b: serverPrivateKey }, () => resolve(null));
});
server.setClientPublicKey(clientPublicKey);
return server.checkClientProof(clientProof);
};
+3
View File
@@ -0,0 +1,3 @@
export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
export const secondsToMillis = (seconds: number) => seconds * 1000;
+60
View File
@@ -0,0 +1,60 @@
/* eslint-disable max-classes-per-file */
export class DatabaseError extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error: unknown }) {
super(message || "Failed to execute db ops");
this.name = name || "DatabaseError";
this.error = error;
}
}
export class InternalServerError extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
super(message || "Something went wrong");
this.name = name || "InternalServerError";
this.error = error;
}
}
export class UnauthorizedError extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown } = {}) {
super(message ?? "You are not allowed to access this resource");
this.name = name || "UnauthorizedError";
this.error = error;
}
}
export class ForbiddenRequestError extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
super(message ?? "You are not allowed to access this resource");
this.name = name || "ForbideenError";
this.error = error;
}
}
export class BadRequestError extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
super(message ?? "The request is invalid");
this.name = name || "BadRequest";
this.error = error;
}
}
+40
View File
@@ -0,0 +1,40 @@
/**
* Sorts an array of items into groups. The return value is a map where the keys are
* the group ids the given getGroupId function produced and the value is an array of
* each item in that group.
*/
export const groupBy = <T, Key extends string | number | symbol>(
array: readonly T[],
getGroupId: (item: T) => Key
): Record<Key, T[]> =>
array.reduce(
(acc, item) => {
const groupId = getGroupId(item);
if (!acc[groupId]) acc[groupId] = [];
acc[groupId].push(item);
return acc;
},
{} as Record<Key, T[]>
);
/**
* Given a list of items returns a new list with only
* unique items. Accepts an optional identity function
* to convert each item in the list to a comparable identity
* value
*/
export const unique = <T, K extends string | number | symbol>(
array: readonly T[],
toKey?: (item: T) => K
): T[] => {
const valueMap = array.reduce(
(acc, item) => {
const key = toKey ? toKey(item) : (item as any as string | number | symbol);
if (acc[key]) return acc;
acc[key] = item;
return acc;
},
{} as Record<string | number | symbol, T>
);
return Object.values(valueMap);
};
+6
View File
@@ -0,0 +1,6 @@
// Some of the functions are taken from https://github.com/rayepps/radash
// Full credits goes to https://github.com/rayapps to those functions
// Code taken to keep in in house and to adjust somethings for our needs
export * from "./array";
export * from "./object";
export * from "./string";
+37
View File
@@ -0,0 +1,37 @@
/**
* Pick a list of properties from an object
* into a new object
*/
export const pick = <T extends object, TKeys extends keyof T>(
obj: T,
keys: TKeys[]
): Pick<T, TKeys> => {
if (!obj) return {} as Pick<T, TKeys>;
return keys.reduce(
(acc, key) => {
if (Object.prototype.hasOwnProperty.call(obj, key)) acc[key] = obj[key];
return acc;
},
{} as Pick<T, TKeys>
);
};
/**
* Removes (shakes out) undefined entries from an
* object. Optional second argument shakes out values
* by custom evaluation.
*/
export const shake = <RemovedKeys extends string, T = {}>(
obj: T,
filter: (value: any) => boolean = (x) => x === undefined || x === null
): Omit<T, RemovedKeys> => {
if (!obj) return {} as T;
const keys = Object.keys(obj) as (keyof T)[];
return keys.reduce((acc, key) => {
if (filter(obj[key])) {
return acc;
}
acc[key] = obj[key];
return acc;
}, {} as T);
};
+5
View File
@@ -0,0 +1,5 @@
import path from "path";
// given two paths irrespective of ending with / or not
// this will return true if its equal
export const isSamePath = async (from: string, to: string) => !path.relative(from, to);
+138
View File
@@ -0,0 +1,138 @@
import net from "node:net";
import { UnauthorizedError } from "../errors";
export enum IPType {
IPV4 = "ipv4",
IPV6 = "ipv6"
}
/**
* Return details of IP [ip]:
* - If [ip] is a specific IP address then return the IPv4/IPv6 address
* - If [ip] is a subnet then return the network IPv4/IPv6 address and prefix
*/
export const extractIPDetails = (ip: string) => {
if (net.isIPv4(ip))
return {
ipAddress: ip,
type: IPType.IPV4
};
if (net.isIPv6(ip))
return {
ipAddress: ip,
type: IPType.IPV6
};
const [ipNet, prefix] = ip.split("/");
let type;
switch (net.isIP(ipNet)) {
case 4:
type = IPType.IPV4;
break;
case 6:
type = IPType.IPV6;
break;
default:
throw new Error("Failed to extract IP details");
}
return {
ipAddress: ipNet,
type,
prefix: parseInt(prefix, 10)
};
};
/**
* Checks if a given string is a valid CIDR block.
*
* The function checks if the input string is a valid IPv4 or IPv6 address in CIDR notation.
*
* CIDR notation includes a network address followed by a slash ('/') and a prefix length.
* For IPv4, the prefix length must be between 0 and 32. For IPv6, it must be between 0 and 128.
* If the input string is not a valid CIDR block, the function returns `false`.
*
*/
export const isValidCidr = (cidr: string): boolean => {
const [ip, prefix] = cidr.split("/");
const prefixNum = parseInt(prefix, 10);
// ensure prefix exists and is a number within the appropriate range for each IP version
if (
!prefix ||
Number.isNaN(prefixNum) ||
(net.isIPv4(ip) && (prefixNum < 0 || prefixNum > 32)) ||
(net.isIPv6(ip) && (prefixNum < 0 || prefixNum > 128))
) {
return false;
}
// ensure the IP portion of the CIDR block is a valid IPv4 or IPv6 address
if (!net.isIPv4(ip) && !net.isIPv6(ip)) {
return false;
}
return true;
};
/**
* Checks if a given string is a valid IPv4/IPv6 address or a valid CIDR block.
*
* If the string contains a slash ('/'), it treats the input as a CIDR block and checks its validity.
* Otherwise, it treats the string as a standalone IP address (either IPv4 or IPv6) and checks its validity.
*
* @param {string} input - The string to be checked. It could be an IP address or a CIDR block.
* @returns {boolean} Returns `true` if the string is a valid IP address (either IPv4 or IPv6) or a valid CIDR block, `false` otherwise.
*
*/
export const isValidIpOrCidr = (ip: string): boolean => {
// if the string contains a slash, treat it as a CIDR block
if (ip.includes("/")) {
return isValidCidr(ip);
}
// otherwise, treat it as a standalone IP address
if (net.isIPv4(ip) || net.isIPv6(ip)) {
return true;
}
return false;
};
export type TIp = {
ipAddress: string;
type: IPType;
prefix: number;
};
/**
* Validates the IP address [ipAddress] against the trusted IPs [trustedIps].
*/
export const checkIPAgainstBlocklist = ({
ipAddress,
trustedIps
}: {
ipAddress: string;
trustedIps: TIp[];
}) => {
const blockList = new net.BlockList();
for (const trustedIp of trustedIps) {
if (trustedIp.prefix !== undefined) {
blockList.addSubnet(trustedIp.ipAddress, trustedIp.prefix, trustedIp.type);
} else {
blockList.addAddress(trustedIp.ipAddress, trustedIp.type);
}
}
const { type } = extractIPDetails(ipAddress);
const check = blockList.check(ipAddress, type);
if (!check)
throw new UnauthorizedError({
message: "Failed to authenticate"
});
};
+160
View File
@@ -0,0 +1,160 @@
import { Knex } from "knex";
import { Tables } from "knex/types/tables";
import { DatabaseError } from "../errors";
export * from "./join";
export * from "./select";
export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
transaction: async <T>(cb: (tx: Knex) => Promise<T>) =>
db.transaction(async (trx) => {
const res = await cb(trx);
return res;
}),
...dal
});
export type TFindFilter<R extends {} = any> = Partial<R> & {
$in?: Partial<{ [k in keyof R]: R[k][] }>;
};
export const buildFindFilter =
<R extends {} = any>({ $in, ...filter }: TFindFilter<R>) =>
(bd: Knex.QueryBuilder<R, R>) => {
bd.where(filter);
if ($in) {
Object.entries($in).forEach(([key, val]) => {
bd.whereIn(key as any, val as any);
});
}
return bd;
};
export type TFindOpt<R extends {} = any> = {
limit?: number;
offset?: number;
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
tx?: Knex;
};
// What is ormify
// It is to inject typical operations like find, findOne, update, delete, create
// This will avoid writing most common ones each time
export const ormify = <DbOps extends object, Tname extends keyof Tables>(
db: Knex,
tableName: Tname,
dal?: DbOps
) => ({
transaction: async <T>(cb: (tx: Knex) => Promise<T>) =>
db.transaction(async (trx) => {
const res = await cb(trx);
return res;
}),
findById: async (id: string, tx?: Knex) => {
try {
const result = await (tx || db)(tableName)
.where({ id } as any)
.first("*");
return result;
} catch (error) {
throw new DatabaseError({ error, name: "Find by id" });
}
},
findOne: async (filter: Partial<Tables[Tname]["base"]>, tx?: Knex) => {
try {
const res = await (tx || db)(tableName).where(filter).first("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
},
find: async (
filter: TFindFilter<Tables[Tname]["base"]>,
{ offset, limit, sort, tx }: TFindOpt<Tables[Tname]["base"]> = {}
) => {
try {
const query = (tx || db)(tableName).where(buildFindFilter(filter));
if (limit) query.limit(limit);
if (offset) query.offset(offset);
if (sort) {
query.orderBy(
sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))
);
}
const res = await query;
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
},
create: async (data: Tables[Tname]["insert"], tx?: Knex) => {
try {
const [res] = await (tx || db)(tableName)
.insert(data as any)
.returning("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Create" });
}
},
insertMany: async (data: readonly Tables[Tname]["insert"][], tx?: Knex) => {
try {
if (!data.length) return [];
const res = await (tx || db)(tableName)
.insert(data as any)
.returning("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Create" });
}
},
updateById: async (id: string, data: Tables[Tname]["update"], tx?: Knex) => {
try {
const [res] = await (tx || db)(tableName)
.where({ id } as any)
.update(data as any)
.returning("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Update by id" });
}
},
update: async (
filter: TFindFilter<Tables[Tname]["base"]>,
data: Tables[Tname]["update"],
tx?: Knex
) => {
try {
const res = await (tx || db)(tableName)
.where(buildFindFilter(filter))
.update(data as any)
.returning("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Update" });
}
},
deleteById: async (id: string, tx?: Knex) => {
try {
const [res] = await (tx || db)(tableName)
.where({ id } as any)
.delete()
.returning("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Delete by id" });
}
},
delete: async (filter: TFindFilter<Tables[Tname]["base"]>, tx?: Knex) => {
try {
const res = await (tx || db)(tableName)
.where(buildFindFilter(filter))
.delete()
.returning("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Delete" });
}
},
...(dal || {})
});
+129
View File
@@ -0,0 +1,129 @@
export const mergeOneToManyRelation = <
T extends Record<string, any>,
Pk extends keyof T,
P extends Record<string, any>,
C extends any,
Ck extends string = "child"
>(
data: T[],
key: Pk,
parentMapper: (arg: T) => P,
childMapper: (arg: T) => C,
childKey: Ck
) => {
let prevPkIndex = -1;
let prevPkId: null | string = null;
const groupedRecord: (P & Record<Ck, C[]>)[] = [];
for (let i = 0; i < data.length; i += 1) {
const pk = data[i][key];
const row = data[i];
if (pk !== prevPkId) {
const parent = parentMapper(row) as any;
parent[childKey] = [];
groupedRecord.push(parent);
prevPkId = pk;
prevPkIndex += 1;
}
groupedRecord[prevPkIndex][childKey].push(childMapper(row));
}
return groupedRecord;
};
export type TSqlPackRelationships<
T extends Record<string, any>,
P extends Record<string, any>,
C extends TChildMapper<T>[]
> = {
data: T[];
key: keyof T;
parentMapper: (arg: T) => P;
childrenMapper: C;
};
export type TChildMapper<T extends {}, U extends string = string, R extends unknown = unknown> = {
key: keyof T;
label: U;
mapper: (arg: T) => R;
childrenMapper?: TChildMapper<T, U, R>[];
};
type MappedRecord<T extends TChildMapper<any>> = {
[K in T["label"]]: Array<
Exclude<ReturnType<Extract<T, { label: K }>["mapper"]>, null | undefined> &
(Extract<T, { label: K }>["childrenMapper"] extends Array<infer TChild>
? TChild extends TChildMapper<any>
? MappedRecord<TChild>
: {}
: {})
>;
};
const sqlChildMapper = <
T extends Record<string, any> = {},
P extends Record<string, any> = {},
C extends TChildMapper<T>[] = TChildMapper<T>[]
>(
doc: T,
docsByPk: P,
lookupTable: Set<string>,
pk: keyof P,
prefix: string,
childrenMapper: C
) => {
if (!docsByPk) return;
type Cm = MappedRecord<(typeof childrenMapper)[number]>;
childrenMapper.forEach(({ label, mapper, key: childPk, childrenMapper: nestedMappers }) => {
// eslint-disable-next-line
if (!docsByPk?.[pk as keyof P]?.[label]) docsByPk[pk as keyof P][label as keyof Cm] = [] as any;
if (doc?.[childPk] !== null && typeof doc?.[childPk] !== "undefined") {
const ck = `${prefix}-${label}-${doc[childPk]}`;
const val = mapper(doc);
if (!lookupTable.has(ck)) {
if (typeof val !== "undefined" && val !== null) docsByPk[pk as keyof P][label].push(val);
lookupTable.add(ck);
}
if (nestedMappers && val) {
sqlChildMapper(
doc,
docsByPk[pk][label as keyof P],
lookupTable,
docsByPk[pk][label as keyof P].length - 1,
ck,
nestedMappers
);
}
}
});
};
export const sqlNestRelationships = <
T extends Record<string, any> = {},
P extends Record<string, any> = {},
C extends TChildMapper<T>[] = TChildMapper<T>[]
>({
data,
key,
parentMapper,
childrenMapper
}: TSqlPackRelationships<T, P, C>) => {
const lookupTable = new Set<string>();
const recordsOrder: string[] = [];
type Cm = MappedRecord<(typeof childrenMapper)[number]>;
const recordsGroupedByPk: Record<string, P & Cm> = {};
data.forEach((doc) => {
const pk = doc[key];
if (!lookupTable.has(pk)) {
recordsGroupedByPk[pk] = parentMapper(doc) as P & Cm;
recordsOrder.push(pk);
lookupTable.add(pk);
}
sqlChildMapper(doc, recordsGroupedByPk, lookupTable, pk, pk, childrenMapper);
});
return recordsOrder.map((pkId) => recordsGroupedByPk[pkId]);
};
+14
View File
@@ -0,0 +1,14 @@
import { Tables } from "knex/types/tables";
export const selectAllTableCols = <Tname extends keyof Tables>(tableName: Tname) =>
`${tableName}.*` as keyof Tables[Tname]["base"];
export const stripUndefinedInWhere = <T extends object>(val: T): Exclude<T, undefined> => {
const copy = val;
Object.entries(copy).forEach(([key, value]) => {
if (typeof value === "undefined") {
delete copy[key as keyof T];
}
});
return copy as Exclude<T, undefined>;
};
+1
View File
@@ -0,0 +1 @@
export { initLogger,logger } from "./logger";
+72
View File
@@ -0,0 +1,72 @@
// logger follows a singleton pattern
// easier to use it that's all.
import pino, { Logger } from "pino";
import { z } from "zod";
const logLevelToSeverityLookup: Record<string, string> = {
"10": "TRACE",
"20": "DEBUG",
"30": "INFO",
"40": "WARNING",
"50": "ERROR",
"60": "CRITICAL"
};
// eslint-disable-next-line import/no-mutable-exports
export let logger: Readonly<Logger>;
// akhilmhdh:
// The logger is not placed in the main app config to avoid a circular dependency.
// The config requires the logger to display errors when an invalid environment is supplied.
// On the other hand, the logger needs the config to obtain credentials for AWS or other transports.
// By keeping the logger separate, it becomes an independent package.
const loggerConfig = z.object({
AWS_CLOUDWATCH_LOG_GROUP_NAME: z.string().default("infisical-log-stream"),
AWS_CLOUDWATCH_LOG_REGION: z.string().default("us-east-1"),
AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID: z.string().min(1).optional(),
AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET: z.string().min(1).optional(),
AWS_CLOUDWATCH_LOG_INTERVAL: z.coerce.number().default(1000)
});
export const initLogger = async () => {
const targets: pino.TransportMultiOptions["targets"][number][] = [
{ level: "info", target: "pino/file", options: {} }
];
const cfg = loggerConfig.parse(process.env);
if (cfg.AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID && cfg.AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET) {
targets.push({
target: "@serdnam/pino-cloudwatch-transport",
level: "info",
options: {
logGroupName: cfg.AWS_CLOUDWATCH_LOG_GROUP_NAME,
logStreamName: cfg.AWS_CLOUDWATCH_LOG_GROUP_NAME,
awsRegion: cfg.AWS_CLOUDWATCH_LOG_REGION,
awsAccessKeyId: cfg.AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID,
awsSecretAccessKey: cfg.AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET,
interval: cfg.AWS_CLOUDWATCH_LOG_INTERVAL
}
});
}
const transport = pino.transport({
targets
});
logger = pino(
{
mixin(_context, level) {
return { severity: logLevelToSeverityLookup[level] || logLevelToSeverityLookup["30"] };
},
level: process.env.PINO_LOG_LEVEL || "info",
formatters: {
bindings: (bindings) => ({
pid: bindings.pid,
hostname: bindings.hostname
// node_version: process.version
})
}
},
transport
);
return logger;
};
+4
View File
@@ -0,0 +1,4 @@
import { customAlphabet } from "nanoid";
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
export const alphaNumericNanoId = customAlphabet(SLUG_ALPHABETS, 10);
+7
View File
@@ -0,0 +1,7 @@
import path from "node:path";
export function containsGlobPatterns(secretPath: string) {
const globChars = ["*", "?", "[", "]", "{", "}", "**"];
const normalizedPath = path.normalize(secretPath);
return globChars.some((char) => normalizedPath.includes(char));
}
+18
View File
@@ -0,0 +1,18 @@
import { request } from "../config/request";
const INTEGRATION_GITHUB_API_URL = "https://api.github.com";
type TGithubEmail = {
email: string;
primary: boolean;
verified: boolean;
visibility: null | string;
};
export const fetchGithubEmails = async (accessToken: string) => {
const { data } = await request.get<TGithubEmail[]>(`${INTEGRATION_GITHUB_API_URL}/user/emails`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
return data;
};
+19
View File
@@ -0,0 +1,19 @@
import { ActorType } from "@app/services/auth/auth-type";
export type TOrgPermission = {
actor: ActorType;
actorId: string;
orgId: string;
};
export type TProjectPermission = {
actor: ActorType;
actorId: string;
projectId: string;
};
export type RequiredKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
export { isDisposableEmail } from "./validate-email";
@@ -0,0 +1,10 @@
import fs from "fs/promises";
import path from "path";
export const isDisposableEmail = async (email: string) => {
const emailDomain = email.split("@")[1];
const disposableEmails = await fs.readFile(path.join(__dirname, "disposable_emails.txt"), "utf8");
if (disposableEmails.split("\n").includes(emailDomain)) return true;
return false;
};
+12
View File
@@ -0,0 +1,12 @@
import { z,ZodTypeAny } from "zod";
// this is a patched zod string to remove empty string to undefined
export const zpStr = <T extends ZodTypeAny>(
schema: T,
opt: { stripNull: boolean } = { stripNull: true }
) =>
z.preprocess((val) => {
if (opt.stripNull && val === null) return undefined;
if (typeof val !== "string") return val;
return val.trim() || undefined;
}, schema);