feat(db): mesh data model — meshes, members, invites, audit log

- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 21:19:32 +01:00
commit d3163a5bff
1384 changed files with 314925 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
export { default as capitalize } from "lodash/capitalize";
export { default as debounce } from "lodash/debounce";
export { default as groupBy } from "lodash/groupBy";
export { default as mapValues } from "lodash/mapValues";
export { default as merge } from "lodash/merge";
export { default as omitBy } from "lodash/omitBy";
export { default as pickBy } from "lodash/pickBy";
export { default as random } from "lodash/random";
export { default as sortBy } from "lodash/sortBy";
export { default as transform } from "lodash/transform";
export { default as slugify } from "slugify";
export function splitArray<T>(array: T[], chunkSize: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
}

View File

@@ -0,0 +1,32 @@
import { HttpStatusCode } from "../constants";
export const isHttpStatus = (status: number): status is HttpStatusCode =>
Object.values<number>(HttpStatusCode).includes(status);
interface HttpExceptionOptions {
message?: string;
code?: string;
}
export class HttpException extends Error {
readonly status?: HttpStatusCode;
readonly code?: string;
constructor(status?: HttpStatusCode, options?: HttpExceptionOptions) {
super(options?.message);
this.status = status;
this.code = options?.code;
}
}
export const getStatusCode = (e: unknown) => {
if (typeof e === "object" && e && "status" in e) {
const status = Number(e.status);
// Guard against NaN or invalid status codes
if (!Number.isNaN(status) && status >= 200 && status <= 599) {
return status;
}
}
return HttpStatusCode.INTERNAL_SERVER_ERROR;
};

View File

@@ -0,0 +1,25 @@
export type GreetingKey = "morning" | "afternoon" | "evening" | "night";
interface Greeting {
text: GreetingKey;
emoji: string;
}
/**
* Returns a time-appropriate greeting key with matching emoji
* The text property is an i18n key to be used with t("greeting.{key}")
*/
export function getGreeting(): Greeting {
const hour = new Date().getHours();
if (hour >= 5 && hour < 12) {
return { text: "morning", emoji: "🌅" };
}
if (hour >= 12 && hour < 17) {
return { text: "afternoon", emoji: "☀️" };
}
if (hour >= 17 && hour < 21) {
return { text: "evening", emoji: "🌆" };
}
return { text: "night", emoji: "🌙" };
}

View File

@@ -0,0 +1,45 @@
/**
Creates an ID generator.
The total length of the ID is the sum of the prefix, separator, and random part length.
Not cryptographically secure.
@param alphabet - The alphabet to use for the ID. Default: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.
@param prefix - The prefix of the ID to generate. Optional.
@param separator - The separator between the prefix and the random part of the ID. Default: '-'.
@param size - The size of the random part of the ID to generate. Default: 16.
*/
export const createIdGenerator = ({
prefix,
size = 32,
alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
separator = "-",
}: {
prefix?: string;
separator?: string;
size?: number;
alphabet?: string;
} = {}) => {
const generator = () => {
const alphabetLength = alphabet.length;
const chars = new Array(size);
for (let i = 0; i < size; i++) {
chars[i] = alphabet[(Math.random() * alphabetLength) | 0];
}
return chars.join("");
};
if (prefix == null) {
return generator;
}
// check that the prefix is not part of the alphabet (otherwise prefix checking can fail randomly)
if (alphabet.includes(separator)) {
throw new Error(
`The separator "${separator}" must not be part of the alphabet "${alphabet}".`,
);
}
return () => `${prefix}${separator}${generator()}`;
};
export const generateId = createIdGenerator();

View File

@@ -0,0 +1,6 @@
export * from "./exceptions";
export * from "./common";
export * from "./id";
export * from "./url";
export * from "./wildcard-match";
export * from "./greeting";

View File

@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { HttpStatusCode } from "../../constants";
import { HttpException, getStatusCode, isHttpStatus } from "../exceptions";
describe("isHttpStatus", () => {
it.each([
[200, true],
[404, true],
[500, true],
[999, false],
[1, false],
])("should return $expected for status $status", (status, expected) => {
expect(isHttpStatus(status)).toBe(expected);
});
});
describe("HttpException", () => {
it("should create an instance with status and message", () => {
const error = new HttpException(HttpStatusCode.BAD_REQUEST, {
message: "Bad Request",
});
expect(error).toBeInstanceOf(Error);
expect(error.status).toBe(HttpStatusCode.BAD_REQUEST);
expect(error.message).toBe("Bad Request");
});
it("should create an instance with code", () => {
const error = new HttpException(HttpStatusCode.BAD_REQUEST, {
code: "INVALID_INPUT",
});
expect(error.code).toBe("INVALID_INPUT");
});
});
describe("getStatusCode", () => {
it.each([
[{ status: 404 }, 404],
[{ message: "error" }, HttpStatusCode.INTERNAL_SERVER_ERROR],
["error", HttpStatusCode.INTERNAL_SERVER_ERROR],
[null, HttpStatusCode.INTERNAL_SERVER_ERROR],
])("should return %s for input %s", (input, expected) => {
expect(getStatusCode(input)).toBe(expected);
});
});

View File

@@ -0,0 +1,79 @@
import { describe, expect, it } from "vitest";
import { createIdGenerator, generateId } from "../id";
describe("generateId", () => {
it("should generate a string of length 32 by default", () => {
const id = generateId();
expect(typeof id).toBe("string");
expect(id.length).toBe(32);
});
it("should generate unique ids", () => {
const id1 = generateId();
const id2 = generateId();
expect(id1).not.toBe(id2);
});
it("should only contain allowed characters (alphanumeric)", () => {
const id = generateId();
expect(id).toMatch(/^[0-9a-zA-Z]+$/);
});
});
describe("createIdGenerator", () => {
it("should allow custom size", () => {
const generateShortId = createIdGenerator({ size: 10 });
const id = generateShortId();
expect(id.length).toBe(10);
});
it("should allow custom alphabet", () => {
const generateBinaryId = createIdGenerator({ alphabet: "01" });
const id = generateBinaryId();
expect(id).toMatch(/^[01]+$/);
});
it("should allow custom alphabet with special characters", () => {
const specialChars = "!@#$%^&*";
const generateSpecialId = createIdGenerator({ alphabet: specialChars });
const id = generateSpecialId();
for (const char of id) {
expect(specialChars).toContain(char);
}
});
it("should allow prefix", () => {
const generatePrefixedId = createIdGenerator({ prefix: "user" });
const id = generatePrefixedId();
expect(id.startsWith("user-")).toBe(true);
expect(id.length).toBe(32 + 5); // 32 random + 4 prefix + 1 separator
});
it("should allow custom separator", () => {
const generateUnderscoreId = createIdGenerator({
prefix: "test",
separator: "_",
});
const id = generateUnderscoreId();
expect(id.startsWith("test_")).toBe(true);
});
it("should throw if separator is in alphabet", () => {
expect(() =>
createIdGenerator({
prefix: "fail",
separator: "a",
alphabet: "abc",
}),
).toThrow('The separator "a" must not be part of the alphabet "abc".');
});
it("should allow empty prefix", () => {
// If prefix is provided as empty string, it works like a prefix
const generateEmptyPrefixedId = createIdGenerator({ prefix: "" });
// prefix="" -> `${""}-${generator()}` -> `-${generator()}`
const id = generateEmptyPrefixedId();
expect(id.startsWith("-")).toBe(true);
});
});

View File

@@ -0,0 +1,93 @@
import { describe, expect, it } from "vitest";
import {
getHost,
getOrigin,
getProtocol,
isExternal,
matchesPattern,
mergeSearchParams,
} from "../url";
describe("isExternal", () => {
it.each([
["https://google.com", true],
["http://example.com", true],
["//cdn.example.com", true],
["mailto:user@example.com", true],
["/dashboard", false],
["about", false],
])("should return $expected for url $url", (url, expected) => {
expect(isExternal(url)).toBe(expected);
});
});
describe("getOrigin", () => {
it.each([
["https://example.com/path", "https://example.com"],
["invalid-url", null],
["exp://192.168.1.1:8081", null],
])("should return %s for %s", (url, expected) => {
expect(getOrigin(url)).toBe(expected);
});
});
describe("getProtocol", () => {
it.each([
["https://example.com", "https:"],
["mailto:user@example.com", "mailto:"],
["not-a-url", null],
])("should return %s for %s", (url, expected) => {
expect(getProtocol(url)).toBe(expected);
});
});
describe("getHost", () => {
it.each([
["https://example.com:8080/path", "example.com:8080"],
["invalid", null],
])("should return %s for %s", (url, expected) => {
expect(getHost(url)).toBe(expected);
});
});
describe("mergeSearchParams", () => {
it("should merge params without overwrite by default", () => {
const target = new URL("https://example.com?a=1");
const source = new URL("https://other.com?a=2&b=3");
mergeSearchParams(target, source);
expect(target.searchParams.get("a")).toBe("1");
expect(target.searchParams.get("b")).toBe("3");
});
it("should overwrite params if specified", () => {
const target = new URL("https://example.com?a=1");
const source = new URL("https://other.com?a=2");
mergeSearchParams(target, source, { overwrite: true });
expect(target.searchParams.get("a")).toBe("2");
});
it("should replace params if specified", () => {
const target = new URL("https://example.com?a=1");
const source = new URL("https://other.com?b=2");
mergeSearchParams(target, source, { replace: true });
expect(target.searchParams.has("a")).toBe(false);
expect(target.searchParams.get("b")).toBe("2");
});
});
describe("matchesPattern", () => {
it.each([
["/path", "https://example.com", false],
["https://sub.example.com", "*.example.com", true],
["https://example.com", "https://example.com", true],
["https://example.org", "https://example.com", false],
["https://example.com/foo", "https://example.com", true],
["https://example.com", "https://*", true],
])(
"should return %s when matching %s with pattern %s",
(url, pattern, expected) => {
expect(matchesPattern(url, pattern)).toBe(expected);
},
);
});

View File

@@ -0,0 +1,86 @@
import { wildcardMatch } from "./wildcard-match";
const isExternal = (url: string) =>
["http", "https", "mailto", "tel", "//", "www"].some((protocol) =>
url.startsWith(protocol),
);
const getOrigin = (url: string) => {
try {
const parsedUrl = new URL(url);
// For custom URL schemes (like exp://), the origin property returns the string "null"
// instead of null. We need to handle this case and return null so the fallback logic works.
return parsedUrl.origin === "null" ? null : parsedUrl.origin;
} catch {
return null;
}
};
const getProtocol = (url: string) => {
try {
const parsedUrl = new URL(url);
return parsedUrl.protocol;
} catch {
return null;
}
};
const getHost = (url: string) => {
try {
const parsedUrl = new URL(url);
return parsedUrl.host;
} catch {
return null;
}
};
const mergeSearchParams = (
target: URL,
source: URL,
options?: { overwrite?: boolean; replace?: boolean },
) => {
const overwrite = options?.overwrite ?? false;
const replace = options?.replace ?? false;
if (replace) {
target.search = source.search;
return;
}
source.searchParams.forEach((value, key) => {
if (overwrite || !target.searchParams.has(key)) {
target.searchParams.set(key, value);
}
});
};
const matchesPattern = (url: string, pattern: string): boolean => {
if (url.startsWith("/")) {
return false;
}
if (pattern.includes("*")) {
// For protocol-specific wildcards, match the full origin
if (pattern.includes("://")) {
return wildcardMatch(pattern)(getOrigin(url) ?? url);
}
const host = getHost(url);
if (!host) {
return false;
}
return wildcardMatch(pattern)(host);
}
const protocol = getProtocol(url);
return protocol === "http:" || protocol === "https:" || !protocol
? pattern === getOrigin(url)
: url.startsWith(pattern);
};
export {
matchesPattern,
getOrigin,
getProtocol,
getHost,
mergeSearchParams,
isExternal,
};

View File

@@ -0,0 +1,246 @@
/* https://github.com/axtgr/wildcard-match */
/**
* Escapes a character if it has a special meaning in regular expressions
* and returns the character as is if it doesn't
*/
function escapeRegExpChar(char: string) {
if (
char === "-" ||
char === "^" ||
char === "$" ||
char === "+" ||
char === "." ||
char === "(" ||
char === ")" ||
char === "|" ||
char === "[" ||
char === "]" ||
char === "{" ||
char === "}" ||
char === "*" ||
char === "?" ||
char === "\\"
) {
return `\\${char}`;
} else {
return char;
}
}
/**
* Escapes all characters in a given string that have a special meaning in regular expressions
*/
function escapeRegExpString(str: string) {
let result = "";
for (const char of str) {
result += escapeRegExpChar(char);
}
return result;
}
/**
* Transforms one or more glob patterns into a RegExp pattern
*/
function transform(
pattern: string | string[],
separator: string | boolean = true,
): string {
if (Array.isArray(pattern)) {
const regExpPatterns = pattern.map((p) => `^${transform(p, separator)}$`);
return `(?:${regExpPatterns.join("|")})`;
}
let separatorSplitter = "";
let separatorMatcher = "";
let wildcard = ".";
if (separator === true) {
// In this case forward slashes in patterns match both forward and backslashes in samples:
//
// `foo/bar` will match `foo/bar`
// will match `foo\bar`
//
separatorSplitter = "/";
separatorMatcher = "[/\\\\]";
wildcard = "[^/\\\\]";
} else if (separator) {
separatorSplitter = separator;
separatorMatcher = escapeRegExpString(separatorSplitter);
if (separatorMatcher.length > 1) {
separatorMatcher = `(?:${separatorMatcher})`;
wildcard = `((?!${separatorMatcher}).)`;
} else {
wildcard = `[^${separatorMatcher}]`;
}
}
// When a separator is explicitly specified in a pattern,
// it MUST match ONE OR MORE separators in a sample:
//
// `foo/bar/` will match `foo//bar///`
// won't match `foo/bar`
//
// When a pattern doesn't have a trailing separator,
// a sample can still optionally have them:
//
// `foo/bar` will match `foo/bar//`
//
// So we use different quantifiers depending on the index of a segment.
const requiredSeparator = separator ? `${separatorMatcher}+?` : "";
const optionalSeparator = separator ? `${separatorMatcher}*?` : "";
const segments = separator ? pattern.split(separatorSplitter) : [pattern];
let result = "";
for (let s = 0; s < segments.length; s++) {
const segment = segments[s]!;
const nextSegment = segments[s + 1]!;
let currentSeparator = "";
if (!segment && s > 0) {
continue;
}
if (separator) {
if (s === segments.length - 1) {
currentSeparator = optionalSeparator;
} else if (nextSegment !== "**") {
currentSeparator = requiredSeparator;
} else {
currentSeparator = "";
}
}
if (separator && segment === "**") {
if (currentSeparator) {
result += s === 0 ? "" : currentSeparator;
result += `(?:${wildcard}*?${currentSeparator})*?`;
}
continue;
}
for (let c = 0; c < segment.length; c++) {
const char = segment[c]!;
if (char === "\\") {
if (c < segment.length - 1) {
result += escapeRegExpChar(segment[c + 1]!);
c++;
}
} else if (char === "?") {
result += wildcard;
} else if (char === "*") {
result += `${wildcard}*?`;
} else {
result += escapeRegExpChar(char);
}
}
result += currentSeparator;
}
return result;
}
export default transform;
interface WildcardMatchOptions {
/** Separator to be used to split patterns and samples into segments */
separator?: string | boolean;
/** Flags to pass to the RegExp */
flags?: string;
}
// This overrides the function's signature because for the end user
// the function is always bound to a RegExp
interface isMatch {
/**
* Tests if a sample string matches the pattern(s)
*
* ```js
* isMatch('foo') //=> true
* ```
*/
(sample: string): boolean;
/** Compiled regular expression */
regexp: RegExp;
/** Original pattern or array of patterns that was used to compile the RegExp */
pattern: string | string[];
/** Options that were used to compile the RegExp */
options: WildcardMatchOptions;
}
function isMatch(regexp: RegExp, sample: string) {
if (typeof sample !== "string") {
throw new TypeError(`Sample must be a string, but ${typeof sample} given`);
}
return regexp.test(sample);
}
/**
* Compiles one or more glob patterns into a RegExp and returns an isMatch function.
* The isMatch function takes a sample string as its only argument and returns `true`
* if the string matches the pattern(s).
*
* ```js
* wildcardMatch('src/*.js')('src/index.js') //=> true
* ```
*
* ```js
* const isMatch = wildcardMatch('*.example.com', '.')
* isMatch('foo.example.com') //=> true
* isMatch('foo.bar.com') //=> false
* ```
*/
function wildcardMatch(
pattern: string | string[],
options?: string | boolean | WildcardMatchOptions,
) {
if (typeof pattern !== "string" && !Array.isArray(pattern)) {
throw new TypeError(
`The first argument must be a single pattern string or an array of patterns, but ${typeof pattern} given`,
);
}
if (typeof options === "string" || typeof options === "boolean") {
options = { separator: options };
}
if (
arguments.length === 2 &&
!(
typeof options === "undefined" ||
(typeof options === "object" && !Array.isArray(options))
)
) {
throw new TypeError(
`The second argument must be an options object or a string/boolean separator, but ${typeof options} given`,
);
}
options = options ?? {};
if (options.separator === "\\") {
throw new Error(
"\\ is not a valid separator because it is used for escaping. Try setting the separator to `true` instead",
);
}
const regexpPattern = transform(pattern, options.separator);
const regexp = new RegExp(`^${regexpPattern}$`, options.flags);
const fn = isMatch.bind(null, regexp) as isMatch;
fn.options = options;
fn.pattern = pattern;
fn.regexp = regexp;
return fn;
}
export { wildcardMatch, isMatch };