From 6d21b5fc295125b342a787638e561bdfc035cdef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Ag=C3=BCero?= <54730752+EmmanuelAR@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:21:33 -0600 Subject: [PATCH 1/7] docs: add design spec for merchant profile API (#11) --- .../2026-06-26-merchant-profile-api-design.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-26-merchant-profile-api-design.md diff --git a/docs/superpowers/specs/2026-06-26-merchant-profile-api-design.md b/docs/superpowers/specs/2026-06-26-merchant-profile-api-design.md new file mode 100644 index 0000000..ed80161 --- /dev/null +++ b/docs/superpowers/specs/2026-06-26-merchant-profile-api-design.md @@ -0,0 +1,128 @@ +# Merchant Profile API (Get & Update) — Design + +**Issue:** [#11](https://github.com/ShadeProtocol/shade-backend/issues/11) +**Date:** 2026-06-26 +**Branch:** `feat/#11` + +## Background + +The frontend dashboard needs a profile/settings page where an authenticated +merchant can view and update their own information. This requires: + +- `GET /merchants/me` — load the current merchant profile. +- `PATCH /merchants/me` — partial update of editable fields. + +Both endpoints operate on the **authenticated** merchant (`req.merchant`), never +on an arbitrary merchant by id. + +## Goals / Acceptance Criteria + +- `GET /merchants/me` returns the full profile of the authenticated merchant. +- `PATCH /merchants/me` with a valid partial payload → `200` with updated profile. +- `PATCH /merchants/me` attempting to change non-editable fields (`address`, + `email`, `merchantId`, `account`) → those fields are silently ignored. +- Invalid `webhook` URL format → `400`. +- Unauthenticated request to either endpoint → `401`. +- Response never exposes internal fields (relations such as `apiKeys`, + `refreshTokens`, etc.). Enforced by the existing allow-list `sanitizeMerchant`. + +## Editable vs Non-Editable Fields + +**Editable via PATCH:** `firstName`, `lastName`, `businessName`, `category`, +`description`, `logo`, `webhook`. + +**Non-editable (silently ignored if sent):** `address`, `email`, `merchantId`, +`account`, and any other field. + +> Note: the issue refers to `accountContract`; the actual schema field is +> `account`. We treat `account` as the non-editable field. + +## Design Decisions + +1. **Clearing optional fields:** `logo` and `webhook` may be cleared by sending + `null` (or `""`, normalized to `null`). A non-empty `webhook` string must be a + valid HTTPS URL. +2. **Required text fields:** `firstName`, `lastName`, `businessName`, `category`, + `description` were required at registration. If present in the PATCH payload + they must be non-empty strings; `""` → `400`. They cannot be cleared via this + endpoint. +3. **Empty payload:** a PATCH with no valid editable field present → `400` + (`"At least one valid field is required"`). +4. **Silently ignore forbidden fields:** the validator/service only ever reads + from the editable allow-list, so non-editable fields are never written and + never error. + +## Architecture + +Follows the existing layering: routes → controllers (thin) → services (prisma + +`AppError`) → `sanitizeMerchant` for output. + +### 1. Routes — `src/routes/merchant.routes.ts` + +```ts +router.get('/me', authenticateMerchant, getMyProfileController); +router.patch('/me', authenticateMerchant, updateMyProfileController); +``` + +**Ordering:** register `/me` routes **before** the existing `/:id` route so +`/me` is not captured by the `:id` param. + +### 2. Validation — `src/utils/validation.ts` + +New `UpdateMerchantInput` type (all fields optional) and +`validateUpdateMerchant(body): ValidationErrors`: + +- Read only from the editable allow-list. +- Required text fields, if present → must be non-empty string, else error. +- `logo`, if present → string or `null`. +- `webhook`, if present → `null`/`""` (clear) or a valid `https:` URL (validated + with the native `URL` constructor + `protocol === 'https:'`), else error. +- If no editable field is present → `_empty` error. + +### 3. Service — `src/services/merchant.services.ts` + +- `getMyProfile(id: string)`: `findUnique({ where: { id } })`; if missing → + `AppError(404, 'Merchant not found')`; return `sanitizeMerchant(merchant)`. +- `updateMyProfile(id: string, data: UpdateMerchantInput)`: build the Prisma + `data` object only from editable fields present in the payload (trim strings; + set `null` for cleared `logo`/`webhook`); `prisma.merchant.update`; return + `sanitizeMerchant(updated)`. +- **Extend `sanitizeMerchant`** to also include `webhook` and `account` so the + "full profile" reflects editable/relevant fields. The allow-list remains + intact — no internal fields or relations are added. + +### 4. Controllers — `src/controllers/merchant.controllers.ts` + +Mirror `registerMerchantController`: + +- `getMyProfileController`: check `req.merchant` → 401; call `getMyProfile`; + 200 with profile; `AppError` → its status; else 500. +- `updateMyProfileController`: check `req.merchant` → 401; + `validateUpdateMerchant` → 400 `{ error: 'Validation failed', errors }`; + call `updateMyProfile`; 200 with updated profile; `AppError` → its status; + else 500. + +## Testing + +Jest + supertest, following the existing `*.routes.test.ts` patterns. + +**GET /merchants/me** +- Authenticated → `200` with full profile. +- No / invalid token → `401`. +- Response does not expose internal fields/relations. + +**PATCH /merchants/me** +- Valid partial payload → `200` with updated profile. +- Sending `address` / `email` / `merchantId` / `account` → silently ignored + (unchanged in response). +- Invalid `webhook` (non-HTTPS / malformed) → `400`. +- `webhook: null` → clears the webhook. +- Required text field as `""` → `400`. +- Empty payload (no editable fields) → `400`. +- No / invalid token → `401`. + +## Out of Scope + +- Updating `email` (separate flow with OTP re-verification). +- Admin-level updates of other merchants. +- Webhook delivery/verification mechanics (only stores the URL). From ab390bc09253496333ae9de3c29cd0c1c868c655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Ag=C3=BCero?= <54730752+EmmanuelAR@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:24:22 -0600 Subject: [PATCH 2/7] docs: add implementation plan for merchant profile API (#11) --- .../plans/2026-06-26-merchant-profile-api.md | 699 ++++++++++++++++++ 1 file changed, 699 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-26-merchant-profile-api.md diff --git a/docs/superpowers/plans/2026-06-26-merchant-profile-api.md b/docs/superpowers/plans/2026-06-26-merchant-profile-api.md new file mode 100644 index 0000000..0ab20af --- /dev/null +++ b/docs/superpowers/plans/2026-06-26-merchant-profile-api.md @@ -0,0 +1,699 @@ +# Merchant Profile API (Get & Update) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add `GET /merchants/me` and `PATCH /merchants/me` so an authenticated merchant can view and partially update their own profile. + +**Architecture:** Follows the existing layering — routes → thin controllers → services (prisma + `AppError`) → `sanitizeMerchant` allow-list for output. A new `validateUpdateMerchant` validator gates the PATCH payload; the service writes only editable fields; output goes through the existing allow-list so internal fields/relations are never exposed. + +**Tech Stack:** TypeScript (ESM), Express v5, Prisma, Jest + ts-jest (ESM), supertest, jest-mock-extended. + +## Global Constraints + +- Base API path is `/api/v1` — merchant routes are mounted at `/api/v1/merchants`. +- ESM project: all relative imports MUST use the `.js` extension (e.g. `'../utils/errors.js'`). +- Tests live under `tests/` (`tests/unit/`, `tests/integration/`) and use the shared `prismaMock` from `tests/__mocks__/prisma.ts` (imported via `await import('../../src/config/prisma.js')`). +- Auth is simulated by mocking `prismaMock.refreshToken.findUnique` to return a session with `expiresAt` in the future and a `merchant` object (see existing `authenticateAs` helper). +- Editable fields: `firstName`, `lastName`, `businessName`, `category`, `description`, `logo`, `webhook`. Non-editable (silently ignored): everything else, explicitly `address`, `email`, `merchantId`, `account`. +- Run a single test file with: `npm test -- `. + +--- + +### Task 1: `validateUpdateMerchant` validator + +**Files:** +- Modify: `src/utils/validation.ts` +- Test: `tests/unit/merchant.update.validation.test.ts` (create) + +**Interfaces:** +- Consumes: existing `ValidationErrors` type and `isNonEmptyString` helper in `src/utils/validation.ts`. +- Produces: + - `interface UpdateMerchantInput { firstName?: string; lastName?: string; businessName?: string; category?: string; description?: string; logo?: string | null; webhook?: string | null; }` + - `validateUpdateMerchant(body: unknown): ValidationErrors` — returns `{}` when valid; on the empty-payload case returns `{ _empty: '...' }`. + +- [ ] **Step 1: Write the failing tests** + +Create `tests/unit/merchant.update.validation.test.ts`: + +```ts +import { validateUpdateMerchant } from '../../src/utils/validation.js'; + +describe('validateUpdateMerchant', () => { + test('accepts a valid partial payload', () => { + expect(validateUpdateMerchant({ firstName: 'Ada' })).toEqual({}); + }); + + test('rejects an empty payload (no editable fields)', () => { + const errors = validateUpdateMerchant({}); + expect(errors._empty).toEqual(expect.any(String)); + }); + + test('treats a payload of only non-editable fields as empty', () => { + const errors = validateUpdateMerchant({ address: '0xabc', email: 'x@y.com', merchantId: 5 }); + expect(errors._empty).toEqual(expect.any(String)); + }); + + test('rejects a required text field sent as empty string', () => { + const errors = validateUpdateMerchant({ firstName: ' ' }); + expect(errors.firstName).toEqual(expect.any(String)); + }); + + test('accepts logo and webhook cleared with null', () => { + expect(validateUpdateMerchant({ logo: null, webhook: null })).toEqual({}); + }); + + test('accepts an empty string webhook as a clear', () => { + expect(validateUpdateMerchant({ webhook: '' })).toEqual({}); + }); + + test('rejects a non-HTTPS webhook URL', () => { + const errors = validateUpdateMerchant({ webhook: 'http://example.com/hook' }); + expect(errors.webhook).toEqual(expect.any(String)); + }); + + test('rejects a malformed webhook URL', () => { + const errors = validateUpdateMerchant({ webhook: 'not-a-url' }); + expect(errors.webhook).toEqual(expect.any(String)); + }); + + test('accepts a valid HTTPS webhook URL', () => { + expect(validateUpdateMerchant({ webhook: 'https://example.com/hook' })).toEqual({}); + }); + + test('rejects a non-string logo', () => { + const errors = validateUpdateMerchant({ logo: 123 }); + expect(errors.logo).toEqual(expect.any(String)); + }); +}); +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `npm test -- tests/unit/merchant.update.validation.test.ts` +Expected: FAIL — `validateUpdateMerchant` is not exported / not a function. + +- [ ] **Step 3: Implement the validator** + +Append to `src/utils/validation.ts` (keep the existing `EMAIL_REGEX`, `isNonEmptyString`, `validateRegisterMerchant`): + +```ts +export interface UpdateMerchantInput { + firstName?: string; + lastName?: string; + businessName?: string; + category?: string; + description?: string; + logo?: string | null; + webhook?: string | null; +} + +const EDITABLE_MERCHANT_FIELDS = [ + 'firstName', + 'lastName', + 'businessName', + 'category', + 'description', + 'logo', + 'webhook', +] as const; + +const UPDATE_REQUIRED_TEXT_FIELDS = [ + 'firstName', + 'lastName', + 'businessName', + 'category', + 'description', +] as const; + +const isValidHttpsUrl = (value: string): boolean => { + try { + return new URL(value).protocol === 'https:'; + } catch { + return false; + } +}; + +export const validateUpdateMerchant = (body: unknown): ValidationErrors => { + const errors: ValidationErrors = {}; + const payload = (body ?? {}) as Record; + + const present = EDITABLE_MERCHANT_FIELDS.filter((field) => payload[field] !== undefined); + if (present.length === 0) { + errors._empty = 'At least one valid field is required'; + return errors; + } + + for (const field of UPDATE_REQUIRED_TEXT_FIELDS) { + if (payload[field] !== undefined && !isNonEmptyString(payload[field])) { + errors[field] = `${field} must be a non-empty string`; + } + } + + if (payload.logo !== undefined && payload.logo !== null && typeof payload.logo !== 'string') { + errors.logo = 'logo must be a string or null'; + } + + if (payload.webhook !== undefined && payload.webhook !== null) { + if (typeof payload.webhook !== 'string') { + errors.webhook = 'webhook must be a string or null'; + } else if (payload.webhook.trim().length > 0 && !isValidHttpsUrl(payload.webhook.trim())) { + errors.webhook = 'webhook must be a valid HTTPS URL'; + } + } + + return errors; +}; +``` + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `npm test -- tests/unit/merchant.update.validation.test.ts` +Expected: PASS (all 10 tests). + +- [ ] **Step 5: Commit** + +```bash +git add src/utils/validation.ts tests/unit/merchant.update.validation.test.ts +git commit -m "feat: add validateUpdateMerchant validator (#11)" +``` + +--- + +### Task 2: Service layer — `getMyProfile`, `updateMyProfile`, extend `sanitizeMerchant` + +**Files:** +- Modify: `src/services/merchant.services.ts` +- Test: `tests/unit/merchant.profile.services.test.ts` (create) + +**Interfaces:** +- Consumes: `prisma` (`../config/prisma.js`), `AppError` (`../utils/errors.js`), `UpdateMerchantInput` (`../utils/validation.js`), existing `sanitizeMerchant`. +- Produces: + - `getMyProfile(id: string): Promise>` — throws `AppError(404, 'Merchant not found')` when missing. + - `updateMyProfile(id: string, data: UpdateMerchantInput): Promise>` — writes only editable fields present in `data`; trims strings; normalizes empty `logo`/`webhook` to `null`. + - `sanitizeMerchant` now additionally returns `account` and `webhook`. + +- [ ] **Step 1: Write the failing tests** + +Create `tests/unit/merchant.profile.services.test.ts`: + +```ts +import { jest } from '@jest/globals'; +import { mockReset } from 'jest-mock-extended'; + +const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; +const { getMyProfile, updateMyProfile } = await import('../../src/services/merchant.services.js'); + +const baseMerchant = { + id: 'uuid-1', + merchantId: 1, + address: '0x123', + account: 'CCONTRACT', + email: 'ada@example.com', + firstName: 'Ada', + lastName: 'Lovelace', + businessName: 'Analytical Engines', + category: 'software', + description: 'We build computing machines.', + logo: 'https://example.com/logo.png', + webhook: null, + active: true, + verified: false, + emailVerified: false, + registered: true, + createdAt: new Date(), + updatedAt: new Date(), +}; + +describe('getMyProfile', () => { + beforeEach(() => mockReset(prismaMock)); + + test('returns the sanitized profile including account and webhook', async () => { + prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant); + + const result = await getMyProfile('uuid-1'); + + expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({ where: { id: 'uuid-1' } }); + expect(result).toMatchObject({ id: 'uuid-1', account: 'CCONTRACT', webhook: null }); + expect(result).not.toHaveProperty('refreshTokens'); + expect(result).not.toHaveProperty('apiKeys'); + }); + + test('throws AppError(404) when the merchant does not exist', async () => { + prismaMock.merchant.findUnique.mockResolvedValue(null); + + await expect(getMyProfile('missing')).rejects.toMatchObject({ statusCode: 404 }); + }); +}); + +describe('updateMyProfile', () => { + beforeEach(() => mockReset(prismaMock)); + + test('writes only the editable fields present in the payload, trimmed', async () => { + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + + await updateMyProfile('uuid-1', { + firstName: ' Grace ', + webhook: 'https://example.com/hook', + }); + + expect(prismaMock.merchant.update).toHaveBeenCalledWith({ + where: { id: 'uuid-1' }, + data: { firstName: 'Grace', webhook: 'https://example.com/hook' }, + }); + }); + + test('normalizes a cleared logo/webhook to null', async () => { + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + + await updateMyProfile('uuid-1', { logo: '', webhook: null }); + + expect(prismaMock.merchant.update).toHaveBeenCalledWith({ + where: { id: 'uuid-1' }, + data: { logo: null, webhook: null }, + }); + }); + + test('returns the sanitized updated profile', async () => { + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + + const result = await updateMyProfile('uuid-1', { businessName: 'New Co' }); + + expect(result).toMatchObject({ businessName: 'New Co' }); + expect(result).not.toHaveProperty('refreshTokens'); + }); +}); +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `npm test -- tests/unit/merchant.profile.services.test.ts` +Expected: FAIL — `getMyProfile`/`updateMyProfile` not exported. + +- [ ] **Step 3: Extend `sanitizeMerchant` and add the service functions** + +In `src/services/merchant.services.ts`, update the imports line to also import `Prisma` and `UpdateMerchantInput`: + +```ts +import { Merchant, Prisma } from '@prisma/client'; +import prisma from '../config/prisma.js'; +import { AppError } from '../utils/errors.js'; +import { RegisterMerchantInput, UpdateMerchantInput } from '../utils/validation.js'; +import { sendOtpEmail } from './otp.services.js'; +``` + +Add `account` and `webhook` to the `sanitizeMerchant` allow-list (place `account` after `address`, `webhook` after `logo`): + +```ts +export const sanitizeMerchant = (merchant: Merchant) => ({ + id: merchant.id, + merchantId: merchant.merchantId, + email: merchant.email, + address: merchant.address, + account: merchant.account, + firstName: merchant.firstName, + lastName: merchant.lastName, + businessName: merchant.businessName, + category: merchant.category, + description: merchant.description, + logo: merchant.logo, + webhook: merchant.webhook, + active: merchant.active, + verified: merchant.verified, + emailVerified: merchant.emailVerified, + registered: merchant.registered, + createdAt: merchant.createdAt, + updatedAt: merchant.updatedAt, +}); +``` + +Append the two new service functions at the end of the file: + +```ts +/** + * Returns the authenticated merchant's own profile. + */ +export const getMyProfile = async (id: string) => { + const merchant = await prisma.merchant.findUnique({ where: { id } }); + + if (!merchant) { + throw new AppError(404, 'Merchant not found'); + } + + return sanitizeMerchant(merchant); +}; + +/** + * Partially updates the authenticated merchant's editable profile fields. + * + * Only fields present in `data` are written. Strings are trimmed; an empty + * `logo`/`webhook` is normalized to null so the merchant can clear them. + * Non-editable fields are never read here, so they cannot be changed. + */ +export const updateMyProfile = async (id: string, data: UpdateMerchantInput) => { + const updateData: Prisma.MerchantUpdateInput = {}; + + const textFields = ['firstName', 'lastName', 'businessName', 'category', 'description'] as const; + for (const field of textFields) { + const value = data[field]; + if (value !== undefined) { + updateData[field] = value.trim(); + } + } + + if (data.logo !== undefined) { + const logo = typeof data.logo === 'string' ? data.logo.trim() : data.logo; + updateData.logo = logo ? logo : null; + } + + if (data.webhook !== undefined) { + const webhook = typeof data.webhook === 'string' ? data.webhook.trim() : data.webhook; + updateData.webhook = webhook ? webhook : null; + } + + const updated = await prisma.merchant.update({ where: { id }, data: updateData }); + + return sanitizeMerchant(updated); +}; +``` + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `npm test -- tests/unit/merchant.profile.services.test.ts` +Expected: PASS (all 5 tests). + +- [ ] **Step 5: Verify the existing service/register tests still pass** + +Run: `npm test -- tests/unit/merchant.services.test.ts tests/integration/merchant.register.test.ts` +Expected: PASS — `sanitizeMerchant` additions are additive (register integration uses `toMatchObject`, so extra fields are fine). + +- [ ] **Step 6: Commit** + +```bash +git add src/services/merchant.services.ts tests/unit/merchant.profile.services.test.ts +git commit -m "feat: add getMyProfile/updateMyProfile services and expose account/webhook (#11)" +``` + +--- + +### Task 3: Controllers + routes wiring + +**Files:** +- Modify: `src/controllers/merchant.controllers.ts` +- Modify: `src/routes/merchant.routes.ts` +- Test: `tests/integration/merchant.profile.test.ts` (create) + +**Interfaces:** +- Consumes: `getMyProfile`, `updateMyProfile` (`../services/merchant.services.js`); `validateUpdateMerchant` (`../utils/validation.js`); `AppError` (`../utils/errors.js`); `authenticateMerchant` (`../middlewares/auth.middleware.js`); `req.merchant`. +- Produces: `getMyProfileController`, `updateMyProfileController` (Express handlers) and the routes `GET /merchants/me`, `PATCH /merchants/me`. + +- [ ] **Step 1: Write the failing integration tests** + +Create `tests/integration/merchant.profile.test.ts`: + +```ts +import { jest } from '@jest/globals'; +import { mockReset } from 'jest-mock-extended'; +import request from 'supertest'; + +const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; +const { default: app } = await import('../../src/app.js'); + +const ME_URL = '/api/v1/merchants/me'; + +const baseMerchant = { + id: 'uuid-1', + merchantId: 1, + address: '0x123', + account: 'CCONTRACT', + email: 'ada@example.com', + firstName: 'Ada', + lastName: 'Lovelace', + businessName: 'Analytical Engines', + category: 'software', + description: 'We build computing machines.', + logo: null, + webhook: null, + active: true, + verified: false, + emailVerified: false, + registered: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + +const authenticateAs = (merchant: Record) => { + prismaMock.refreshToken.findUnique.mockResolvedValue({ + id: 'session-1', + merchantId: merchant.id, + token: 'valid-token', + expiresAt: new Date(Date.now() + 60 * 60 * 1000), + createdAt: new Date(), + merchant, + } as any); +}; + +describe('GET /api/v1/merchants/me', () => { + beforeEach(() => mockReset(prismaMock)); + + test('returns 401 when unauthenticated', async () => { + const response = await request(app).get(ME_URL); + expect(response.status).toBe(401); + }); + + test('returns 200 with the full profile and no internal fields', async () => { + authenticateAs(baseMerchant); + prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any); + + const response = await request(app).get(ME_URL).set('Authorization', 'Bearer valid-token'); + + expect(response.status).toBe(200); + expect(response.body).toMatchObject({ id: 'uuid-1', account: 'CCONTRACT', webhook: null }); + expect(response.body).not.toHaveProperty('refreshTokens'); + expect(response.body).not.toHaveProperty('apiKeys'); + }); +}); + +describe('PATCH /api/v1/merchants/me', () => { + beforeEach(() => mockReset(prismaMock)); + + test('returns 401 when unauthenticated', async () => { + const response = await request(app).patch(ME_URL).send({ firstName: 'Grace' }); + expect(response.status).toBe(401); + expect(prismaMock.merchant.update).not.toHaveBeenCalled(); + }); + + test('updates a valid partial payload and returns 200', async () => { + authenticateAs(baseMerchant); + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + + const response = await request(app) + .patch(ME_URL) + .set('Authorization', 'Bearer valid-token') + .send({ firstName: 'Grace', webhook: 'https://example.com/hook' }); + + expect(response.status).toBe(200); + expect(response.body).toMatchObject({ firstName: 'Grace', webhook: 'https://example.com/hook' }); + }); + + test('silently ignores non-editable fields (address/email)', async () => { + authenticateAs(baseMerchant); + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + + const response = await request(app) + .patch(ME_URL) + .set('Authorization', 'Bearer valid-token') + .send({ firstName: 'Grace', address: '0xHACK', email: 'evil@example.com' }); + + expect(response.status).toBe(200); + const updateArg = prismaMock.merchant.update.mock.calls[0][0]; + expect(updateArg.data).toEqual({ firstName: 'Grace' }); + expect(response.body.address).toBe('0x123'); + expect(response.body.email).toBe('ada@example.com'); + }); + + test('returns 400 for an invalid (non-HTTPS) webhook', async () => { + authenticateAs(baseMerchant); + + const response = await request(app) + .patch(ME_URL) + .set('Authorization', 'Bearer valid-token') + .send({ webhook: 'http://example.com/hook' }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Validation failed'); + expect(prismaMock.merchant.update).not.toHaveBeenCalled(); + }); + + test('clears the webhook when sent null', async () => { + authenticateAs(baseMerchant); + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + + const response = await request(app) + .patch(ME_URL) + .set('Authorization', 'Bearer valid-token') + .send({ webhook: null }); + + expect(response.status).toBe(200); + expect(response.body.webhook).toBeNull(); + }); + + test('returns 400 for a required text field sent empty', async () => { + authenticateAs(baseMerchant); + + const response = await request(app) + .patch(ME_URL) + .set('Authorization', 'Bearer valid-token') + .send({ firstName: '' }); + + expect(response.status).toBe(400); + expect(prismaMock.merchant.update).not.toHaveBeenCalled(); + }); + + test('returns 400 for an empty payload', async () => { + authenticateAs(baseMerchant); + + const response = await request(app) + .patch(ME_URL) + .set('Authorization', 'Bearer valid-token') + .send({}); + + expect(response.status).toBe(400); + expect(prismaMock.merchant.update).not.toHaveBeenCalled(); + }); +}); +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `npm test -- tests/integration/merchant.profile.test.ts` +Expected: FAIL — routes return 404 / controllers not defined. + +- [ ] **Step 3: Add the controllers** + +In `src/controllers/merchant.controllers.ts`, extend the service import and add `validateUpdateMerchant` to the validation import: + +```ts +import { + createMerchant, + getMerchant, + listMerchants, + registerMerchant, + getMyProfile, + updateMyProfile, +} from '../services/merchant.services.js'; +import { validateRegisterMerchant, validateUpdateMerchant } from '../utils/validation.js'; +``` + +Append the two controllers at the end of the file: + +```ts +export const getMyProfileController = async (req: Request, res: Response): Promise => { + const merchant = req.merchant; + + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + + try { + const profile = await getMyProfile(merchant.id); + res.status(200).json(profile); + } catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; + +export const updateMyProfileController = async (req: Request, res: Response): Promise => { + const merchant = req.merchant; + + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + + const errors = validateUpdateMerchant(req.body); + if (Object.keys(errors).length > 0) { + res.status(400).json({ error: 'Validation failed', errors }); + return; + } + + try { + const profile = await updateMyProfile(merchant.id, req.body); + res.status(200).json(profile); + } catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +``` + +- [ ] **Step 4: Wire the routes** + +Replace the body of `src/routes/merchant.routes.ts` so the `/me` routes are registered **before** `/:id`: + +```ts +import { Router } from 'express'; +import { + createMerchantController, + getMerchantController, + listMerchantsController, + registerMerchantController, + getMyProfileController, + updateMyProfileController, +} from '../controllers/merchant.controllers.js'; +import { authenticateMerchant } from '../middlewares/auth.middleware.js'; + +const router = Router(); + +router.post('/register', authenticateMerchant, registerMerchantController); +router.get('/me', authenticateMerchant, getMyProfileController); +router.patch('/me', authenticateMerchant, updateMyProfileController); +router.post('/', createMerchantController); +router.get('/:id', getMerchantController); +router.get('/', listMerchantsController); + +export default router; +``` + +- [ ] **Step 5: Run the tests to verify they pass** + +Run: `npm test -- tests/integration/merchant.profile.test.ts` +Expected: PASS (all 9 tests). + +- [ ] **Step 6: Run the full check + suite** + +Run: `npm run check && npm test` +Expected: type-check, lint, format all clean; entire test suite PASS. + +- [ ] **Step 7: Commit** + +```bash +git add src/controllers/merchant.controllers.ts src/routes/merchant.routes.ts tests/integration/merchant.profile.test.ts +git commit -m "feat: add GET/PATCH /merchants/me endpoints (#11)" +``` + +--- + +## Self-Review + +**Spec coverage:** +- `GET /merchants/me` returns full profile → Task 2 (`getMyProfile`) + Task 3 (route/controller, integration test). +- `PATCH /merchants/me` partial update → Task 1 (validator) + Task 2 (`updateMyProfile`) + Task 3 (route/controller). +- Non-editable fields silently ignored → Task 1 (allow-list read) + Task 3 (`address`/`email` ignored test). +- Invalid webhook → 400 → Task 1 tests + Task 3 integration test. +- Unauthenticated → 401 → Task 3 integration tests (relies on existing `authenticateMerchant`). +- Never expose internal fields → Task 2 (`sanitizeMerchant` allow-list) + Task 3 (`not.toHaveProperty` assertions). +- Clear logo/webhook with null → Task 1 + Task 2 + Task 3. +- Required text empty → 400 → Task 1 + Task 3. +- Empty payload → 400 → Task 1 + Task 3. + +**Placeholder scan:** none — all steps contain concrete code and exact commands. + +**Type consistency:** `UpdateMerchantInput` defined in Task 1, consumed by `updateMyProfile` in Task 2 and the controller in Task 3. `getMyProfile(id: string)` / `updateMyProfile(id: string, data)` signatures match across service tests (Task 2) and controller calls (Task 3). `sanitizeMerchant` field additions (`account`, `webhook`) are consistent across Tasks 2 and 3 assertions. From a3c95f6ded7ed13c6de0c18d96f15bf4d9569d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Ag=C3=BCero?= <54730752+EmmanuelAR@users.noreply.github.com> Date: Fri, 26 Jun 2026 23:14:15 -0600 Subject: [PATCH 3/7] feat: implement merchant profile retrieval and update endpoints - Added `getMyProfileController` and `updateMyProfileController` to handle fetching and updating the authenticated merchant's profile. - Introduced validation for updating merchant profiles with `validateUpdateMerchant`. - Updated routes to include new endpoints for profile management. - Created integration and unit tests for the new profile functionalities, ensuring proper authentication and validation handling. --- package-lock.json | 242 +++++++----------- src/controllers/merchant.controllers.ts | 50 +++- src/routes/merchant.routes.ts | 4 + src/services/merchant.services.ts | 52 +++- src/utils/validation.ts | 67 +++++ tests/integration/merchant.profile.test.ts | 150 +++++++++++ tests/unit/merchant.profile.services.test.ts | 85 ++++++ tests/unit/merchant.update.validation.test.ts | 49 ++++ 8 files changed, 549 insertions(+), 150 deletions(-) create mode 100644 tests/integration/merchant.profile.test.ts create mode 100644 tests/unit/merchant.profile.services.test.ts create mode 100644 tests/unit/merchant.update.validation.test.ts diff --git a/package-lock.json b/package-lock.json index 0bcda1a..1599496 100644 --- a/package-lock.json +++ b/package-lock.json @@ -589,7 +589,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@chevrotain/gast": "10.5.0", @@ -601,7 +601,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@chevrotain/types": "10.5.0", @@ -612,21 +612,21 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@chevrotain/utils": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -638,14 +638,14 @@ "version": "0.3.15", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.0.20", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.20.tgz", "integrity": "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "pglite-server": "dist/scripts/server.js" @@ -658,7 +658,7 @@ "version": "0.2.20", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.20.tgz", "integrity": "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "peerDependencies": { "@electric-sql/pglite": "0.3.15" @@ -1316,7 +1316,7 @@ "version": "1.19.9", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=18.14.1" @@ -1942,7 +1942,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6.0.0" } @@ -1951,13 +1951,13 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "devOptional": true + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -1967,7 +1967,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz", "integrity": "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chevrotain": "^10.5.0", @@ -2139,7 +2139,7 @@ "version": "7.4.0", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.4.0.tgz", "integrity": "sha512-EnNrZMwZ9+O6UlG+YO9SP3VhVw4zwMahDRzQm3r0DQn9KeU5NwzmaDAY+BzACrgmaU71Id1/0FtWIDdl7xQp9g==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "c12": "3.1.0", @@ -2152,14 +2152,14 @@ "version": "7.4.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.4.0.tgz", "integrity": "sha512-fZicwzgFHvvPMrRLCUinrsBTdadJsi/1oirzShjmFvNLwtu2DYlkxwRVy5zEGhp85mrEGnLeS/PdNRCdE027+Q==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@prisma/dev": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.20.0.tgz", "integrity": "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "@electric-sql/pglite": "0.3.15", @@ -2200,7 +2200,7 @@ "version": "7.4.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.4.0.tgz", "integrity": "sha512-H+dgpbbY3VN/j5hOSVP1LXsv/rU0w/4C2zh5PZUwo/Q3NqZjOvBlVvkhtziioRmeEZ3SBAqPCsf1sQ74sI3O/w==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -2214,14 +2214,14 @@ "version": "7.4.0-20.ab56fe763f921d033a6c195e7ddeb3e255bdbb57", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.4.0-20.ab56fe763f921d033a6c195e7ddeb3e255bdbb57.tgz", "integrity": "sha512-5o3/bubIYdUeg38cyNf+VDq+LVtxvvi2393Fd1Uru52LPfkGJnmVbCaX1wBOAncgKR3BCloMJFD+Koog9LtYqQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.0.tgz", "integrity": "sha512-fOUIoGzAPgtjHVs4DsVSnEDPBEauAmFeZr4Ej3tMwxywam7hHdRtCzgKagQBKcYIJuya8gzYrTqUoukzXtWJaA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.4.0" @@ -2231,7 +2231,7 @@ "version": "7.4.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.4.0.tgz", "integrity": "sha512-IXPOYskT89UTVsntuSnMTiKRWCuTg5JMWflgEDV1OSKFpuhwP5vqbfF01/iwo9y6rCjR0sDIO+jdV5kq38/hgA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.4.0", @@ -2243,7 +2243,7 @@ "version": "7.4.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.0.tgz", "integrity": "sha512-fOUIoGzAPgtjHVs4DsVSnEDPBEauAmFeZr4Ej3tMwxywam7hHdRtCzgKagQBKcYIJuya8gzYrTqUoukzXtWJaA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.4.0" @@ -2253,7 +2253,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.2.0" @@ -2263,21 +2263,21 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@prisma/query-plan-executor": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@prisma/studio-core": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.13.1.tgz", "integrity": "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", @@ -2321,7 +2321,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@stellar/js-xdr": { @@ -2374,25 +2374,25 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true + "dev": true }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", @@ -2628,17 +2628,6 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } - }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", @@ -3184,7 +3173,7 @@ "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "devOptional": true, + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -3205,7 +3194,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "devOptional": true, + "dev": true, "dependencies": { "acorn": "^8.11.0" }, @@ -3300,7 +3289,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true + "dev": true }, "node_modules/argparse": { "version": "2.0.1", @@ -3340,7 +3329,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 6.0.0" @@ -3664,7 +3653,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.3", @@ -3693,7 +3682,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -3709,7 +3698,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -3870,7 +3859,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", @@ -3925,7 +3914,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "consola": "^3.2.3" @@ -4093,14 +4082,14 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" @@ -4171,7 +4160,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true + "dev": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -4186,14 +4175,6 @@ "node": ">= 8" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT", - "peer": true - }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -4250,7 +4231,7 @@ "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=16.0.0" @@ -4277,7 +4258,7 @@ "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/delayed-stream": { @@ -4293,7 +4274,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=0.10" @@ -4311,7 +4292,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/detect-newline": { @@ -4339,7 +4320,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.3.1" } @@ -4392,7 +4373,7 @@ "version": "3.18.4", "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -4428,7 +4409,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -4943,14 +4924,14 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "devOptional": true, + "dev": true, "funding": [ { "type": "individual", @@ -5273,7 +5254,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-property": "^1.0.2" @@ -5334,7 +5315,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/get-proto": { @@ -5379,7 +5360,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "citty": "^0.1.6", @@ -5451,14 +5432,14 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/grammex": { "version": "3.1.12", "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/graphemer": { @@ -5471,7 +5452,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/handlebars": { @@ -5566,7 +5547,7 @@ "version": "4.11.4", "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=16.9.0" @@ -5598,7 +5579,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/human-signals": { @@ -5814,7 +5795,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/is-retry-allowed": { @@ -6626,7 +6607,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -6781,7 +6762,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -6813,7 +6794,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/lodash.includes": { @@ -6875,7 +6856,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/lru-cache": { @@ -6887,7 +6868,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "bun": ">=1.0.0", @@ -6919,7 +6900,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true + "dev": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -7080,7 +7061,7 @@ "version": "3.15.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", @@ -7101,7 +7082,7 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7118,7 +7099,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "lru.min": "^1.1.0" @@ -7168,7 +7149,7 @@ "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/node-int64": { @@ -7261,7 +7242,7 @@ "version": "0.6.5", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "citty": "^0.2.0", @@ -7279,7 +7260,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/object-assign": { @@ -7305,7 +7286,7 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/on-finished": { @@ -7498,14 +7479,14 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/pg": { @@ -7696,7 +7677,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "confbox": "^0.2.2", @@ -7717,7 +7698,7 @@ "version": "3.4.7", "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", - "devOptional": true, + "dev": true, "license": "Unlicense", "engines": { "node": ">=12" @@ -7830,7 +7811,7 @@ "version": "7.4.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.4.0.tgz", "integrity": "sha512-n2xU9vSaH4uxZF/l2aKoGYtKtC7BL936jM9Q94Syk1zOD39t/5hjDUxMgaPkVRDX5wWEMsIqvzQxoebNIesOKw==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -7864,7 +7845,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -7876,7 +7857,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/proxy-addr": { @@ -7916,7 +7897,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "individual", @@ -7999,38 +7980,13 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, - "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "devOptional": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "devOptional": true, - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -8059,14 +8015,14 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/remeda": { "version": "2.33.4", "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/remeda" @@ -8126,7 +8082,7 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -8204,14 +8160,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "devOptional": true, - "license": "MIT", - "peer": true - }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -8249,7 +8197,7 @@ "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", - "devOptional": true + "dev": true }, "node_modules/serve-static": { "version": "2.2.0", @@ -8482,7 +8430,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8523,7 +8471,7 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/string-length": { @@ -8812,7 +8760,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -8971,7 +8919,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "devOptional": true, + "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9205,7 +9153,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "devOptional": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9366,7 +9314,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true + "dev": true }, "node_modules/v8-to-istanbul": { "version": "9.3.0", @@ -9398,7 +9346,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "typescript": ">=5" @@ -9670,7 +9618,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6" } @@ -9691,7 +9639,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "grammex": "^3.1.11", diff --git a/src/controllers/merchant.controllers.ts b/src/controllers/merchant.controllers.ts index 2840499..91b5353 100644 --- a/src/controllers/merchant.controllers.ts +++ b/src/controllers/merchant.controllers.ts @@ -4,8 +4,10 @@ import { getMerchant, listMerchants, registerMerchant, + getMyProfile, + updateMyProfile, } from '../services/merchant.services.js'; -import { validateRegisterMerchant } from '../utils/validation.js'; +import { validateRegisterMerchant, validateUpdateMerchant } from '../utils/validation.js'; import { AppError } from '../utils/errors.js'; export const createMerchantController = async (req: Request, res: Response) => { @@ -60,3 +62,49 @@ export const registerMerchantController = async (req: Request, res: Response): P res.status(500).json({ error: 'Internal Server Error' }); } }; + +export const getMyProfileController = async (req: Request, res: Response): Promise => { + const merchant = req.merchant; + + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + + try { + const profile = await getMyProfile(merchant.id); + res.status(200).json(profile); + } catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; + +export const updateMyProfileController = async (req: Request, res: Response): Promise => { + const merchant = req.merchant; + + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + + const errors = validateUpdateMerchant(req.body); + if (Object.keys(errors).length > 0) { + res.status(400).json({ error: 'Validation failed', errors }); + return; + } + + try { + const profile = await updateMyProfile(merchant.id, req.body); + res.status(200).json(profile); + } catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; diff --git a/src/routes/merchant.routes.ts b/src/routes/merchant.routes.ts index 0648d50..758e991 100644 --- a/src/routes/merchant.routes.ts +++ b/src/routes/merchant.routes.ts @@ -4,12 +4,16 @@ import { getMerchantController, listMerchantsController, registerMerchantController, + getMyProfileController, + updateMyProfileController, } from '../controllers/merchant.controllers.js'; import { authenticateMerchant } from '../middlewares/auth.middleware.js'; const router = Router(); router.post('/register', authenticateMerchant, registerMerchantController); +router.get('/me', authenticateMerchant, getMyProfileController); +router.patch('/me', authenticateMerchant, updateMyProfileController); router.post('/', createMerchantController); router.get('/:id', getMerchantController); router.get('/', listMerchantsController); diff --git a/src/services/merchant.services.ts b/src/services/merchant.services.ts index 91ef296..e4c4e59 100644 --- a/src/services/merchant.services.ts +++ b/src/services/merchant.services.ts @@ -1,7 +1,7 @@ -import { Merchant } from '@prisma/client'; +import { Merchant, Prisma } from '@prisma/client'; import prisma from '../config/prisma.js'; import { AppError } from '../utils/errors.js'; -import { RegisterMerchantInput } from '../utils/validation.js'; +import { RegisterMerchantInput, UpdateMerchantInput } from '../utils/validation.js'; import { sendOtpEmail } from './otp.services.js'; interface MerchantData { @@ -21,12 +21,14 @@ export const sanitizeMerchant = (merchant: Merchant) => ({ merchantId: merchant.merchantId, email: merchant.email, address: merchant.address, + account: merchant.account, firstName: merchant.firstName, lastName: merchant.lastName, businessName: merchant.businessName, category: merchant.category, description: merchant.description, logo: merchant.logo, + webhook: merchant.webhook, active: merchant.active, verified: merchant.verified, emailVerified: merchant.emailVerified, @@ -123,3 +125,49 @@ export const registerMerchant = async (merchantId: string, data: RegisterMerchan return sanitizeMerchant(updatedMerchant); }; + +/** + * Returns the authenticated merchant's own profile. + */ +export const getMyProfile = async (id: string) => { + const merchant = await prisma.merchant.findUnique({ where: { id } }); + + if (!merchant) { + throw new AppError(404, 'Merchant not found'); + } + + return sanitizeMerchant(merchant); +}; + +/** + * Partially updates the authenticated merchant's editable profile fields. + * + * Only fields present in `data` are written. Strings are trimmed; an empty + * `logo`/`webhook` is normalized to null so the merchant can clear them. + * Non-editable fields are never read here, so they cannot be changed. + */ +export const updateMyProfile = async (id: string, data: UpdateMerchantInput) => { + const updateData: Prisma.MerchantUpdateInput = {}; + + const textFields = ['firstName', 'lastName', 'businessName', 'category', 'description'] as const; + for (const field of textFields) { + const value = data[field]; + if (value !== undefined) { + updateData[field] = value.trim(); + } + } + + if (data.logo !== undefined) { + const logo = typeof data.logo === 'string' ? data.logo.trim() : data.logo; + updateData.logo = logo ? logo : null; + } + + if (data.webhook !== undefined) { + const webhook = typeof data.webhook === 'string' ? data.webhook.trim() : data.webhook; + updateData.webhook = webhook ? webhook : null; + } + + const updated = await prisma.merchant.update({ where: { id }, data: updateData }); + + return sanitizeMerchant(updated); +}; diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 68b739a..f1eb583 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -44,3 +44,70 @@ export const validateRegisterMerchant = (body: unknown): ValidationErrors => { return errors; }; + +export interface UpdateMerchantInput { + firstName?: string; + lastName?: string; + businessName?: string; + category?: string; + description?: string; + logo?: string | null; + webhook?: string | null; +} + +const EDITABLE_MERCHANT_FIELDS = [ + 'firstName', + 'lastName', + 'businessName', + 'category', + 'description', + 'logo', + 'webhook', +] as const; + +const UPDATE_REQUIRED_TEXT_FIELDS = [ + 'firstName', + 'lastName', + 'businessName', + 'category', + 'description', +] as const; + +const isValidHttpsUrl = (value: string): boolean => { + try { + return new URL(value).protocol === 'https:'; + } catch { + return false; + } +}; + +export const validateUpdateMerchant = (body: unknown): ValidationErrors => { + const errors: ValidationErrors = {}; + const payload = (body ?? {}) as Record; + + const present = EDITABLE_MERCHANT_FIELDS.filter(field => payload[field] !== undefined); + if (present.length === 0) { + errors._empty = 'At least one valid field is required'; + return errors; + } + + for (const field of UPDATE_REQUIRED_TEXT_FIELDS) { + if (payload[field] !== undefined && !isNonEmptyString(payload[field])) { + errors[field] = `${field} must be a non-empty string`; + } + } + + if (payload.logo !== undefined && payload.logo !== null && typeof payload.logo !== 'string') { + errors.logo = 'logo must be a string or null'; + } + + if (payload.webhook !== undefined && payload.webhook !== null) { + if (typeof payload.webhook !== 'string') { + errors.webhook = 'webhook must be a string or null'; + } else if (payload.webhook.trim().length > 0 && !isValidHttpsUrl(payload.webhook.trim())) { + errors.webhook = 'webhook must be a valid HTTPS URL'; + } + } + + return errors; +}; diff --git a/tests/integration/merchant.profile.test.ts b/tests/integration/merchant.profile.test.ts new file mode 100644 index 0000000..160a05c --- /dev/null +++ b/tests/integration/merchant.profile.test.ts @@ -0,0 +1,150 @@ +import { jest } from '@jest/globals'; +import { mockReset } from 'jest-mock-extended'; +import request from 'supertest'; + +const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; +const { default: app } = await import('../../src/app.js'); + +const ME_URL = '/api/v1/merchants/me'; + +const baseMerchant = { + id: 'uuid-1', + merchantId: 1, + address: '0x123', + account: 'CCONTRACT', + email: 'ada@example.com', + firstName: 'Ada', + lastName: 'Lovelace', + businessName: 'Analytical Engines', + category: 'software', + description: 'We build computing machines.', + logo: null, + webhook: null, + active: true, + verified: false, + emailVerified: false, + registered: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + +const authenticateAs = (merchant: Record) => { + prismaMock.refreshToken.findUnique.mockResolvedValue({ + id: 'session-1', + merchantId: merchant.id, + token: 'valid-token', + expiresAt: new Date(Date.now() + 60 * 60 * 1000), + createdAt: new Date(), + merchant, + } as any); +}; + +describe('GET /api/v1/merchants/me', () => { + beforeEach(() => mockReset(prismaMock)); + + test('returns 401 when unauthenticated', async () => { + const response = await request(app).get(ME_URL); + expect(response.status).toBe(401); + }); + + test('returns 200 with the full profile and no internal fields', async () => { + authenticateAs(baseMerchant); + prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any); + + const response = await request(app).get(ME_URL).set('Authorization', 'Bearer valid-token'); + + expect(response.status).toBe(200); + expect(response.body).toMatchObject({ id: 'uuid-1', account: 'CCONTRACT', webhook: null }); + expect(response.body).not.toHaveProperty('refreshTokens'); + expect(response.body).not.toHaveProperty('apiKeys'); + }); +}); + +describe('PATCH /api/v1/merchants/me', () => { + beforeEach(() => mockReset(prismaMock)); + + test('returns 401 when unauthenticated', async () => { + const response = await request(app).patch(ME_URL).send({ firstName: 'Grace' }); + expect(response.status).toBe(401); + expect(prismaMock.merchant.update).not.toHaveBeenCalled(); + }); + + test('updates a valid partial payload and returns 200', async () => { + authenticateAs(baseMerchant); + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + + const response = await request(app) + .patch(ME_URL) + .set('Authorization', 'Bearer valid-token') + .send({ firstName: 'Grace', webhook: 'https://example.com/hook' }); + + expect(response.status).toBe(200); + expect(response.body).toMatchObject({ firstName: 'Grace', webhook: 'https://example.com/hook' }); + }); + + test('silently ignores non-editable fields (address/email)', async () => { + authenticateAs(baseMerchant); + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + + const response = await request(app) + .patch(ME_URL) + .set('Authorization', 'Bearer valid-token') + .send({ firstName: 'Grace', address: '0xHACK', email: 'evil@example.com' }); + + expect(response.status).toBe(200); + const updateArg = prismaMock.merchant.update.mock.calls[0][0]; + expect(updateArg.data).toEqual({ firstName: 'Grace' }); + expect(response.body.address).toBe('0x123'); + expect(response.body.email).toBe('ada@example.com'); + }); + + test('returns 400 for an invalid (non-HTTPS) webhook', async () => { + authenticateAs(baseMerchant); + + const response = await request(app) + .patch(ME_URL) + .set('Authorization', 'Bearer valid-token') + .send({ webhook: 'http://example.com/hook' }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Validation failed'); + expect(prismaMock.merchant.update).not.toHaveBeenCalled(); + }); + + test('clears the webhook when sent null', async () => { + authenticateAs(baseMerchant); + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + + const response = await request(app) + .patch(ME_URL) + .set('Authorization', 'Bearer valid-token') + .send({ webhook: null }); + + expect(response.status).toBe(200); + expect(response.body.webhook).toBeNull(); + }); + + test('returns 400 for a required text field sent empty', async () => { + authenticateAs(baseMerchant); + + const response = await request(app) + .patch(ME_URL) + .set('Authorization', 'Bearer valid-token') + .send({ firstName: '' }); + + expect(response.status).toBe(400); + expect(prismaMock.merchant.update).not.toHaveBeenCalled(); + }); + + test('returns 400 for an empty payload', async () => { + authenticateAs(baseMerchant); + + const response = await request(app) + .patch(ME_URL) + .set('Authorization', 'Bearer valid-token') + .send({}); + + expect(response.status).toBe(400); + expect(prismaMock.merchant.update).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/merchant.profile.services.test.ts b/tests/unit/merchant.profile.services.test.ts new file mode 100644 index 0000000..af6c46f --- /dev/null +++ b/tests/unit/merchant.profile.services.test.ts @@ -0,0 +1,85 @@ +import { jest } from '@jest/globals'; +import { mockReset } from 'jest-mock-extended'; + +const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; +const { getMyProfile, updateMyProfile } = await import('../../src/services/merchant.services.js'); + +const baseMerchant = { + id: 'uuid-1', + merchantId: 1, + address: '0x123', + account: 'CCONTRACT', + email: 'ada@example.com', + firstName: 'Ada', + lastName: 'Lovelace', + businessName: 'Analytical Engines', + category: 'software', + description: 'We build computing machines.', + logo: 'https://example.com/logo.png', + webhook: null, + active: true, + verified: false, + emailVerified: false, + registered: true, + createdAt: new Date(), + updatedAt: new Date(), +}; + +describe('getMyProfile', () => { + beforeEach(() => mockReset(prismaMock)); + + test('returns the sanitized profile including account and webhook', async () => { + prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant); + + const result = await getMyProfile('uuid-1'); + + expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({ where: { id: 'uuid-1' } }); + expect(result).toMatchObject({ id: 'uuid-1', account: 'CCONTRACT', webhook: null }); + expect(result).not.toHaveProperty('refreshTokens'); + expect(result).not.toHaveProperty('apiKeys'); + }); + + test('throws AppError(404) when the merchant does not exist', async () => { + prismaMock.merchant.findUnique.mockResolvedValue(null); + + await expect(getMyProfile('missing')).rejects.toMatchObject({ statusCode: 404 }); + }); +}); + +describe('updateMyProfile', () => { + beforeEach(() => mockReset(prismaMock)); + + test('writes only the editable fields present in the payload, trimmed', async () => { + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + + await updateMyProfile('uuid-1', { + firstName: ' Grace ', + webhook: 'https://example.com/hook', + }); + + expect(prismaMock.merchant.update).toHaveBeenCalledWith({ + where: { id: 'uuid-1' }, + data: { firstName: 'Grace', webhook: 'https://example.com/hook' }, + }); + }); + + test('normalizes a cleared logo/webhook to null', async () => { + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + + await updateMyProfile('uuid-1', { logo: '', webhook: null }); + + expect(prismaMock.merchant.update).toHaveBeenCalledWith({ + where: { id: 'uuid-1' }, + data: { logo: null, webhook: null }, + }); + }); + + test('returns the sanitized updated profile', async () => { + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + + const result = await updateMyProfile('uuid-1', { businessName: 'New Co' }); + + expect(result).toMatchObject({ businessName: 'New Co' }); + expect(result).not.toHaveProperty('refreshTokens'); + }); +}); diff --git a/tests/unit/merchant.update.validation.test.ts b/tests/unit/merchant.update.validation.test.ts new file mode 100644 index 0000000..4eab43a --- /dev/null +++ b/tests/unit/merchant.update.validation.test.ts @@ -0,0 +1,49 @@ +import { validateUpdateMerchant } from '../../src/utils/validation.js'; + +describe('validateUpdateMerchant', () => { + test('accepts a valid partial payload', () => { + expect(validateUpdateMerchant({ firstName: 'Ada' })).toEqual({}); + }); + + test('rejects an empty payload (no editable fields)', () => { + const errors = validateUpdateMerchant({}); + expect(errors._empty).toEqual(expect.any(String)); + }); + + test('treats a payload of only non-editable fields as empty', () => { + const errors = validateUpdateMerchant({ address: '0xabc', email: 'x@y.com', merchantId: 5 }); + expect(errors._empty).toEqual(expect.any(String)); + }); + + test('rejects a required text field sent as empty string', () => { + const errors = validateUpdateMerchant({ firstName: ' ' }); + expect(errors.firstName).toEqual(expect.any(String)); + }); + + test('accepts logo and webhook cleared with null', () => { + expect(validateUpdateMerchant({ logo: null, webhook: null })).toEqual({}); + }); + + test('accepts an empty string webhook as a clear', () => { + expect(validateUpdateMerchant({ webhook: '' })).toEqual({}); + }); + + test('rejects a non-HTTPS webhook URL', () => { + const errors = validateUpdateMerchant({ webhook: 'http://example.com/hook' }); + expect(errors.webhook).toEqual(expect.any(String)); + }); + + test('rejects a malformed webhook URL', () => { + const errors = validateUpdateMerchant({ webhook: 'not-a-url' }); + expect(errors.webhook).toEqual(expect.any(String)); + }); + + test('accepts a valid HTTPS webhook URL', () => { + expect(validateUpdateMerchant({ webhook: 'https://example.com/hook' })).toEqual({}); + }); + + test('rejects a non-string logo', () => { + const errors = validateUpdateMerchant({ logo: 123 }); + expect(errors.logo).toEqual(expect.any(String)); + }); +}); From b7308aa9dcc37a10d2c770a87538b079bf7744a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Ag=C3=BCero?= <54730752+EmmanuelAR@users.noreply.github.com> Date: Fri, 26 Jun 2026 23:25:27 -0600 Subject: [PATCH 4/7] chore: remove outdated Merchant Profile API documentation files - Deleted the implementation plan and design spec for the Merchant Profile API as they are no longer relevant to the current project structure. --- .../plans/2026-06-26-merchant-profile-api.md | 699 ------------------ .../2026-06-26-merchant-profile-api-design.md | 128 ---- 2 files changed, 827 deletions(-) delete mode 100644 docs/superpowers/plans/2026-06-26-merchant-profile-api.md delete mode 100644 docs/superpowers/specs/2026-06-26-merchant-profile-api-design.md diff --git a/docs/superpowers/plans/2026-06-26-merchant-profile-api.md b/docs/superpowers/plans/2026-06-26-merchant-profile-api.md deleted file mode 100644 index 0ab20af..0000000 --- a/docs/superpowers/plans/2026-06-26-merchant-profile-api.md +++ /dev/null @@ -1,699 +0,0 @@ -# Merchant Profile API (Get & Update) Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add `GET /merchants/me` and `PATCH /merchants/me` so an authenticated merchant can view and partially update their own profile. - -**Architecture:** Follows the existing layering — routes → thin controllers → services (prisma + `AppError`) → `sanitizeMerchant` allow-list for output. A new `validateUpdateMerchant` validator gates the PATCH payload; the service writes only editable fields; output goes through the existing allow-list so internal fields/relations are never exposed. - -**Tech Stack:** TypeScript (ESM), Express v5, Prisma, Jest + ts-jest (ESM), supertest, jest-mock-extended. - -## Global Constraints - -- Base API path is `/api/v1` — merchant routes are mounted at `/api/v1/merchants`. -- ESM project: all relative imports MUST use the `.js` extension (e.g. `'../utils/errors.js'`). -- Tests live under `tests/` (`tests/unit/`, `tests/integration/`) and use the shared `prismaMock` from `tests/__mocks__/prisma.ts` (imported via `await import('../../src/config/prisma.js')`). -- Auth is simulated by mocking `prismaMock.refreshToken.findUnique` to return a session with `expiresAt` in the future and a `merchant` object (see existing `authenticateAs` helper). -- Editable fields: `firstName`, `lastName`, `businessName`, `category`, `description`, `logo`, `webhook`. Non-editable (silently ignored): everything else, explicitly `address`, `email`, `merchantId`, `account`. -- Run a single test file with: `npm test -- `. - ---- - -### Task 1: `validateUpdateMerchant` validator - -**Files:** -- Modify: `src/utils/validation.ts` -- Test: `tests/unit/merchant.update.validation.test.ts` (create) - -**Interfaces:** -- Consumes: existing `ValidationErrors` type and `isNonEmptyString` helper in `src/utils/validation.ts`. -- Produces: - - `interface UpdateMerchantInput { firstName?: string; lastName?: string; businessName?: string; category?: string; description?: string; logo?: string | null; webhook?: string | null; }` - - `validateUpdateMerchant(body: unknown): ValidationErrors` — returns `{}` when valid; on the empty-payload case returns `{ _empty: '...' }`. - -- [ ] **Step 1: Write the failing tests** - -Create `tests/unit/merchant.update.validation.test.ts`: - -```ts -import { validateUpdateMerchant } from '../../src/utils/validation.js'; - -describe('validateUpdateMerchant', () => { - test('accepts a valid partial payload', () => { - expect(validateUpdateMerchant({ firstName: 'Ada' })).toEqual({}); - }); - - test('rejects an empty payload (no editable fields)', () => { - const errors = validateUpdateMerchant({}); - expect(errors._empty).toEqual(expect.any(String)); - }); - - test('treats a payload of only non-editable fields as empty', () => { - const errors = validateUpdateMerchant({ address: '0xabc', email: 'x@y.com', merchantId: 5 }); - expect(errors._empty).toEqual(expect.any(String)); - }); - - test('rejects a required text field sent as empty string', () => { - const errors = validateUpdateMerchant({ firstName: ' ' }); - expect(errors.firstName).toEqual(expect.any(String)); - }); - - test('accepts logo and webhook cleared with null', () => { - expect(validateUpdateMerchant({ logo: null, webhook: null })).toEqual({}); - }); - - test('accepts an empty string webhook as a clear', () => { - expect(validateUpdateMerchant({ webhook: '' })).toEqual({}); - }); - - test('rejects a non-HTTPS webhook URL', () => { - const errors = validateUpdateMerchant({ webhook: 'http://example.com/hook' }); - expect(errors.webhook).toEqual(expect.any(String)); - }); - - test('rejects a malformed webhook URL', () => { - const errors = validateUpdateMerchant({ webhook: 'not-a-url' }); - expect(errors.webhook).toEqual(expect.any(String)); - }); - - test('accepts a valid HTTPS webhook URL', () => { - expect(validateUpdateMerchant({ webhook: 'https://example.com/hook' })).toEqual({}); - }); - - test('rejects a non-string logo', () => { - const errors = validateUpdateMerchant({ logo: 123 }); - expect(errors.logo).toEqual(expect.any(String)); - }); -}); -``` - -- [ ] **Step 2: Run the tests to verify they fail** - -Run: `npm test -- tests/unit/merchant.update.validation.test.ts` -Expected: FAIL — `validateUpdateMerchant` is not exported / not a function. - -- [ ] **Step 3: Implement the validator** - -Append to `src/utils/validation.ts` (keep the existing `EMAIL_REGEX`, `isNonEmptyString`, `validateRegisterMerchant`): - -```ts -export interface UpdateMerchantInput { - firstName?: string; - lastName?: string; - businessName?: string; - category?: string; - description?: string; - logo?: string | null; - webhook?: string | null; -} - -const EDITABLE_MERCHANT_FIELDS = [ - 'firstName', - 'lastName', - 'businessName', - 'category', - 'description', - 'logo', - 'webhook', -] as const; - -const UPDATE_REQUIRED_TEXT_FIELDS = [ - 'firstName', - 'lastName', - 'businessName', - 'category', - 'description', -] as const; - -const isValidHttpsUrl = (value: string): boolean => { - try { - return new URL(value).protocol === 'https:'; - } catch { - return false; - } -}; - -export const validateUpdateMerchant = (body: unknown): ValidationErrors => { - const errors: ValidationErrors = {}; - const payload = (body ?? {}) as Record; - - const present = EDITABLE_MERCHANT_FIELDS.filter((field) => payload[field] !== undefined); - if (present.length === 0) { - errors._empty = 'At least one valid field is required'; - return errors; - } - - for (const field of UPDATE_REQUIRED_TEXT_FIELDS) { - if (payload[field] !== undefined && !isNonEmptyString(payload[field])) { - errors[field] = `${field} must be a non-empty string`; - } - } - - if (payload.logo !== undefined && payload.logo !== null && typeof payload.logo !== 'string') { - errors.logo = 'logo must be a string or null'; - } - - if (payload.webhook !== undefined && payload.webhook !== null) { - if (typeof payload.webhook !== 'string') { - errors.webhook = 'webhook must be a string or null'; - } else if (payload.webhook.trim().length > 0 && !isValidHttpsUrl(payload.webhook.trim())) { - errors.webhook = 'webhook must be a valid HTTPS URL'; - } - } - - return errors; -}; -``` - -- [ ] **Step 4: Run the tests to verify they pass** - -Run: `npm test -- tests/unit/merchant.update.validation.test.ts` -Expected: PASS (all 10 tests). - -- [ ] **Step 5: Commit** - -```bash -git add src/utils/validation.ts tests/unit/merchant.update.validation.test.ts -git commit -m "feat: add validateUpdateMerchant validator (#11)" -``` - ---- - -### Task 2: Service layer — `getMyProfile`, `updateMyProfile`, extend `sanitizeMerchant` - -**Files:** -- Modify: `src/services/merchant.services.ts` -- Test: `tests/unit/merchant.profile.services.test.ts` (create) - -**Interfaces:** -- Consumes: `prisma` (`../config/prisma.js`), `AppError` (`../utils/errors.js`), `UpdateMerchantInput` (`../utils/validation.js`), existing `sanitizeMerchant`. -- Produces: - - `getMyProfile(id: string): Promise>` — throws `AppError(404, 'Merchant not found')` when missing. - - `updateMyProfile(id: string, data: UpdateMerchantInput): Promise>` — writes only editable fields present in `data`; trims strings; normalizes empty `logo`/`webhook` to `null`. - - `sanitizeMerchant` now additionally returns `account` and `webhook`. - -- [ ] **Step 1: Write the failing tests** - -Create `tests/unit/merchant.profile.services.test.ts`: - -```ts -import { jest } from '@jest/globals'; -import { mockReset } from 'jest-mock-extended'; - -const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; -const { getMyProfile, updateMyProfile } = await import('../../src/services/merchant.services.js'); - -const baseMerchant = { - id: 'uuid-1', - merchantId: 1, - address: '0x123', - account: 'CCONTRACT', - email: 'ada@example.com', - firstName: 'Ada', - lastName: 'Lovelace', - businessName: 'Analytical Engines', - category: 'software', - description: 'We build computing machines.', - logo: 'https://example.com/logo.png', - webhook: null, - active: true, - verified: false, - emailVerified: false, - registered: true, - createdAt: new Date(), - updatedAt: new Date(), -}; - -describe('getMyProfile', () => { - beforeEach(() => mockReset(prismaMock)); - - test('returns the sanitized profile including account and webhook', async () => { - prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant); - - const result = await getMyProfile('uuid-1'); - - expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({ where: { id: 'uuid-1' } }); - expect(result).toMatchObject({ id: 'uuid-1', account: 'CCONTRACT', webhook: null }); - expect(result).not.toHaveProperty('refreshTokens'); - expect(result).not.toHaveProperty('apiKeys'); - }); - - test('throws AppError(404) when the merchant does not exist', async () => { - prismaMock.merchant.findUnique.mockResolvedValue(null); - - await expect(getMyProfile('missing')).rejects.toMatchObject({ statusCode: 404 }); - }); -}); - -describe('updateMyProfile', () => { - beforeEach(() => mockReset(prismaMock)); - - test('writes only the editable fields present in the payload, trimmed', async () => { - prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); - - await updateMyProfile('uuid-1', { - firstName: ' Grace ', - webhook: 'https://example.com/hook', - }); - - expect(prismaMock.merchant.update).toHaveBeenCalledWith({ - where: { id: 'uuid-1' }, - data: { firstName: 'Grace', webhook: 'https://example.com/hook' }, - }); - }); - - test('normalizes a cleared logo/webhook to null', async () => { - prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); - - await updateMyProfile('uuid-1', { logo: '', webhook: null }); - - expect(prismaMock.merchant.update).toHaveBeenCalledWith({ - where: { id: 'uuid-1' }, - data: { logo: null, webhook: null }, - }); - }); - - test('returns the sanitized updated profile', async () => { - prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); - - const result = await updateMyProfile('uuid-1', { businessName: 'New Co' }); - - expect(result).toMatchObject({ businessName: 'New Co' }); - expect(result).not.toHaveProperty('refreshTokens'); - }); -}); -``` - -- [ ] **Step 2: Run the tests to verify they fail** - -Run: `npm test -- tests/unit/merchant.profile.services.test.ts` -Expected: FAIL — `getMyProfile`/`updateMyProfile` not exported. - -- [ ] **Step 3: Extend `sanitizeMerchant` and add the service functions** - -In `src/services/merchant.services.ts`, update the imports line to also import `Prisma` and `UpdateMerchantInput`: - -```ts -import { Merchant, Prisma } from '@prisma/client'; -import prisma from '../config/prisma.js'; -import { AppError } from '../utils/errors.js'; -import { RegisterMerchantInput, UpdateMerchantInput } from '../utils/validation.js'; -import { sendOtpEmail } from './otp.services.js'; -``` - -Add `account` and `webhook` to the `sanitizeMerchant` allow-list (place `account` after `address`, `webhook` after `logo`): - -```ts -export const sanitizeMerchant = (merchant: Merchant) => ({ - id: merchant.id, - merchantId: merchant.merchantId, - email: merchant.email, - address: merchant.address, - account: merchant.account, - firstName: merchant.firstName, - lastName: merchant.lastName, - businessName: merchant.businessName, - category: merchant.category, - description: merchant.description, - logo: merchant.logo, - webhook: merchant.webhook, - active: merchant.active, - verified: merchant.verified, - emailVerified: merchant.emailVerified, - registered: merchant.registered, - createdAt: merchant.createdAt, - updatedAt: merchant.updatedAt, -}); -``` - -Append the two new service functions at the end of the file: - -```ts -/** - * Returns the authenticated merchant's own profile. - */ -export const getMyProfile = async (id: string) => { - const merchant = await prisma.merchant.findUnique({ where: { id } }); - - if (!merchant) { - throw new AppError(404, 'Merchant not found'); - } - - return sanitizeMerchant(merchant); -}; - -/** - * Partially updates the authenticated merchant's editable profile fields. - * - * Only fields present in `data` are written. Strings are trimmed; an empty - * `logo`/`webhook` is normalized to null so the merchant can clear them. - * Non-editable fields are never read here, so they cannot be changed. - */ -export const updateMyProfile = async (id: string, data: UpdateMerchantInput) => { - const updateData: Prisma.MerchantUpdateInput = {}; - - const textFields = ['firstName', 'lastName', 'businessName', 'category', 'description'] as const; - for (const field of textFields) { - const value = data[field]; - if (value !== undefined) { - updateData[field] = value.trim(); - } - } - - if (data.logo !== undefined) { - const logo = typeof data.logo === 'string' ? data.logo.trim() : data.logo; - updateData.logo = logo ? logo : null; - } - - if (data.webhook !== undefined) { - const webhook = typeof data.webhook === 'string' ? data.webhook.trim() : data.webhook; - updateData.webhook = webhook ? webhook : null; - } - - const updated = await prisma.merchant.update({ where: { id }, data: updateData }); - - return sanitizeMerchant(updated); -}; -``` - -- [ ] **Step 4: Run the tests to verify they pass** - -Run: `npm test -- tests/unit/merchant.profile.services.test.ts` -Expected: PASS (all 5 tests). - -- [ ] **Step 5: Verify the existing service/register tests still pass** - -Run: `npm test -- tests/unit/merchant.services.test.ts tests/integration/merchant.register.test.ts` -Expected: PASS — `sanitizeMerchant` additions are additive (register integration uses `toMatchObject`, so extra fields are fine). - -- [ ] **Step 6: Commit** - -```bash -git add src/services/merchant.services.ts tests/unit/merchant.profile.services.test.ts -git commit -m "feat: add getMyProfile/updateMyProfile services and expose account/webhook (#11)" -``` - ---- - -### Task 3: Controllers + routes wiring - -**Files:** -- Modify: `src/controllers/merchant.controllers.ts` -- Modify: `src/routes/merchant.routes.ts` -- Test: `tests/integration/merchant.profile.test.ts` (create) - -**Interfaces:** -- Consumes: `getMyProfile`, `updateMyProfile` (`../services/merchant.services.js`); `validateUpdateMerchant` (`../utils/validation.js`); `AppError` (`../utils/errors.js`); `authenticateMerchant` (`../middlewares/auth.middleware.js`); `req.merchant`. -- Produces: `getMyProfileController`, `updateMyProfileController` (Express handlers) and the routes `GET /merchants/me`, `PATCH /merchants/me`. - -- [ ] **Step 1: Write the failing integration tests** - -Create `tests/integration/merchant.profile.test.ts`: - -```ts -import { jest } from '@jest/globals'; -import { mockReset } from 'jest-mock-extended'; -import request from 'supertest'; - -const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; -const { default: app } = await import('../../src/app.js'); - -const ME_URL = '/api/v1/merchants/me'; - -const baseMerchant = { - id: 'uuid-1', - merchantId: 1, - address: '0x123', - account: 'CCONTRACT', - email: 'ada@example.com', - firstName: 'Ada', - lastName: 'Lovelace', - businessName: 'Analytical Engines', - category: 'software', - description: 'We build computing machines.', - logo: null, - webhook: null, - active: true, - verified: false, - emailVerified: false, - registered: true, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), -}; - -const authenticateAs = (merchant: Record) => { - prismaMock.refreshToken.findUnique.mockResolvedValue({ - id: 'session-1', - merchantId: merchant.id, - token: 'valid-token', - expiresAt: new Date(Date.now() + 60 * 60 * 1000), - createdAt: new Date(), - merchant, - } as any); -}; - -describe('GET /api/v1/merchants/me', () => { - beforeEach(() => mockReset(prismaMock)); - - test('returns 401 when unauthenticated', async () => { - const response = await request(app).get(ME_URL); - expect(response.status).toBe(401); - }); - - test('returns 200 with the full profile and no internal fields', async () => { - authenticateAs(baseMerchant); - prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any); - - const response = await request(app).get(ME_URL).set('Authorization', 'Bearer valid-token'); - - expect(response.status).toBe(200); - expect(response.body).toMatchObject({ id: 'uuid-1', account: 'CCONTRACT', webhook: null }); - expect(response.body).not.toHaveProperty('refreshTokens'); - expect(response.body).not.toHaveProperty('apiKeys'); - }); -}); - -describe('PATCH /api/v1/merchants/me', () => { - beforeEach(() => mockReset(prismaMock)); - - test('returns 401 when unauthenticated', async () => { - const response = await request(app).patch(ME_URL).send({ firstName: 'Grace' }); - expect(response.status).toBe(401); - expect(prismaMock.merchant.update).not.toHaveBeenCalled(); - }); - - test('updates a valid partial payload and returns 200', async () => { - authenticateAs(baseMerchant); - prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); - - const response = await request(app) - .patch(ME_URL) - .set('Authorization', 'Bearer valid-token') - .send({ firstName: 'Grace', webhook: 'https://example.com/hook' }); - - expect(response.status).toBe(200); - expect(response.body).toMatchObject({ firstName: 'Grace', webhook: 'https://example.com/hook' }); - }); - - test('silently ignores non-editable fields (address/email)', async () => { - authenticateAs(baseMerchant); - prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); - - const response = await request(app) - .patch(ME_URL) - .set('Authorization', 'Bearer valid-token') - .send({ firstName: 'Grace', address: '0xHACK', email: 'evil@example.com' }); - - expect(response.status).toBe(200); - const updateArg = prismaMock.merchant.update.mock.calls[0][0]; - expect(updateArg.data).toEqual({ firstName: 'Grace' }); - expect(response.body.address).toBe('0x123'); - expect(response.body.email).toBe('ada@example.com'); - }); - - test('returns 400 for an invalid (non-HTTPS) webhook', async () => { - authenticateAs(baseMerchant); - - const response = await request(app) - .patch(ME_URL) - .set('Authorization', 'Bearer valid-token') - .send({ webhook: 'http://example.com/hook' }); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('Validation failed'); - expect(prismaMock.merchant.update).not.toHaveBeenCalled(); - }); - - test('clears the webhook when sent null', async () => { - authenticateAs(baseMerchant); - prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); - - const response = await request(app) - .patch(ME_URL) - .set('Authorization', 'Bearer valid-token') - .send({ webhook: null }); - - expect(response.status).toBe(200); - expect(response.body.webhook).toBeNull(); - }); - - test('returns 400 for a required text field sent empty', async () => { - authenticateAs(baseMerchant); - - const response = await request(app) - .patch(ME_URL) - .set('Authorization', 'Bearer valid-token') - .send({ firstName: '' }); - - expect(response.status).toBe(400); - expect(prismaMock.merchant.update).not.toHaveBeenCalled(); - }); - - test('returns 400 for an empty payload', async () => { - authenticateAs(baseMerchant); - - const response = await request(app) - .patch(ME_URL) - .set('Authorization', 'Bearer valid-token') - .send({}); - - expect(response.status).toBe(400); - expect(prismaMock.merchant.update).not.toHaveBeenCalled(); - }); -}); -``` - -- [ ] **Step 2: Run the tests to verify they fail** - -Run: `npm test -- tests/integration/merchant.profile.test.ts` -Expected: FAIL — routes return 404 / controllers not defined. - -- [ ] **Step 3: Add the controllers** - -In `src/controllers/merchant.controllers.ts`, extend the service import and add `validateUpdateMerchant` to the validation import: - -```ts -import { - createMerchant, - getMerchant, - listMerchants, - registerMerchant, - getMyProfile, - updateMyProfile, -} from '../services/merchant.services.js'; -import { validateRegisterMerchant, validateUpdateMerchant } from '../utils/validation.js'; -``` - -Append the two controllers at the end of the file: - -```ts -export const getMyProfileController = async (req: Request, res: Response): Promise => { - const merchant = req.merchant; - - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - - try { - const profile = await getMyProfile(merchant.id); - res.status(200).json(profile); - } catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; - -export const updateMyProfileController = async (req: Request, res: Response): Promise => { - const merchant = req.merchant; - - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - - const errors = validateUpdateMerchant(req.body); - if (Object.keys(errors).length > 0) { - res.status(400).json({ error: 'Validation failed', errors }); - return; - } - - try { - const profile = await updateMyProfile(merchant.id, req.body); - res.status(200).json(profile); - } catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -``` - -- [ ] **Step 4: Wire the routes** - -Replace the body of `src/routes/merchant.routes.ts` so the `/me` routes are registered **before** `/:id`: - -```ts -import { Router } from 'express'; -import { - createMerchantController, - getMerchantController, - listMerchantsController, - registerMerchantController, - getMyProfileController, - updateMyProfileController, -} from '../controllers/merchant.controllers.js'; -import { authenticateMerchant } from '../middlewares/auth.middleware.js'; - -const router = Router(); - -router.post('/register', authenticateMerchant, registerMerchantController); -router.get('/me', authenticateMerchant, getMyProfileController); -router.patch('/me', authenticateMerchant, updateMyProfileController); -router.post('/', createMerchantController); -router.get('/:id', getMerchantController); -router.get('/', listMerchantsController); - -export default router; -``` - -- [ ] **Step 5: Run the tests to verify they pass** - -Run: `npm test -- tests/integration/merchant.profile.test.ts` -Expected: PASS (all 9 tests). - -- [ ] **Step 6: Run the full check + suite** - -Run: `npm run check && npm test` -Expected: type-check, lint, format all clean; entire test suite PASS. - -- [ ] **Step 7: Commit** - -```bash -git add src/controllers/merchant.controllers.ts src/routes/merchant.routes.ts tests/integration/merchant.profile.test.ts -git commit -m "feat: add GET/PATCH /merchants/me endpoints (#11)" -``` - ---- - -## Self-Review - -**Spec coverage:** -- `GET /merchants/me` returns full profile → Task 2 (`getMyProfile`) + Task 3 (route/controller, integration test). -- `PATCH /merchants/me` partial update → Task 1 (validator) + Task 2 (`updateMyProfile`) + Task 3 (route/controller). -- Non-editable fields silently ignored → Task 1 (allow-list read) + Task 3 (`address`/`email` ignored test). -- Invalid webhook → 400 → Task 1 tests + Task 3 integration test. -- Unauthenticated → 401 → Task 3 integration tests (relies on existing `authenticateMerchant`). -- Never expose internal fields → Task 2 (`sanitizeMerchant` allow-list) + Task 3 (`not.toHaveProperty` assertions). -- Clear logo/webhook with null → Task 1 + Task 2 + Task 3. -- Required text empty → 400 → Task 1 + Task 3. -- Empty payload → 400 → Task 1 + Task 3. - -**Placeholder scan:** none — all steps contain concrete code and exact commands. - -**Type consistency:** `UpdateMerchantInput` defined in Task 1, consumed by `updateMyProfile` in Task 2 and the controller in Task 3. `getMyProfile(id: string)` / `updateMyProfile(id: string, data)` signatures match across service tests (Task 2) and controller calls (Task 3). `sanitizeMerchant` field additions (`account`, `webhook`) are consistent across Tasks 2 and 3 assertions. diff --git a/docs/superpowers/specs/2026-06-26-merchant-profile-api-design.md b/docs/superpowers/specs/2026-06-26-merchant-profile-api-design.md deleted file mode 100644 index ed80161..0000000 --- a/docs/superpowers/specs/2026-06-26-merchant-profile-api-design.md +++ /dev/null @@ -1,128 +0,0 @@ -# Merchant Profile API (Get & Update) — Design - -**Issue:** [#11](https://github.com/ShadeProtocol/shade-backend/issues/11) -**Date:** 2026-06-26 -**Branch:** `feat/#11` - -## Background - -The frontend dashboard needs a profile/settings page where an authenticated -merchant can view and update their own information. This requires: - -- `GET /merchants/me` — load the current merchant profile. -- `PATCH /merchants/me` — partial update of editable fields. - -Both endpoints operate on the **authenticated** merchant (`req.merchant`), never -on an arbitrary merchant by id. - -## Goals / Acceptance Criteria - -- `GET /merchants/me` returns the full profile of the authenticated merchant. -- `PATCH /merchants/me` with a valid partial payload → `200` with updated profile. -- `PATCH /merchants/me` attempting to change non-editable fields (`address`, - `email`, `merchantId`, `account`) → those fields are silently ignored. -- Invalid `webhook` URL format → `400`. -- Unauthenticated request to either endpoint → `401`. -- Response never exposes internal fields (relations such as `apiKeys`, - `refreshTokens`, etc.). Enforced by the existing allow-list `sanitizeMerchant`. - -## Editable vs Non-Editable Fields - -**Editable via PATCH:** `firstName`, `lastName`, `businessName`, `category`, -`description`, `logo`, `webhook`. - -**Non-editable (silently ignored if sent):** `address`, `email`, `merchantId`, -`account`, and any other field. - -> Note: the issue refers to `accountContract`; the actual schema field is -> `account`. We treat `account` as the non-editable field. - -## Design Decisions - -1. **Clearing optional fields:** `logo` and `webhook` may be cleared by sending - `null` (or `""`, normalized to `null`). A non-empty `webhook` string must be a - valid HTTPS URL. -2. **Required text fields:** `firstName`, `lastName`, `businessName`, `category`, - `description` were required at registration. If present in the PATCH payload - they must be non-empty strings; `""` → `400`. They cannot be cleared via this - endpoint. -3. **Empty payload:** a PATCH with no valid editable field present → `400` - (`"At least one valid field is required"`). -4. **Silently ignore forbidden fields:** the validator/service only ever reads - from the editable allow-list, so non-editable fields are never written and - never error. - -## Architecture - -Follows the existing layering: routes → controllers (thin) → services (prisma + -`AppError`) → `sanitizeMerchant` for output. - -### 1. Routes — `src/routes/merchant.routes.ts` - -```ts -router.get('/me', authenticateMerchant, getMyProfileController); -router.patch('/me', authenticateMerchant, updateMyProfileController); -``` - -**Ordering:** register `/me` routes **before** the existing `/:id` route so -`/me` is not captured by the `:id` param. - -### 2. Validation — `src/utils/validation.ts` - -New `UpdateMerchantInput` type (all fields optional) and -`validateUpdateMerchant(body): ValidationErrors`: - -- Read only from the editable allow-list. -- Required text fields, if present → must be non-empty string, else error. -- `logo`, if present → string or `null`. -- `webhook`, if present → `null`/`""` (clear) or a valid `https:` URL (validated - with the native `URL` constructor + `protocol === 'https:'`), else error. -- If no editable field is present → `_empty` error. - -### 3. Service — `src/services/merchant.services.ts` - -- `getMyProfile(id: string)`: `findUnique({ where: { id } })`; if missing → - `AppError(404, 'Merchant not found')`; return `sanitizeMerchant(merchant)`. -- `updateMyProfile(id: string, data: UpdateMerchantInput)`: build the Prisma - `data` object only from editable fields present in the payload (trim strings; - set `null` for cleared `logo`/`webhook`); `prisma.merchant.update`; return - `sanitizeMerchant(updated)`. -- **Extend `sanitizeMerchant`** to also include `webhook` and `account` so the - "full profile" reflects editable/relevant fields. The allow-list remains - intact — no internal fields or relations are added. - -### 4. Controllers — `src/controllers/merchant.controllers.ts` - -Mirror `registerMerchantController`: - -- `getMyProfileController`: check `req.merchant` → 401; call `getMyProfile`; - 200 with profile; `AppError` → its status; else 500. -- `updateMyProfileController`: check `req.merchant` → 401; - `validateUpdateMerchant` → 400 `{ error: 'Validation failed', errors }`; - call `updateMyProfile`; 200 with updated profile; `AppError` → its status; - else 500. - -## Testing - -Jest + supertest, following the existing `*.routes.test.ts` patterns. - -**GET /merchants/me** -- Authenticated → `200` with full profile. -- No / invalid token → `401`. -- Response does not expose internal fields/relations. - -**PATCH /merchants/me** -- Valid partial payload → `200` with updated profile. -- Sending `address` / `email` / `merchantId` / `account` → silently ignored - (unchanged in response). -- Invalid `webhook` (non-HTTPS / malformed) → `400`. -- `webhook: null` → clears the webhook. -- Required text field as `""` → `400`. -- Empty payload (no editable fields) → `400`. -- No / invalid token → `401`. - -## Out of Scope - -- Updating `email` (separate flow with OTP re-verification). -- Admin-level updates of other merchants. -- Webhook delivery/verification mechanics (only stores the URL). From 3395ebb9c2c58125944b6340a9d93cd59df1afb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Ag=C3=BCero?= <54730752+EmmanuelAR@users.noreply.github.com> Date: Sat, 27 Jun 2026 12:51:00 -0600 Subject: [PATCH 5/7] test: enhance merchant profile tests with additional fields and validations - Updated integration and unit tests for merchant profile to include internal relations like refreshTokens and apiKeys. - Modified the test for non-editable fields to account for additional fields: merchantId and account. - Added validation for accepting empty string values for logo and webhook in update validation tests. --- tests/integration/merchant.profile.test.ts | 15 +++++++++++++-- tests/unit/merchant.profile.services.test.ts | 3 +++ tests/unit/merchant.update.validation.test.ts | 4 ++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/integration/merchant.profile.test.ts b/tests/integration/merchant.profile.test.ts index 160a05c..4921198 100644 --- a/tests/integration/merchant.profile.test.ts +++ b/tests/integration/merchant.profile.test.ts @@ -26,6 +26,9 @@ const baseMerchant = { registered: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), + // Internal relations that the sanitizer allow-list must strip from responses. + refreshTokens: [{ id: 'rt-1', token: 'secret-token' }], + apiKeys: [{ id: 'ak-1', keyHash: 'hashed-secret' }], }; const authenticateAs = (merchant: Record) => { @@ -82,20 +85,28 @@ describe('PATCH /api/v1/merchants/me', () => { expect(response.body).toMatchObject({ firstName: 'Grace', webhook: 'https://example.com/hook' }); }); - test('silently ignores non-editable fields (address/email)', async () => { + test('silently ignores non-editable fields (address/email/merchantId/account)', async () => { authenticateAs(baseMerchant); prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); const response = await request(app) .patch(ME_URL) .set('Authorization', 'Bearer valid-token') - .send({ firstName: 'Grace', address: '0xHACK', email: 'evil@example.com' }); + .send({ + firstName: 'Grace', + address: '0xHACK', + email: 'evil@example.com', + merchantId: 999, + account: '0xHACKED', + }); expect(response.status).toBe(200); const updateArg = prismaMock.merchant.update.mock.calls[0][0]; expect(updateArg.data).toEqual({ firstName: 'Grace' }); expect(response.body.address).toBe('0x123'); expect(response.body.email).toBe('ada@example.com'); + expect(response.body.merchantId).toBe(1); + expect(response.body.account).toBe('CCONTRACT'); }); test('returns 400 for an invalid (non-HTTPS) webhook', async () => { diff --git a/tests/unit/merchant.profile.services.test.ts b/tests/unit/merchant.profile.services.test.ts index af6c46f..ac4983a 100644 --- a/tests/unit/merchant.profile.services.test.ts +++ b/tests/unit/merchant.profile.services.test.ts @@ -23,6 +23,9 @@ const baseMerchant = { registered: true, createdAt: new Date(), updatedAt: new Date(), + // Internal relations that the sanitizer allow-list must strip from its output. + refreshTokens: [{ id: 'rt-1', token: 'secret-token' }], + apiKeys: [{ id: 'ak-1', keyHash: 'hashed-secret' }], }; describe('getMyProfile', () => { diff --git a/tests/unit/merchant.update.validation.test.ts b/tests/unit/merchant.update.validation.test.ts index 4eab43a..501092d 100644 --- a/tests/unit/merchant.update.validation.test.ts +++ b/tests/unit/merchant.update.validation.test.ts @@ -24,6 +24,10 @@ describe('validateUpdateMerchant', () => { expect(validateUpdateMerchant({ logo: null, webhook: null })).toEqual({}); }); + test('accepts an empty string logo as a clear', () => { + expect(validateUpdateMerchant({ logo: '' })).toEqual({}); + }); + test('accepts an empty string webhook as a clear', () => { expect(validateUpdateMerchant({ webhook: '' })).toEqual({}); }); From 7cc99e5e4e8acedbff7064b52cd6e10839759d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Ag=C3=BCero?= <54730752+EmmanuelAR@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:56:48 -0600 Subject: [PATCH 6/7] test: update JWT verification in auth services tests to use environment variable for secret - Imported environment configuration to access the JWT secret. - Modified the JWT verification in tests to utilize the environment variable instead of a hardcoded value. --- tests/unit/auth.services.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/auth.services.test.ts b/tests/unit/auth.services.test.ts index b19c124..145760e 100644 --- a/tests/unit/auth.services.test.ts +++ b/tests/unit/auth.services.test.ts @@ -18,6 +18,7 @@ jest.unstable_mockModule('@stellar/stellar-sdk', () => ({ })); const { default: prismaMock } = await import('../../src/config/prisma.js') as any; +const { environment } = await import('../../src/config/environment.js'); const { authenticateWallet, createNonce, @@ -172,7 +173,7 @@ describe('Auth Services', () => { expect(token.split('.')).toHaveLength(3); const jwt = await import('jsonwebtoken'); - const decoded = jwt.default.verify(token, 'dev-jwt-secret-change-in-production'); + const decoded = jwt.default.verify(token, environment.jwtSecret); expect(decoded).toMatchObject({ sub: 'merchant-uuid', address: 'GABCDEF123', From 330efe972761beea00e3293cd2d006c9f9e9c7de Mon Sep 17 00:00:00 2001 From: codebestia Date: Mon, 29 Jun 2026 08:18:30 +0100 Subject: [PATCH 7/7] fix: restore package-lock.json --- package-lock.json | 242 ++++++++++++++++++++++++++++------------------ 1 file changed, 147 insertions(+), 95 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0784999..5dadd9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -594,7 +594,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@chevrotain/gast": "10.5.0", @@ -606,7 +606,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@chevrotain/types": "10.5.0", @@ -617,21 +617,21 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@chevrotain/utils": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -643,14 +643,14 @@ "version": "0.3.15", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.0.20", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.20.tgz", "integrity": "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "pglite-server": "dist/scripts/server.js" @@ -663,7 +663,7 @@ "version": "0.2.20", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.20.tgz", "integrity": "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "peerDependencies": { "@electric-sql/pglite": "0.3.15" @@ -1321,7 +1321,7 @@ "version": "1.19.9", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18.14.1" @@ -1947,7 +1947,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -1956,13 +1956,13 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -1972,7 +1972,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz", "integrity": "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "chevrotain": "^10.5.0", @@ -2144,7 +2144,7 @@ "version": "7.4.0", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.4.0.tgz", "integrity": "sha512-EnNrZMwZ9+O6UlG+YO9SP3VhVw4zwMahDRzQm3r0DQn9KeU5NwzmaDAY+BzACrgmaU71Id1/0FtWIDdl7xQp9g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "c12": "3.1.0", @@ -2157,14 +2157,14 @@ "version": "7.4.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.4.0.tgz", "integrity": "sha512-fZicwzgFHvvPMrRLCUinrsBTdadJsi/1oirzShjmFvNLwtu2DYlkxwRVy5zEGhp85mrEGnLeS/PdNRCdE027+Q==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/dev": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.20.0.tgz", "integrity": "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "@electric-sql/pglite": "0.3.15", @@ -2205,7 +2205,7 @@ "version": "7.4.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.4.0.tgz", "integrity": "sha512-H+dgpbbY3VN/j5hOSVP1LXsv/rU0w/4C2zh5PZUwo/Q3NqZjOvBlVvkhtziioRmeEZ3SBAqPCsf1sQ74sI3O/w==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -2219,14 +2219,14 @@ "version": "7.4.0-20.ab56fe763f921d033a6c195e7ddeb3e255bdbb57", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.4.0-20.ab56fe763f921d033a6c195e7ddeb3e255bdbb57.tgz", "integrity": "sha512-5o3/bubIYdUeg38cyNf+VDq+LVtxvvi2393Fd1Uru52LPfkGJnmVbCaX1wBOAncgKR3BCloMJFD+Koog9LtYqQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.0.tgz", "integrity": "sha512-fOUIoGzAPgtjHVs4DsVSnEDPBEauAmFeZr4Ej3tMwxywam7hHdRtCzgKagQBKcYIJuya8gzYrTqUoukzXtWJaA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.4.0" @@ -2236,7 +2236,7 @@ "version": "7.4.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.4.0.tgz", "integrity": "sha512-IXPOYskT89UTVsntuSnMTiKRWCuTg5JMWflgEDV1OSKFpuhwP5vqbfF01/iwo9y6rCjR0sDIO+jdV5kq38/hgA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.4.0", @@ -2248,7 +2248,7 @@ "version": "7.4.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.0.tgz", "integrity": "sha512-fOUIoGzAPgtjHVs4DsVSnEDPBEauAmFeZr4Ej3tMwxywam7hHdRtCzgKagQBKcYIJuya8gzYrTqUoukzXtWJaA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.4.0" @@ -2258,7 +2258,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "7.2.0" @@ -2268,21 +2268,21 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/query-plan-executor": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/studio-core": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.13.1.tgz", "integrity": "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", @@ -2332,7 +2332,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@stellar/js-xdr": { @@ -2385,25 +2385,25 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true + "devOptional": true }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", @@ -2659,6 +2659,17 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", @@ -3204,7 +3215,7 @@ "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -3225,7 +3236,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, + "devOptional": true, "dependencies": { "acorn": "^8.11.0" }, @@ -3320,7 +3331,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "devOptional": true }, "node_modules/argparse": { "version": "2.0.1", @@ -3360,7 +3371,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 6.0.0" @@ -3698,7 +3709,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.3", @@ -3727,7 +3738,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -3743,7 +3754,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -3904,7 +3915,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", @@ -3959,7 +3970,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "consola": "^3.2.3" @@ -4127,14 +4138,14 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" @@ -4205,7 +4216,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "devOptional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -4220,6 +4231,14 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT", + "peer": true + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -4276,7 +4295,7 @@ "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=16.0.0" @@ -4303,7 +4322,7 @@ "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/delayed-stream": { @@ -4319,7 +4338,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=0.10" @@ -4337,7 +4356,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/detect-newline": { @@ -4365,7 +4384,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.3.1" } @@ -4418,7 +4437,7 @@ "version": "3.18.4", "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -4454,7 +4473,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=14" @@ -4969,14 +4988,14 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "dev": true, + "devOptional": true, "funding": [ { "type": "individual", @@ -5305,7 +5324,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-property": "^1.0.2" @@ -5366,7 +5385,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/get-proto": { @@ -5411,7 +5430,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "citty": "^0.1.6", @@ -5483,14 +5502,14 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/grammex": { "version": "3.1.12", "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/graphemer": { @@ -5503,7 +5522,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/handlebars": { @@ -5598,7 +5617,7 @@ "version": "4.11.4", "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=16.9.0" @@ -5630,7 +5649,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/human-signals": { @@ -5846,7 +5865,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/is-retry-allowed": { @@ -6658,7 +6677,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -6813,7 +6832,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -6845,7 +6864,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/lodash.includes": { @@ -6907,7 +6926,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/lru-cache": { @@ -6919,7 +6938,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "bun": ">=1.0.0", @@ -6951,7 +6970,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "devOptional": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -7112,7 +7131,7 @@ "version": "3.15.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", @@ -7133,7 +7152,7 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7150,7 +7169,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "lru.min": "^1.1.0" @@ -7209,7 +7228,7 @@ "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/node-gyp-build": { @@ -7322,7 +7341,7 @@ "version": "0.6.5", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "citty": "^0.2.0", @@ -7340,7 +7359,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/object-assign": { @@ -7366,7 +7385,7 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/on-finished": { @@ -7559,14 +7578,14 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/pg": { @@ -7757,7 +7776,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "confbox": "^0.2.2", @@ -7784,7 +7803,7 @@ "version": "3.4.7", "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", - "dev": true, + "devOptional": true, "license": "Unlicense", "engines": { "node": ">=12" @@ -7897,7 +7916,7 @@ "version": "7.4.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.4.0.tgz", "integrity": "sha512-n2xU9vSaH4uxZF/l2aKoGYtKtC7BL936jM9Q94Syk1zOD39t/5hjDUxMgaPkVRDX5wWEMsIqvzQxoebNIesOKw==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -7931,7 +7950,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -7943,7 +7962,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/proxy-addr": { @@ -7983,7 +8002,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "individual", @@ -8066,13 +8085,38 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -8101,14 +8145,14 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/remeda": { "version": "2.33.4", "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/remeda" @@ -8189,7 +8233,7 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 4" @@ -8267,6 +8311,14 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "devOptional": true, + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -8304,7 +8356,7 @@ "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", - "dev": true + "devOptional": true }, "node_modules/serve-static": { "version": "2.2.0", @@ -8537,7 +8589,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8588,7 +8640,7 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/string-length": { @@ -8877,7 +8929,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -9036,7 +9088,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, + "devOptional": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9270,7 +9322,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9431,7 +9483,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "devOptional": true }, "node_modules/v8-to-istanbul": { "version": "9.3.0", @@ -9463,7 +9515,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "typescript": ">=5" @@ -9735,7 +9787,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } @@ -9756,7 +9808,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "grammex": "^3.1.11",