Malet Vaults & Verification โ Developer Guide
Overview
The Malets subgraph provides two interconnected trust-and-safety systems for Malet Owners:
- Settings Vault: AES-256-GCM encrypted storage for sensitive merchant configuration (API keys, webhook secrets) with masked GraphQL read-only output.
- Automated Verification: A state-machine workflow that guides Malet Owners through a 4-step admin verification process before their Malet receives "verified" status.
| Component | Purpose | Access |
|---|---|---|
MaletSettings |
Encrypted embedded document on the Malet entity | Owner-only (write), masked (read) |
MaletSettingsView |
GraphQL-safe view with masked sensitive values | Owner-only |
VerificationWorkflow |
State machine tracking 4-step admin checklist | Owner (read), Admin (write) |
VerificationService |
Submit, advance-step, auto-verify/reject logic | Internal |
Settings Vault Architecture
graph TD
A["Malet Owner"] -->|"updateMaletSettings"| B["Resolver"]
B --> C{"Merge fields"}
C --> D["Mongoose pre-save hook"]
D --> E["AES-256-GCM Encryption"]
E --> F[("MongoDB: encrypted at rest")]
A -->|"maletSettings"| G["Resolver"]
G --> H["Decrypt sensitive fields"]
H --> I["maskSensitiveValue()"]
I --> J["MaletSettingsView (โขโขโขโขโขโข7890)"]
style E fill:#ef4444,color:#fff
style I fill:#f59e0b,color:#fff
style F fill:#3b82f6,color:#fff
Sensitive vs Non-Sensitive Fields
The vault distinguishes between fields that require encryption and those stored in plain text:
| Field | Type | Encrypted | GraphQL Output |
|---|---|---|---|
paymentGatewayKey |
Sensitive | โ AES-256-GCM | โขโขโขโขโขโข7890 |
webhookSecret |
Sensitive | โ AES-256-GCM | โขโขโขโขโขโขcret |
customApiKey |
Sensitive | โ AES-256-GCM | โขโขโขโขโขโขkey1 |
notificationEmail |
Non-sensitive | โ Plain text | owner@malet.com |
autoFulfillment |
Non-sensitive | โ Plain text | true |
orderPrefix |
Non-sensitive | โ Plain text | "ORD-" |
Encryption Flow
// Sensitive fields constant
const SETTINGS_SENSITIVE_FIELDS = ['paymentGatewayKey', 'webhookSecret', 'customApiKey'] as const;
// Mongoose pre-save hook on the Malet entity:
// 1. For each sensitive field in settings
// 2. If the value is present and NOT already encrypted
// 3. Encrypt with AES-256-GCM using ENCRYPTION_KEY env var
// 4. Store the IV + authTag + ciphertext as a single string
Masking Logic
Sensitive values are never returned in plain text through GraphQL. The maskSensitiveValue() utility:
function maskSensitiveValue(decrypted: string | undefined): string | null {
if (!decrypted) return null;
if (decrypted.length <= 4) return 'โขโขโขโข';
return 'โขโขโขโขโขโข' + decrypted.slice(-4);
}
// "sk_live_abc123xyz7890" โ "โขโขโขโขโขโข7890"
// "wh_sec" โ "โขโขโขโข"
// undefined โ null
Per-Field Merge Update
The updateMaletSettings mutation uses per-field merging rather than full replacement. This prevents the client from accidentally re-encrypting already-encrypted values:
mutation {
updateMaletSettings(
maletId: "malet-123"
input: { paymentGatewayKey: "sk_live_new_key_here", autoFulfillment: true }
) {
paymentGatewayKey # "โขโขโขโขโขโขhere"
autoFulfillment # true
notificationEmail # unchanged from previous value
}
}
Verification State Machine
Statuses
The verification workflow tracks these 6 statuses:
stateDiagram-v2
[*] --> PENDING
PENDING --> DOCUMENTS_REQUIRED: submitForVerification
DOCUMENTS_REQUIRED --> UNDER_REVIEW: first step approved
UNDER_REVIEW --> UNDER_REVIEW: subsequent steps approved
UNDER_REVIEW --> VERIFIED: all 4 steps approved
DOCUMENTS_REQUIRED --> REJECTED: any step rejected
UNDER_REVIEW --> REJECTED: any step rejected
REJECTED --> DOCUMENTS_REQUIRED: resubmit
VERIFIED --> SUSPENDED: admin action
| Status | Description |
|---|---|
PENDING |
Default state โ Malet Owner has not yet submitted |
DOCUMENTS_REQUIRED |
Submitted โ awaiting admin review of first checklist item |
UNDER_REVIEW |
At least one step approved, awaiting remaining |
VERIFIED |
All 4 steps approved โ Malet is verified โ |
REJECTED |
Any single step rejected โ short-circuit failure |
SUSPENDED |
Admin manually suspended a verified Malet |
4-Step Checklist
Every submission initialises a checklist of 4 steps, each reviewed independently by an admin:
| # | Step | Purpose | Order |
|---|---|---|---|
| 1 | IDENTITY_CHECK |
Verify Malet Owner's identity (ID document, selfie) | First |
| 2 | BUSINESS_DOCS |
Verify business registration, tax IDs, licenses | Second |
| 3 | COMPLIANCE_REVIEW |
Platform policy adherence, content review | Third |
| 4 | FINAL_APPROVAL |
Final sign-off by senior admin | Last |
Each step has its own lifecycle:
interface VerificationChecklistItem {
step: VerificationStep; // IDENTITY_CHECK | BUSINESS_DOCS | ...
status: ChecklistItemStatus; // PENDING | APPROVED | REJECTED
note?: string; // Reviewer's comment
completedAt?: Date; // When reviewed
reviewedBy?: string; // Admin user ID
}
Key Behaviours
Auto-verify: When all 4 steps are APPROVED, the workflow automatically transitions to VERIFIED. No separate "approve all" admin action is needed.
Rejection short-circuit: If any single step is REJECTED, the entire workflow transitions to REJECTED immediately โ remaining steps are not evaluated.
Resubmission: A REJECTED Malet can resubmit via submitForVerification, which resets the checklist to all-PENDING.
Audit trail: Every status transition is recorded in the history array with fromStatus, toStatus, reason, changedBy, and changedAt.
GraphQL API
Mutations
# Malet Owner submits their Malet for verification
mutation {
submitForVerification(maletId: "malet-123") {
id
verificationWorkflow {
status
currentStep
checklist {
step
status
}
}
}
}
# Admin advances a verification step
mutation {
advanceVerificationStep(
maletId: "malet-123"
step: IDENTITY_CHECK
approved: true
note: "ID verified via government database"
) {
verificationWorkflow {
status # UNDER_REVIEW (or VERIFIED if all done)
checklist {
step
status
note
reviewedBy
completedAt
}
history {
fromStatus
toStatus
reason
changedBy
changedAt
}
}
}
}
Queries
# Malet Owner views their settings (masked)
query {
maletSettings(maletId: "malet-123") {
paymentGatewayKey # "โขโขโขโขโขโข7890"
webhookSecret # "โขโขโขโขโขโขcret"
customApiKey # null (not set)
notificationEmail # "owner@example.com"
autoFulfillment # true
orderPrefix # "ORD-"
}
}
# View verification workflow
query {
verificationStatus(maletId: "malet-123") {
status
currentStep
submittedAt
reviewedBy
reviewedAt
checklist {
step
status
note
completedAt
}
history {
fromStatus
toStatus
reason
changedAt
}
}
}
Security Considerations
Encryption Key Management
| Concern | Approach |
|---|---|
| Algorithm | AES-256-GCM (authenticated encryption) |
| Key source | ENCRYPTION_KEY environment variable |
| Key rotation | Not yet implemented โ planned as future enhancement |
| IV (nonce) | Randomly generated per encryption, stored alongside ciphertext |
| Auth tag | Verified on decryption to detect tampering |
Access Control
| Operation | Guard | Who |
|---|---|---|
updateMaletSettings |
Owner guard (x-org-id) |
Malet Owner only |
maletSettings query |
Owner guard | Malet Owner only |
submitForVerification |
Owner guard | Malet Owner only |
advanceVerificationStep |
Admin guard | Platform admins only |
Legacy Compatibility
The Malet.verificationStatus string field ('pending', 'verified', 'rejected') is kept in sync with the new workflow status. This ensures existing queries and federation references continue to work without migration.
Environment Variables
| Variable | Default | Description |
|---|---|---|
ENCRYPTION_KEY |
โ | Required. 32-byte hex key for AES-256-GCM |
Generate a key:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Module Structure
apps/malets/src/malet/
โโโ malet.entity.ts # Core Malet entity with settings + workflow
โโโ malet-settings.entity.ts # MaletSettings, MaletSettingsView, mask helpers
โโโ verification.types.ts # Enums, checklist items, workflow type, defaults
โโโ verification.service.ts # Submit + advance step logic
โโโ verification.service.spec.ts # State machine unit tests
โโโ malet-query.resolver.ts # maletSettings, verificationStatus queries
โโโ malet-crud.resolver.ts # updateMaletSettings, submit/advance mutations
โโโ dto/
โ โโโ create-malet.input.ts # Settings input type
โโโ malet.module.ts # Module wiring
Testing
Unit Tests
# Run malets unit tests (91 tests across 6 suites)
npm run test -- apps/malets --no-coverage
Key coverage:
- VerificationService: submit flow, step approval, auto-verify on all-approved, rejection short-circuit, resubmission, guard rails (already verified, unknown step, already processed step)
- MaletQueryResolver: settings masking, verification workflow query
- MaletCrudResolver: settings merge update, encryption integration
E2E Tests
npx jest --config apps/malets/test/jest-e2e.json --forceExit
Related
- Developer Docs Overview โ Back to the main documentation index.
- Malets Subgraph โ The core Malet entity and dashboard features.
- Organizations Subgraph โ Team management and verification roles.
- Crypto โ Encryption Microservice โ Shares the AES-256-GCM algorithm; centralized encryption for uChat file attachments and future cross-service use.