Files
the_order/packages/database/src/credential-templates.ts
defiQUG 2633de4d33 feat(eresidency): Complete eResidency service implementation
- Implement credential revocation endpoint with proper database integration
- Fix database row mapping (snake_case to camelCase) for eResidency applications
- Add missing imports (getRiskAssessmentEngine, VeriffKYCProvider, ComplyAdvantageSanctionsProvider)
- Fix environment variable type checking for Veriff and ComplyAdvantage providers
- Add required 'message' field to notification service calls
- Fix risk assessment type mismatches
- Update audit logging to use 'verified' action type (supported by schema)
- Resolve all TypeScript errors and unused variable warnings
- Add TypeScript ignore comments for placeholder implementations
- Temporarily disable security/detect-non-literal-regexp rule due to ESLint 9 compatibility
- Service now builds successfully with no linter errors

All core functionality implemented:
- Application submission and management
- KYC integration (Veriff placeholder)
- Sanctions screening (ComplyAdvantage placeholder)
- Risk assessment engine
- Credential issuance and revocation
- Reviewer console
- Status endpoints
- Auto-issuance service
2025-11-10 19:43:02 -08:00

203 lines
5.6 KiB
TypeScript

/**
* Credential template management
*/
import { query } from './client';
import { z } from 'zod';
export const CredentialTemplateSchema = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().optional(),
credential_type: z.array(z.string()),
template_data: z.record(z.unknown()),
version: z.number().int().positive(),
is_active: z.boolean(),
created_by: z.string().uuid().nullable(),
created_at: z.date(),
updated_at: z.date(),
});
export type CredentialTemplate = z.infer<typeof CredentialTemplateSchema>;
/**
* Create a credential template
*/
export async function createCredentialTemplate(
template: Omit<CredentialTemplate, 'id' | 'created_at' | 'updated_at'>
): Promise<CredentialTemplate> {
const result = await query<CredentialTemplate>(
`INSERT INTO credential_templates
(name, description, credential_type, template_data, version, is_active, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *`,
[
template.name,
template.description || null,
template.credential_type,
JSON.stringify(template.template_data),
template.version,
template.is_active,
template.created_by || null,
]
);
return result.rows[0]!;
}
/**
* Get credential template by ID
*/
export async function getCredentialTemplate(id: string): Promise<CredentialTemplate | null> {
const result = await query<CredentialTemplate>(
`SELECT * FROM credential_templates WHERE id = $1`,
[id]
);
return result.rows[0] || null;
}
/**
* Get credential template by name and version
*/
export async function getCredentialTemplateByName(
name: string,
version?: number
): Promise<CredentialTemplate | null> {
if (version) {
const result = await query<CredentialTemplate>(
`SELECT * FROM credential_templates WHERE name = $1 AND version = $2`,
[name, version]
);
return result.rows[0] || null;
} else {
// Get latest active version
const result = await query<CredentialTemplate>(
`SELECT * FROM credential_templates
WHERE name = $1 AND is_active = TRUE
ORDER BY version DESC
LIMIT 1`,
[name]
);
return result.rows[0] || null;
}
}
/**
* List all credential templates
*/
export async function listCredentialTemplates(
activeOnly = true,
limit = 100,
offset = 0
): Promise<CredentialTemplate[]> {
const whereClause = activeOnly ? 'WHERE is_active = TRUE' : '';
const result = await query<CredentialTemplate>(
`SELECT * FROM credential_templates
${whereClause}
ORDER BY name, version DESC
LIMIT $1 OFFSET $2`,
[limit, offset]
);
return result.rows;
}
/**
* Update credential template
*/
export async function updateCredentialTemplate(
id: string,
updates: Partial<Pick<CredentialTemplate, 'description' | 'template_data' | 'is_active'>>
): Promise<CredentialTemplate | null> {
const fields: string[] = [];
const values: unknown[] = [];
let paramIndex = 1;
if (updates.description !== undefined) {
fields.push(`description = $${paramIndex++}`);
values.push(updates.description);
}
if (updates.template_data !== undefined) {
fields.push(`template_data = $${paramIndex++}`);
values.push(JSON.stringify(updates.template_data));
}
if (updates.is_active !== undefined) {
fields.push(`is_active = $${paramIndex++}`);
values.push(updates.is_active);
}
if (fields.length === 0) {
return getCredentialTemplate(id);
}
fields.push(`updated_at = NOW()`);
values.push(id);
const result = await query<CredentialTemplate>(
`UPDATE credential_templates
SET ${fields.join(', ')}
WHERE id = $${paramIndex}
RETURNING *`,
values
);
return result.rows[0] || null;
}
/**
* Create new version of credential template
*/
export async function createTemplateVersion(
templateId: string,
updates: Partial<Pick<CredentialTemplate, 'template_data' | 'description'>>
): Promise<CredentialTemplate> {
const original = await getCredentialTemplate(templateId);
if (!original) {
throw new Error(`Template ${templateId} not found`);
}
// Get next version number
const versionResult = await query<{ max_version: number }>(
`SELECT MAX(version) as max_version FROM credential_templates WHERE name = $1`,
[original.name]
);
const nextVersion = (versionResult.rows[0]?.max_version || 0) + 1;
return createCredentialTemplate({
name: original.name,
description: updates.description || original.description,
credential_type: original.credential_type,
template_data: updates.template_data || original.template_data,
version: nextVersion,
is_active: true,
created_by: original.created_by,
});
}
/**
* Render credential from template with variable substitution
*/
export function renderCredentialFromTemplate(
template: CredentialTemplate,
variables: Record<string, unknown>
): Record<string, unknown> {
const rendered = JSON.parse(JSON.stringify(template.template_data));
function substitute(obj: unknown): unknown {
if (typeof obj === 'string') {
// Replace {{variable}} patterns
return obj.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
return variables[varName] !== undefined ? String(variables[varName]) : match;
});
} else if (Array.isArray(obj)) {
return obj.map(substitute);
} else if (obj && typeof obj === 'object') {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = substitute(value);
}
return result;
}
return obj;
}
return substitute(rendered) as Record<string, unknown>;
}