From 3f7cc0f85417d7520fdfab47616c8ce07b9b88ef Mon Sep 17 00:00:00 2001 From: defiQUG Date: Tue, 7 Apr 2026 22:59:32 -0700 Subject: [PATCH] feat(finance): BTC basket flows, client scoping, and jewelry-box store - Finance API: baskets, holdings, rebalances, deposits, bridge withdrawals, vault checks. - Schemas: btc-basket; api-client finance types; workspace lockfile update. - Vitest config for finance service; expanded tests. Made-with: Cursor --- packages/api-client/src/finance.ts | 151 +++- packages/api-client/src/index.ts | 11 +- packages/auth/src/did.ts | 7 +- packages/auth/src/entra-credential-images.ts | 4 +- packages/database/src/document-search.ts | 8 +- packages/database/src/document-templates.ts | 6 +- packages/schemas/src/btc-basket.ts | 136 ++++ packages/schemas/src/index.ts | 2 +- packages/test-utils/src/security-helpers.ts | 5 +- packages/test-utils/src/server-factory.ts | 2 - packages/workflows/src/review.ts | 6 +- pnpm-lock.yaml | 522 ++++++++------ services/finance/package.json | 8 +- services/finance/src/index.test.ts | 370 ++++++++-- services/finance/src/index.ts | 682 ++++++++++++++----- services/finance/src/jewelry-box-store.ts | 296 ++++++++ services/finance/tests/finance.test.ts | 85 ++- services/finance/vitest.config.mjs | 18 + 18 files changed, 1825 insertions(+), 494 deletions(-) create mode 100644 packages/schemas/src/btc-basket.ts create mode 100644 services/finance/src/jewelry-box-store.ts create mode 100644 services/finance/vitest.config.mjs diff --git a/packages/api-client/src/finance.ts b/packages/api-client/src/finance.ts index 6576719..6e1fb6b 100644 --- a/packages/api-client/src/finance.ts +++ b/packages/api-client/src/finance.ts @@ -21,6 +21,81 @@ export interface LedgerEntry { reference?: string; } +export interface BasketAllocation { + symbol: string; + targetWeightBps: number; + routeHint?: string; +} + +export interface BasketMandate { + id: string; + clientId: string; + mandateName: string; + chain138VaultAddress: string; + baseAssetSymbol: string; + status: 'draft' | 'active' | 'rebalancing' | 'closed'; + allocations: BasketAllocation[]; + createdAt: string; + updatedAt: string; +} + +export interface BtcDeposit { + id: string; + clientId: string; + basketId: string; + chain138VaultAddress: string; + depositAddress: string; + expectedAmountSats?: number; + confirmationsRequired: number; + currentConfirmations: number; + status: + | 'instruction_created' + | 'pending_confirmations' + | 'confirmed' + | 'minted' + | 'frozen'; + observedTxId?: string; + freezeReason?: string; + createdAt: string; + updatedAt: string; +} + +export interface Holding { + clientId: string; + basketId: string; + symbol: string; + allocationWeightBps: number; + bookValueSats: number; + status: 'pending_funding' | 'funded'; + updatedAt: string; +} + +export interface Rebalance { + id: string; + clientId: string; + basketId: string; + sourceSymbol: string; + targetSymbols: string[]; + reason: string; + status: 'planned' | 'queued' | 'executed' | 'failed'; + createdAt: string; + updatedAt: string; +} + +export interface BridgeWithdrawal { + id: string; + clientId: string; + basketId: string; + sourceSymbol: string; + destinationSymbol: string; + destinationChainId: number; + destinationAddress: string; + amount: string; + status: 'pending' | 'approved' | 'submitted' | 'failed'; + createdAt: string; + updatedAt: string; +} + export class FinanceClient { protected client: AxiosInstance; @@ -28,8 +103,8 @@ export class FinanceClient { const apiBaseURL = baseURL || (typeof window !== 'undefined' - ? process.env.NEXT_PUBLIC_FINANCE_SERVICE_URL || 'http://localhost:4005' - : 'http://localhost:4005'); + ? process.env.NEXT_PUBLIC_FINANCE_SERVICE_URL || 'http://localhost:4003' + : 'http://localhost:4003'); this.client = axios.create({ baseURL: apiBaseURL, @@ -38,7 +113,6 @@ export class FinanceClient { }, }); - // Set up request interceptor for authentication this.client.interceptors.request.use( (config) => { const token = this.getAuthToken(); @@ -47,7 +121,7 @@ export class FinanceClient { } return config; }, - (error) => Promise.reject(error) + (error) => Promise.reject(error), ); } @@ -74,8 +148,8 @@ export class FinanceClient { paymentMethod: string; description?: string; }): Promise { - const response = await this.client.post('/api/v1/payments', data); - return response.data; + const response = await this.client.post<{ payment: Payment }>('/api/v1/payments', data); + return response.data.payment; } async getPayment(paymentId: string): Promise { @@ -104,8 +178,71 @@ export class FinanceClient { }): Promise<{ entries: LedgerEntry[]; total: number }> { const response = await this.client.get<{ entries: LedgerEntry[]; total: number }>( '/api/v1/ledger', - { params: filters } + { params: filters }, ); return response.data; } + + async createBasket(data: { + clientId: string; + mandateName: string; + chain138VaultAddress: string; + allocations: BasketAllocation[]; + baseAssetSymbol?: string; + }): Promise { + const response = await this.client.post<{ basket: BasketMandate }>('/api/v1/baskets', data); + return response.data.basket; + } + + async createBtcDeposit(data: { + clientId: string; + basketId?: string; + mandateName?: string; + chain138VaultAddress: string; + allocations?: BasketAllocation[]; + expectedAmountSats?: number; + clientReference?: string; + }): Promise<{ deposit: BtcDeposit; basket: BasketMandate; createdBasket: boolean }> { + const response = await this.client.post<{ + deposit: BtcDeposit; + basket: BasketMandate; + createdBasket: boolean; + }>('/api/v1/btc-deposits', data); + return response.data; + } + + async getBtcDeposit(id: string): Promise { + const response = await this.client.get<{ deposit: BtcDeposit }>(`/api/v1/btc-deposits/${id}`); + return response.data.deposit; + } + + async getHoldings(filters?: { clientId?: string; basketId?: string }): Promise { + const response = await this.client.get<{ holdings: Holding[] }>('/api/v1/holdings', { + params: filters, + }); + return response.data.holdings; + } + + async getRebalances(filters?: { clientId?: string; basketId?: string }): Promise { + const response = await this.client.get<{ rebalances: Rebalance[] }>('/api/v1/rebalances', { + params: filters, + }); + return response.data.rebalances; + } + + async requestBridgeWithdrawal(data: { + clientId: string; + basketId: string; + sourceSymbol?: string; + destinationSymbol?: string; + destinationChainId: number; + destinationAddress: string; + amount: string; + }): Promise { + const response = await this.client.post<{ withdrawal: BridgeWithdrawal }>( + '/api/v1/withdrawals/bridge', + data, + ); + return response.data.withdrawal; + } } diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index 8ce5e37..fdb8c35 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -14,5 +14,14 @@ export type { } from './identity'; export type { SubmitApplicationRequest, AdjudicateRequest } from './eresidency'; export type { DocumentUpload, DocumentMetadata } from './intake'; -export type { Payment, LedgerEntry } from './finance'; +export type { + Payment, + LedgerEntry, + BasketAllocation, + BasketMandate, + BtcDeposit, + Holding, + Rebalance, + BridgeWithdrawal, +} from './finance'; export type { DealRoom, Document } from './dataroom'; diff --git a/packages/auth/src/did.ts b/packages/auth/src/did.ts index 52c4213..2aa3d6c 100644 --- a/packages/auth/src/did.ts +++ b/packages/auth/src/did.ts @@ -6,11 +6,13 @@ import fetch from 'node-fetch'; import { createVerify, createPublicKey } from 'crypto'; import { decode as multibaseDecode } from 'multibase'; -import base58 from 'base58-universal'; import { importJWK } from 'jose'; import forge from 'node-forge'; import { verify as ed25519Verify } from '@noble/ed25519'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { decode: base58Decode }: { decode: (value: string) => Uint8Array } = require('base58-universal'); + export interface DIDDocument { id: string; '@context': string[]; @@ -89,7 +91,7 @@ export class DIDResolver { if (multibaseKey.startsWith('z')) { try { const base58Encoded = multibaseKey.slice(1); - const decoded = base58.decode(base58Encoded); + const decoded = base58Decode(base58Encoded); return Buffer.from(decoded); } catch { throw new Error('Failed to decode multibase key'); @@ -296,4 +298,3 @@ export class DIDResolver { } } } - diff --git a/packages/auth/src/entra-credential-images.ts b/packages/auth/src/entra-credential-images.ts index a618cd4..8a05b33 100644 --- a/packages/auth/src/entra-credential-images.ts +++ b/packages/auth/src/entra-credential-images.ts @@ -122,6 +122,9 @@ export async function prepareCredentialImage( if (imageData.startsWith('data:')) { // Extract base64 data const base64Data = imageData.split(',')[1]; + if (!base64Data) { + throw new Error('Invalid image data URL: missing base64 payload'); + } imageBuffer = Buffer.from(base64Data, 'base64'); } else { imageBuffer = Buffer.from(imageData); @@ -180,4 +183,3 @@ export function getRecommendedImageSpecs(): { maxSizeKB: 100, // Max 100KB recommended }; } - diff --git a/packages/database/src/document-search.ts b/packages/database/src/document-search.ts index fe76a49..2b4bcce 100644 --- a/packages/database/src/document-search.ts +++ b/packages/database/src/document-search.ts @@ -4,7 +4,6 @@ */ import { query } from './client'; -import { listDocuments, getDocumentById } from './schema'; export interface DocumentSearchResult { documents: Array<{ @@ -102,8 +101,8 @@ export async function searchDocuments( /** * Get search suggestions */ -export async function getSearchSuggestions(query: string, limit = 10): Promise { - if (!query || query.length < 2) { +export async function getSearchSuggestions(searchTerm: string, limit = 10): Promise { + if (!searchTerm || searchTerm.length < 2) { return []; } @@ -113,9 +112,8 @@ export async function getSearchSuggestions(query: string, limit = 10): Promise row.title); } - diff --git a/packages/database/src/document-templates.ts b/packages/database/src/document-templates.ts index 67d5cea..be2af74 100644 --- a/packages/database/src/document-templates.ts +++ b/packages/database/src/document-templates.ts @@ -320,9 +320,11 @@ export function extractTemplateVariables(template_content: string): string[] { const matches = template_content.matchAll(/\{\{(\w+(?:\.\w+)*)\}\}/g); for (const match of matches) { - variables.add(match[1]); + const variableName = match[1]; + if (variableName) { + variables.add(variableName); + } } return Array.from(variables).sort(); } - diff --git a/packages/schemas/src/btc-basket.ts b/packages/schemas/src/btc-basket.ts new file mode 100644 index 0000000..cf97bb2 --- /dev/null +++ b/packages/schemas/src/btc-basket.ts @@ -0,0 +1,136 @@ +import { z } from 'zod'; + +const EthAddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/); + +export const BasketAllocationSchema = z.object({ + symbol: z.string().min(2).max(16), + targetWeightBps: z.number().int().positive().max(10_000), + routeHint: z.string().optional(), +}); + +export type BasketAllocation = z.infer; + +export const BasketStatusSchema = z.enum(['draft', 'active', 'rebalancing', 'closed']); + +export const BasketMandateSchema = z.object({ + id: z.string().uuid(), + clientId: z.string().min(1), + mandateName: z.string().min(1), + chain138VaultAddress: EthAddressSchema, + baseAssetSymbol: z.string().min(2).max(16).default('cBTC'), + status: BasketStatusSchema, + allocations: z.array(BasketAllocationSchema).min(1), + createdAt: z.date().or(z.string().datetime()), + updatedAt: z.date().or(z.string().datetime()), +}); + +export type BasketMandate = z.infer; + +export const CreateBasketMandateSchema = BasketMandateSchema.omit({ + id: true, + status: true, + createdAt: true, + updatedAt: true, +}).extend({ + baseAssetSymbol: z.string().min(2).max(16).default('cBTC'), +}); + +export type CreateBasketMandate = z.infer; + +export const BtcDepositStatusSchema = z.enum([ + 'instruction_created', + 'pending_confirmations', + 'confirmed', + 'minted', + 'frozen', +]); + +export const BtcDepositSchema = z.object({ + id: z.string().uuid(), + clientId: z.string().min(1), + basketId: z.string().uuid(), + chain138VaultAddress: EthAddressSchema, + depositAddress: z.string().min(16), + expectedAmountSats: z.number().int().positive().optional(), + confirmationsRequired: z.number().int().positive(), + currentConfirmations: z.number().int().nonnegative(), + status: BtcDepositStatusSchema, + observedTxId: z.string().optional(), + freezeReason: z.string().optional(), + createdAt: z.date().or(z.string().datetime()), + updatedAt: z.date().or(z.string().datetime()), +}); + +export type BtcDeposit = z.infer; + +export const CreateBtcDepositSchema = z.object({ + clientId: z.string().min(1), + basketId: z.string().uuid().optional(), + mandateName: z.string().min(1).optional(), + chain138VaultAddress: EthAddressSchema, + allocations: z.array(BasketAllocationSchema).min(1).optional(), + expectedAmountSats: z.number().int().positive().optional(), + clientReference: z.string().min(1).optional(), +}); + +export type CreateBtcDeposit = z.infer; + +export const HoldingStatusSchema = z.enum(['pending_funding', 'funded']); + +export const HoldingSchema = z.object({ + clientId: z.string().min(1), + basketId: z.string().uuid(), + symbol: z.string().min(2).max(16), + allocationWeightBps: z.number().int().min(0).max(10_000), + bookValueSats: z.number().int().nonnegative(), + status: HoldingStatusSchema, + updatedAt: z.date().or(z.string().datetime()), +}); + +export type Holding = z.infer; + +export const RebalanceStatusSchema = z.enum(['planned', 'queued', 'executed', 'failed']); + +export const RebalanceSchema = z.object({ + id: z.string().uuid(), + clientId: z.string().min(1), + basketId: z.string().uuid(), + sourceSymbol: z.string().min(2).max(16), + targetSymbols: z.array(z.string().min(2).max(16)).min(1), + reason: z.string().min(1), + status: RebalanceStatusSchema, + createdAt: z.date().or(z.string().datetime()), + updatedAt: z.date().or(z.string().datetime()), +}); + +export type Rebalance = z.infer; + +export const BridgeWithdrawalStatusSchema = z.enum(['pending', 'approved', 'submitted', 'failed']); + +export const BridgeWithdrawalSchema = z.object({ + id: z.string().uuid(), + clientId: z.string().min(1), + basketId: z.string().uuid(), + sourceSymbol: z.string().min(2).max(16), + destinationSymbol: z.string().min(2).max(16), + destinationChainId: z.number().int().positive(), + destinationAddress: EthAddressSchema, + amount: z.string().min(1), + status: BridgeWithdrawalStatusSchema, + createdAt: z.date().or(z.string().datetime()), + updatedAt: z.date().or(z.string().datetime()), +}); + +export type BridgeWithdrawal = z.infer; + +export const CreateBridgeWithdrawalSchema = BridgeWithdrawalSchema.omit({ + id: true, + status: true, + createdAt: true, + updatedAt: true, +}).extend({ + sourceSymbol: z.string().min(2).max(16).optional(), + destinationSymbol: z.string().min(2).max(16).optional(), +}); + +export type CreateBridgeWithdrawal = z.infer; diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts index 4af8aca..aa88261 100644 --- a/packages/schemas/src/index.ts +++ b/packages/schemas/src/index.ts @@ -9,4 +9,4 @@ export * from './vc'; export * from './payment'; export * from './ledger'; export * from './eresidency'; - +export * from './btc-basket'; diff --git a/packages/test-utils/src/security-helpers.ts b/packages/test-utils/src/security-helpers.ts index 939b2a5..233a41e 100644 --- a/packages/test-utils/src/security-helpers.ts +++ b/packages/test-utils/src/security-helpers.ts @@ -77,9 +77,9 @@ export function createSecurityTestData() { export async function testAuthenticationBypass( makeRequest: (headers?: Record) => Promise<{ status: number }> ): Promise { - const testCases = [ + const testCases: Array | undefined> = [ // Missing token - {}, + undefined, // Invalid token { Authorization: 'Bearer invalid-token' }, // Expired token @@ -226,4 +226,3 @@ export async function testCSRFProtection( return true; // CSRF protection is working correctly } - diff --git a/packages/test-utils/src/server-factory.ts b/packages/test-utils/src/server-factory.ts index bce7f4b..46a4b84 100644 --- a/packages/test-utils/src/server-factory.ts +++ b/packages/test-utils/src/server-factory.ts @@ -5,7 +5,6 @@ import { FastifyInstance } from 'fastify'; import Fastify from 'fastify'; -import { getEnv } from '@the-order/shared'; export interface ServerFactoryOptions { port?: number; @@ -41,4 +40,3 @@ export async function createTestServer( export function closeTestServer(server: FastifyInstance): Promise { return server.close(); } - diff --git a/packages/workflows/src/review.ts b/packages/workflows/src/review.ts index eaf9398..ee4d6a4 100644 --- a/packages/workflows/src/review.ts +++ b/packages/workflows/src/review.ts @@ -62,7 +62,10 @@ export async function reviewWorkflow( if (process.env.APPROVAL_SERVICE_URL) { try { const res = await fetch(`${process.env.APPROVAL_SERVICE_URL}/status/${input.documentId}/${input.reviewerId}`); - if (res.ok) approved = (await res.json()).approved ?? false; + if (res.ok) { + const approvalResponse = (await res.json()) as { approved?: boolean }; + approved = approvalResponse.approved ?? false; + } } catch { /* fall through */ } } if (!approved && getApprovalStatus) { @@ -151,4 +154,3 @@ export async function checkApprovalStatus( // Fallback: assume not approved if we can't check return false; } - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0bae81..f227580 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -527,31 +527,6 @@ importers: specifier: ^5.3.3 version: 5.9.3 - packages/secrets: - dependencies: - '@aws-sdk/client-secrets-manager': - specifier: ^3.490.0 - version: 3.927.0 - '@azure/identity': - specifier: ^4.0.1 - version: 4.13.0 - '@azure/keyvault-secrets': - specifier: ^4.7.0 - version: 4.10.0 - zod: - specifier: ^3.22.4 - version: 3.25.76 - devDependencies: - '@types/node': - specifier: ^20.10.6 - version: 20.19.24 - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vitest: - specifier: ^1.6.1 - version: 1.6.1(@types/node@20.19.24)(@vitest/ui@1.6.1) - packages/shared: dependencies: '@fastify/cors': @@ -796,6 +771,9 @@ importers: '@fastify/swagger-ui': specifier: ^2.0.0 version: 2.1.0 + '@the-order/database': + specifier: workspace:* + version: link:../../packages/database '@the-order/payment-gateway': specifier: workspace:^ version: link:../../packages/payment-gateway @@ -821,6 +799,9 @@ importers: typescript: specifier: ^5.3.3 version: 5.9.3 + vitest: + specifier: ^1.6.1 + version: 1.6.1(@types/node@20.19.24)(@vitest/ui@1.6.1) services/identity: dependencies: @@ -899,6 +880,55 @@ importers: specifier: ^5.3.3 version: 5.9.3 + services/legal-documents: + dependencies: + '@fastify/swagger': + specifier: ^8.12.0 + version: 8.15.0 + '@fastify/swagger-ui': + specifier: ^1.9.3 + version: 1.10.2 + '@the-order/database': + specifier: workspace:* + version: link:../../packages/database + '@the-order/schemas': + specifier: workspace:* + version: link:../../packages/schemas + '@the-order/shared': + specifier: workspace:* + version: link:../../packages/shared + '@the-order/storage': + specifier: workspace:* + version: link:../../packages/storage + docx: + specifier: ^8.5.0 + version: 8.6.0 + fastify: + specifier: ^4.24.3 + version: 4.29.1 + pdf-lib: + specifier: ^1.17.1 + version: 1.17.1 + pdfkit: + specifier: ^0.15.0 + version: 0.15.2 + zod: + specifier: ^3.22.4 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^20.10.0 + version: 20.19.24 + tsx: + specifier: ^4.7.0 + version: 4.20.6 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vitest: + specifier: ^1.1.0 + version: 1.6.1(@types/node@20.19.24)(@vitest/ui@1.6.1) + packages: /@alloc/quick-lru@5.2.0: @@ -1081,54 +1111,6 @@ packages: - aws-crt dev: false - /@aws-sdk/client-secrets-manager@3.927.0: - resolution: {integrity: sha512-6hdJ4OHyM4NvOS/uzt06Ko1NLhI04qwIq1nwH1kmo2W6VOc9ozLJnzIRlUCVw6s/F8dQtvfsCzFf0rJshGyT6Q==} - engines: {node: '>=18.0.0'} - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.927.0 - '@aws-sdk/credential-provider-node': 3.927.0 - '@aws-sdk/middleware-host-header': 3.922.0 - '@aws-sdk/middleware-logger': 3.922.0 - '@aws-sdk/middleware-recursion-detection': 3.922.0 - '@aws-sdk/middleware-user-agent': 3.927.0 - '@aws-sdk/region-config-resolver': 3.925.0 - '@aws-sdk/types': 3.922.0 - '@aws-sdk/util-endpoints': 3.922.0 - '@aws-sdk/util-user-agent-browser': 3.922.0 - '@aws-sdk/util-user-agent-node': 3.927.0 - '@smithy/config-resolver': 4.4.2 - '@smithy/core': 3.17.2 - '@smithy/fetch-http-handler': 5.3.5 - '@smithy/hash-node': 4.2.4 - '@smithy/invalid-dependency': 4.2.4 - '@smithy/middleware-content-length': 4.2.4 - '@smithy/middleware-endpoint': 4.3.6 - '@smithy/middleware-retry': 4.4.6 - '@smithy/middleware-serde': 4.2.4 - '@smithy/middleware-stack': 4.2.4 - '@smithy/node-config-provider': 4.3.4 - '@smithy/node-http-handler': 4.4.4 - '@smithy/protocol-http': 5.3.4 - '@smithy/smithy-client': 4.9.2 - '@smithy/types': 4.8.1 - '@smithy/url-parser': 4.2.4 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.5 - '@smithy/util-defaults-mode-node': 4.2.8 - '@smithy/util-endpoints': 3.2.4 - '@smithy/util-middleware': 4.2.4 - '@smithy/util-retry': 4.2.4 - '@smithy/util-utf8': 4.2.0 - '@smithy/uuid': 1.1.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - dev: false - /@aws-sdk/client-sfn@3.928.0: resolution: {integrity: sha512-GiFBGqoh+69XxN3yvykxNibpnjDOnBxzzyfxWl4k8Q73qdKWnr/x+4SiCRqZXKbbUiszbPGexYJG9L5w9emfAA==} engines: {node: '>=18.0.0'} @@ -1921,20 +1903,6 @@ packages: engines: {node: '>=18.0.0'} dev: false - /@azure-rest/core-client@2.5.1: - resolution: {integrity: sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==} - engines: {node: '>=20.0.0'} - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.1 - '@azure/core-rest-pipeline': 1.22.2 - '@azure/core-tracing': 1.3.1 - '@typespec/ts-http-runtime': 0.3.2 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - dev: false - /@azure/abort-controller@2.1.2: resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} engines: {node: '>=18.0.0'} @@ -1968,36 +1936,6 @@ packages: - supports-color dev: false - /@azure/core-http-compat@2.3.1: - resolution: {integrity: sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==} - engines: {node: '>=20.0.0'} - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-client': 1.10.1 - '@azure/core-rest-pipeline': 1.22.2 - transitivePeerDependencies: - - supports-color - dev: false - - /@azure/core-lro@2.7.2: - resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} - engines: {node: '>=18.0.0'} - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-util': 1.13.1 - '@azure/logger': 1.3.0 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - dev: false - - /@azure/core-paging@1.6.2: - resolution: {integrity: sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==} - engines: {node: '>=18.0.0'} - dependencies: - tslib: 2.8.1 - dev: false - /@azure/core-rest-pipeline@1.22.2: resolution: {integrity: sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==} engines: {node: '>=20.0.0'} @@ -2050,42 +1988,6 @@ packages: - supports-color dev: false - /@azure/keyvault-common@2.0.0: - resolution: {integrity: sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==} - engines: {node: '>=18.0.0'} - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.1 - '@azure/core-client': 1.10.1 - '@azure/core-rest-pipeline': 1.22.2 - '@azure/core-tracing': 1.3.1 - '@azure/core-util': 1.13.1 - '@azure/logger': 1.3.0 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - dev: false - - /@azure/keyvault-secrets@4.10.0: - resolution: {integrity: sha512-WvXc3h2hqHL1pMzUU7ANE2RBKoxjK3JQc0YNn6GUFvOWQtf2ZR+sH4/5cZu8zAg62v9qLCduBN7065nHKl+AOA==} - engines: {node: '>=18.0.0'} - dependencies: - '@azure-rest/core-client': 2.5.1 - '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.1 - '@azure/core-http-compat': 2.3.1 - '@azure/core-lro': 2.7.2 - '@azure/core-paging': 1.6.2 - '@azure/core-rest-pipeline': 1.22.2 - '@azure/core-tracing': 1.3.1 - '@azure/core-util': 1.13.1 - '@azure/keyvault-common': 2.0.0 - '@azure/logger': 1.3.0 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - dev: false - /@azure/logger@1.3.0: resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} engines: {node: '>=20.0.0'} @@ -2757,6 +2659,16 @@ packages: p-limit: 3.1.0 dev: false + /@fastify/swagger-ui@1.10.2: + resolution: {integrity: sha512-f2mRqtblm6eRAFQ3e8zSngxVNEtiYY7rISKQVjPA++ZsWc5WYlPVTb6Bx0G/zy0BIoucNqDr/Q2Vb/kTYkOq1A==} + dependencies: + '@fastify/static': 6.12.0 + fastify-plugin: 4.5.1 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.8.1 + dev: false + /@fastify/swagger-ui@2.1.0: resolution: {integrity: sha512-mu0C28kMEQDa3miE8f3LmI/OQSmqaKS3dYhZVFO5y4JdgBIPbzZj6COCoRU/P/9nu7UogzzcCJtg89wwLwKtWg==} dependencies: @@ -4665,6 +4577,18 @@ packages: '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) dev: false + /@pdf-lib/standard-fonts@1.0.0: + resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==} + dependencies: + pako: 1.0.11 + dev: false + + /@pdf-lib/upng@1.0.1: + resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==} + dependencies: + pako: 1.0.11 + dev: false + /@pinojs/redact@0.4.0: resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} dev: false @@ -5440,6 +5364,12 @@ packages: resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} dev: false + /@swc/helpers@0.3.17: + resolution: {integrity: sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==} + dependencies: + tslib: 2.8.1 + dev: false + /@swc/helpers@0.5.5: resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} dependencies: @@ -6384,7 +6314,6 @@ packages: dependencies: call-bound: 1.0.4 is-array-buffer: 3.0.5 - dev: true /array-includes@3.1.9: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} @@ -6522,7 +6451,6 @@ packages: engines: {node: '>= 0.4'} dependencies: possible-typed-array-names: 1.1.0 - dev: true /avvio@8.4.0: resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==} @@ -6559,6 +6487,11 @@ packages: engines: {node: '>=14'} dev: false + /base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -6619,6 +6552,12 @@ packages: dependencies: fill-range: 7.1.1 + /brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + dependencies: + base64-js: 1.5.1 + dev: false + /browserslist@4.28.0: resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -6701,7 +6640,6 @@ packages: es-define-property: 1.0.1 get-intrinsic: 1.3.0 set-function-length: 1.2.2 - dev: true /call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} @@ -6891,6 +6829,11 @@ packages: engines: {node: '>=0.8'} dev: true + /clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + dev: false + /clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -6980,6 +6923,10 @@ packages: requiresBuild: true dev: true + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: false + /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true @@ -6999,6 +6946,10 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + dev: false + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -7160,6 +7111,30 @@ packages: dependencies: type-detect: 4.1.0 + /deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + dev: false + /deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -7195,7 +7170,6 @@ packages: es-define-property: 1.0.1 es-errors: 1.3.0 gopd: 1.2.0 - dev: true /define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} @@ -7209,7 +7183,6 @@ packages: define-data-property: 1.1.4 has-property-descriptors: 1.0.2 object-keys: 1.1.1 - dev: true /degenerator@5.0.1: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} @@ -7254,6 +7227,10 @@ packages: requiresBuild: true dev: false + /dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + dev: false + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true @@ -7292,6 +7269,18 @@ packages: esutils: 2.0.3 dev: true + /docx@8.6.0: + resolution: {integrity: sha512-JEzPozEsuGIyUkEqdGlCv/b1avYeXjR4PjwiLdPwRKdsI9spBCq4WP0QcGYfIANpgYdJSphw4IT8M/a9dpnpvQ==} + engines: {node: '>=10'} + deprecated: A API breaking change was introduced here, which was incompatible with semantic versioning + dependencies: + '@types/node': 20.19.24 + jszip: 3.10.1 + nanoid: 5.1.7 + xml: 1.0.1 + xml-js: 1.6.11 + dev: false + /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: @@ -7417,6 +7406,20 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + /es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + dev: false + /es-iterator-helpers@1.2.1: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} @@ -8212,12 +8215,25 @@ packages: optional: true dev: false + /fontkit@1.9.0: + resolution: {integrity: sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==} + dependencies: + '@swc/helpers': 0.3.17 + brotli: 1.3.3 + clone: 2.1.2 + deep-equal: 2.2.3 + dfa: 1.2.0 + restructure: 2.0.1 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + dev: false + /for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} dependencies: is-callable: 1.2.7 - dev: true /foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} @@ -8282,7 +8298,6 @@ packages: /functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - dev: true /gaxios@6.7.1: resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} @@ -8513,7 +8528,6 @@ packages: /has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} - dev: true /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} @@ -8529,7 +8543,6 @@ packages: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} dependencies: es-define-property: 1.0.1 - dev: true /has-proto@1.2.0: resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} @@ -8644,6 +8657,10 @@ packages: engines: {node: '>= 4'} dev: true + /immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + dev: false + /import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -8743,7 +8760,6 @@ packages: es-errors: 1.3.0 hasown: 2.0.2 side-channel: 1.1.0 - dev: true /internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} @@ -8777,6 +8793,14 @@ packages: engines: {node: '>= 0.10'} dev: false + /is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + dev: false + /is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -8784,7 +8808,6 @@ packages: call-bind: 1.0.8 call-bound: 1.0.4 get-intrinsic: 1.3.0 - dev: true /is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} @@ -8802,7 +8825,6 @@ packages: engines: {node: '>= 0.4'} dependencies: has-bigints: 1.1.0 - dev: true /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} @@ -8817,7 +8839,6 @@ packages: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 - dev: true /is-bun-module@2.0.0: resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} @@ -8828,7 +8849,6 @@ packages: /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - dev: true /is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} @@ -8851,7 +8871,6 @@ packages: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 - dev: true /is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} @@ -8924,7 +8943,6 @@ packages: /is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} - dev: true /is-negative-zero@2.0.3: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} @@ -8937,7 +8955,6 @@ packages: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 - dev: true /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} @@ -8961,19 +8978,16 @@ packages: gopd: 1.2.0 has-tostringtag: 1.0.2 hasown: 2.0.2 - dev: true /is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} - dev: true /is-shared-array-buffer@1.0.4: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} dependencies: call-bound: 1.0.4 - dev: true /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} @@ -8989,7 +9003,6 @@ packages: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 - dev: true /is-symbol@1.1.1: resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} @@ -8998,7 +9011,6 @@ packages: call-bound: 1.0.4 has-symbols: 1.1.0 safe-regex-test: 1.1.0 - dev: true /is-typed-array@1.1.15: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} @@ -9025,7 +9037,6 @@ packages: /is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} - dev: true /is-weakref@1.1.1: resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} @@ -9040,7 +9051,6 @@ packages: dependencies: call-bound: 1.0.4 get-intrinsic: 1.3.0 - dev: true /is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} @@ -9049,9 +9059,12 @@ packages: is-inside-container: 1.0.0 dev: false + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: false + /isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - dev: true /isbinaryfile@4.0.10: resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} @@ -9096,6 +9109,11 @@ packages: engines: {node: '>=10'} dev: false + /jpeg-exif@1.1.4: + resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + dev: false + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -9189,6 +9207,15 @@ packages: object.values: 1.2.1 dev: true + /jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + dev: false + /jwa@1.4.2: resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} dependencies: @@ -9229,6 +9256,12 @@ packages: type-check: 0.4.0 dev: true + /lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + dependencies: + immediate: 3.0.6 + dev: false + /light-my-request@5.14.0: resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} dependencies: @@ -9242,6 +9275,13 @@ packages: engines: {node: '>=14'} dev: true + /linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + dev: false + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true @@ -9590,6 +9630,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@5.1.7: + resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==} + engines: {node: ^18 || >=20} + hasBin: true + dev: false + /napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -9763,10 +9809,17 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + /object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + dev: false + /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} - dev: true /object.assign@4.1.7: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} @@ -9778,7 +9831,6 @@ packages: es-object-atoms: 1.1.1 has-symbols: 1.1.0 object-keys: 1.1.1 - dev: true /object.entries@1.1.9: resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} @@ -9977,6 +10029,14 @@ packages: netmask: 2.0.2 dev: true + /pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + dev: false + + /pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: false + /param-case@2.1.1: resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} dependencies: @@ -10046,6 +10106,25 @@ packages: /pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + /pdf-lib@1.17.1: + resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==} + dependencies: + '@pdf-lib/standard-fonts': 1.0.0 + '@pdf-lib/upng': 1.0.1 + pako: 1.0.11 + tslib: 1.14.1 + dev: false + + /pdfkit@0.15.2: + resolution: {integrity: sha512-s3GjpdBFSCaeDSX/v73MI5UsPqH1kjKut2AXCgxQ5OH10lPVOu5q5vLAG0OCpz/EYqKsTSw1WHpENqMvp43RKg==} + dependencies: + crypto-js: 4.2.0 + fontkit: 1.9.0 + jpeg-exif: 1.1.4 + linebreak: 1.1.0 + png-js: 1.0.0 + dev: false + /pg-cloudflare@1.2.7: resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} requiresBuild: true @@ -10215,10 +10294,13 @@ packages: mlly: 1.8.0 pathe: 2.0.3 + /png-js@1.0.0: + resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==} + dev: false + /possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - dev: true /postcss-import@15.1.0(postcss@8.5.6): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} @@ -10341,6 +10423,10 @@ packages: ansi-styles: 5.2.0 react-is: 18.3.1 + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: false + /process-warning@3.0.0: resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} dev: false @@ -10532,6 +10618,18 @@ packages: pify: 2.3.0 dev: true + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: false + /readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -10645,7 +10743,6 @@ packages: get-proto: 1.0.1 gopd: 1.2.0 set-function-name: 2.0.2 - dev: true /registry-auth-token@3.3.2: resolution: {integrity: sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==} @@ -10725,6 +10822,10 @@ packages: signal-exit: 4.1.0 dev: true + /restructure@2.0.1: + resolution: {integrity: sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==} + dev: false + /ret@0.4.3: resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==} engines: {node: '>=10'} @@ -10815,6 +10916,10 @@ packages: isarray: 2.0.5 dev: true + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: false + /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -10833,7 +10938,6 @@ packages: call-bound: 1.0.4 es-errors: 1.3.0 is-regex: 1.2.1 - dev: true /safe-regex2@3.1.0: resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} @@ -10855,6 +10959,11 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + /sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + dev: false + /scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} dependencies: @@ -10896,7 +11005,6 @@ packages: get-intrinsic: 1.3.0 gopd: 1.2.0 has-property-descriptors: 1.0.2 - dev: true /set-function-name@2.0.2: resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} @@ -10906,7 +11014,6 @@ packages: es-errors: 1.3.0 functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 - dev: true /set-proto@1.0.0: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} @@ -10917,6 +11024,10 @@ packages: es-object-atoms: 1.1.1 dev: true + /setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + dev: false + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false @@ -11119,7 +11230,6 @@ packages: dependencies: es-errors: 1.3.0 internal-slot: 1.1.0 - dev: true /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} @@ -11232,6 +11342,12 @@ packages: es-object-atoms: 1.1.1 dev: true + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: false + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -11451,6 +11567,10 @@ packages: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: true + /tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + dev: false + /tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} dev: false @@ -11578,7 +11698,6 @@ packages: /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: true /tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -11767,6 +11886,20 @@ packages: /undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + /unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + dev: false + + /unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + dev: false + /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -11847,7 +11980,6 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: true /uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} @@ -12038,7 +12170,6 @@ packages: is-number-object: 1.1.1 is-string: 1.1.1 is-symbol: 1.1.1 - dev: true /which-builtin-type@1.2.1: resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} @@ -12067,7 +12198,6 @@ packages: is-set: 2.0.3 is-weakmap: 2.0.2 is-weakset: 2.0.4 - dev: true /which-typed-array@1.1.19: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} @@ -12080,7 +12210,6 @@ packages: get-proto: 1.0.1 gopd: 1.2.0 has-tostringtag: 1.0.2 - dev: true /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} @@ -12151,6 +12280,17 @@ packages: is-wsl: 3.1.0 dev: false + /xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + dependencies: + sax: 1.6.0 + dev: false + + /xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + dev: false + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/services/finance/package.json b/services/finance/package.json index ddb99a1..d7c861f 100644 --- a/services/finance/package.json +++ b/services/finance/package.json @@ -9,11 +9,14 @@ "build": "tsc", "start": "node dist/index.js", "lint": "eslint src --ext .ts", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@fastify/swagger": "^8.13.0", "@fastify/swagger-ui": "^2.0.0", + "@the-order/database": "workspace:*", "@the-order/payment-gateway": "workspace:^", "@the-order/schemas": "workspace:*", "@the-order/shared": "workspace:*", @@ -23,6 +26,7 @@ "@types/node": "^20.10.6", "eslint": "^9.17.0", "tsx": "^4.7.0", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^1.6.1" } } diff --git a/services/finance/src/index.test.ts b/services/finance/src/index.test.ts index d3973f2..a1ded6a 100644 --- a/services/finance/src/index.test.ts +++ b/services/finance/src/index.test.ts @@ -1,62 +1,324 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import Fastify, { FastifyInstance } from 'fastify'; -import { createApiHelpers } from '@the-order/test-utils'; +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { createHmac } from 'crypto'; +import type { FastifyInstance } from 'fastify'; -describe('Finance Service', () => { - let app: FastifyInstance; - let api: ReturnType; +const originalEnv = { ...process.env }; - beforeEach(async () => { - app = Fastify({ - logger: false, - }); +function setTestEnv(): void { + process.env.NODE_ENV = 'development'; + process.env.PORT = '4003'; + process.env.DATABASE_URL = 'postgresql://postgres:postgres@127.0.0.1:5432/the_order_test'; + process.env.STORAGE_BUCKET = 'test-bucket'; + process.env.KMS_KEY_ID = 'test-kms-key'; + process.env.JWT_SECRET = 'test-jwt-secret-which-is-long-enough-1234567890'; +} - app.get('/health', async () => { - return { status: 'ok', service: 'finance' }; - }); +function base64Url(value: string): string { + return Buffer.from(value).toString('base64url'); +} - await app.ready(); - api = createApiHelpers(app); - }); +function createJwt(payload: Record): string { + const header = base64Url(JSON.stringify({ alg: 'HS256', typ: 'JWT' })); + const body = base64Url(JSON.stringify(payload)); + const signature = createHmac('sha256', process.env.JWT_SECRET as string) + .update(`${header}.${body}`) + .digest('base64url'); + return `${header}.${body}.${signature}`; +} - afterEach(async () => { - if (app) { - await app.close(); - } - }); +let createServer: (typeof import('./index'))['createServer']; - describe('GET /health', () => { - it('should return health status', async () => { - const response = await api.get('/health'); - expect(response.status).toBe(200); - expect(response.body).toHaveProperty('status'); - expect(response.body).toHaveProperty('service', 'finance'); - }); - }); - - describe('POST /ledger/entry', () => { - it('should require authentication', async () => { - const response = await api.post('/ledger/entry', { - accountId: 'test-account', - type: 'debit', - amount: 100, - currency: 'USD', - }); - - expect([401, 500]).toContain(response.status); - }); - }); - - describe('POST /payments', () => { - it('should require authentication', async () => { - const response = await api.post('/payments', { - amount: 100, - currency: 'USD', - paymentMethod: 'credit_card', - }); - - expect([401, 500]).toContain(response.status); - }); - }); +beforeAll(async () => { + setTestEnv(); + ({ createServer } = await import('./index')); }); +describe('Finance Service', () => { + let server: FastifyInstance; + + afterEach(async () => { + if (server) { + await server.close(); + } + process.env = { ...originalEnv, ...process.env }; + }); + + it('returns service health', async () => { + server = await createServer({ disableSwagger: true }); + await server.ready(); + + const response = await server.inject({ + method: 'GET', + url: '/health', + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toMatchObject({ + service: 'finance', + }); + }); + + it('creates a basket-backed BTC deposit and exposes holdings plus rebalance plan', async () => { + server = await createServer({ disableSwagger: true }); + await server.ready(); + + const token = createJwt({ + id: 'client-1', + roles: ['client', 'finance'], + }); + + const depositResponse = await server.inject({ + method: 'POST', + url: '/api/v1/btc-deposits', + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + clientId: 'client-1', + mandateName: 'Solace BTC jewelry box', + chain138VaultAddress: '0x1111111111111111111111111111111111111111', + allocations: [ + { symbol: 'cBTC', targetWeightBps: 4000 }, + { symbol: 'cUSDT', targetWeightBps: 3500 }, + { symbol: 'cXAUC', targetWeightBps: 2500 }, + ], + expectedAmountSats: 250000000, + }, + }); + + expect(depositResponse.statusCode).toBe(201); + const depositPayload = depositResponse.json() as { + deposit: { id: string; basketId: string; status: string; confirmationsRequired: number; depositAddress: string }; + basket: { id: string }; + createdBasket: boolean; + }; + + expect(depositPayload.createdBasket).toBe(true); + expect(depositPayload.deposit.status).toBe('instruction_created'); + expect(depositPayload.deposit.confirmationsRequired).toBe(6); + expect(depositPayload.deposit.depositAddress.startsWith('bc1q')).toBe(true); + expect(depositPayload.deposit.basketId).toBe(depositPayload.basket.id); + + const holdingsResponse = await server.inject({ + method: 'GET', + url: `/api/v1/holdings?clientId=client-1&basketId=${depositPayload.basket.id}`, + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(holdingsResponse.statusCode).toBe(200); + const holdingsPayload = holdingsResponse.json() as { + holdings: Array<{ symbol: string; allocationWeightBps: number; status: string }>; + }; + expect(holdingsPayload.holdings).toHaveLength(3); + expect(holdingsPayload.holdings[1]).toMatchObject({ + symbol: 'cUSDT', + allocationWeightBps: 3500, + status: 'pending_funding', + }); + + const rebalancesResponse = await server.inject({ + method: 'GET', + url: `/api/v1/rebalances?basketId=${depositPayload.basket.id}`, + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(rebalancesResponse.statusCode).toBe(200); + const rebalancesPayload = rebalancesResponse.json() as { + rebalances: Array<{ sourceSymbol: string; targetSymbols: string[]; status: string }>; + }; + expect(rebalancesPayload.rebalances).toHaveLength(1); + expect(rebalancesPayload.rebalances[0]).toMatchObject({ + sourceSymbol: 'cBTC', + status: 'planned', + }); + expect(rebalancesPayload.rebalances[0].targetSymbols).toEqual(['cUSDT', 'cXAUC']); + }); + + it('defaults bridge withdrawals to cBTC -> cWBTC', async () => { + server = await createServer({ disableSwagger: true }); + await server.ready(); + + const token = createJwt({ + id: 'client-2', + roles: ['client'], + }); + + const basketResponse = await server.inject({ + method: 'POST', + url: '/api/v1/baskets', + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + clientId: 'client-2', + mandateName: 'Client 2 basket', + chain138VaultAddress: '0x2222222222222222222222222222222222222222', + allocations: [{ symbol: 'cBTC', targetWeightBps: 10000 }], + }, + }); + + const basketId = (basketResponse.json() as { basket: { id: string } }).basket.id; + + const withdrawalResponse = await server.inject({ + method: 'POST', + url: '/api/v1/withdrawals/bridge', + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + clientId: 'client-2', + basketId, + destinationChainId: 1, + destinationAddress: '0x3333333333333333333333333333333333333333', + amount: '125000000', + }, + }); + + expect(withdrawalResponse.statusCode).toBe(201); + expect(withdrawalResponse.json()).toMatchObject({ + withdrawal: { + sourceSymbol: 'cBTC', + destinationSymbol: 'cWBTC', + destinationChainId: 1, + }, + }); + }); + + it('rejects cross-client access to another client basket', async () => { + server = await createServer({ disableSwagger: true }); + await server.ready(); + + const ownerToken = createJwt({ + id: 'client-owner', + roles: ['client'], + }); + const otherClientToken = createJwt({ + id: 'client-other', + roles: ['client'], + }); + + const basketResponse = await server.inject({ + method: 'POST', + url: '/api/v1/baskets', + headers: { + authorization: `Bearer ${ownerToken}`, + }, + payload: { + clientId: 'client-owner', + mandateName: 'Owner basket', + chain138VaultAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + allocations: [{ symbol: 'cBTC', targetWeightBps: 10000 }], + }, + }); + + const basketId = (basketResponse.json() as { basket: { id: string } }).basket.id; + + const holdingsResponse = await server.inject({ + method: 'GET', + url: `/api/v1/holdings?basketId=${basketId}`, + headers: { + authorization: `Bearer ${otherClientToken}`, + }, + }); + + expect(holdingsResponse.statusCode).toBe(403); + + const withdrawalResponse = await server.inject({ + method: 'POST', + url: '/api/v1/withdrawals/bridge', + headers: { + authorization: `Bearer ${otherClientToken}`, + }, + payload: { + clientId: 'client-other', + basketId, + destinationChainId: 1, + destinationAddress: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + amount: '1000', + }, + }); + + expect(withdrawalResponse.statusCode).toBe(403); + }); + + it('rejects unknown basket ids and mismatched vault addresses on deposit creation', async () => { + server = await createServer({ disableSwagger: true }); + await server.ready(); + + const token = createJwt({ + id: 'client-3', + roles: ['client'], + }); + + const unknownBasketResponse = await server.inject({ + method: 'POST', + url: '/api/v1/btc-deposits', + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + clientId: 'client-3', + basketId: '11111111-1111-4111-8111-111111111111', + chain138VaultAddress: '0xcccccccccccccccccccccccccccccccccccccccc', + }, + }); + + expect(unknownBasketResponse.statusCode).toBe(404); + + const basketResponse = await server.inject({ + method: 'POST', + url: '/api/v1/baskets', + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + clientId: 'client-3', + mandateName: 'Vault match basket', + chain138VaultAddress: '0xdddddddddddddddddddddddddddddddddddddddd', + allocations: [{ symbol: 'cBTC', targetWeightBps: 10000 }], + }, + }); + + const basketId = (basketResponse.json() as { basket: { id: string } }).basket.id; + + const mismatchedVaultResponse = await server.inject({ + method: 'POST', + url: '/api/v1/btc-deposits', + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + clientId: 'client-3', + basketId, + chain138VaultAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + }, + }); + + expect(mismatchedVaultResponse.statusCode).toBe(400); + }); + + it('validates payment payloads on the versioned route', async () => { + server = await createServer({ disableSwagger: true }); + await server.ready(); + + const token = createJwt({ + id: 'finance-user', + roles: ['finance'], + }); + + const response = await server.inject({ + method: 'POST', + url: '/api/v1/payments', + headers: { + authorization: `Bearer ${token}`, + }, + payload: {}, + }); + + expect(response.statusCode).toBe(400); + }); +}); diff --git a/services/finance/src/index.ts b/services/finance/src/index.ts index 3d32a77..0c7a17e 100644 --- a/services/finance/src/index.ts +++ b/services/finance/src/index.ts @@ -1,191 +1,236 @@ /** * Finance Service - * Handles payments, ledgers, rate models, and invoicing + * Handles payments, ledgers, and BTC jewelry-box workflows. */ -import Fastify, { type FastifyRequest, type FastifyReply } from 'fastify'; +import { fileURLToPath } from 'url'; +import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from 'fastify'; import fastifySwagger from '@fastify/swagger'; import fastifySwaggerUI from '@fastify/swagger-ui'; import { + AppError, errorHandler, createLogger, registerSecurityPlugins, addCorrelationId, addRequestLogging, getEnv, - createBodySchema, authenticateJWT, requireRole, } from '@the-order/shared'; -import { CreateLedgerEntrySchema, CreatePaymentSchema } from '@the-order/schemas'; -import { healthCheck as dbHealthCheck, getPool, createLedgerEntry, createPayment, updatePaymentStatus } from '@the-order/database'; +import { + type BasketMandate, + type CreateBasketMandate, + type CreateBtcDeposit, + type CreateBridgeWithdrawal, +} from '@the-order/schemas'; +import { + healthCheck as dbHealthCheck, + getPool, + createLedgerEntry, + createPayment, + updatePaymentStatus, +} from '@the-order/database'; import { StripePaymentGateway } from '@the-order/payment-gateway'; +import { + JewelryBoxStore, + createDefaultBridgeWithdrawal, +} from './jewelry-box-store'; const logger = createLogger('finance-service'); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const server: any = Fastify({ - logger: logger as any, - requestIdLogLabel: 'requestId', - disableRequestLogging: false, -}); - -// Initialize database pool const env = getEnv(); -if (env.DATABASE_URL) { - getPool({ connectionString: env.DATABASE_URL }); +const PRIVILEGED_FINANCE_ROLES = new Set(['admin', 'accountant', 'finance']); + +export type CreateServerOptions = { + jewelryBoxStore?: JewelryBoxStore; + disableSwagger?: boolean; +}; + +function isPrivilegedFinanceUser(request: FastifyRequest): boolean { + return (request.user?.roles ?? []).some((role) => PRIVILEGED_FINANCE_ROLES.has(role)); } -// Initialize payment gateway -let paymentGateway: StripePaymentGateway | null = null; -try { - if (env.PAYMENT_GATEWAY_API_KEY) { - paymentGateway = new StripePaymentGateway(); +function assertClientAccess(request: FastifyRequest, clientId: string): void { + if (!request.user) { + throw new AppError(401, 'UNAUTHORIZED', 'Authentication required'); } -} catch (error) { - logger.warn({ err: error }, 'Payment gateway not configured'); + + if (request.user.id === clientId || isPrivilegedFinanceUser(request)) { + return; + } + + throw new AppError(403, 'FORBIDDEN', 'Cannot access another client\'s finance basket'); } -// Initialize server -async function initializeServer(): Promise { - // Register Swagger - const swaggerUrl = env.SWAGGER_SERVER_URL || (env.NODE_ENV === 'development' ? 'http://localhost:4003' : undefined); +function resolveClientScope(request: FastifyRequest, clientId?: string): string | undefined { + if (clientId) { + assertClientAccess(request, clientId); + return clientId; + } + + return isPrivilegedFinanceUser(request) ? undefined : request.user?.id; +} + +function getAccessibleBasket( + request: FastifyRequest, + store: JewelryBoxStore, + basketId: string, +): BasketMandate { + const basket = store.getBasket(basketId); + if (!basket) { + throw new AppError(404, 'NOT_FOUND', 'Basket not found'); + } + + assertClientAccess(request, basket.clientId); + return basket; +} + +function createPaymentGateway(): StripePaymentGateway | null { + try { + return env.PAYMENT_GATEWAY_API_KEY ? new StripePaymentGateway() : null; + } catch (error) { + logger.warn({ err: error }, 'Payment gateway not configured'); + return null; + } +} + +async function safeDbHealthCheck(): Promise { + try { + return await dbHealthCheck(); + } catch (error) { + logger.warn({ err: error }, 'Database health check failed'); + return false; + } +} + +async function registerDocs(server: any, disableSwagger = false): Promise { + if (disableSwagger) { + return; + } + + const swaggerUrl = + env.SWAGGER_SERVER_URL || (env.NODE_ENV === 'development' ? 'http://localhost:4003' : undefined); + if (!swaggerUrl) { logger.warn('SWAGGER_SERVER_URL not set, Swagger documentation will not be available'); - } else { - await server.register(fastifySwagger, { - openapi: { - info: { - title: 'Finance Service API', - description: 'Payments, ledgers, rate models, and invoicing', - version: '1.0.0', - }, - servers: [ - { - url: swaggerUrl, - description: env.NODE_ENV || 'Development server', - }, - ], - }, - }); - - await server.register(fastifySwaggerUI, { - routePrefix: '/docs', - }); + return; } - await registerSecurityPlugins(server as any); - addCorrelationId(server as any); - addRequestLogging(server as any); - server.setErrorHandler(errorHandler as any); + await server.register(fastifySwagger, { + openapi: { + info: { + title: 'Finance Service API', + description: 'Payments, ledgers, and BTC jewelry-box workflows', + version: '1.0.0', + }, + servers: [ + { + url: swaggerUrl, + description: env.NODE_ENV || 'Development server', + }, + ], + }, + }); + + await server.register(fastifySwaggerUI, { + routePrefix: '/docs', + }); } -// Health check -server.get( - '/health', - { - schema: { - description: 'Health check endpoint', - tags: ['health'], - response: { - 200: { - type: 'object', - properties: { - status: { type: 'string' }, - service: { type: 'string' }, - database: { type: 'string' }, - }, - }, - }, - }, - }, - async () => { - const dbHealthy = await dbHealthCheck(); - return { - status: dbHealthy ? 'ok' : 'degraded', - service: 'finance', - database: dbHealthy ? 'connected' : 'disconnected', - }; - } -); - -// Ledger operations -server.post( - '/ledger/entry', - { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - preHandler: [authenticateJWT as any, requireRole('admin', 'accountant', 'finance') as any], - schema: { - ...createBodySchema(CreateLedgerEntrySchema), - description: 'Create a ledger entry', - tags: ['ledger'], - response: { - 201: { - type: 'object', - properties: { - entry: { - type: 'object', +function registerHealthRoute(server: any): void { + server.get( + '/health', + { + schema: { + description: 'Health check endpoint', + tags: ['health'], + response: { + 200: { + type: 'object', + properties: { + status: { type: 'string' }, + service: { type: 'string' }, + database: { type: 'string' }, }, }, }, }, }, - }, - async (request: FastifyRequest, reply: FastifyReply) => { - const body = request.body as { - accountId: string; - type: 'debit' | 'credit'; - amount: number; - currency: string; - description?: string; - reference?: string; - }; + async () => { + const dbHealthy = await safeDbHealthCheck(); + return { + status: dbHealthy ? 'ok' : 'degraded', + service: 'finance', + database: dbHealthy ? 'connected' : 'disconnected', + }; + }, + ); +} - // Save to database - const entry = await createLedgerEntry({ - account_id: body.accountId, - type: body.type, - amount: body.amount, - currency: body.currency, - description: body.description, - reference: body.reference, - }); - - return reply.status(201).send({ entry }); - } -); - -// Payment processing -server.post( - '/payments', - { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - preHandler: [authenticateJWT as any], - schema: { - ...createBodySchema(CreatePaymentSchema), - description: 'Process a payment', - tags: ['payments'], - response: { - 201: { +function registerLedgerRoute(server: any): void { + server.post( + '/ledger/entry', + { + preHandler: [authenticateJWT as never, requireRole('admin', 'accountant', 'finance') as never], + schema: { + description: 'Create a ledger entry', + tags: ['ledger'], + body: { type: 'object', + additionalProperties: false, + required: ['accountId', 'type', 'amount', 'currency'], properties: { - payment: { - type: 'object', - }, + accountId: { type: 'string' }, + type: { type: 'string', enum: ['debit', 'credit'] }, + amount: { type: 'number', exclusiveMinimum: 0 }, + currency: { type: 'string', minLength: 3, maxLength: 3 }, + description: { type: 'string' }, + reference: { type: 'string' }, }, }, }, }, - }, - async (request: FastifyRequest, reply: FastifyReply) => { - const body = request.body as { - amount: number; - currency: string; - paymentMethod: string; - metadata?: Record; - }; + async ( + request: FastifyRequest<{ + Body: { + accountId: string; + type: 'debit' | 'credit'; + amount: number; + currency: string; + description?: string; + reference?: string; + }; + }>, + reply: FastifyReply, + ) => { + const body = request.body; + const entry = await createLedgerEntry({ + account_id: body.accountId, + type: body.type, + amount: body.amount, + currency: body.currency, + description: body.description, + reference: body.reference, + }); - // Create payment record + return reply.status(201).send({ entry }); + }, + ); +} + +function registerPaymentsRoute(server: any, paymentGateway: StripePaymentGateway | null): void { + const handler = async ( + request: FastifyRequest<{ + Body: { + amount: number; + currency: string; + paymentMethod: string; + metadata?: Record; + }; + }>, + reply: FastifyReply, + ) => { + const body = request.body; const payment = await createPayment({ amount: body.amount, currency: body.currency, @@ -193,53 +238,324 @@ server.post( payment_method: body.paymentMethod, }); - // Process payment through gateway if available - if (paymentGateway) { - try { - const result = await paymentGateway.processPayment( - body.amount, - body.currency, - body.paymentMethod, - { - payment_id: payment.id, - ...body.metadata, - } - ); - - // Update payment status - const updatedPayment = await updatePaymentStatus( - payment.id, - result.status, - result.transactionId, - result.gatewayResponse - ); - - return reply.status(201).send({ payment: updatedPayment }); - } catch (error) { - logger.error({ err: error, paymentId: payment.id }, 'Payment processing failed'); - await updatePaymentStatus(payment.id, 'failed', undefined, { error: String(error) }); - throw error; - } - } else { - // No payment gateway configured - return pending status + if (!paymentGateway) { return reply.status(201).send({ payment }); } - } -); -// Start server -const start = async () => { + try { + const result = await paymentGateway.processPayment( + body.amount, + body.currency, + body.paymentMethod, + { + payment_id: payment.id, + ...body.metadata, + }, + ); + + const updatedPayment = await updatePaymentStatus( + payment.id, + result.status, + result.transactionId, + result.gatewayResponse, + ); + + return reply.status(201).send({ payment: updatedPayment }); + } catch (error) { + logger.error({ err: error, paymentId: payment.id }, 'Payment processing failed'); + await updatePaymentStatus(payment.id, 'failed', undefined, { error: String(error) }); + throw error; + } + }; + + const options = { + preHandler: [authenticateJWT as never], + schema: { + description: 'Process a payment', + tags: ['payments'], + body: { + type: 'object', + additionalProperties: false, + required: ['amount', 'currency', 'paymentMethod'], + properties: { + amount: { type: 'number', exclusiveMinimum: 0 }, + currency: { type: 'string', minLength: 3, maxLength: 3 }, + paymentMethod: { type: 'string' }, + description: { type: 'string' }, + metadata: { + type: 'object', + additionalProperties: { type: 'string' }, + }, + }, + }, + }, + }; + + server.post('/payments', options, handler); + server.post('/api/v1/payments', options, handler); +} + +function registerJewelryBoxRoutes(server: any, store: JewelryBoxStore): void { + server.post( + '/api/v1/baskets', + { + preHandler: [authenticateJWT as never], + schema: { + description: 'Create a Chain 138 basket mandate for BTC settlement flows', + tags: ['jewelry-box'], + body: { + type: 'object', + additionalProperties: false, + required: ['clientId', 'mandateName', 'chain138VaultAddress', 'allocations'], + properties: { + clientId: { type: 'string', minLength: 1 }, + mandateName: { type: 'string', minLength: 1 }, + chain138VaultAddress: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' }, + baseAssetSymbol: { type: 'string', minLength: 2, maxLength: 16 }, + allocations: { + type: 'array', + minItems: 1, + items: { + type: 'object', + additionalProperties: false, + required: ['symbol', 'targetWeightBps'], + properties: { + symbol: { type: 'string', minLength: 2, maxLength: 16 }, + targetWeightBps: { type: 'integer', minimum: 1, maximum: 10000 }, + routeHint: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + async ( + request: FastifyRequest<{ Body: CreateBasketMandate }>, + reply: FastifyReply, + ) => { + assertClientAccess(request, request.body.clientId); + const basket = store.createBasket(request.body); + return reply.status(201).send({ basket }); + }, + ); + + server.post( + '/api/v1/btc-deposits', + { + preHandler: [authenticateJWT as never], + schema: { + description: 'Create a BTC deposit instruction and associate it with a jewelry-box basket', + tags: ['jewelry-box'], + body: { + type: 'object', + additionalProperties: false, + required: ['clientId', 'chain138VaultAddress'], + properties: { + clientId: { type: 'string', minLength: 1 }, + basketId: { type: 'string' }, + mandateName: { type: 'string', minLength: 1 }, + chain138VaultAddress: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' }, + expectedAmountSats: { type: 'integer', minimum: 1 }, + clientReference: { type: 'string', minLength: 1 }, + allocations: { + type: 'array', + minItems: 1, + items: { + type: 'object', + additionalProperties: false, + required: ['symbol', 'targetWeightBps'], + properties: { + symbol: { type: 'string', minLength: 2, maxLength: 16 }, + targetWeightBps: { type: 'integer', minimum: 1, maximum: 10000 }, + routeHint: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + async ( + request: FastifyRequest<{ Body: CreateBtcDeposit }>, + reply: FastifyReply, + ) => { + assertClientAccess(request, request.body.clientId); + + if (request.body.basketId) { + const basket = getAccessibleBasket(request, store, request.body.basketId); + if (basket.clientId !== request.body.clientId) { + throw new AppError(400, 'CLIENT_MISMATCH', 'Deposit client does not match basket client'); + } + if (basket.chain138VaultAddress !== request.body.chain138VaultAddress) { + throw new AppError( + 400, + 'VAULT_ADDRESS_MISMATCH', + 'Deposit vault address does not match the selected basket', + ); + } + } + + const result = store.createDeposit(request.body); + return reply.status(201).send(result); + }, + ); + + server.get( + '/api/v1/btc-deposits/:id', + { + preHandler: [authenticateJWT as never], + schema: { + description: 'Get BTC deposit status', + tags: ['jewelry-box'], + }, + }, + async ( + request: FastifyRequest<{ Params: { id: string } }>, + ) => { + const deposit = store.getDeposit(request.params.id); + if (!deposit) { + throw new AppError(404, 'NOT_FOUND', 'Deposit not found'); + } + + assertClientAccess(request, deposit.clientId); + return { deposit }; + }, + ); + + server.get( + '/api/v1/holdings', + { + preHandler: [authenticateJWT as never], + schema: { + description: 'List basket holdings derived from underlying allocations', + tags: ['jewelry-box'], + }, + }, + async ( + request: FastifyRequest<{ Querystring: { clientId?: string; basketId?: string } }>, + ) => { + if (request.query.basketId) { + const basket = getAccessibleBasket(request, store, request.query.basketId); + if (request.query.clientId && basket.clientId !== request.query.clientId) { + throw new AppError(400, 'CLIENT_MISMATCH', 'Requested client does not own the selected basket'); + } + } + + const holdings = store.listHoldings({ + ...request.query, + clientId: resolveClientScope(request, request.query.clientId), + }); + return { holdings }; + }, + ); + + server.get( + '/api/v1/rebalances', + { + preHandler: [authenticateJWT as never], + schema: { + description: 'List planned or queued rebalances for jewelry-box baskets', + tags: ['jewelry-box'], + }, + }, + async ( + request: FastifyRequest<{ Querystring: { clientId?: string; basketId?: string } }>, + ) => { + if (request.query.basketId) { + const basket = getAccessibleBasket(request, store, request.query.basketId); + if (request.query.clientId && basket.clientId !== request.query.clientId) { + throw new AppError(400, 'CLIENT_MISMATCH', 'Requested client does not own the selected basket'); + } + } + + const rebalances = store.listRebalances({ + ...request.query, + clientId: resolveClientScope(request, request.query.clientId), + }); + return { rebalances }; + }, + ); + + server.post( + '/api/v1/withdrawals/bridge', + { + preHandler: [authenticateJWT as never], + schema: { + description: 'Request a public-chain bridge withdrawal from a jewelry-box basket', + tags: ['jewelry-box'], + body: { + type: 'object', + additionalProperties: false, + required: ['clientId', 'basketId', 'destinationChainId', 'destinationAddress', 'amount'], + properties: { + clientId: { type: 'string', minLength: 1 }, + basketId: { type: 'string', minLength: 1 }, + sourceSymbol: { type: 'string', minLength: 2, maxLength: 16 }, + destinationSymbol: { type: 'string', minLength: 2, maxLength: 16 }, + destinationChainId: { type: 'integer', minimum: 1 }, + destinationAddress: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' }, + amount: { type: 'string', minLength: 1 }, + }, + }, + }, + }, + async ( + request: FastifyRequest<{ Body: CreateBridgeWithdrawal }>, + reply: FastifyReply, + ) => { + const basket = getAccessibleBasket(request, store, request.body.basketId); + if (basket.clientId !== request.body.clientId) { + throw new AppError(400, 'CLIENT_MISMATCH', 'Withdrawal client does not match basket client'); + } + + const withdrawal = store.createBridgeWithdrawal( + createDefaultBridgeWithdrawal(request.body as CreateBridgeWithdrawal), + ); + return reply.status(201).send({ withdrawal }); + }, + ); +} + +export async function createServer(options: CreateServerOptions = {}): Promise { + if (env.DATABASE_URL) { + getPool({ connectionString: env.DATABASE_URL }); + } + + const server: any = Fastify({ + logger: logger as any, + requestIdLogLabel: 'requestId', + disableRequestLogging: false, + }); + + await registerDocs(server, options.disableSwagger); + await registerSecurityPlugins(server as any); + addCorrelationId(server); + addRequestLogging(server); + server.setErrorHandler(errorHandler as any); + + registerHealthRoute(server); + registerLedgerRoute(server); + registerPaymentsRoute(server, createPaymentGateway()); + registerJewelryBoxRoutes(server, options.jewelryBoxStore || new JewelryBoxStore()); + + return server as FastifyInstance; +} + +export async function start(): Promise { try { - await initializeServer(); - const env = getEnv(); + const server = await createServer(); const port = env.PORT || 4003; await server.listen({ port, host: '0.0.0.0' }); logger.info({ port }, 'Finance service listening'); - } catch (err) { - logger.error({ err }, 'Failed to start server'); + } catch (error) { + logger.error({ err: error }, 'Failed to start server'); process.exit(1); } -}; +} -start(); +const isDirectExecution = process.argv[1] === fileURLToPath(import.meta.url); +if (isDirectExecution) { + void start(); +} diff --git a/services/finance/src/jewelry-box-store.ts b/services/finance/src/jewelry-box-store.ts new file mode 100644 index 0000000..ff22c86 --- /dev/null +++ b/services/finance/src/jewelry-box-store.ts @@ -0,0 +1,296 @@ +import { randomUUID } from 'crypto'; +import type { + BasketAllocation, + BasketMandate, + BtcDeposit, + BridgeWithdrawal, + CreateBasketMandate, + CreateBridgeWithdrawal, + CreateBtcDeposit, + Holding, + Rebalance, +} from '@the-order/schemas'; + +const BTC_CONFIRMATIONS_REQUIRED = 6; +const DEFAULT_ALLOCATION: BasketAllocation = { symbol: 'cBTC', targetWeightBps: 10_000 }; +const BTC_BRIDGE_SYMBOL = 'cWBTC'; +const BECH32_ALPHABET = '023456789acdefghjklmnpqrstuvwxyz'; + +function nowIso(): string { + return new Date().toISOString(); +} + +function sumWeights(allocations: BasketAllocation[]): number { + return allocations.reduce((total, allocation) => total + allocation.targetWeightBps, 0); +} + +function toPseudoBitcoinAddress(seed: string): string { + const normalized = seed.replace(/[^a-f0-9]/gi, '').toLowerCase() || 'cb7c138'; + let out = 'bc1q'; + for (let index = 0; index < 32; index += 1) { + const source = normalized[index % normalized.length] ?? '0'; + const value = parseInt(source, 16); + out += Number.isNaN(value) ? 'q' : BECH32_ALPHABET[value]; + } + return out; +} + +function clone(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +export class JewelryBoxStore { + private readonly baskets = new Map(); + private readonly deposits = new Map(); + private readonly rebalances = new Map(); + private readonly withdrawals = new Map(); + + createBasket(input: CreateBasketMandate): BasketMandate { + const allocations = input.allocations.length > 0 ? input.allocations : [DEFAULT_ALLOCATION]; + if (sumWeights(allocations) !== 10_000) { + throw new Error('Basket allocations must sum to exactly 10,000 bps'); + } + + const timestamp = nowIso(); + const basket: BasketMandate = { + id: randomUUID(), + clientId: input.clientId, + mandateName: input.mandateName, + chain138VaultAddress: input.chain138VaultAddress, + baseAssetSymbol: input.baseAssetSymbol || 'cBTC', + status: 'active', + allocations: clone(allocations), + createdAt: timestamp, + updatedAt: timestamp, + }; + + this.baskets.set(basket.id, basket); + this.seedRebalancePlan(basket); + return clone(basket); + } + + getBasket(id: string): BasketMandate | null { + const basket = this.baskets.get(id); + return basket ? clone(basket) : null; + } + + createDeposit(input: CreateBtcDeposit): { deposit: BtcDeposit; basket: BasketMandate; createdBasket: boolean } { + let basket = input.basketId ? this.baskets.get(input.basketId) ?? null : null; + let createdBasket = false; + + if (input.basketId && !basket) { + throw new Error(`Unknown basket: ${input.basketId}`); + } + + if (!basket) { + basket = this.createBasket({ + clientId: input.clientId, + mandateName: input.mandateName || `BTC jewelry box ${input.clientId}`, + chain138VaultAddress: input.chain138VaultAddress, + allocations: input.allocations && input.allocations.length > 0 ? input.allocations : [DEFAULT_ALLOCATION], + baseAssetSymbol: 'cBTC', + }); + createdBasket = true; + } else { + if (basket.clientId !== input.clientId) { + throw new Error('Basket client does not match deposit client'); + } + if (basket.chain138VaultAddress !== input.chain138VaultAddress) { + throw new Error('Basket vault address does not match deposit vault address'); + } + } + + const timestamp = nowIso(); + const depositId = randomUUID(); + const deposit: BtcDeposit = { + id: depositId, + clientId: input.clientId, + basketId: basket.id, + chain138VaultAddress: basket.chain138VaultAddress, + depositAddress: toPseudoBitcoinAddress(depositId), + expectedAmountSats: input.expectedAmountSats, + confirmationsRequired: BTC_CONFIRMATIONS_REQUIRED, + currentConfirmations: 0, + status: 'instruction_created', + createdAt: timestamp, + updatedAt: timestamp, + }; + + this.deposits.set(deposit.id, deposit); + return { deposit: clone(deposit), basket: clone(basket), createdBasket }; + } + + getDeposit(id: string): BtcDeposit | null { + const deposit = this.deposits.get(id); + return deposit ? clone(deposit) : null; + } + + listHoldings(filters: { clientId?: string; basketId?: string }): Holding[] { + const baskets = Array.from(this.baskets.values()).filter((basket) => { + if (filters.clientId && basket.clientId !== filters.clientId) { + return false; + } + if (filters.basketId && basket.id !== filters.basketId) { + return false; + } + return true; + }); + + return baskets.flatMap((basket) => { + const fundedSats = Array.from(this.deposits.values()) + .filter((deposit) => deposit.basketId === basket.id && deposit.status === 'minted') + .reduce((total, deposit) => total + (deposit.expectedAmountSats ?? 0), 0); + + return basket.allocations.map((allocation: BasketAllocation) => ({ + clientId: basket.clientId, + basketId: basket.id, + symbol: allocation.symbol, + allocationWeightBps: allocation.targetWeightBps, + bookValueSats: Math.floor((fundedSats * allocation.targetWeightBps) / 10_000), + status: fundedSats > 0 ? 'funded' : 'pending_funding', + updatedAt: basket.updatedAt, + })); + }); + } + + listRebalances(filters: { clientId?: string; basketId?: string }): Rebalance[] { + return Array.from(this.rebalances.values()) + .filter((rebalance) => { + if (filters.clientId && rebalance.clientId !== filters.clientId) { + return false; + } + if (filters.basketId && rebalance.basketId !== filters.basketId) { + return false; + } + return true; + }) + .map((rebalance) => clone(rebalance)); + } + + createBridgeWithdrawal(input: CreateBridgeWithdrawal): BridgeWithdrawal { + if (!this.baskets.has(input.basketId)) { + throw new Error(`Unknown basket: ${input.basketId}`); + } + + const timestamp = nowIso(); + const withdrawal: BridgeWithdrawal = { + id: randomUUID(), + clientId: input.clientId, + basketId: input.basketId, + sourceSymbol: input.sourceSymbol || 'cBTC', + destinationSymbol: input.destinationSymbol || BTC_BRIDGE_SYMBOL, + destinationChainId: input.destinationChainId, + destinationAddress: input.destinationAddress, + amount: input.amount, + status: 'pending', + createdAt: timestamp, + updatedAt: timestamp, + }; + + this.withdrawals.set(withdrawal.id, withdrawal); + return clone(withdrawal); + } + + markDepositObserved( + depositId: string, + observedTxId: string, + confirmations: number, + expectedAmountSats?: number, + ): BtcDeposit { + const deposit = this.deposits.get(depositId); + if (!deposit) { + throw new Error(`Unknown BTC deposit: ${depositId}`); + } + + const nextConfirmations = Math.max(0, confirmations); + deposit.observedTxId = observedTxId; + deposit.currentConfirmations = nextConfirmations; + if (expectedAmountSats !== undefined) { + deposit.expectedAmountSats = expectedAmountSats; + } + deposit.status = + nextConfirmations >= deposit.confirmationsRequired ? 'confirmed' : 'pending_confirmations'; + deposit.updatedAt = nowIso(); + + return clone(deposit); + } + + markDepositMinted(depositId: string): BtcDeposit { + const deposit = this.deposits.get(depositId); + if (!deposit) { + throw new Error(`Unknown BTC deposit: ${depositId}`); + } + + deposit.currentConfirmations = Math.max(deposit.currentConfirmations, deposit.confirmationsRequired); + deposit.status = 'minted'; + deposit.updatedAt = nowIso(); + + const basket = this.baskets.get(deposit.basketId); + if (basket) { + basket.updatedAt = deposit.updatedAt; + if (basket.allocations.some((allocation: BasketAllocation) => allocation.symbol !== 'cBTC')) { + this.promoteRebalancePlan(basket); + } + } + + return clone(deposit); + } + + freezeDeposit(depositId: string, reason: string): BtcDeposit { + const deposit = this.deposits.get(depositId); + if (!deposit) { + throw new Error(`Unknown BTC deposit: ${depositId}`); + } + deposit.status = 'frozen'; + deposit.freezeReason = reason; + deposit.updatedAt = nowIso(); + return clone(deposit); + } + + private seedRebalancePlan(basket: BasketMandate): void { + const targetSymbols = basket.allocations + .map((allocation: BasketAllocation) => allocation.symbol) + .filter((symbol: string) => symbol !== 'cBTC'); + + if (targetSymbols.length === 0) { + return; + } + + const timestamp = nowIso(); + const rebalance: Rebalance = { + id: randomUUID(), + clientId: basket.clientId, + basketId: basket.id, + sourceSymbol: 'cBTC', + targetSymbols, + reason: 'Awaiting funded BTC deposit before Chain 138 rebalance', + status: 'planned', + createdAt: timestamp, + updatedAt: timestamp, + }; + + this.rebalances.set(rebalance.id, rebalance); + } + + private promoteRebalancePlan(basket: BasketMandate): void { + for (const rebalance of this.rebalances.values()) { + if (rebalance.basketId === basket.id && rebalance.status === 'planned') { + rebalance.status = 'queued'; + rebalance.updatedAt = nowIso(); + } + } + } +} + +export function createDefaultBridgeWithdrawal( + input: Omit & { + sourceSymbol?: string; + destinationSymbol?: string; + }, +): CreateBridgeWithdrawal { + return { + ...input, + sourceSymbol: input.sourceSymbol || 'cBTC', + destinationSymbol: input.destinationSymbol || BTC_BRIDGE_SYMBOL, + }; +} diff --git a/services/finance/tests/finance.test.ts b/services/finance/tests/finance.test.ts index 2c815ab..9453453 100644 --- a/services/finance/tests/finance.test.ts +++ b/services/finance/tests/finance.test.ts @@ -1,45 +1,56 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { FastifyInstance } from 'fastify'; -import { createServer } from '../src/index'; +import { describe, expect, it } from 'vitest'; +import { JewelryBoxStore } from '../src/jewelry-box-store'; -describe('Finance Service', () => { - let server: FastifyInstance; +describe('JewelryBoxStore', () => { + it('creates queued rebalance work only for non-cBTC allocations', () => { + const store = new JewelryBoxStore(); + const basket = store.createBasket({ + clientId: 'client-1', + mandateName: 'Multi-asset jewelry box', + chain138VaultAddress: '0x4444444444444444444444444444444444444444', + allocations: [ + { symbol: 'cBTC', targetWeightBps: 2500 }, + { symbol: 'cUSDC', targetWeightBps: 5000 }, + { symbol: 'cXAUC', targetWeightBps: 2500 }, + ], + baseAssetSymbol: 'cBTC', + }); - beforeEach(async () => { - server = await createServer(); - await server.ready(); - }); - - afterEach(async () => { - await server.close(); - }); - - describe('Health Check', () => { - it('should return 200 on health check', async () => { - const response = await server.inject({ - method: 'GET', - url: '/health', - }); - - expect(response.statusCode).toBe(200); - expect(response.json()).toMatchObject({ - status: 'ok', - }); + const rebalances = store.listRebalances({ basketId: basket.id }); + expect(rebalances).toHaveLength(1); + expect(rebalances[0]).toMatchObject({ + sourceSymbol: 'cBTC', + targetSymbols: ['cUSDC', 'cXAUC'], + status: 'planned', }); }); - describe('Payment Processing', () => { - it('should validate payment request schema', async () => { - const response = await server.inject({ - method: 'POST', - url: '/api/v1/payments', - payload: { - // Invalid payload to test validation - }, - }); - - expect(response.statusCode).toBe(400); + it('promotes rebalance plans once a deposit is marked minted', () => { + const store = new JewelryBoxStore(); + const { basket, deposit } = store.createDeposit({ + clientId: 'client-2', + chain138VaultAddress: '0x5555555555555555555555555555555555555555', + allocations: [ + { symbol: 'cBTC', targetWeightBps: 3000 }, + { symbol: 'cUSDT', targetWeightBps: 7000 }, + ], + expectedAmountSats: 300000000, }); + + store.markDepositObserved(deposit.id, 'btc-tx-1', 6, 300000000); + store.markDepositMinted(deposit.id); + + const updatedDeposit = store.getDeposit(deposit.id); + expect(updatedDeposit?.status).toBe('minted'); + + const holdings = store.listHoldings({ basketId: basket.id }); + expect(holdings[1]).toMatchObject({ + symbol: 'cUSDT', + bookValueSats: 210000000, + status: 'funded', + }); + + const rebalances = store.listRebalances({ basketId: basket.id }); + expect(rebalances[0]?.status).toBe('queued'); }); }); - diff --git a/services/finance/vitest.config.mjs b/services/finance/vitest.config.mjs new file mode 100644 index 0000000..87a1cca --- /dev/null +++ b/services/finance/vitest.config.mjs @@ -0,0 +1,18 @@ +import path from 'path'; + +const root = '/home/intlc/projects/proxmox/the-order'; + +export default { + resolve: { + alias: { + '@the-order/auth': path.join(root, 'packages/auth/src/index.ts'), + '@the-order/shared': path.join(root, 'packages/shared/src/index.ts'), + '@the-order/schemas': path.join(root, 'packages/schemas/src/index.ts'), + '@the-order/database': path.join(root, 'packages/database/src/index.ts'), + '@the-order/payment-gateway': path.join(root, 'packages/payment-gateway/src/index.ts'), + }, + }, + test: { + environment: 'node', + }, +};