feat: add web testing foundation with infra-backed suites (#349)
* feat: add web test framework with infra-backed suites * fix: honor DATABASE_URL env in integration prepare script * fix: apply web test review feedback * fix: streamline web test infra lifecycle and workflow scope
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
name: Web Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "apps/web/**"
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "apps/web/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
web-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
NODE_ENV: test
|
||||||
|
NEXTAUTH_URL: http://localhost:3000
|
||||||
|
NEXTAUTH_SECRET: test-secret
|
||||||
|
DATABASE_URL: postgresql://usesend:password@127.0.0.1:5432/usesend_test
|
||||||
|
REDIS_URL: redis://127.0.0.1:6379/15
|
||||||
|
AWS_ACCESS_KEY: test-access-key
|
||||||
|
AWS_SECRET_KEY: test-secret-key
|
||||||
|
AWS_DEFAULT_REGION: us-east-1
|
||||||
|
NEXT_PUBLIC_IS_CLOUD: "true"
|
||||||
|
API_RATE_LIMIT: "2"
|
||||||
|
AUTH_EMAIL_RATE_LIMIT: "5"
|
||||||
|
RUN_INTEGRATION: "true"
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: usesend
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
POSTGRES_DB: usesend_test
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd "pg_isready -U usesend -d usesend_test"
|
||||||
|
--health-interval 5s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 20
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
options: >-
|
||||||
|
--health-cmd "redis-cli ping"
|
||||||
|
--health-interval 5s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 20
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 8.9.0
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Generate Prisma client
|
||||||
|
run: pnpm --filter=web db:generate
|
||||||
|
|
||||||
|
- name: Prepare test database schema
|
||||||
|
run: pnpm --filter=web test:integration:prepare
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: pnpm --filter=web test:unit
|
||||||
|
|
||||||
|
- name: Run tRPC tests
|
||||||
|
run: pnpm --filter=web test:trpc
|
||||||
|
|
||||||
|
- name: Run API tests
|
||||||
|
run: pnpm --filter=web test:api
|
||||||
|
|
||||||
|
- name: Run integration tests
|
||||||
|
run: pnpm --filter=web test:integration
|
||||||
@@ -36,7 +36,13 @@
|
|||||||
|
|
||||||
## Testing Guidelines
|
## Testing Guidelines
|
||||||
|
|
||||||
- No repo-wide test runner is configured yet. do not add any tests unless required
|
- Web testing is configured with Vitest in `apps/web`; add tests when changes impact logic, APIs, or behavior.
|
||||||
|
- Prefer targeted suites first: `pnpm test:web:unit`, `pnpm test:web:trpc`, `pnpm test:web:api`; use `pnpm test:web` for default non-integration coverage.
|
||||||
|
- Test file conventions: `*.unit.test.ts`, `*.trpc.test.ts`, `*.api.test.ts`, `*.integration.test.ts`.
|
||||||
|
- Integration tests require infra and env (`RUN_INTEGRATION=true` with Postgres/Redis available). Root commands `pnpm test:web:all` and `pnpm test:web:integration:full` auto-manage infra lifecycle.
|
||||||
|
- Use `pnpm test:infra:up` / `pnpm test:infra:down` when running targeted integration commands manually.
|
||||||
|
- `pnpm test:web:integration:full` and `test:integration:prepare` run Prisma migrations (`prisma migrate deploy`); never run these unless the user explicitly asks.
|
||||||
|
- Test defaults are cloud mode (`NEXT_PUBLIC_IS_CLOUD=true`); keep new tests compatible with cloud behavior unless the task says otherwise.
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
NODE_ENV=test
|
||||||
|
NEXTAUTH_URL=http://localhost:3000
|
||||||
|
NEXTAUTH_SECRET=test-secret
|
||||||
|
|
||||||
|
DATABASE_URL=postgresql://usesend:password@127.0.0.1:54329/usesend_test
|
||||||
|
REDIS_URL=redis://127.0.0.1:6380/15
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY=test-access-key
|
||||||
|
AWS_SECRET_KEY=test-secret-key
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
|
||||||
|
NEXT_PUBLIC_IS_CLOUD=true
|
||||||
|
API_RATE_LIMIT=2
|
||||||
|
AUTH_EMAIL_RATE_LIMIT=5
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Testing in `apps/web`
|
||||||
|
|
||||||
|
This app now supports four testing layers:
|
||||||
|
|
||||||
|
- Unit tests (`*.unit.test.ts`)
|
||||||
|
- tRPC tests (`*.trpc.test.ts`)
|
||||||
|
- API tests (`*.api.test.ts`)
|
||||||
|
- Infra-backed integration tests (`*.integration.test.ts`)
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- Runner: Vitest
|
||||||
|
- Coverage: V8 provider via `@vitest/coverage-v8`
|
||||||
|
- Path aliases: `vite-tsconfig-paths`
|
||||||
|
- Infra for integration: PostgreSQL + Redis via Docker Compose
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
From repo root:
|
||||||
|
|
||||||
|
- `pnpm test:web`
|
||||||
|
- `pnpm test:web:all`
|
||||||
|
- `pnpm test:web:unit`
|
||||||
|
- `pnpm test:web:trpc`
|
||||||
|
- `pnpm test:web:api`
|
||||||
|
- `pnpm test:web:integration`
|
||||||
|
- `pnpm test:web:integration:full`
|
||||||
|
|
||||||
|
Infra helpers:
|
||||||
|
|
||||||
|
- `pnpm test:infra:up`
|
||||||
|
- `pnpm test:infra:down`
|
||||||
|
|
||||||
|
Full integration flow:
|
||||||
|
|
||||||
|
1. `pnpm test:infra:up`
|
||||||
|
2. `pnpm test:web:integration:full` (or `pnpm test:web:all`)
|
||||||
|
3. `pnpm test:infra:down`
|
||||||
|
|
||||||
|
## Infra configuration
|
||||||
|
|
||||||
|
- Compose file: `docker/testing/compose.yml`
|
||||||
|
- Postgres: `127.0.0.1:54329` (`usesend_test`)
|
||||||
|
- Redis: `127.0.0.1:6380` (test DB index `15`)
|
||||||
|
|
||||||
|
The default test env is bootstrapped in `src/test/setup/setup-env.ts`.
|
||||||
|
Override values by exporting env vars before running tests.
|
||||||
|
|
||||||
|
## Test layout
|
||||||
|
|
||||||
|
- `src/test/setup/*`: global test bootstrap
|
||||||
|
- `src/test/integration/*`: integration reset helpers
|
||||||
|
- Tests colocated next to modules under `src/**`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Integration suites only run when `RUN_INTEGRATION=true`.
|
||||||
|
- Integration helpers truncate all public Postgres tables (except `_prisma_migrations`) and flush Redis DB before each test.
|
||||||
|
- Queue and Redis tests rely on `REDIS_URL` test DB index to avoid polluting local dev state.
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
GitHub Actions workflow: `.github/workflows/test-web.yml`
|
||||||
|
|
||||||
|
The workflow runs unit, tRPC, API, and integration tests with PostgreSQL and Redis services.
|
||||||
+15
-1
@@ -8,6 +8,17 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint . --max-warnings 0",
|
"lint": "eslint . --max-warnings 0",
|
||||||
|
"test": "vitest run -c vitest.default.config.ts",
|
||||||
|
"test:watch": "vitest -c vitest.default.config.ts",
|
||||||
|
"test:all": "pnpm test:unit && pnpm test:trpc && pnpm test:api && pnpm test:integration:full",
|
||||||
|
"test:unit": "vitest run -c vitest.unit.config.ts",
|
||||||
|
"test:trpc": "vitest run -c vitest.trpc.config.ts",
|
||||||
|
"test:api": "vitest run -c vitest.api.config.ts",
|
||||||
|
"test:integration:prepare": "prisma migrate deploy",
|
||||||
|
"test:integration:prepare:local": "DATABASE_URL=postgresql://usesend:password@127.0.0.1:54329/usesend_test pnpm test:integration:prepare",
|
||||||
|
"test:integration": "RUN_INTEGRATION=true vitest run -c vitest.integration.config.ts",
|
||||||
|
"test:integration:full": "pnpm test:integration:prepare:local && pnpm test:integration",
|
||||||
|
"test:coverage": "vitest run -c vitest.default.config.ts --coverage",
|
||||||
"db:post-install": "prisma generate",
|
"db:post-install": "prisma generate",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:push": "prisma db push --skip-generate",
|
"db:push": "prisma db push --skip-generate",
|
||||||
@@ -97,7 +108,10 @@
|
|||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"vitest": "^3.2.4",
|
||||||
|
"@vitest/coverage-v8": "^3.2.4"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"react-is": "^19.0.0-rc-69d4b800-20241021"
|
"react-is": "^19.0.0-rc-69d4b800-20241021"
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { GET } from "~/app/api/health/route";
|
||||||
|
|
||||||
|
describe("health route", () => {
|
||||||
|
it("returns healthy response", async () => {
|
||||||
|
const response = await GET();
|
||||||
|
const body = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(body).toEqual({ data: "Healthy" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const { state } = vi.hoisted(() => ({
|
||||||
|
state: {
|
||||||
|
signature: null as string | null,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("next/headers", () => ({
|
||||||
|
headers: vi.fn(async () => {
|
||||||
|
const headers = new Headers();
|
||||||
|
if (state.signature) {
|
||||||
|
headers.set("Stripe-Signature", state.signature);
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("~/server/billing/payments", () => ({
|
||||||
|
getStripe: vi.fn(() => ({
|
||||||
|
webhooks: {
|
||||||
|
constructEvent: vi.fn(),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
syncStripeData: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { POST } from "~/app/api/webhook/stripe/route";
|
||||||
|
|
||||||
|
describe("stripe webhook route", () => {
|
||||||
|
it("returns 400 when signature header is missing", async () => {
|
||||||
|
state.signature = null;
|
||||||
|
|
||||||
|
const response = await POST(
|
||||||
|
new Request("http://localhost", { method: "POST" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
await expect(response.text()).resolves.toBe("No signature");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 400 when webhook secret is not configured", async () => {
|
||||||
|
state.signature = "test-signature";
|
||||||
|
|
||||||
|
const response = await POST(
|
||||||
|
new Request("http://localhost", {
|
||||||
|
method: "POST",
|
||||||
|
body: "{}",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
await expect(response.text()).resolves.toBe("No webhook secret");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -54,9 +54,9 @@ export function getUsageDate(): string {
|
|||||||
* @param transactionUsage Number of transaction emails sent
|
* @param transactionUsage Number of transaction emails sent
|
||||||
* @returns Total usage units rounded down to nearest integer
|
* @returns Total usage units rounded down to nearest integer
|
||||||
*/
|
*/
|
||||||
export function getUsageUinits(
|
export function getUsageUnits(
|
||||||
marketingUsage: number,
|
marketingUsage: number,
|
||||||
transactionUsage: number
|
transactionUsage: number,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
marketingUsage +
|
marketingUsage +
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { EmailUsageType } from "@prisma/client";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
getCost,
|
||||||
|
getUsageDate,
|
||||||
|
getUsageTimestamp,
|
||||||
|
getUsageUnits,
|
||||||
|
TRANSACTIONAL_UNIT_CONVERSION,
|
||||||
|
} from "~/lib/usage";
|
||||||
|
|
||||||
|
describe("usage helpers", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns yesterday date and timestamp", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-02-08T12:00:00.000Z"));
|
||||||
|
|
||||||
|
expect(getUsageDate()).toBe("2026-02-07");
|
||||||
|
expect(getUsageTimestamp()).toBe(
|
||||||
|
Math.floor(new Date("2026-02-07T12:00:00.000Z").getTime() / 1000),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts transactional usage into billing units", () => {
|
||||||
|
const units = getUsageUnits(100, 40);
|
||||||
|
expect(units).toBe(100 + Math.floor(40 / TRANSACTIONAL_UNIT_CONVERSION));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calculates cost per email type", () => {
|
||||||
|
expect(getCost(10, EmailUsageType.MARKETING)).toBe(0.01);
|
||||||
|
expect(getCost(4, EmailUsageType.TRANSACTIONAL)).toBe(0.001);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("~/server/auth", () => ({
|
||||||
|
getServerAuthSession: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
createCallerFactory,
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
teamProcedure,
|
||||||
|
} from "~/server/api/trpc";
|
||||||
|
import { Role } from "@prisma/client";
|
||||||
|
import {
|
||||||
|
closeIntegrationConnections,
|
||||||
|
integrationEnabled,
|
||||||
|
resetDatabase,
|
||||||
|
resetRedis,
|
||||||
|
} from "~/test/integration/helpers";
|
||||||
|
import { createTeamWithUser, createUser } from "~/test/factories/core";
|
||||||
|
|
||||||
|
const describeIntegration = integrationEnabled ? describe : describe.skip;
|
||||||
|
|
||||||
|
const testRouter = createTRPCRouter({
|
||||||
|
protectedPing: protectedProcedure.query(({ ctx }) => ({
|
||||||
|
userId: ctx.session.user.id,
|
||||||
|
})),
|
||||||
|
teamPing: teamProcedure.query(({ ctx }) => ({
|
||||||
|
teamId: ctx.team.id,
|
||||||
|
role: ctx.teamUser.role,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createCaller = createCallerFactory(testRouter);
|
||||||
|
|
||||||
|
function createContext(user: {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
isWaitlisted: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
|
isBetaUser: boolean;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
headers: new Headers(),
|
||||||
|
session: {
|
||||||
|
user,
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describeIntegration("tRPC integration", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await resetDatabase();
|
||||||
|
await resetRedis();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await closeIntegrationConnections();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runs protected procedure with persisted user context", async () => {
|
||||||
|
const user = await createUser({
|
||||||
|
email: "protected@example.com",
|
||||||
|
isBetaUser: true,
|
||||||
|
isWaitlisted: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const caller = createCaller(
|
||||||
|
createContext({
|
||||||
|
id: user.id,
|
||||||
|
email: user.email as string,
|
||||||
|
isWaitlisted: false,
|
||||||
|
isAdmin: false,
|
||||||
|
isBetaUser: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(caller.protectedPing()).resolves.toEqual({ userId: user.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves team procedure from postgres team membership", async () => {
|
||||||
|
const { user, team } = await createTeamWithUser(Role.ADMIN);
|
||||||
|
|
||||||
|
const caller = createCaller(
|
||||||
|
createContext({
|
||||||
|
id: user.id,
|
||||||
|
email: user.email as string,
|
||||||
|
isWaitlisted: false,
|
||||||
|
isAdmin: false,
|
||||||
|
isBetaUser: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(caller.teamPing()).resolves.toEqual({
|
||||||
|
teamId: team.id,
|
||||||
|
role: "ADMIN",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails team procedure when user has no team", async () => {
|
||||||
|
const user = await createUser({
|
||||||
|
email: "no-team@example.com",
|
||||||
|
isBetaUser: true,
|
||||||
|
isWaitlisted: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const caller = createCaller(
|
||||||
|
createContext({
|
||||||
|
id: user.id,
|
||||||
|
email: user.email as string,
|
||||||
|
isWaitlisted: false,
|
||||||
|
isAdmin: false,
|
||||||
|
isBetaUser: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const teamPingPromise = caller.teamPing();
|
||||||
|
|
||||||
|
await expect(teamPingPromise).rejects.toBeInstanceOf(TRPCError);
|
||||||
|
await expect(teamPingPromise).rejects.toMatchObject({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const { mockDb } = vi.hoisted(() => ({
|
||||||
|
mockDb: {
|
||||||
|
teamUser: {
|
||||||
|
findFirst: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("~/server/db", () => ({
|
||||||
|
db: mockDb,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("~/server/auth", () => ({
|
||||||
|
getServerAuthSession: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
authedProcedure,
|
||||||
|
createCallerFactory,
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
teamAdminProcedure,
|
||||||
|
teamProcedure,
|
||||||
|
} from "~/server/api/trpc";
|
||||||
|
|
||||||
|
const testRouter = createTRPCRouter({
|
||||||
|
authedPing: authedProcedure.query(({ ctx }) => ({
|
||||||
|
userId: ctx.session.user.id,
|
||||||
|
})),
|
||||||
|
protectedPing: protectedProcedure.query(({ ctx }) => ({
|
||||||
|
userId: ctx.session.user.id,
|
||||||
|
})),
|
||||||
|
teamPing: teamProcedure.query(({ ctx }) => ({ teamId: ctx.team.id })),
|
||||||
|
teamAdminPing: teamAdminProcedure.query(({ ctx }) => ({
|
||||||
|
role: ctx.teamUser.role,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createCaller = createCallerFactory(testRouter);
|
||||||
|
|
||||||
|
function getContext(session: Record<string, unknown> | null) {
|
||||||
|
return {
|
||||||
|
db: mockDb,
|
||||||
|
session,
|
||||||
|
headers: new Headers(),
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUser = {
|
||||||
|
id: 1,
|
||||||
|
isBetaUser: true,
|
||||||
|
isAdmin: false,
|
||||||
|
isWaitlisted: false,
|
||||||
|
email: "user@example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("tRPC middleware procedures", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockDb.teamUser.findFirst.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks authed procedure without session", async () => {
|
||||||
|
const caller = createCaller(getContext(null));
|
||||||
|
await expect(caller.authedPing()).rejects.toBeInstanceOf(TRPCError);
|
||||||
|
await expect(caller.authedPing()).rejects.toMatchObject({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks protected procedure for waitlisted users", async () => {
|
||||||
|
const caller = createCaller(
|
||||||
|
getContext({
|
||||||
|
user: { ...baseUser, isWaitlisted: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(caller.protectedPing()).rejects.toMatchObject({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads team context for team procedure", async () => {
|
||||||
|
mockDb.teamUser.findFirst.mockResolvedValue({
|
||||||
|
teamId: 10,
|
||||||
|
userId: 1,
|
||||||
|
role: "ADMIN",
|
||||||
|
team: { id: 10, name: "Acme" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const caller = createCaller(
|
||||||
|
getContext({
|
||||||
|
user: baseUser,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(caller.teamPing()).resolves.toEqual({ teamId: 10 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks team admin procedure for non-admin team users", async () => {
|
||||||
|
mockDb.teamUser.findFirst.mockResolvedValue({
|
||||||
|
teamId: 10,
|
||||||
|
userId: 1,
|
||||||
|
role: "MEMBER",
|
||||||
|
team: { id: 10, name: "Acme" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const caller = createCaller(
|
||||||
|
getContext({
|
||||||
|
user: baseUser,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(caller.teamAdminPing()).rejects.toMatchObject({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails team procedure when user has no team", async () => {
|
||||||
|
mockDb.teamUser.findFirst.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const caller = createCaller(
|
||||||
|
getContext({
|
||||||
|
user: baseUser,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(caller.teamPing()).rejects.toMatchObject({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Queue, Worker } from "bullmq";
|
import { Queue, Worker } from "bullmq";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import { getUsageDate, getUsageUinits } from "~/lib/usage";
|
import { getUsageDate, getUsageUnits } from "~/lib/usage";
|
||||||
import { sendUsageToStripe } from "~/server/billing/usage";
|
import { sendUsageToStripe } from "~/server/billing/usage";
|
||||||
import { getRedis } from "~/server/redis";
|
import { getRedis } from "~/server/redis";
|
||||||
import { DEFAULT_QUEUE_OPTIONS } from "../queue/queue-constants";
|
import { DEFAULT_QUEUE_OPTIONS } from "../queue/queue-constants";
|
||||||
@@ -47,13 +47,13 @@ const worker = new Worker(
|
|||||||
.filter((usage) => usage.type === "MARKETING")
|
.filter((usage) => usage.type === "MARKETING")
|
||||||
.reduce((sum, usage) => sum + usage.sent, 0);
|
.reduce((sum, usage) => sum + usage.sent, 0);
|
||||||
|
|
||||||
const totalUsage = getUsageUinits(marketingUsage, transactionUsage);
|
const totalUsage = getUsageUnits(marketingUsage, transactionUsage);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendUsageToStripe(team.stripeCustomerId, totalUsage);
|
await sendUsageToStripe(team.stripeCustomerId, totalUsage);
|
||||||
logger.info(
|
logger.info(
|
||||||
{ teamId: team.id, date: getUsageDate(), usage: totalUsage },
|
{ teamId: team.id, date: getUsageDate(), usage: totalUsage },
|
||||||
`[Usage Reporting] Reported usage for team`
|
`[Usage Reporting] Reported usage for team`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -62,14 +62,14 @@ const worker = new Worker(
|
|||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
message: error instanceof Error ? error.message : error,
|
message: error instanceof Error ? error.message : error,
|
||||||
},
|
},
|
||||||
`[Usage Reporting] Failed to report usage for team`
|
`[Usage Reporting] Failed to report usage for team`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
connection: getRedis(),
|
connection: getRedis(),
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Schedule job to run daily
|
// Schedule job to run daily
|
||||||
@@ -83,7 +83,7 @@ await usageQueue.upsertJobScheduler(
|
|||||||
opts: {
|
opts: {
|
||||||
...DEFAULT_QUEUE_OPTIONS,
|
...DEFAULT_QUEUE_OPTIONS,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on("completed", (job) => {
|
worker.on("completed", (job) => {
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { UnsendApiError } from "~/server/public-api/api-error";
|
||||||
|
|
||||||
|
const { mockGetTeamFromToken, mockRedis } = vi.hoisted(() => ({
|
||||||
|
mockGetTeamFromToken: vi.fn(),
|
||||||
|
mockRedis: {
|
||||||
|
incr: vi.fn(),
|
||||||
|
expire: vi.fn(),
|
||||||
|
ttl: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("~/server/public-api/auth", () => ({
|
||||||
|
getTeamFromToken: mockGetTeamFromToken,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("~/server/redis", () => ({
|
||||||
|
getRedis: () => mockRedis,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("~/utils/common", () => ({
|
||||||
|
isSelfHosted: () => false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { getApp } from "~/server/public-api/hono";
|
||||||
|
|
||||||
|
describe("public API Hono middleware", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGetTeamFromToken.mockReset();
|
||||||
|
mockRedis.incr.mockReset();
|
||||||
|
mockRedis.expire.mockReset();
|
||||||
|
mockRedis.ttl.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies auth and rate limit headers", async () => {
|
||||||
|
mockGetTeamFromToken.mockResolvedValue({
|
||||||
|
id: 1,
|
||||||
|
apiRateLimit: 2,
|
||||||
|
apiKeyId: 11,
|
||||||
|
apiKey: { domainId: null },
|
||||||
|
});
|
||||||
|
mockRedis.incr.mockResolvedValue(1);
|
||||||
|
mockRedis.expire.mockResolvedValue(1);
|
||||||
|
mockRedis.ttl.mockResolvedValue(1);
|
||||||
|
|
||||||
|
const app = getApp();
|
||||||
|
app.get("/v1/ping", (c) => c.json({ ok: true }));
|
||||||
|
|
||||||
|
const response = await app.request("http://localhost/api/v1/ping", {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test-key",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("X-RateLimit-Limit")).toBe("2");
|
||||||
|
expect(response.headers.get("X-RateLimit-Remaining")).toBe("1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 429 when limit is exceeded", async () => {
|
||||||
|
mockGetTeamFromToken.mockResolvedValue({
|
||||||
|
id: 1,
|
||||||
|
apiRateLimit: 2,
|
||||||
|
apiKeyId: 11,
|
||||||
|
apiKey: { domainId: null },
|
||||||
|
});
|
||||||
|
mockRedis.incr.mockResolvedValue(3);
|
||||||
|
mockRedis.ttl.mockResolvedValue(1);
|
||||||
|
|
||||||
|
const app = getApp();
|
||||||
|
app.get("/v1/ping", (c) => c.json({ ok: true }));
|
||||||
|
|
||||||
|
const response = await app.request("http://localhost/api/v1/ping", {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test-key",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(429);
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
error: {
|
||||||
|
code: "RATE_LIMITED",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns auth error from middleware", async () => {
|
||||||
|
mockGetTeamFromToken.mockRejectedValue(
|
||||||
|
new UnsendApiError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "No Authorization header provided",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const app = getApp();
|
||||||
|
app.get("/v1/ping", (c) => c.json({ ok: true }));
|
||||||
|
|
||||||
|
const response = await app.request("http://localhost/api/v1/ping");
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
error: {
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { ApiPermission } from "@prisma/client";
|
||||||
|
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { getApp } from "~/server/public-api/hono";
|
||||||
|
import { addApiKey } from "~/server/service/api-service";
|
||||||
|
import { createTeam } from "~/test/factories/core";
|
||||||
|
import {
|
||||||
|
closeIntegrationConnections,
|
||||||
|
integrationEnabled,
|
||||||
|
resetDatabase,
|
||||||
|
resetRedis,
|
||||||
|
} from "~/test/integration/helpers";
|
||||||
|
|
||||||
|
const describeIntegration = integrationEnabled ? describe : describe.skip;
|
||||||
|
|
||||||
|
describeIntegration("Hono public API integration", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await resetDatabase();
|
||||||
|
await resetRedis();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await closeIntegrationConnections();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("authenticates request with persisted API key", async () => {
|
||||||
|
const team = await createTeam({
|
||||||
|
name: "Auth Team",
|
||||||
|
apiRateLimit: 2,
|
||||||
|
});
|
||||||
|
const apiKey = await addApiKey({
|
||||||
|
name: "integration-key",
|
||||||
|
permission: ApiPermission.FULL,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = getApp();
|
||||||
|
app.get("/v1/ping", (c) => c.json({ teamId: c.var.team.id }));
|
||||||
|
|
||||||
|
const response = await app.request("http://localhost/api/v1/ping", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
await expect(response.json()).resolves.toEqual({ teamId: team.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns forbidden when API key is invalid", async () => {
|
||||||
|
const app = getApp();
|
||||||
|
app.get("/v1/ping", (c) => c.json({ ok: true }));
|
||||||
|
|
||||||
|
const response = await app.request("http://localhost/api/v1/ping", {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer us_bad_token",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
await expect(response.json()).resolves.toMatchObject({
|
||||||
|
error: {
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enforces Redis rate limits when cloud mode is enabled", async () => {
|
||||||
|
const team = await createTeam({
|
||||||
|
name: "Rate Team",
|
||||||
|
apiRateLimit: 1,
|
||||||
|
});
|
||||||
|
const apiKey = await addApiKey({
|
||||||
|
name: "rate-key",
|
||||||
|
permission: ApiPermission.FULL,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = getApp();
|
||||||
|
app.get("/v1/ping", (c) => c.json({ ok: true }));
|
||||||
|
|
||||||
|
const first = await app.request("http://localhost/api/v1/ping", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const second = await app.request("http://localhost/api/v1/ping", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(first.status).toBe(200);
|
||||||
|
expect(second.status).toBe(429);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,7 @@ import { env } from "~/env";
|
|||||||
export let connection: IORedis | null = null;
|
export let connection: IORedis | null = null;
|
||||||
|
|
||||||
export const getRedis = () => {
|
export const getRedis = () => {
|
||||||
if (!connection) {
|
if (!connection || connection.status === "end") {
|
||||||
connection = new IORedis(`${env.REDIS_URL}?family=0`, {
|
connection = new IORedis(`${env.REDIS_URL}?family=0`, {
|
||||||
maxRetriesPerRequest: null,
|
maxRetriesPerRequest: null,
|
||||||
});
|
});
|
||||||
@@ -19,7 +19,7 @@ export const getRedis = () => {
|
|||||||
export async function withCache<T>(
|
export async function withCache<T>(
|
||||||
key: string,
|
key: string,
|
||||||
fetcher: () => Promise<T>,
|
fetcher: () => Promise<T>,
|
||||||
options?: { ttlSeconds?: number; disable?: boolean }
|
options?: { ttlSeconds?: number; disable?: boolean },
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const { ttlSeconds = 120, disable = false } = options ?? {};
|
const { ttlSeconds = 120, disable = false } = options ?? {};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { ApiPermission } from "@prisma/client";
|
||||||
|
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { addApiKey, getTeamAndApiKey } from "~/server/service/api-service";
|
||||||
|
import { createTeam } from "~/test/factories/core";
|
||||||
|
import {
|
||||||
|
closeIntegrationConnections,
|
||||||
|
integrationEnabled,
|
||||||
|
resetDatabase,
|
||||||
|
resetRedis,
|
||||||
|
} from "~/test/integration/helpers";
|
||||||
|
|
||||||
|
const describeIntegration = integrationEnabled ? describe : describe.skip;
|
||||||
|
|
||||||
|
describeIntegration("api-service integration", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await resetDatabase();
|
||||||
|
await resetRedis();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await closeIntegrationConnections();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates and verifies API key against postgres", async () => {
|
||||||
|
const team = await createTeam({ name: "Integration Team" });
|
||||||
|
|
||||||
|
const apiKey = await addApiKey({
|
||||||
|
name: "primary",
|
||||||
|
permission: ApiPermission.FULL,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(apiKey.startsWith("us_")).toBe(true);
|
||||||
|
|
||||||
|
const result = await getTeamAndApiKey(apiKey);
|
||||||
|
|
||||||
|
expect(result?.team?.id).toBe(team.id);
|
||||||
|
expect(result?.apiKey.name).toBe("primary");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects domain-restricted key when domain does not belong to team", async () => {
|
||||||
|
const team = await createTeam({ name: "Team Domain Check" });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
addApiKey({
|
||||||
|
name: "restricted",
|
||||||
|
permission: ApiPermission.SENDING,
|
||||||
|
teamId: team.id,
|
||||||
|
domainId: 999999,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("DOMAIN_NOT_FOUND");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
IDEMPOTENCY_CONSTANTS,
|
||||||
|
IdempotencyService,
|
||||||
|
} from "~/server/service/idempotency-service";
|
||||||
|
import {
|
||||||
|
closeIntegrationConnections,
|
||||||
|
integrationEnabled,
|
||||||
|
resetRedis,
|
||||||
|
} from "~/test/integration/helpers";
|
||||||
|
|
||||||
|
const describeIntegration = integrationEnabled ? describe : describe.skip;
|
||||||
|
|
||||||
|
describeIntegration("idempotency redis integration", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await resetRedis();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await closeIntegrationConnections();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores and retrieves idempotency result", async () => {
|
||||||
|
const teamId = 1;
|
||||||
|
const key = "idem-1";
|
||||||
|
|
||||||
|
await IdempotencyService.setResult(teamId, key, {
|
||||||
|
bodyHash: "hash-123",
|
||||||
|
emailIds: ["em_1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(IdempotencyService.getResult(teamId, key)).resolves.toEqual({
|
||||||
|
bodyHash: "hash-123",
|
||||||
|
emailIds: ["em_1"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("acquires lock only once for same key", async () => {
|
||||||
|
const teamId = 99;
|
||||||
|
const key = "lock-test";
|
||||||
|
|
||||||
|
const first = await IdempotencyService.acquireLock(teamId, key);
|
||||||
|
const second = await IdempotencyService.acquireLock(teamId, key);
|
||||||
|
|
||||||
|
expect(first).toBe(true);
|
||||||
|
expect(second).toBe(false);
|
||||||
|
|
||||||
|
await IdempotencyService.releaseLock(teamId, key);
|
||||||
|
await expect(IdempotencyService.acquireLock(teamId, key)).resolves.toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns cached response for repeated payload", async () => {
|
||||||
|
const operation = vi.fn(async () => ({ id: "first", emailIds: ["em_1"] }));
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
teamId: 25,
|
||||||
|
idemKey: "request-1",
|
||||||
|
payload: { to: "a@b.com", subject: "hello" },
|
||||||
|
operation,
|
||||||
|
extractEmailIds: (result: { emailIds: string[] }) => result.emailIds,
|
||||||
|
formatCachedResponse: (emailIds: string[]) => ({
|
||||||
|
id: "cached",
|
||||||
|
emailIds,
|
||||||
|
}),
|
||||||
|
logContext: "integration-test",
|
||||||
|
};
|
||||||
|
|
||||||
|
const first = await IdempotencyService.withIdempotency(options);
|
||||||
|
const second = await IdempotencyService.withIdempotency(options);
|
||||||
|
|
||||||
|
expect(first).toEqual({ id: "first", emailIds: ["em_1"] });
|
||||||
|
expect(second).toEqual({ id: "cached", emailIds: ["em_1"] });
|
||||||
|
expect(operation).toHaveBeenCalledTimes(1);
|
||||||
|
expect(IDEMPOTENCY_CONSTANTS.RESULT_TTL_SECONDS).toBe(24 * 60 * 60);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { escapeHtml, toPlainHtml } from "~/server/utils/email-content";
|
||||||
|
|
||||||
|
describe("email-content utils", () => {
|
||||||
|
it("escapes unsafe HTML characters", () => {
|
||||||
|
const value = `<script>alert('x') & \"y\"</script>`;
|
||||||
|
expect(escapeHtml(value)).toBe(
|
||||||
|
"<script>alert('x') & "y"</script>",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wraps plain text into preformatted safe html", () => {
|
||||||
|
const result = toPlainHtml("Line 1\nLine <2>");
|
||||||
|
expect(result).toContain("<pre");
|
||||||
|
expect(result).toContain("Line 1");
|
||||||
|
expect(result).toContain("Line <2>");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
buildHeaders,
|
||||||
|
sanitizeCustomHeaders,
|
||||||
|
sanitizeHeader,
|
||||||
|
} from "~/server/utils/email-headers";
|
||||||
|
|
||||||
|
describe("email header sanitization", () => {
|
||||||
|
it("removes reserved and invalid headers", () => {
|
||||||
|
expect(sanitizeHeader("x-usesend-email-id", "123")).toBeUndefined();
|
||||||
|
expect(sanitizeHeader("X-Test", "ok\r\nInjected: true")).toBeUndefined();
|
||||||
|
expect(sanitizeHeader(123, "ok")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for empty sanitized map", () => {
|
||||||
|
const result = sanitizeCustomHeaders({
|
||||||
|
"x-usesend-email-id": "blocked",
|
||||||
|
"x-bad": "hello\nworld",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds defaults and keeps valid custom headers", () => {
|
||||||
|
const headers = buildHeaders({
|
||||||
|
emailId: "em_1",
|
||||||
|
headers: {
|
||||||
|
"X-Custom-Trace": "trace-1",
|
||||||
|
},
|
||||||
|
unsubUrl: "https://example.com/unsub",
|
||||||
|
isBulk: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(headers["X-Usesend-Email-ID"]).toBe("em_1");
|
||||||
|
expect(headers["X-Custom-Trace"]).toBe("trace-1");
|
||||||
|
expect(headers["List-Unsubscribe"]).toBe("<https://example.com/unsub>");
|
||||||
|
expect(headers["Precedence"]).toBe("bulk");
|
||||||
|
expect(headers["X-Entity-Ref-ID"]).toBeTypeOf("string");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { canonicalizePayload } from "~/server/utils/idempotency";
|
||||||
|
|
||||||
|
describe("canonicalizePayload", () => {
|
||||||
|
it("generates same hash for different key ordering", () => {
|
||||||
|
const payloadA = { b: 1, a: { y: 2, x: 1 } };
|
||||||
|
const payloadB = { a: { x: 1, y: 2 }, b: 1 };
|
||||||
|
|
||||||
|
const a = canonicalizePayload(payloadA);
|
||||||
|
const b = canonicalizePayload(payloadB);
|
||||||
|
|
||||||
|
expect(a.canonical).toBe(b.canonical);
|
||||||
|
expect(a.bodyHash).toBe(b.bodyHash);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes dates and undefined values deterministically", () => {
|
||||||
|
const payload = {
|
||||||
|
createdAt: new Date("2025-01-01T00:00:00.000Z"),
|
||||||
|
name: "alpha",
|
||||||
|
skip: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = canonicalizePayload(payload);
|
||||||
|
|
||||||
|
expect(result.canonical).toBe(
|
||||||
|
'{"createdAt":"2025-01-01T00:00:00.000Z","name":"alpha"}',
|
||||||
|
);
|
||||||
|
expect(result.bodyHash).toHaveLength(64);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Role, type Prisma, type Team, type User } from "@prisma/client";
|
||||||
|
import { db } from "~/server/db";
|
||||||
|
|
||||||
|
let sequence = 1;
|
||||||
|
|
||||||
|
function nextValue() {
|
||||||
|
const value = sequence;
|
||||||
|
sequence += 1;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUser(data?: Prisma.UserCreateInput): Promise<User> {
|
||||||
|
const n = nextValue();
|
||||||
|
return db.user.create({
|
||||||
|
data: {
|
||||||
|
email: `user-${n}@example.com`,
|
||||||
|
isBetaUser: true,
|
||||||
|
isWaitlisted: false,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTeam(data?: Prisma.TeamCreateInput): Promise<Team> {
|
||||||
|
const n = nextValue();
|
||||||
|
return db.team.create({
|
||||||
|
data: {
|
||||||
|
name: `Team ${n}`,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function attachUserToTeam(
|
||||||
|
userId: number,
|
||||||
|
teamId: number,
|
||||||
|
role: Role = Role.ADMIN,
|
||||||
|
) {
|
||||||
|
return db.teamUser.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
role,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTeamWithUser(role: Role = Role.ADMIN) {
|
||||||
|
const user = await createUser();
|
||||||
|
const team = await createTeam();
|
||||||
|
const teamUser = await attachUserToTeam(user.id, team.id, role);
|
||||||
|
|
||||||
|
return { user, team, teamUser };
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { db } from "~/server/db";
|
||||||
|
import { getRedis } from "~/server/redis";
|
||||||
|
|
||||||
|
export const integrationEnabled = process.env.RUN_INTEGRATION === "true";
|
||||||
|
|
||||||
|
export async function resetDatabase() {
|
||||||
|
const rows = await db.$queryRaw<Array<{ tablename: string }>>(Prisma.sql`
|
||||||
|
SELECT tablename
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
AND tablename != '_prisma_migrations'
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tables = rows.map((row) => `"public"."${row.tablename}"`).join(", ");
|
||||||
|
|
||||||
|
await db.$executeRawUnsafe(
|
||||||
|
`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE;`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetRedis() {
|
||||||
|
await getRedis().flushdb();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeIntegrationConnections() {
|
||||||
|
await db.$disconnect();
|
||||||
|
|
||||||
|
const redis = getRedis();
|
||||||
|
if (redis.status !== "end") {
|
||||||
|
await redis.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const defaultEnv: Record<string, string> = {
|
||||||
|
NODE_ENV: "test",
|
||||||
|
NEXTAUTH_URL: "http://localhost:3000",
|
||||||
|
NEXTAUTH_SECRET: "test-secret",
|
||||||
|
DATABASE_URL: "postgresql://usesend:password@127.0.0.1:54329/usesend_test",
|
||||||
|
REDIS_URL: "redis://127.0.0.1:6380/15",
|
||||||
|
AWS_ACCESS_KEY: "test-access-key",
|
||||||
|
AWS_SECRET_KEY: "test-secret-key",
|
||||||
|
AWS_DEFAULT_REGION: "us-east-1",
|
||||||
|
NEXT_PUBLIC_IS_CLOUD: "true",
|
||||||
|
API_RATE_LIMIT: "2",
|
||||||
|
AUTH_EMAIL_RATE_LIMIT: "5",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(defaultEnv)) {
|
||||||
|
if (process.env[key] === undefined) {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { afterEach, vi } from "vitest";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig, mergeConfig } from "vitest/config";
|
||||||
|
import baseConfig from "./vitest.config";
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
baseConfig,
|
||||||
|
defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ["src/**/*.api.test.ts"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tsconfigPaths()],
|
||||||
|
test: {
|
||||||
|
environment: "node",
|
||||||
|
globals: true,
|
||||||
|
setupFiles: [
|
||||||
|
"./src/test/setup/setup-env.ts",
|
||||||
|
"./src/test/setup/setup-tests.ts",
|
||||||
|
],
|
||||||
|
clearMocks: true,
|
||||||
|
restoreMocks: true,
|
||||||
|
mockReset: true,
|
||||||
|
coverage: {
|
||||||
|
provider: "v8",
|
||||||
|
reporter: ["text", "html"],
|
||||||
|
include: ["src/**/*.{ts,tsx}"],
|
||||||
|
exclude: [
|
||||||
|
"src/**/*.test.{ts,tsx}",
|
||||||
|
"src/**/*.spec.{ts,tsx}",
|
||||||
|
"src/test/**",
|
||||||
|
"src/env.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig, mergeConfig } from "vitest/config";
|
||||||
|
import baseConfig from "./vitest.config";
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
baseConfig,
|
||||||
|
defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ["src/**/*.test.{ts,tsx}", "src/**/*.spec.{ts,tsx}"],
|
||||||
|
exclude: ["src/**/*.integration.test.{ts,tsx}"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig, mergeConfig } from "vitest/config";
|
||||||
|
import baseConfig from "./vitest.config";
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
baseConfig,
|
||||||
|
defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ["src/**/*.integration.test.ts"],
|
||||||
|
pool: "forks",
|
||||||
|
poolOptions: {
|
||||||
|
forks: {
|
||||||
|
singleFork: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig, mergeConfig } from "vitest/config";
|
||||||
|
import baseConfig from "./vitest.config";
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
baseConfig,
|
||||||
|
defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ["src/**/*.trpc.test.ts"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig, mergeConfig } from "vitest/config";
|
||||||
|
import baseConfig from "./vitest.config";
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
baseConfig,
|
||||||
|
defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ["src/**/*.unit.test.ts"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: unsend-test
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: usesend
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
POSTGRES_DB: usesend_test
|
||||||
|
ports:
|
||||||
|
- "54329:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U usesend -d usesend_test"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["redis-server", "--maxmemory-policy", "noeviction"]
|
||||||
|
ports:
|
||||||
|
- "6380:6379"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
@@ -10,6 +10,16 @@
|
|||||||
"build:web:local": "pnpm load-env -- turbo build --filter=@usesend/email-editor --filter=web ",
|
"build:web:local": "pnpm load-env -- turbo build --filter=@usesend/email-editor --filter=web ",
|
||||||
"start:web:local": "pnpm load-env -- cd apps/web && pnpm start",
|
"start:web:local": "pnpm load-env -- cd apps/web && pnpm start",
|
||||||
"dev": "pnpm load-env -- turbo dev",
|
"dev": "pnpm load-env -- turbo dev",
|
||||||
|
"test:web": "pnpm --filter=web test",
|
||||||
|
"test:web:all": "pnpm test:infra:up && (pnpm --filter=web test:all; code=$?; pnpm test:infra:down; exit $code)",
|
||||||
|
"test:web:all:raw": "pnpm --filter=web test:all",
|
||||||
|
"test:web:unit": "pnpm --filter=web test:unit",
|
||||||
|
"test:web:trpc": "pnpm --filter=web test:trpc",
|
||||||
|
"test:web:api": "pnpm --filter=web test:api",
|
||||||
|
"test:web:integration": "pnpm --filter=web test:integration",
|
||||||
|
"test:web:integration:full": "pnpm test:infra:up && (pnpm --filter=web test:integration:full; code=$?; pnpm test:infra:down; exit $code)",
|
||||||
|
"test:infra:up": "docker compose -f docker/testing/compose.yml up -d --wait",
|
||||||
|
"test:infra:down": "docker compose -f docker/testing/compose.yml down -v",
|
||||||
"dev:docs": "cd apps/docs && mintlify dev",
|
"dev:docs": "cd apps/docs && mintlify dev",
|
||||||
"dev:marketing": "cd apps/marketing && turbo dev",
|
"dev:marketing": "cd apps/marketing && turbo dev",
|
||||||
"lint": "turbo lint",
|
"lint": "turbo lint",
|
||||||
|
|||||||
Generated
+451
-27
@@ -357,6 +357,9 @@ importers:
|
|||||||
'@usesend/typescript-config':
|
'@usesend/typescript-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/typescript-config
|
version: link:../../packages/typescript-config
|
||||||
|
'@vitest/coverage-v8':
|
||||||
|
specifier: ^3.2.4
|
||||||
|
version: 3.2.4(vitest@3.2.4)
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.57.1
|
specifier: ^8.57.1
|
||||||
version: 8.57.1
|
version: 8.57.1
|
||||||
@@ -378,6 +381,12 @@ importers:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.8.3
|
specifier: ^5.8.3
|
||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
|
vite-tsconfig-paths:
|
||||||
|
specifier: ^5.1.4
|
||||||
|
version: 5.1.4(typescript@5.8.3)(vite@5.4.18)
|
||||||
|
vitest:
|
||||||
|
specifier: ^3.2.4
|
||||||
|
version: 3.2.4(@types/node@22.15.2)
|
||||||
|
|
||||||
packages/email-editor:
|
packages/email-editor:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2162,6 +2171,11 @@ packages:
|
|||||||
'@babel/helper-string-parser': 7.25.9
|
'@babel/helper-string-parser': 7.25.9
|
||||||
'@babel/helper-validator-identifier': 7.25.9
|
'@babel/helper-validator-identifier': 7.25.9
|
||||||
|
|
||||||
|
/@bcoe/v8-coverage@1.0.2:
|
||||||
|
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@canvas/image-data@1.1.0:
|
/@canvas/image-data@1.1.0:
|
||||||
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
|
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -2209,7 +2223,6 @@ packages:
|
|||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [aix]
|
os: [aix]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/aix-ppc64@0.24.2:
|
/@esbuild/aix-ppc64@0.24.2:
|
||||||
@@ -2235,7 +2248,6 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/android-arm64@0.24.2:
|
/@esbuild/android-arm64@0.24.2:
|
||||||
@@ -2261,7 +2273,6 @@ packages:
|
|||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [android]
|
os: [android]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/android-arm@0.24.2:
|
/@esbuild/android-arm@0.24.2:
|
||||||
@@ -2287,7 +2298,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [android]
|
os: [android]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/android-x64@0.24.2:
|
/@esbuild/android-x64@0.24.2:
|
||||||
@@ -2313,7 +2323,6 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/darwin-arm64@0.24.2:
|
/@esbuild/darwin-arm64@0.24.2:
|
||||||
@@ -2339,7 +2348,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/darwin-x64@0.24.2:
|
/@esbuild/darwin-x64@0.24.2:
|
||||||
@@ -2365,7 +2373,6 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/freebsd-arm64@0.24.2:
|
/@esbuild/freebsd-arm64@0.24.2:
|
||||||
@@ -2391,7 +2398,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/freebsd-x64@0.24.2:
|
/@esbuild/freebsd-x64@0.24.2:
|
||||||
@@ -2417,7 +2423,6 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/linux-arm64@0.24.2:
|
/@esbuild/linux-arm64@0.24.2:
|
||||||
@@ -2443,7 +2448,6 @@ packages:
|
|||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/linux-arm@0.24.2:
|
/@esbuild/linux-arm@0.24.2:
|
||||||
@@ -2469,7 +2473,6 @@ packages:
|
|||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/linux-ia32@0.24.2:
|
/@esbuild/linux-ia32@0.24.2:
|
||||||
@@ -2495,7 +2498,6 @@ packages:
|
|||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/linux-loong64@0.24.2:
|
/@esbuild/linux-loong64@0.24.2:
|
||||||
@@ -2521,7 +2523,6 @@ packages:
|
|||||||
cpu: [mips64el]
|
cpu: [mips64el]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/linux-mips64el@0.24.2:
|
/@esbuild/linux-mips64el@0.24.2:
|
||||||
@@ -2547,7 +2548,6 @@ packages:
|
|||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/linux-ppc64@0.24.2:
|
/@esbuild/linux-ppc64@0.24.2:
|
||||||
@@ -2573,7 +2573,6 @@ packages:
|
|||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/linux-riscv64@0.24.2:
|
/@esbuild/linux-riscv64@0.24.2:
|
||||||
@@ -2599,7 +2598,6 @@ packages:
|
|||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/linux-s390x@0.24.2:
|
/@esbuild/linux-s390x@0.24.2:
|
||||||
@@ -2625,7 +2623,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/linux-x64@0.24.2:
|
/@esbuild/linux-x64@0.24.2:
|
||||||
@@ -2668,7 +2665,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [netbsd]
|
os: [netbsd]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/netbsd-x64@0.24.2:
|
/@esbuild/netbsd-x64@0.24.2:
|
||||||
@@ -2711,7 +2707,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [openbsd]
|
os: [openbsd]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/openbsd-x64@0.24.2:
|
/@esbuild/openbsd-x64@0.24.2:
|
||||||
@@ -2737,7 +2732,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [sunos]
|
os: [sunos]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/sunos-x64@0.24.2:
|
/@esbuild/sunos-x64@0.24.2:
|
||||||
@@ -2763,7 +2757,6 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/win32-arm64@0.24.2:
|
/@esbuild/win32-arm64@0.24.2:
|
||||||
@@ -2789,7 +2782,6 @@ packages:
|
|||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/win32-ia32@0.24.2:
|
/@esbuild/win32-ia32@0.24.2:
|
||||||
@@ -2815,7 +2807,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: false
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@esbuild/win32-x64@0.24.2:
|
/@esbuild/win32-x64@0.24.2:
|
||||||
@@ -3621,6 +3612,11 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@istanbuljs/schema@0.1.3:
|
||||||
|
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@jridgewell/gen-mapping@0.3.8:
|
/@jridgewell/gen-mapping@0.3.8:
|
||||||
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
|
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -3646,6 +3642,13 @@ packages:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
|
/@jridgewell/trace-mapping@0.3.31:
|
||||||
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@jsep-plugin/assignment@1.3.0(jsep@1.4.0):
|
/@jsep-plugin/assignment@1.3.0(jsep@1.4.0):
|
||||||
resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==}
|
resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==}
|
||||||
engines: {node: '>= 10.16.0'}
|
engines: {node: '>= 10.16.0'}
|
||||||
@@ -8164,6 +8167,13 @@ packages:
|
|||||||
'@babel/types': 7.27.0
|
'@babel/types': 7.27.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/chai@5.2.3:
|
||||||
|
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/deep-eql': 4.0.2
|
||||||
|
assertion-error: 2.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/cookie@0.4.1:
|
/@types/cookie@0.4.1:
|
||||||
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
|
resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -8225,6 +8235,10 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
|
|
||||||
|
/@types/deep-eql@4.0.2:
|
||||||
|
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/es-aggregate-error@1.0.6:
|
/@types/es-aggregate-error@1.0.6:
|
||||||
resolution: {integrity: sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==}
|
resolution: {integrity: sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8985,6 +8999,96 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@vitest/coverage-v8@3.2.4(vitest@3.2.4):
|
||||||
|
resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vitest/browser': 3.2.4
|
||||||
|
vitest: 3.2.4
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vitest/browser':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@ampproject/remapping': 2.3.0
|
||||||
|
'@bcoe/v8-coverage': 1.0.2
|
||||||
|
ast-v8-to-istanbul: 0.3.11
|
||||||
|
debug: 4.4.3
|
||||||
|
istanbul-lib-coverage: 3.2.2
|
||||||
|
istanbul-lib-report: 3.0.1
|
||||||
|
istanbul-lib-source-maps: 5.0.6
|
||||||
|
istanbul-reports: 3.2.0
|
||||||
|
magic-string: 0.30.17
|
||||||
|
magicast: 0.3.5
|
||||||
|
std-env: 3.9.0
|
||||||
|
test-exclude: 7.0.1
|
||||||
|
tinyrainbow: 2.0.0
|
||||||
|
vitest: 3.2.4(@types/node@22.15.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@vitest/expect@3.2.4:
|
||||||
|
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
|
||||||
|
dependencies:
|
||||||
|
'@types/chai': 5.2.3
|
||||||
|
'@vitest/spy': 3.2.4
|
||||||
|
'@vitest/utils': 3.2.4
|
||||||
|
chai: 5.3.3
|
||||||
|
tinyrainbow: 2.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@vitest/mocker@3.2.4(vite@5.4.18):
|
||||||
|
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
|
||||||
|
peerDependencies:
|
||||||
|
msw: ^2.4.9
|
||||||
|
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
msw:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@vitest/spy': 3.2.4
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
magic-string: 0.30.17
|
||||||
|
vite: 5.4.18(@types/node@22.15.2)
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@vitest/pretty-format@3.2.4:
|
||||||
|
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
|
||||||
|
dependencies:
|
||||||
|
tinyrainbow: 2.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@vitest/runner@3.2.4:
|
||||||
|
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
|
||||||
|
dependencies:
|
||||||
|
'@vitest/utils': 3.2.4
|
||||||
|
pathe: 2.0.3
|
||||||
|
strip-literal: 3.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@vitest/snapshot@3.2.4:
|
||||||
|
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 3.2.4
|
||||||
|
magic-string: 0.30.17
|
||||||
|
pathe: 2.0.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@vitest/spy@3.2.4:
|
||||||
|
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
|
||||||
|
dependencies:
|
||||||
|
tinyspy: 4.0.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@vitest/utils@3.2.4:
|
||||||
|
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 3.2.4
|
||||||
|
loupe: 3.2.1
|
||||||
|
tinyrainbow: 2.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/abbrev@2.0.0:
|
/abbrev@2.0.0:
|
||||||
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
@@ -9328,6 +9432,11 @@ packages:
|
|||||||
is-array-buffer: 3.0.5
|
is-array-buffer: 3.0.5
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/assertion-error@2.0.1:
|
||||||
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ast-types-flow@0.0.8:
|
/ast-types-flow@0.0.8:
|
||||||
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -9339,6 +9448,14 @@ packages:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/ast-v8-to-istanbul@0.3.11:
|
||||||
|
resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==}
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
js-tokens: 10.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/astring@1.9.0:
|
/astring@1.9.0:
|
||||||
resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==}
|
resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -9691,6 +9808,17 @@ packages:
|
|||||||
/ccount@2.0.1:
|
/ccount@2.0.1:
|
||||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||||
|
|
||||||
|
/chai@5.3.3:
|
||||||
|
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
assertion-error: 2.0.1
|
||||||
|
check-error: 2.1.3
|
||||||
|
deep-eql: 5.0.2
|
||||||
|
loupe: 3.2.1
|
||||||
|
pathval: 2.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/chalk@4.1.2:
|
/chalk@4.1.2:
|
||||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -9733,6 +9861,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
|
resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/check-error@2.1.3:
|
||||||
|
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/chokidar@3.5.3:
|
/chokidar@3.5.3:
|
||||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
@@ -10279,6 +10412,18 @@ packages:
|
|||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
supports-color: 9.4.0
|
supports-color: 9.4.0
|
||||||
|
|
||||||
|
/debug@4.4.3:
|
||||||
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
|
engines: {node: '>=6.0'}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/decimal.js-light@2.5.1:
|
/decimal.js-light@2.5.1:
|
||||||
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -10317,6 +10462,11 @@ packages:
|
|||||||
mimic-response: 3.1.0
|
mimic-response: 3.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/deep-eql@5.0.2:
|
||||||
|
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/deep-is@0.1.4:
|
/deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
|
|
||||||
@@ -10761,6 +10911,10 @@ packages:
|
|||||||
safe-array-concat: 1.1.3
|
safe-array-concat: 1.1.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/es-module-lexer@1.7.0:
|
||||||
|
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/es-object-atoms@1.1.1:
|
/es-object-atoms@1.1.1:
|
||||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -10852,7 +11006,6 @@ packages:
|
|||||||
'@esbuild/win32-arm64': 0.21.5
|
'@esbuild/win32-arm64': 0.21.5
|
||||||
'@esbuild/win32-ia32': 0.21.5
|
'@esbuild/win32-ia32': 0.21.5
|
||||||
'@esbuild/win32-x64': 0.21.5
|
'@esbuild/win32-x64': 0.21.5
|
||||||
dev: false
|
|
||||||
|
|
||||||
/esbuild@0.24.2:
|
/esbuild@0.24.2:
|
||||||
resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
|
resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
|
||||||
@@ -11495,6 +11648,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/expect-type@1.3.0:
|
||||||
|
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/express@4.18.2:
|
/express@4.18.2:
|
||||||
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
|
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
@@ -11673,6 +11831,18 @@ packages:
|
|||||||
picomatch: 4.0.2
|
picomatch: 4.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/fdir@6.5.0(picomatch@4.0.3):
|
||||||
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
picomatch: ^3 || ^4
|
||||||
|
peerDependenciesMeta:
|
||||||
|
picomatch:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
picomatch: 4.0.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/file-entry-cache@6.0.1:
|
/file-entry-cache@6.0.1:
|
||||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||||
engines: {node: ^10.12.0 || >=12.0.0}
|
engines: {node: ^10.12.0 || >=12.0.0}
|
||||||
@@ -12076,6 +12246,10 @@ packages:
|
|||||||
unicorn-magic: 0.1.0
|
unicorn-magic: 0.1.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/globrex@0.1.2:
|
||||||
|
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/gopd@1.2.0:
|
/gopd@1.2.0:
|
||||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -12453,6 +12627,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-fxfswuADQ6N6RmCUYoCEIw09Zbk/h8GJSJsbiQ3Uw3mkQegJ5b7Eu5Tpxl2xDUq9meWmivHe0GFieG2qHl2j4A==}
|
resolution: {integrity: sha512-fxfswuADQ6N6RmCUYoCEIw09Zbk/h8GJSJsbiQ3Uw3mkQegJ5b7Eu5Tpxl2xDUq9meWmivHe0GFieG2qHl2j4A==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/html-escaper@2.0.2:
|
||||||
|
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/html-to-text@9.0.5:
|
/html-to-text@9.0.5:
|
||||||
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
|
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -13061,6 +13239,39 @@ packages:
|
|||||||
/isexe@2.0.0:
|
/isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
|
/istanbul-lib-coverage@3.2.2:
|
||||||
|
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/istanbul-lib-report@3.0.1:
|
||||||
|
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
istanbul-lib-coverage: 3.2.2
|
||||||
|
make-dir: 4.0.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/istanbul-lib-source-maps@5.0.6:
|
||||||
|
resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/trace-mapping': 0.3.25
|
||||||
|
debug: 4.4.3
|
||||||
|
istanbul-lib-coverage: 3.2.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/istanbul-reports@3.2.0:
|
||||||
|
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
html-escaper: 2.0.2
|
||||||
|
istanbul-lib-report: 3.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/iterator.prototype@1.1.5:
|
/iterator.prototype@1.1.5:
|
||||||
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
|
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -13122,9 +13333,17 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/js-tokens@10.0.0:
|
||||||
|
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/js-tokens@4.0.0:
|
/js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
/js-tokens@9.0.1:
|
||||||
|
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/js-yaml@3.14.1:
|
/js-yaml@3.14.1:
|
||||||
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
|
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -13462,6 +13681,10 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
|
|
||||||
|
/loupe@3.2.1:
|
||||||
|
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/lowercase-keys@3.0.0:
|
/lowercase-keys@3.0.0:
|
||||||
resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==}
|
resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@@ -13504,7 +13727,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
dev: false
|
|
||||||
|
/magicast@0.3.5:
|
||||||
|
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.27.0
|
||||||
|
'@babel/types': 7.27.0
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/mailparser@3.7.2:
|
/mailparser@3.7.2:
|
||||||
resolution: {integrity: sha512-iI0p2TCcIodR1qGiRoDBBwboSSff50vQAWytM5JRggLfABa4hHYCf3YVujtuzV454xrOP352VsAPIzviqMTo4Q==}
|
resolution: {integrity: sha512-iI0p2TCcIodR1qGiRoDBBwboSSff50vQAWytM5JRggLfABa4hHYCf3YVujtuzV454xrOP352VsAPIzviqMTo4Q==}
|
||||||
@@ -13529,6 +13759,13 @@ packages:
|
|||||||
libqp: 2.1.1
|
libqp: 2.1.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/make-dir@4.0.0:
|
||||||
|
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
semver: 7.7.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/markdown-extensions@2.0.0:
|
/markdown-extensions@2.0.0:
|
||||||
resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
|
resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -15032,6 +15269,15 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/pathe@2.0.3:
|
||||||
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/pathval@2.0.1:
|
||||||
|
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
||||||
|
engines: {node: '>= 14.16'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/peberminta@0.9.0:
|
/peberminta@0.9.0:
|
||||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -15052,6 +15298,11 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/picomatch@4.0.3:
|
||||||
|
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/pify@2.3.0:
|
/pify@2.3.0:
|
||||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -16976,6 +17227,10 @@ packages:
|
|||||||
side-channel-map: 1.0.1
|
side-channel-map: 1.0.1
|
||||||
side-channel-weakmap: 1.0.2
|
side-channel-weakmap: 1.0.2
|
||||||
|
|
||||||
|
/siginfo@2.0.0:
|
||||||
|
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/signal-exit@3.0.7:
|
/signal-exit@3.0.7:
|
||||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -17210,6 +17465,10 @@ packages:
|
|||||||
escape-string-regexp: 2.0.0
|
escape-string-regexp: 2.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/stackback@0.0.2:
|
||||||
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/standard-as-callback@2.1.0:
|
/standard-as-callback@2.1.0:
|
||||||
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -17221,7 +17480,6 @@ packages:
|
|||||||
|
|
||||||
/std-env@3.9.0:
|
/std-env@3.9.0:
|
||||||
resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
|
resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/streamx@2.22.0:
|
/streamx@2.22.0:
|
||||||
resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==}
|
resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==}
|
||||||
@@ -17358,6 +17616,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
/strip-literal@3.1.0:
|
||||||
|
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
|
||||||
|
dependencies:
|
||||||
|
js-tokens: 9.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/stripe@18.0.0:
|
/stripe@18.0.0:
|
||||||
resolution: {integrity: sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA==}
|
resolution: {integrity: sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA==}
|
||||||
engines: {node: '>=12.*'}
|
engines: {node: '>=12.*'}
|
||||||
@@ -17549,6 +17813,15 @@ packages:
|
|||||||
yallist: 4.0.0
|
yallist: 4.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/test-exclude@7.0.1:
|
||||||
|
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
'@istanbuljs/schema': 0.1.3
|
||||||
|
glob: 10.4.5
|
||||||
|
minimatch: 9.0.5
|
||||||
|
dev: true
|
||||||
|
|
||||||
/text-decoder@1.2.3:
|
/text-decoder@1.2.3:
|
||||||
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
|
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17583,6 +17856,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tinybench@2.9.0:
|
||||||
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tinyexec@0.3.2:
|
/tinyexec@0.3.2:
|
||||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -17595,6 +17872,29 @@ packages:
|
|||||||
picomatch: 4.0.2
|
picomatch: 4.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tinyglobby@0.2.15:
|
||||||
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dependencies:
|
||||||
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
|
picomatch: 4.0.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/tinypool@1.1.1:
|
||||||
|
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/tinyrainbow@2.0.0:
|
||||||
|
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/tinyspy@4.0.4:
|
||||||
|
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tippy.js@6.3.7:
|
/tippy.js@6.3.7:
|
||||||
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
|
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17686,6 +17986,19 @@ packages:
|
|||||||
/ts-interface-checker@0.1.13:
|
/ts-interface-checker@0.1.13:
|
||||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||||
|
|
||||||
|
/tsconfck@3.1.6(typescript@5.8.3):
|
||||||
|
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
|
||||||
|
engines: {node: ^18 || >=20}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
typescript: ^5.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
typescript: 5.8.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tsconfig-paths@3.15.0:
|
/tsconfig-paths@3.15.0:
|
||||||
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
|
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -18426,6 +18739,45 @@ packages:
|
|||||||
d3-timer: 3.0.1
|
d3-timer: 3.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/vite-node@3.2.4(@types/node@22.15.2):
|
||||||
|
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
|
||||||
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
cac: 6.7.14
|
||||||
|
debug: 4.4.3
|
||||||
|
es-module-lexer: 1.7.0
|
||||||
|
pathe: 2.0.3
|
||||||
|
vite: 5.4.18(@types/node@22.15.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/node'
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- sass
|
||||||
|
- sass-embedded
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@5.4.18):
|
||||||
|
resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==}
|
||||||
|
peerDependencies:
|
||||||
|
vite: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.0(supports-color@9.4.0)
|
||||||
|
globrex: 0.1.2
|
||||||
|
tsconfck: 3.1.6(typescript@5.8.3)
|
||||||
|
vite: 5.4.18(@types/node@22.15.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
- typescript
|
||||||
|
dev: true
|
||||||
|
|
||||||
/vite@5.4.18(@types/node@22.15.2):
|
/vite@5.4.18(@types/node@22.15.2):
|
||||||
resolution: {integrity: sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==}
|
resolution: {integrity: sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
@@ -18463,7 +18815,70 @@ packages:
|
|||||||
rollup: 4.40.0
|
rollup: 4.40.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
dev: false
|
|
||||||
|
/vitest@3.2.4(@types/node@22.15.2):
|
||||||
|
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
|
||||||
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@edge-runtime/vm': '*'
|
||||||
|
'@types/debug': ^4.1.12
|
||||||
|
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||||
|
'@vitest/browser': 3.2.4
|
||||||
|
'@vitest/ui': 3.2.4
|
||||||
|
happy-dom: '*'
|
||||||
|
jsdom: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@edge-runtime/vm':
|
||||||
|
optional: true
|
||||||
|
'@types/debug':
|
||||||
|
optional: true
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser':
|
||||||
|
optional: true
|
||||||
|
'@vitest/ui':
|
||||||
|
optional: true
|
||||||
|
happy-dom:
|
||||||
|
optional: true
|
||||||
|
jsdom:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@types/chai': 5.2.3
|
||||||
|
'@types/node': 22.15.2
|
||||||
|
'@vitest/expect': 3.2.4
|
||||||
|
'@vitest/mocker': 3.2.4(vite@5.4.18)
|
||||||
|
'@vitest/pretty-format': 3.2.4
|
||||||
|
'@vitest/runner': 3.2.4
|
||||||
|
'@vitest/snapshot': 3.2.4
|
||||||
|
'@vitest/spy': 3.2.4
|
||||||
|
'@vitest/utils': 3.2.4
|
||||||
|
chai: 5.3.3
|
||||||
|
debug: 4.4.3
|
||||||
|
expect-type: 1.3.0
|
||||||
|
magic-string: 0.30.17
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.2
|
||||||
|
std-env: 3.9.0
|
||||||
|
tinybench: 2.9.0
|
||||||
|
tinyexec: 0.3.2
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
tinypool: 1.1.1
|
||||||
|
tinyrainbow: 2.0.0
|
||||||
|
vite: 5.4.18(@types/node@22.15.2)
|
||||||
|
vite-node: 3.2.4(@types/node@22.15.2)
|
||||||
|
why-is-node-running: 2.3.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- msw
|
||||||
|
- sass
|
||||||
|
- sass-embedded
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
dev: true
|
||||||
|
|
||||||
/w3c-keyname@2.2.8:
|
/w3c-keyname@2.2.8:
|
||||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||||
@@ -18553,6 +18968,15 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
|
|
||||||
|
/why-is-node-running@2.3.0:
|
||||||
|
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
siginfo: 2.0.0
|
||||||
|
stackback: 0.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/widest-line@5.0.0:
|
/widest-line@5.0.0:
|
||||||
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
|
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|||||||
Reference in New Issue
Block a user