Merge remote-tracking branch 'origin' into pg-ssl

This commit is contained in:
Tuan Dang
2024-02-03 15:16:47 -08:00
232 changed files with 2524 additions and 4438 deletions
+3 -2
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { buildMongoQueryMatcher, MongoAbility } from "@casl/ability";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch";
@@ -12,7 +13,7 @@ const $glob: FieldInstruction<string> = {
};
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const secretPath = context.get(object, node.field);
const secretPath = context.get(object, node.field) as string;
const permissionSecretGlobPath = node.value;
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
};
@@ -23,7 +24,7 @@ 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}`);
ability.rules.map((permission) => `${permission.action as string}_${permission.subject as string}`);
/**
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
+8 -5
View File
@@ -39,9 +39,7 @@ const envSchema = z
// 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")
),
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
@@ -57,7 +55,12 @@ const envSchema = z
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
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()),
@@ -122,7 +125,7 @@ export const initEnvConfig = (logger: Logger) => {
logger.error(parsedEnv.error.issues);
process.exit(-1);
}
envCfg = Object.freeze(parsedEnv.data);
return envCfg;
};
+1
View File
@@ -5,6 +5,7 @@ export const request = axios.create();
axiosRetry(request, {
retries: 3,
// eslint-disable-next-line
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (err) => axiosRetry.isNetworkError(err) || axiosRetry.isRetryableError(err)
});
+5 -23
View File
@@ -20,11 +20,7 @@ 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")
);
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");
@@ -62,17 +58,8 @@ export const encryptSymmetric128BitHexKeyUTF8 = (plaintext: string, key: string)
};
};
export const decryptSymmetric128BitHexKeyUTF8 = ({
ciphertext,
iv,
tag,
key
}: TDecryptSymmetricInput): string => {
const decipher = crypto.createDecipheriv(
SecretEncryptionAlgo.AES_256_GCM,
key,
Buffer.from(iv, "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"));
@@ -104,12 +91,7 @@ export type TDecryptAsymmetricInput = {
privateKey: string;
};
export const decryptAsymmetric = ({
ciphertext,
nonce,
publicKey,
privateKey
}: TDecryptAsymmetricInput) => {
export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }: TDecryptAsymmetricInput) => {
const plaintext: Uint8Array | null = nacl.box.open(
naclUtils.decodeBase64(ciphertext),
naclUtils.decodeBase64(nonce),
@@ -223,7 +205,7 @@ export const infisicalSymmetricEncypt = (data: string) => {
throw new Error("Missing both encryption keys");
};
export const infisicalSymmetricDecrypt = <T extends any = string>({
export const infisicalSymmetricDecrypt = <T = string>({
keyEncoding,
ciphertext,
tag,
+2 -5
View File
@@ -23,13 +23,10 @@ export const groupBy = <T, Key extends string | number | symbol>(
* 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[] => {
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);
const key = toKey ? toKey(item) : (item as unknown as string | number | symbol);
if (acc[key]) return acc;
acc[key] = item;
return acc;
View File
+3 -6
View File
@@ -2,10 +2,7 @@
* 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> => {
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) => {
@@ -21,9 +18,9 @@ export const pick = <T extends object, TKeys extends keyof T>(
* object. Optional second argument shakes out values
* by custom evaluation.
*/
export const shake = <RemovedKeys extends string, T = {}>(
export const shake = <RemovedKeys extends string, T = object>(
obj: T,
filter: (value: any) => boolean = (x) => x === undefined || x === null
filter: (value: unknown) => boolean = (x) => x === undefined || x === null
): Omit<T, RemovedKeys> => {
if (!obj) return {} as T;
const keys = Object.keys(obj) as (keyof T)[];
+1 -7
View File
@@ -111,13 +111,7 @@ export type TIp = {
/**
* Validates the IP address [ipAddress] against the trusted IPs [trustedIps].
*/
export const checkIPAgainstBlocklist = ({
ipAddress,
trustedIps
}: {
ipAddress: string;
trustedIps: TIp[];
}) => {
export const checkIPAgainstBlocklist = ({ ipAddress, trustedIps }: { ipAddress: string; trustedIps: TIp[] }) => {
const blockList = new net.BlockList();
for (const trustedIp of trustedIps) {
+19 -31
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import { Knex } from "knex";
import { Tables } from "knex/types/tables";
@@ -15,22 +16,22 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
...dal
});
export type TFindFilter<R extends {} = any> = Partial<R> & {
export type TFindFilter<R extends object = object> = Partial<R> & {
$in?: Partial<{ [k in keyof R]: R[k][] }>;
};
export const buildFindFilter =
<R extends {} = any>({ $in, ...filter }: TFindFilter<R>) =>
<R extends object = object>({ $in, ...filter }: TFindFilter<R>) =>
(bd: Knex.QueryBuilder<R, R>) => {
bd.where(filter);
void bd.where(filter);
if ($in) {
Object.entries($in).forEach(([key, val]) => {
bd.whereIn(key as any, val as any);
void bd.whereIn(key as never, val as never);
});
}
return bd;
};
export type TFindOpt<R extends {} = any> = {
export type TFindOpt<R extends object = object> = {
limit?: number;
offset?: number;
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
@@ -40,11 +41,7 @@ export type TFindOpt<R extends {} = any> = {
// 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
) => ({
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);
@@ -53,7 +50,7 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(
findById: async (id: string, tx?: Knex) => {
try {
const result = await (tx || db)(tableName)
.where({ id } as any)
.where({ id } as never)
.first("*");
return result;
} catch (error) {
@@ -74,12 +71,10 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(
) => {
try {
const query = (tx || db)(tableName).where(buildFindFilter(filter));
if (limit) query.limit(limit);
if (offset) query.offset(offset);
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
query.orderBy(
sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))
);
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const res = await query;
return res;
@@ -90,7 +85,7 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(
create: async (data: Tables[Tname]["insert"], tx?: Knex) => {
try {
const [res] = await (tx || db)(tableName)
.insert(data as any)
.insert(data as never)
.returning("*");
return res;
} catch (error) {
@@ -101,7 +96,7 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(
try {
if (!data.length) return [];
const res = await (tx || db)(tableName)
.insert(data as any)
.insert(data as never)
.returning("*");
return res;
} catch (error) {
@@ -111,23 +106,19 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(
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)
.where({ id } as never)
.update(data as never)
.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
) => {
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)
.update(data as never)
.returning("*");
return res;
} catch (error) {
@@ -137,7 +128,7 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(
deleteById: async (id: string, tx?: Knex) => {
try {
const [res] = await (tx || db)(tableName)
.where({ id } as any)
.where({ id } as never)
.delete()
.returning("*");
return res;
@@ -147,10 +138,7 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(
},
delete: async (filter: TFindFilter<Tables[Tname]["base"]>, tx?: Knex) => {
try {
const res = await (tx || db)(tableName)
.where(buildFindFilter(filter))
.delete()
.returning("*");
const res = await (tx || db)(tableName).where(buildFindFilter(filter)).delete().returning("*");
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Delete" });
+14 -4
View File
@@ -1,7 +1,16 @@
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// TODO(akhilmhdh): make this better later
export const mergeOneToManyRelation = <
T extends Record<string, any>,
T extends Record<string, unknown>,
Pk extends keyof T,
P extends Record<string, any>,
P extends Record<string, unknown>,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
C extends any,
Ck extends string = "child"
>(
@@ -22,7 +31,7 @@ export const mergeOneToManyRelation = <
const parent = parentMapper(row) as any;
parent[childKey] = [];
groupedRecord.push(parent);
prevPkId = pk;
prevPkId = pk as string;
prevPkIndex += 1;
}
groupedRecord[prevPkIndex][childKey].push(childMapper(row));
@@ -41,6 +50,7 @@ export type TSqlPackRelationships<
childrenMapper: C;
};
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
export type TChildMapper<T extends {}, U extends string = string, R extends unknown = unknown> = {
key: keyof T;
label: U;
@@ -82,7 +92,7 @@ const sqlChildMapper = <
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);
if (typeof val !== "undefined" && val !== null) docsByPk[pk][label].push(val);
lookupTable.add(ck);
}
if (nestedMappers && val) {
+1 -1
View File
@@ -1 +1 @@
export { initLogger,logger } from "./logger";
export { initLogger, logger } from "./logger";
+19 -8
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// logger follows a singleton pattern
// easier to use it that's all.
import pino, { Logger } from "pino";
@@ -14,10 +15,10 @@ const logLevelToSeverityLookup: Record<string, string> = {
// 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.
// 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({
@@ -25,14 +26,23 @@ const loggerConfig = z.object({
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)
AWS_CLOUDWATCH_LOG_INTERVAL: z.coerce.number().default(1000),
NODE_ENV: z.enum(["development", "test", "production"]).default("production")
});
export const initLogger = async () => {
const targets: pino.TransportMultiOptions["targets"][number][] = [
{ level: "info", target: "pino/file", options: {} }
];
const cfg = loggerConfig.parse(process.env);
const targets: pino.TransportMultiOptions["targets"][number][] = [
{
level: "info",
target: "pino/file",
options: {
destination: 1,
mkdir: true
}
}
];
if (cfg.AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID && cfg.AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET) {
targets.push({
target: "@serdnam/pino-cloudwatch-transport",
@@ -66,6 +76,7 @@ export const initLogger = async () => {
})
}
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
transport
);
return logger;
+2 -5
View File
@@ -1,10 +1,7 @@
import { z,ZodTypeAny } from "zod";
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 }
) =>
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;