mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 03:29:35 +00:00
feat: changed backend-pg to backend
This commit is contained in:
@@ -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;
|
||||
};
|
||||
@@ -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}>`
|
||||
});
|
||||
@@ -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)
|
||||
});
|
||||
@@ -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");
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
export {
|
||||
buildSecretBlindIndexFromName,
|
||||
createSecretBlindIndex,
|
||||
decryptAsymmetric,
|
||||
decryptSymmetric,
|
||||
decryptSymmetric128BitHexKeyUTF8,
|
||||
encryptAsymmetric,
|
||||
encryptSymmetric,
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
generateAsymmetricKeyPair
|
||||
} from "./encryption";
|
||||
export { generateSrpServerKey, srpCheckClientProof } from "./srp";
|
||||
@@ -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);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
|
||||
|
||||
export const secondsToMillis = (seconds: number) => seconds * 1000;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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"
|
||||
});
|
||||
};
|
||||
@@ -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 || {})
|
||||
});
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { initLogger,logger } from "./logger";
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
export const alphaNumericNanoId = customAlphabet(SLUG_ALPHABETS, 10);
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user