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
This commit is contained in:
@@ -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<Payment> {
|
||||
const response = await this.client.post<Payment>('/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<Payment> {
|
||||
@@ -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<BasketMandate> {
|
||||
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<BtcDeposit> {
|
||||
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<Holding[]> {
|
||||
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<Rebalance[]> {
|
||||
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<BridgeWithdrawal> {
|
||||
const response = await this.client.post<{ withdrawal: BridgeWithdrawal }>(
|
||||
'/api/v1/withdrawals/bridge',
|
||||
data,
|
||||
);
|
||||
return response.data.withdrawal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string[]> {
|
||||
if (!query || query.length < 2) {
|
||||
export async function getSearchSuggestions(searchTerm: string, limit = 10): Promise<string[]> {
|
||||
if (!searchTerm || searchTerm.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -113,9 +112,8 @@ export async function getSearchSuggestions(query: string, limit = 10): Promise<s
|
||||
WHERE title ILIKE $1
|
||||
ORDER BY title
|
||||
LIMIT $2`,
|
||||
[`%${query}%`, limit]
|
||||
[`%${searchTerm}%`, limit]
|
||||
);
|
||||
|
||||
return result.rows.map((row) => row.title);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
136
packages/schemas/src/btc-basket.ts
Normal file
136
packages/schemas/src/btc-basket.ts
Normal file
@@ -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<typeof BasketAllocationSchema>;
|
||||
|
||||
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<typeof BasketMandateSchema>;
|
||||
|
||||
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<typeof CreateBasketMandateSchema>;
|
||||
|
||||
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<typeof BtcDepositSchema>;
|
||||
|
||||
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<typeof CreateBtcDepositSchema>;
|
||||
|
||||
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<typeof HoldingSchema>;
|
||||
|
||||
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<typeof RebalanceSchema>;
|
||||
|
||||
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<typeof BridgeWithdrawalSchema>;
|
||||
|
||||
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<typeof CreateBridgeWithdrawalSchema>;
|
||||
@@ -9,4 +9,4 @@ export * from './vc';
|
||||
export * from './payment';
|
||||
export * from './ledger';
|
||||
export * from './eresidency';
|
||||
|
||||
export * from './btc-basket';
|
||||
|
||||
@@ -77,9 +77,9 @@ export function createSecurityTestData() {
|
||||
export async function testAuthenticationBypass(
|
||||
makeRequest: (headers?: Record<string, string>) => Promise<{ status: number }>
|
||||
): Promise<boolean> {
|
||||
const testCases = [
|
||||
const testCases: Array<Record<string, string> | 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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
return server.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
522
pnpm-lock.yaml
generated
522
pnpm-lock.yaml
generated
@@ -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'}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<typeof createApiHelpers>;
|
||||
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, unknown>): 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<void> {
|
||||
// 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<boolean> {
|
||||
try {
|
||||
return await dbHealthCheck();
|
||||
} catch (error) {
|
||||
logger.warn({ err: error }, 'Database health check failed');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function registerDocs(server: any, disableSwagger = false): Promise<void> {
|
||||
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<string, string>;
|
||||
};
|
||||
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<string, string>;
|
||||
};
|
||||
}>,
|
||||
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<FastifyInstance> {
|
||||
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<void> {
|
||||
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();
|
||||
}
|
||||
|
||||
296
services/finance/src/jewelry-box-store.ts
Normal file
296
services/finance/src/jewelry-box-store.ts
Normal file
@@ -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<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
export class JewelryBoxStore {
|
||||
private readonly baskets = new Map<string, BasketMandate>();
|
||||
private readonly deposits = new Map<string, BtcDeposit>();
|
||||
private readonly rebalances = new Map<string, Rebalance>();
|
||||
private readonly withdrawals = new Map<string, BridgeWithdrawal>();
|
||||
|
||||
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<CreateBridgeWithdrawal, 'destinationSymbol' | 'sourceSymbol'> & {
|
||||
sourceSymbol?: string;
|
||||
destinationSymbol?: string;
|
||||
},
|
||||
): CreateBridgeWithdrawal {
|
||||
return {
|
||||
...input,
|
||||
sourceSymbol: input.sourceSymbol || 'cBTC',
|
||||
destinationSymbol: input.destinationSymbol || BTC_BRIDGE_SYMBOL,
|
||||
};
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
18
services/finance/vitest.config.mjs
Normal file
18
services/finance/vitest.config.mjs
Normal file
@@ -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',
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user