@sip-protocol/sdk 0.1.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +3236 -1554
- package/dist/index.d.ts +3236 -1554
- package/dist/index.js +9185 -3521
- package/dist/index.mjs +8995 -3376
- package/package.json +5 -2
- package/src/adapters/near-intents.ts +48 -35
- package/src/adapters/oneclick-client.ts +9 -1
- package/src/compliance/compliance-manager.ts +1035 -0
- package/src/compliance/index.ts +43 -0
- package/src/index.ts +129 -2
- package/src/payment/index.ts +54 -0
- package/src/payment/payment.ts +623 -0
- package/src/payment/stablecoins.ts +306 -0
- package/src/privacy.ts +127 -94
- package/src/proofs/circuits/fulfillment_proof.json +1 -0
- package/src/proofs/circuits/funding_proof.json +1 -0
- package/src/proofs/circuits/validity_proof.json +1 -0
- package/src/proofs/interface.ts +13 -1
- package/src/proofs/noir.ts +967 -97
- package/src/secure-memory.ts +147 -0
- package/src/sip.ts +399 -37
- package/src/stealth.ts +116 -84
- package/src/treasury/index.ts +43 -0
- package/src/treasury/treasury.ts +911 -0
- package/src/wallet/hardware/index.ts +87 -0
- package/src/wallet/hardware/ledger.ts +628 -0
- package/src/wallet/hardware/mock.ts +667 -0
- package/src/wallet/hardware/trezor.ts +657 -0
- package/src/wallet/hardware/types.ts +317 -0
- package/src/wallet/index.ts +40 -0
- package/src/zcash/shielded-service.ts +59 -1
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise Compliance Manager for SIP Protocol
|
|
3
|
+
*
|
|
4
|
+
* Provides compliance management, auditor access control,
|
|
5
|
+
* transaction disclosure, and reporting functionality.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // Initialize compliance manager
|
|
10
|
+
* const compliance = await ComplianceManager.create({
|
|
11
|
+
* organizationName: 'Acme Corp',
|
|
12
|
+
* riskThreshold: 70,
|
|
13
|
+
* highValueThreshold: 100000_000000n,
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* // Register an auditor
|
|
17
|
+
* const auditor = await compliance.registerAuditor({
|
|
18
|
+
* organization: 'Big Four Audit',
|
|
19
|
+
* contactName: 'John Auditor',
|
|
20
|
+
* contactEmail: 'john@bigfour.com',
|
|
21
|
+
* publicKey: '0x...',
|
|
22
|
+
* scope: {
|
|
23
|
+
* transactionTypes: ['all'],
|
|
24
|
+
* chains: ['ethereum'],
|
|
25
|
+
* tokens: [],
|
|
26
|
+
* startDate: Date.now() / 1000 - 365 * 24 * 60 * 60,
|
|
27
|
+
* },
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* // Disclose a transaction to the auditor
|
|
31
|
+
* const disclosed = compliance.discloseTransaction(txId, auditor.auditorId, viewingKey)
|
|
32
|
+
*
|
|
33
|
+
* // Generate a compliance report
|
|
34
|
+
* const report = await compliance.generateReport({
|
|
35
|
+
* type: 'transaction_summary',
|
|
36
|
+
* title: 'Q4 2024 Report',
|
|
37
|
+
* format: 'json',
|
|
38
|
+
* startDate: quarterStart,
|
|
39
|
+
* endDate: quarterEnd,
|
|
40
|
+
* })
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import {
|
|
45
|
+
ReportStatus,
|
|
46
|
+
PrivacyLevel,
|
|
47
|
+
type ComplianceConfig,
|
|
48
|
+
type ComplianceRole,
|
|
49
|
+
type AuditScope,
|
|
50
|
+
type AuditorRegistration,
|
|
51
|
+
type DisclosedTransaction,
|
|
52
|
+
type ComplianceReport,
|
|
53
|
+
type ReportData,
|
|
54
|
+
type CreateComplianceConfigParams,
|
|
55
|
+
type RegisterAuditorParams,
|
|
56
|
+
type GenerateReportParams,
|
|
57
|
+
type DisclosureRequest,
|
|
58
|
+
type AuditLogEntry,
|
|
59
|
+
type ViewingKey,
|
|
60
|
+
type HexString,
|
|
61
|
+
type ChainId,
|
|
62
|
+
type Asset,
|
|
63
|
+
type ShieldedPayment,
|
|
64
|
+
type PaymentPurpose,
|
|
65
|
+
} from '@sip-protocol/types'
|
|
66
|
+
import { bytesToHex, randomBytes } from '@noble/hashes/utils'
|
|
67
|
+
|
|
68
|
+
import { ValidationError, ErrorCode } from '../errors'
|
|
69
|
+
import { generateViewingKey, deriveViewingKey, decryptWithViewing } from '../privacy'
|
|
70
|
+
import { isValidChainId } from '../validation'
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Default configuration values
|
|
74
|
+
*/
|
|
75
|
+
const DEFAULTS = {
|
|
76
|
+
riskThreshold: 70,
|
|
77
|
+
highValueThreshold: 10000_000000n, // 10,000 USDC equivalent
|
|
78
|
+
retentionPeriodDays: 2555, // ~7 years
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* ComplianceManager - Enterprise compliance and auditing
|
|
83
|
+
*/
|
|
84
|
+
export class ComplianceManager {
|
|
85
|
+
private config: ComplianceConfig
|
|
86
|
+
private auditors: Map<string, AuditorRegistration> = new Map()
|
|
87
|
+
private disclosedTransactions: Map<string, DisclosedTransaction> = new Map()
|
|
88
|
+
private reports: Map<string, ComplianceReport> = new Map()
|
|
89
|
+
private disclosureRequests: Map<string, DisclosureRequest> = new Map()
|
|
90
|
+
private auditLog: AuditLogEntry[] = []
|
|
91
|
+
|
|
92
|
+
private constructor(config: ComplianceConfig) {
|
|
93
|
+
this.config = config
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a new compliance manager
|
|
98
|
+
*/
|
|
99
|
+
static async create(params: CreateComplianceConfigParams): Promise<ComplianceManager> {
|
|
100
|
+
if (!params.organizationName || params.organizationName.trim().length === 0) {
|
|
101
|
+
throw new ValidationError(
|
|
102
|
+
'organization name is required',
|
|
103
|
+
'organizationName',
|
|
104
|
+
undefined,
|
|
105
|
+
ErrorCode.MISSING_REQUIRED
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const now = Math.floor(Date.now() / 1000)
|
|
110
|
+
const organizationId = generateId('org')
|
|
111
|
+
|
|
112
|
+
// Generate master viewing key
|
|
113
|
+
const masterViewingKey = generateViewingKey(`compliance/${organizationId}`)
|
|
114
|
+
|
|
115
|
+
const config: ComplianceConfig = {
|
|
116
|
+
organizationId,
|
|
117
|
+
organizationName: params.organizationName,
|
|
118
|
+
masterViewingKey,
|
|
119
|
+
defaultAuditScope: {
|
|
120
|
+
transactionTypes: ['all'],
|
|
121
|
+
chains: [],
|
|
122
|
+
tokens: [],
|
|
123
|
+
},
|
|
124
|
+
riskThreshold: params.riskThreshold ?? DEFAULTS.riskThreshold,
|
|
125
|
+
highValueThreshold: params.highValueThreshold ?? DEFAULTS.highValueThreshold,
|
|
126
|
+
retentionPeriodDays: params.retentionPeriodDays ?? DEFAULTS.retentionPeriodDays,
|
|
127
|
+
autoReporting: {
|
|
128
|
+
enabled: false,
|
|
129
|
+
frequency: 'monthly',
|
|
130
|
+
reportTypes: ['transaction_summary'],
|
|
131
|
+
},
|
|
132
|
+
createdAt: now,
|
|
133
|
+
updatedAt: now,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return new ComplianceManager(config)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Load from existing config
|
|
141
|
+
*/
|
|
142
|
+
static fromConfig(config: ComplianceConfig): ComplianceManager {
|
|
143
|
+
return new ComplianceManager(config)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Getters ─────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
get organizationId(): string {
|
|
149
|
+
return this.config.organizationId
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get organizationName(): string {
|
|
153
|
+
return this.config.organizationName
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
get masterViewingKey(): ViewingKey {
|
|
157
|
+
return this.config.masterViewingKey
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getConfig(): ComplianceConfig {
|
|
161
|
+
return { ...this.config }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Auditor Management ──────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Register a new auditor
|
|
168
|
+
*/
|
|
169
|
+
async registerAuditor(
|
|
170
|
+
params: RegisterAuditorParams,
|
|
171
|
+
registeredBy: string,
|
|
172
|
+
): Promise<AuditorRegistration> {
|
|
173
|
+
validateRegisterAuditorParams(params)
|
|
174
|
+
|
|
175
|
+
const auditorId = generateId('auditor')
|
|
176
|
+
const now = Math.floor(Date.now() / 1000)
|
|
177
|
+
|
|
178
|
+
// Derive auditor-specific viewing key
|
|
179
|
+
const viewingKey = deriveViewingKey(
|
|
180
|
+
this.config.masterViewingKey,
|
|
181
|
+
`auditor/${auditorId}`
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const auditor: AuditorRegistration = {
|
|
185
|
+
auditorId,
|
|
186
|
+
organization: params.organization,
|
|
187
|
+
contactName: params.contactName,
|
|
188
|
+
contactEmail: params.contactEmail,
|
|
189
|
+
publicKey: params.publicKey,
|
|
190
|
+
viewingKey,
|
|
191
|
+
scope: params.scope,
|
|
192
|
+
role: params.role ?? 'auditor',
|
|
193
|
+
registeredAt: now,
|
|
194
|
+
registeredBy,
|
|
195
|
+
isActive: true,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.auditors.set(auditorId, auditor)
|
|
199
|
+
|
|
200
|
+
// Log the action
|
|
201
|
+
this.addAuditLog(registeredBy, 'auditor_registered', {
|
|
202
|
+
auditorId,
|
|
203
|
+
organization: params.organization,
|
|
204
|
+
role: auditor.role,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
return auditor
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get an auditor by ID
|
|
212
|
+
*/
|
|
213
|
+
getAuditor(auditorId: string): AuditorRegistration | undefined {
|
|
214
|
+
return this.auditors.get(auditorId)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get all auditors
|
|
219
|
+
*/
|
|
220
|
+
getAllAuditors(): AuditorRegistration[] {
|
|
221
|
+
return Array.from(this.auditors.values())
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get active auditors
|
|
226
|
+
*/
|
|
227
|
+
getActiveAuditors(): AuditorRegistration[] {
|
|
228
|
+
return this.getAllAuditors().filter(a => a.isActive)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Deactivate an auditor
|
|
233
|
+
*/
|
|
234
|
+
deactivateAuditor(
|
|
235
|
+
auditorId: string,
|
|
236
|
+
deactivatedBy: string,
|
|
237
|
+
reason: string,
|
|
238
|
+
): AuditorRegistration {
|
|
239
|
+
const auditor = this.auditors.get(auditorId)
|
|
240
|
+
if (!auditor) {
|
|
241
|
+
throw new ValidationError(
|
|
242
|
+
`auditor not found: ${auditorId}`,
|
|
243
|
+
'auditorId',
|
|
244
|
+
undefined,
|
|
245
|
+
ErrorCode.INVALID_INPUT
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
auditor.isActive = false
|
|
250
|
+
auditor.deactivatedAt = Math.floor(Date.now() / 1000)
|
|
251
|
+
auditor.deactivationReason = reason
|
|
252
|
+
|
|
253
|
+
this.addAuditLog(deactivatedBy, 'auditor_deactivated', {
|
|
254
|
+
auditorId,
|
|
255
|
+
reason,
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
return auditor
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Update auditor scope
|
|
263
|
+
*/
|
|
264
|
+
updateAuditorScope(
|
|
265
|
+
auditorId: string,
|
|
266
|
+
scope: AuditScope,
|
|
267
|
+
updatedBy: string,
|
|
268
|
+
): AuditorRegistration {
|
|
269
|
+
const auditor = this.auditors.get(auditorId)
|
|
270
|
+
if (!auditor) {
|
|
271
|
+
throw new ValidationError(
|
|
272
|
+
`auditor not found: ${auditorId}`,
|
|
273
|
+
'auditorId',
|
|
274
|
+
undefined,
|
|
275
|
+
ErrorCode.INVALID_INPUT
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
auditor.scope = scope
|
|
280
|
+
|
|
281
|
+
this.addAuditLog(updatedBy, 'config_updated', {
|
|
282
|
+
auditorId,
|
|
283
|
+
field: 'scope',
|
|
284
|
+
newScope: scope,
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
return auditor
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Transaction Disclosure ──────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Disclose a transaction to an auditor
|
|
294
|
+
*
|
|
295
|
+
* @param payment - The shielded payment to disclose
|
|
296
|
+
* @param auditorId - The auditor to disclose to
|
|
297
|
+
* @param viewingKey - The viewing key to decrypt the payment
|
|
298
|
+
* @param disclosedBy - Who authorized the disclosure
|
|
299
|
+
* @returns The disclosed transaction
|
|
300
|
+
*/
|
|
301
|
+
discloseTransaction(
|
|
302
|
+
payment: ShieldedPayment,
|
|
303
|
+
auditorId: string,
|
|
304
|
+
viewingKey: ViewingKey,
|
|
305
|
+
disclosedBy: string,
|
|
306
|
+
additionalInfo?: {
|
|
307
|
+
txHash?: string
|
|
308
|
+
blockNumber?: number
|
|
309
|
+
riskScore?: number
|
|
310
|
+
riskFlags?: string[]
|
|
311
|
+
notes?: string
|
|
312
|
+
tags?: string[]
|
|
313
|
+
},
|
|
314
|
+
): DisclosedTransaction {
|
|
315
|
+
const auditor = this.auditors.get(auditorId)
|
|
316
|
+
if (!auditor) {
|
|
317
|
+
throw new ValidationError(
|
|
318
|
+
`auditor not found: ${auditorId}`,
|
|
319
|
+
'auditorId',
|
|
320
|
+
undefined,
|
|
321
|
+
ErrorCode.INVALID_INPUT
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!auditor.isActive) {
|
|
326
|
+
throw new ValidationError(
|
|
327
|
+
'auditor is not active',
|
|
328
|
+
'auditorId',
|
|
329
|
+
undefined,
|
|
330
|
+
ErrorCode.INVALID_INPUT
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check if transaction is within auditor's scope
|
|
335
|
+
if (!this.isWithinScope(payment, auditor.scope)) {
|
|
336
|
+
throw new ValidationError(
|
|
337
|
+
'transaction is outside auditor scope',
|
|
338
|
+
'scope',
|
|
339
|
+
undefined,
|
|
340
|
+
ErrorCode.INVALID_INPUT
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const disclosureId = generateId('disc')
|
|
345
|
+
const now = Math.floor(Date.now() / 1000)
|
|
346
|
+
|
|
347
|
+
// Decrypt memo if available
|
|
348
|
+
let decryptedMemo: string | undefined
|
|
349
|
+
let memoDecryptionError: string | undefined
|
|
350
|
+
if (payment.encryptedMemo && viewingKey) {
|
|
351
|
+
try {
|
|
352
|
+
decryptedMemo = this.decryptMemoSafe(payment.encryptedMemo, viewingKey)
|
|
353
|
+
} catch (error) {
|
|
354
|
+
// Log warning for monitoring - memo decryption failure may indicate
|
|
355
|
+
// key mismatch, data corruption, or unauthorized viewing key
|
|
356
|
+
memoDecryptionError = error instanceof Error ? error.message : 'Unknown decryption error'
|
|
357
|
+
console.warn(
|
|
358
|
+
`[ComplianceManager] Failed to decrypt memo for payment ${payment.paymentId}: ${memoDecryptionError}`
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const disclosed: DisclosedTransaction = {
|
|
364
|
+
transactionId: payment.paymentId,
|
|
365
|
+
disclosureId,
|
|
366
|
+
auditorId,
|
|
367
|
+
disclosedAt: now,
|
|
368
|
+
disclosedBy,
|
|
369
|
+
type: 'payment',
|
|
370
|
+
direction: 'outbound', // Payments are outbound
|
|
371
|
+
token: payment.token,
|
|
372
|
+
amount: payment.amount,
|
|
373
|
+
sender: payment.senderCommitment?.value ?? 'hidden',
|
|
374
|
+
recipient: payment.recipientStealth?.address ?? payment.recipientAddress ?? 'unknown',
|
|
375
|
+
txHash: additionalInfo?.txHash ?? '',
|
|
376
|
+
blockNumber: additionalInfo?.blockNumber ?? 0,
|
|
377
|
+
timestamp: payment.createdAt,
|
|
378
|
+
chain: payment.sourceChain,
|
|
379
|
+
privacyLevel: payment.privacyLevel,
|
|
380
|
+
memo: decryptedMemo ?? payment.memo,
|
|
381
|
+
purpose: payment.purpose,
|
|
382
|
+
riskScore: additionalInfo?.riskScore,
|
|
383
|
+
riskFlags: additionalInfo?.riskFlags,
|
|
384
|
+
notes: additionalInfo?.notes,
|
|
385
|
+
tags: additionalInfo?.tags,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this.disclosedTransactions.set(disclosureId, disclosed)
|
|
389
|
+
|
|
390
|
+
this.addAuditLog(disclosedBy, 'transaction_disclosed', {
|
|
391
|
+
disclosureId,
|
|
392
|
+
transactionId: payment.paymentId,
|
|
393
|
+
auditorId,
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
return disclosed
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get disclosed transactions for an auditor
|
|
401
|
+
*/
|
|
402
|
+
getDisclosedTransactions(auditorId?: string): DisclosedTransaction[] {
|
|
403
|
+
const all = Array.from(this.disclosedTransactions.values())
|
|
404
|
+
if (auditorId) {
|
|
405
|
+
return all.filter(t => t.auditorId === auditorId)
|
|
406
|
+
}
|
|
407
|
+
return all
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Get a disclosed transaction by ID
|
|
412
|
+
*/
|
|
413
|
+
getDisclosedTransaction(disclosureId: string): DisclosedTransaction | undefined {
|
|
414
|
+
return this.disclosedTransactions.get(disclosureId)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ─── Disclosure Requests ─────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Create a disclosure request
|
|
421
|
+
*/
|
|
422
|
+
createDisclosureRequest(
|
|
423
|
+
transactionId: string,
|
|
424
|
+
auditorId: string,
|
|
425
|
+
reason: string,
|
|
426
|
+
): DisclosureRequest {
|
|
427
|
+
const auditor = this.auditors.get(auditorId)
|
|
428
|
+
if (!auditor) {
|
|
429
|
+
throw new ValidationError(
|
|
430
|
+
`auditor not found: ${auditorId}`,
|
|
431
|
+
'auditorId',
|
|
432
|
+
undefined,
|
|
433
|
+
ErrorCode.INVALID_INPUT
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const requestId = generateId('req')
|
|
438
|
+
const now = Math.floor(Date.now() / 1000)
|
|
439
|
+
|
|
440
|
+
const request: DisclosureRequest = {
|
|
441
|
+
requestId,
|
|
442
|
+
transactionId,
|
|
443
|
+
auditorId,
|
|
444
|
+
reason,
|
|
445
|
+
requestedAt: now,
|
|
446
|
+
status: 'pending',
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
this.disclosureRequests.set(requestId, request)
|
|
450
|
+
|
|
451
|
+
this.addAuditLog(auditorId, 'disclosure_requested', {
|
|
452
|
+
requestId,
|
|
453
|
+
transactionId,
|
|
454
|
+
reason,
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
return request
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Approve a disclosure request
|
|
462
|
+
*/
|
|
463
|
+
approveDisclosureRequest(requestId: string, approvedBy: string): DisclosureRequest {
|
|
464
|
+
const request = this.disclosureRequests.get(requestId)
|
|
465
|
+
if (!request) {
|
|
466
|
+
throw new ValidationError(
|
|
467
|
+
`request not found: ${requestId}`,
|
|
468
|
+
'requestId',
|
|
469
|
+
undefined,
|
|
470
|
+
ErrorCode.INVALID_INPUT
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
request.status = 'approved'
|
|
475
|
+
request.approvedBy = approvedBy
|
|
476
|
+
request.resolvedAt = Math.floor(Date.now() / 1000)
|
|
477
|
+
|
|
478
|
+
this.addAuditLog(approvedBy, 'disclosure_approved', {
|
|
479
|
+
requestId,
|
|
480
|
+
transactionId: request.transactionId,
|
|
481
|
+
auditorId: request.auditorId,
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
return request
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Deny a disclosure request
|
|
489
|
+
*/
|
|
490
|
+
denyDisclosureRequest(
|
|
491
|
+
requestId: string,
|
|
492
|
+
deniedBy: string,
|
|
493
|
+
reason: string,
|
|
494
|
+
): DisclosureRequest {
|
|
495
|
+
const request = this.disclosureRequests.get(requestId)
|
|
496
|
+
if (!request) {
|
|
497
|
+
throw new ValidationError(
|
|
498
|
+
`request not found: ${requestId}`,
|
|
499
|
+
'requestId',
|
|
500
|
+
undefined,
|
|
501
|
+
ErrorCode.INVALID_INPUT
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
request.status = 'denied'
|
|
506
|
+
request.approvedBy = deniedBy
|
|
507
|
+
request.resolvedAt = Math.floor(Date.now() / 1000)
|
|
508
|
+
request.denialReason = reason
|
|
509
|
+
|
|
510
|
+
this.addAuditLog(deniedBy, 'disclosure_denied', {
|
|
511
|
+
requestId,
|
|
512
|
+
transactionId: request.transactionId,
|
|
513
|
+
auditorId: request.auditorId,
|
|
514
|
+
reason,
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
return request
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Get pending disclosure requests
|
|
522
|
+
*/
|
|
523
|
+
getPendingRequests(): DisclosureRequest[] {
|
|
524
|
+
return Array.from(this.disclosureRequests.values())
|
|
525
|
+
.filter(r => r.status === 'pending')
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ─── Reporting ───────────────────────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Generate a compliance report
|
|
532
|
+
*/
|
|
533
|
+
async generateReport(
|
|
534
|
+
params: GenerateReportParams,
|
|
535
|
+
requestedBy: string,
|
|
536
|
+
): Promise<ComplianceReport> {
|
|
537
|
+
validateReportParams(params)
|
|
538
|
+
|
|
539
|
+
const reportId = generateId('report')
|
|
540
|
+
const now = Math.floor(Date.now() / 1000)
|
|
541
|
+
|
|
542
|
+
const report: ComplianceReport = {
|
|
543
|
+
reportId,
|
|
544
|
+
type: params.type,
|
|
545
|
+
title: params.title,
|
|
546
|
+
description: params.description,
|
|
547
|
+
format: params.format,
|
|
548
|
+
status: ReportStatus.GENERATING,
|
|
549
|
+
requestedBy,
|
|
550
|
+
requestedAt: now,
|
|
551
|
+
startDate: params.startDate,
|
|
552
|
+
endDate: params.endDate,
|
|
553
|
+
chains: params.chains ?? [],
|
|
554
|
+
tokens: params.tokens ?? [],
|
|
555
|
+
includeInbound: params.includeInbound ?? true,
|
|
556
|
+
includeOutbound: params.includeOutbound ?? true,
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
this.reports.set(reportId, report)
|
|
560
|
+
|
|
561
|
+
// Generate report data
|
|
562
|
+
try {
|
|
563
|
+
const transactions = this.filterTransactions(
|
|
564
|
+
params.startDate,
|
|
565
|
+
params.endDate,
|
|
566
|
+
params.chains,
|
|
567
|
+
params.tokens,
|
|
568
|
+
report.includeInbound,
|
|
569
|
+
report.includeOutbound,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
const reportData = this.computeReportData(
|
|
573
|
+
transactions,
|
|
574
|
+
params.includeTransactions ?? false,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
if (params.format === 'json') {
|
|
578
|
+
report.data = reportData
|
|
579
|
+
} else if (params.format === 'csv') {
|
|
580
|
+
report.content = this.generateCSV(transactions)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
report.status = ReportStatus.COMPLETED
|
|
584
|
+
report.generatedAt = Math.floor(Date.now() / 1000)
|
|
585
|
+
} catch (error) {
|
|
586
|
+
report.status = ReportStatus.FAILED
|
|
587
|
+
report.error = error instanceof Error ? error.message : 'Unknown error'
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
this.addAuditLog(requestedBy, 'report_generated', {
|
|
591
|
+
reportId,
|
|
592
|
+
type: params.type,
|
|
593
|
+
status: report.status,
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
return report
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Get a report by ID
|
|
601
|
+
*/
|
|
602
|
+
getReport(reportId: string): ComplianceReport | undefined {
|
|
603
|
+
return this.reports.get(reportId)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Get all reports
|
|
608
|
+
*/
|
|
609
|
+
getAllReports(): ComplianceReport[] {
|
|
610
|
+
return Array.from(this.reports.values())
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ─── Audit Log ───────────────────────────────────────────────────────────────
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Get audit log entries
|
|
617
|
+
*/
|
|
618
|
+
getAuditLog(options?: {
|
|
619
|
+
startDate?: number
|
|
620
|
+
endDate?: number
|
|
621
|
+
actor?: string
|
|
622
|
+
action?: AuditLogEntry['action']
|
|
623
|
+
limit?: number
|
|
624
|
+
}): AuditLogEntry[] {
|
|
625
|
+
let entries = [...this.auditLog]
|
|
626
|
+
|
|
627
|
+
if (options?.startDate) {
|
|
628
|
+
entries = entries.filter(e => e.timestamp >= options.startDate!)
|
|
629
|
+
}
|
|
630
|
+
if (options?.endDate) {
|
|
631
|
+
entries = entries.filter(e => e.timestamp <= options.endDate!)
|
|
632
|
+
}
|
|
633
|
+
if (options?.actor) {
|
|
634
|
+
entries = entries.filter(e => e.actor === options.actor)
|
|
635
|
+
}
|
|
636
|
+
if (options?.action) {
|
|
637
|
+
entries = entries.filter(e => e.action === options.action)
|
|
638
|
+
}
|
|
639
|
+
if (options?.limit) {
|
|
640
|
+
entries = entries.slice(-options.limit)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return entries
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ─── Export ──────────────────────────────────────────────────────────────────
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Export transactions to CSV
|
|
650
|
+
*/
|
|
651
|
+
exportToCSV(auditorId?: string): string {
|
|
652
|
+
const transactions = this.getDisclosedTransactions(auditorId)
|
|
653
|
+
return this.generateCSV(transactions)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Export transactions to JSON
|
|
658
|
+
*/
|
|
659
|
+
exportToJSON(auditorId?: string): string {
|
|
660
|
+
const transactions = this.getDisclosedTransactions(auditorId)
|
|
661
|
+
return JSON.stringify(transactions, (_, value) =>
|
|
662
|
+
typeof value === 'bigint' ? value.toString() : value
|
|
663
|
+
, 2)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ─── Serialization ───────────────────────────────────────────────────────────
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Serialize to JSON
|
|
670
|
+
*/
|
|
671
|
+
toJSON(): string {
|
|
672
|
+
return JSON.stringify({
|
|
673
|
+
config: this.config,
|
|
674
|
+
auditors: Array.from(this.auditors.entries()),
|
|
675
|
+
disclosedTransactions: Array.from(this.disclosedTransactions.entries()),
|
|
676
|
+
reports: Array.from(this.reports.entries()),
|
|
677
|
+
disclosureRequests: Array.from(this.disclosureRequests.entries()),
|
|
678
|
+
auditLog: this.auditLog,
|
|
679
|
+
}, (_, value) => typeof value === 'bigint' ? value.toString() : value)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Deserialize from JSON
|
|
684
|
+
*/
|
|
685
|
+
static fromJSON(json: string): ComplianceManager {
|
|
686
|
+
const data = JSON.parse(json, (key, value) => {
|
|
687
|
+
if (typeof value === 'string' && /^\d+$/.test(value) &&
|
|
688
|
+
['amount', 'highValueThreshold', 'minAmount', 'maxAmount'].includes(key)) {
|
|
689
|
+
return BigInt(value)
|
|
690
|
+
}
|
|
691
|
+
return value
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
const manager = new ComplianceManager(data.config)
|
|
695
|
+
manager.auditors = new Map(data.auditors)
|
|
696
|
+
manager.disclosedTransactions = new Map(data.disclosedTransactions)
|
|
697
|
+
manager.reports = new Map(data.reports)
|
|
698
|
+
manager.disclosureRequests = new Map(data.disclosureRequests)
|
|
699
|
+
manager.auditLog = data.auditLog
|
|
700
|
+
|
|
701
|
+
return manager
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ─── Private Helpers ─────────────────────────────────────────────────────────
|
|
705
|
+
|
|
706
|
+
private addAuditLog(
|
|
707
|
+
actor: string,
|
|
708
|
+
action: AuditLogEntry['action'],
|
|
709
|
+
details: Record<string, unknown>,
|
|
710
|
+
): void {
|
|
711
|
+
this.auditLog.push({
|
|
712
|
+
entryId: generateId('log'),
|
|
713
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
714
|
+
actor,
|
|
715
|
+
action,
|
|
716
|
+
details,
|
|
717
|
+
})
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private isWithinScope(payment: ShieldedPayment, scope: AuditScope): boolean {
|
|
721
|
+
// Check chains
|
|
722
|
+
if (scope.chains.length > 0 && !scope.chains.includes(payment.sourceChain)) {
|
|
723
|
+
return false
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Check tokens
|
|
727
|
+
if (scope.tokens.length > 0 && !scope.tokens.includes(payment.token.symbol)) {
|
|
728
|
+
return false
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Check date range
|
|
732
|
+
if (scope.startDate && payment.createdAt < scope.startDate) {
|
|
733
|
+
return false
|
|
734
|
+
}
|
|
735
|
+
if (scope.endDate && payment.createdAt > scope.endDate) {
|
|
736
|
+
return false
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Check amount range
|
|
740
|
+
if (scope.minAmount && payment.amount < scope.minAmount) {
|
|
741
|
+
return false
|
|
742
|
+
}
|
|
743
|
+
if (scope.maxAmount && payment.amount > scope.maxAmount) {
|
|
744
|
+
return false
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return true
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private decryptMemoSafe(encryptedMemo: HexString, viewingKey: ViewingKey): string {
|
|
751
|
+
// This is a simplified implementation
|
|
752
|
+
// In practice, you'd need proper encrypted transaction data
|
|
753
|
+
try {
|
|
754
|
+
const decrypted = decryptWithViewing(
|
|
755
|
+
{
|
|
756
|
+
ciphertext: encryptedMemo,
|
|
757
|
+
nonce: '0x' + '00'.repeat(24) as HexString,
|
|
758
|
+
viewingKeyHash: viewingKey.hash,
|
|
759
|
+
},
|
|
760
|
+
viewingKey
|
|
761
|
+
)
|
|
762
|
+
return decrypted.sender // Simplified - actual implementation varies
|
|
763
|
+
} catch {
|
|
764
|
+
throw new Error('Failed to decrypt memo')
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
private filterTransactions(
|
|
769
|
+
startDate: number,
|
|
770
|
+
endDate: number,
|
|
771
|
+
chains?: ChainId[],
|
|
772
|
+
tokens?: string[],
|
|
773
|
+
includeInbound?: boolean,
|
|
774
|
+
includeOutbound?: boolean,
|
|
775
|
+
): DisclosedTransaction[] {
|
|
776
|
+
return Array.from(this.disclosedTransactions.values()).filter(tx => {
|
|
777
|
+
if (tx.timestamp < startDate || tx.timestamp > endDate) return false
|
|
778
|
+
if (chains?.length && !chains.includes(tx.chain)) return false
|
|
779
|
+
if (tokens?.length && !tokens.includes(tx.token.symbol)) return false
|
|
780
|
+
if (!includeInbound && tx.direction === 'inbound') return false
|
|
781
|
+
if (!includeOutbound && tx.direction === 'outbound') return false
|
|
782
|
+
return true
|
|
783
|
+
})
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
private computeReportData(
|
|
787
|
+
transactions: DisclosedTransaction[],
|
|
788
|
+
includeTransactions: boolean,
|
|
789
|
+
): ReportData {
|
|
790
|
+
const volumeByToken: Record<string, bigint> = {}
|
|
791
|
+
const avgByToken: Record<string, bigint> = {}
|
|
792
|
+
const countByToken: Record<string, number> = {}
|
|
793
|
+
const byChain: Record<ChainId, number> = {} as Record<ChainId, number>
|
|
794
|
+
const byPrivacy: Record<PrivacyLevel, number> = {} as Record<PrivacyLevel, number>
|
|
795
|
+
const counterparties = new Set<string>()
|
|
796
|
+
|
|
797
|
+
let totalInbound = 0
|
|
798
|
+
let totalOutbound = 0
|
|
799
|
+
let payments = 0
|
|
800
|
+
let swaps = 0
|
|
801
|
+
let deposits = 0
|
|
802
|
+
let withdrawals = 0
|
|
803
|
+
let lowRisk = 0
|
|
804
|
+
let mediumRisk = 0
|
|
805
|
+
let highRisk = 0
|
|
806
|
+
let flagged = 0
|
|
807
|
+
|
|
808
|
+
const highValueTxs: DisclosedTransaction[] = []
|
|
809
|
+
|
|
810
|
+
for (const tx of transactions) {
|
|
811
|
+
// Volume tracking
|
|
812
|
+
const symbol = tx.token.symbol
|
|
813
|
+
volumeByToken[symbol] = (volumeByToken[symbol] ?? 0n) + tx.amount
|
|
814
|
+
countByToken[symbol] = (countByToken[symbol] ?? 0) + 1
|
|
815
|
+
|
|
816
|
+
// Direction
|
|
817
|
+
if (tx.direction === 'inbound') totalInbound++
|
|
818
|
+
else totalOutbound++
|
|
819
|
+
|
|
820
|
+
// Type
|
|
821
|
+
if (tx.type === 'payment') payments++
|
|
822
|
+
else if (tx.type === 'swap') swaps++
|
|
823
|
+
else if (tx.type === 'deposit') deposits++
|
|
824
|
+
else if (tx.type === 'withdrawal') withdrawals++
|
|
825
|
+
|
|
826
|
+
// Chain
|
|
827
|
+
byChain[tx.chain] = (byChain[tx.chain] ?? 0) + 1
|
|
828
|
+
|
|
829
|
+
// Privacy
|
|
830
|
+
byPrivacy[tx.privacyLevel] = (byPrivacy[tx.privacyLevel] ?? 0) + 1
|
|
831
|
+
|
|
832
|
+
// Counterparty
|
|
833
|
+
counterparties.add(tx.recipient)
|
|
834
|
+
counterparties.add(tx.sender)
|
|
835
|
+
|
|
836
|
+
// Risk
|
|
837
|
+
if (tx.riskScore !== undefined) {
|
|
838
|
+
if (tx.riskScore < 30) lowRisk++
|
|
839
|
+
else if (tx.riskScore < 70) mediumRisk++
|
|
840
|
+
else highRisk++
|
|
841
|
+
}
|
|
842
|
+
if (tx.riskFlags?.length) flagged++
|
|
843
|
+
|
|
844
|
+
// High value
|
|
845
|
+
if (tx.amount >= this.config.highValueThreshold) {
|
|
846
|
+
highValueTxs.push(tx)
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Calculate averages
|
|
851
|
+
for (const symbol of Object.keys(volumeByToken)) {
|
|
852
|
+
avgByToken[symbol] = volumeByToken[symbol] / BigInt(countByToken[symbol])
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const data: ReportData = {
|
|
856
|
+
summary: {
|
|
857
|
+
totalTransactions: transactions.length,
|
|
858
|
+
totalInbound,
|
|
859
|
+
totalOutbound,
|
|
860
|
+
totalVolume: volumeByToken,
|
|
861
|
+
uniqueCounterparties: counterparties.size,
|
|
862
|
+
averageTransactionSize: avgByToken,
|
|
863
|
+
dateRange: {
|
|
864
|
+
start: Math.min(...transactions.map(t => t.timestamp)),
|
|
865
|
+
end: Math.max(...transactions.map(t => t.timestamp)),
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
byType: { payments, swaps, deposits, withdrawals },
|
|
869
|
+
byChain,
|
|
870
|
+
byPrivacyLevel: byPrivacy,
|
|
871
|
+
highValueTransactions: highValueTxs,
|
|
872
|
+
riskSummary: {
|
|
873
|
+
lowRisk,
|
|
874
|
+
mediumRisk,
|
|
875
|
+
highRisk,
|
|
876
|
+
flaggedTransactions: flagged,
|
|
877
|
+
},
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (includeTransactions) {
|
|
881
|
+
data.transactions = transactions
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return data
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
private generateCSV(transactions: DisclosedTransaction[]): string {
|
|
888
|
+
const headers = [
|
|
889
|
+
'Transaction ID',
|
|
890
|
+
'Disclosure ID',
|
|
891
|
+
'Type',
|
|
892
|
+
'Direction',
|
|
893
|
+
'Token',
|
|
894
|
+
'Amount',
|
|
895
|
+
'Sender',
|
|
896
|
+
'Recipient',
|
|
897
|
+
'Chain',
|
|
898
|
+
'Privacy Level',
|
|
899
|
+
'Timestamp',
|
|
900
|
+
'TX Hash',
|
|
901
|
+
'Block',
|
|
902
|
+
'Risk Score',
|
|
903
|
+
'Purpose',
|
|
904
|
+
'Memo',
|
|
905
|
+
]
|
|
906
|
+
|
|
907
|
+
const rows = transactions.map(tx => [
|
|
908
|
+
tx.transactionId,
|
|
909
|
+
tx.disclosureId,
|
|
910
|
+
tx.type,
|
|
911
|
+
tx.direction,
|
|
912
|
+
tx.token.symbol,
|
|
913
|
+
tx.amount.toString(),
|
|
914
|
+
tx.sender,
|
|
915
|
+
tx.recipient,
|
|
916
|
+
tx.chain,
|
|
917
|
+
tx.privacyLevel,
|
|
918
|
+
new Date(tx.timestamp * 1000).toISOString(),
|
|
919
|
+
tx.txHash,
|
|
920
|
+
tx.blockNumber.toString(),
|
|
921
|
+
tx.riskScore?.toString() ?? '',
|
|
922
|
+
tx.purpose ?? '',
|
|
923
|
+
tx.memo ?? '',
|
|
924
|
+
])
|
|
925
|
+
|
|
926
|
+
// Escape CSV cells to prevent formula injection attacks
|
|
927
|
+
// Formulas starting with =, +, -, @, |, or tab can be malicious
|
|
928
|
+
const escapeForCSV = (val: string): string => {
|
|
929
|
+
// First handle formula injection: prefix with single quote if starts with dangerous chars
|
|
930
|
+
let escaped = val
|
|
931
|
+
if (/^[=+\-@|\t]/.test(escaped)) {
|
|
932
|
+
escaped = `'${escaped}`
|
|
933
|
+
}
|
|
934
|
+
// Then escape double quotes and wrap in quotes
|
|
935
|
+
return `"${escaped.replace(/"/g, '""')}"`
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const csvRows = [headers, ...rows].map(row => row.map(escapeForCSV).join(','))
|
|
939
|
+
|
|
940
|
+
return csvRows.join('\n')
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
945
|
+
|
|
946
|
+
function generateId(prefix: string): string {
|
|
947
|
+
return `${prefix}_${bytesToHex(randomBytes(12))}`
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function validateRegisterAuditorParams(params: RegisterAuditorParams): void {
|
|
951
|
+
if (!params.organization?.trim()) {
|
|
952
|
+
throw new ValidationError(
|
|
953
|
+
'organization is required',
|
|
954
|
+
'organization',
|
|
955
|
+
undefined,
|
|
956
|
+
ErrorCode.MISSING_REQUIRED
|
|
957
|
+
)
|
|
958
|
+
}
|
|
959
|
+
if (!params.contactName?.trim()) {
|
|
960
|
+
throw new ValidationError(
|
|
961
|
+
'contact name is required',
|
|
962
|
+
'contactName',
|
|
963
|
+
undefined,
|
|
964
|
+
ErrorCode.MISSING_REQUIRED
|
|
965
|
+
)
|
|
966
|
+
}
|
|
967
|
+
if (!params.contactEmail?.trim()) {
|
|
968
|
+
throw new ValidationError(
|
|
969
|
+
'contact email is required',
|
|
970
|
+
'contactEmail',
|
|
971
|
+
undefined,
|
|
972
|
+
ErrorCode.MISSING_REQUIRED
|
|
973
|
+
)
|
|
974
|
+
}
|
|
975
|
+
if (!params.publicKey?.trim()) {
|
|
976
|
+
throw new ValidationError(
|
|
977
|
+
'public key is required',
|
|
978
|
+
'publicKey',
|
|
979
|
+
undefined,
|
|
980
|
+
ErrorCode.MISSING_REQUIRED
|
|
981
|
+
)
|
|
982
|
+
}
|
|
983
|
+
if (!params.scope) {
|
|
984
|
+
throw new ValidationError(
|
|
985
|
+
'audit scope is required',
|
|
986
|
+
'scope',
|
|
987
|
+
undefined,
|
|
988
|
+
ErrorCode.MISSING_REQUIRED
|
|
989
|
+
)
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function validateReportParams(params: GenerateReportParams): void {
|
|
994
|
+
if (!params.title?.trim()) {
|
|
995
|
+
throw new ValidationError(
|
|
996
|
+
'report title is required',
|
|
997
|
+
'title',
|
|
998
|
+
undefined,
|
|
999
|
+
ErrorCode.MISSING_REQUIRED
|
|
1000
|
+
)
|
|
1001
|
+
}
|
|
1002
|
+
if (!params.type) {
|
|
1003
|
+
throw new ValidationError(
|
|
1004
|
+
'report type is required',
|
|
1005
|
+
'type',
|
|
1006
|
+
undefined,
|
|
1007
|
+
ErrorCode.MISSING_REQUIRED
|
|
1008
|
+
)
|
|
1009
|
+
}
|
|
1010
|
+
if (!params.format) {
|
|
1011
|
+
throw new ValidationError(
|
|
1012
|
+
'report format is required',
|
|
1013
|
+
'format',
|
|
1014
|
+
undefined,
|
|
1015
|
+
ErrorCode.MISSING_REQUIRED
|
|
1016
|
+
)
|
|
1017
|
+
}
|
|
1018
|
+
if (params.startDate === undefined || params.startDate === null ||
|
|
1019
|
+
params.endDate === undefined || params.endDate === null) {
|
|
1020
|
+
throw new ValidationError(
|
|
1021
|
+
'date range is required',
|
|
1022
|
+
'dateRange',
|
|
1023
|
+
undefined,
|
|
1024
|
+
ErrorCode.MISSING_REQUIRED
|
|
1025
|
+
)
|
|
1026
|
+
}
|
|
1027
|
+
if (params.startDate >= params.endDate) {
|
|
1028
|
+
throw new ValidationError(
|
|
1029
|
+
'start date must be before end date',
|
|
1030
|
+
'dateRange',
|
|
1031
|
+
undefined,
|
|
1032
|
+
ErrorCode.INVALID_INPUT
|
|
1033
|
+
)
|
|
1034
|
+
}
|
|
1035
|
+
}
|