Plug & Play Secure Payload
SecurePayload is the first reusable Plug & Play decoding pipeline in ZinTrust.
It standardizes a repeated service workflow:
- decrypt an incoming payload
- parse JSON safely
- coerce primitive values
- validate schema
- return a typed result
The pipeline is invocation-scoped. It does not retain decoded payloads or request state after resolution.
Why It Exists
Without a shared pipeline, every service re-implements variations of:
const decrypted = await decryptPayload(rawPayload);
const parsed = JSON.parse(decrypted);
const record = parsed as Record<string, unknown>;
const amount = Number(record.amount ?? 0);That duplicates error handling and encourages inconsistent coercion and validation behavior.
Core API
import { SecurePayload } from '@zintrust/core';
const pipeline = SecurePayload.decode(rawPayload, {
decryptor: 'payments',
context: {
encryption: Encryption.connection('default'),
},
});Available pipeline stages:
.decrypt().json().coerce({...}).validate(schema).typed<T>().value()
Registering a decryptor
SecurePayload.registerDecryptor('payments', async (raw, context) => {
return context.encryption.decryptString(raw);
});Use registerDecryptor(...) for shared app-level decryptors that are configured during startup.
You can inspect and manage the registry explicitly:
SecurePayload.hasDecryptor('payments');
SecurePayload.listDecryptors();
SecurePayload.unregisterDecryptor('payments');Using an inline decryptor
Use an inline decryptor when the decryption behavior depends on request-scoped context that should not be kept globally.
const payload = await SecurePayload.decode(rawPayload, {
decryptor: async (raw) => customDecrypt(raw, requestScopedKey),
})
.decrypt()
.json()
.typed<MyPayload>();Using EncryptedEnvelope
If your service already uses ZinTrust encrypted envelopes, create a decryptor directly from the built-in envelope primitive:
const decryptor = SecurePayload.createEnvelopeDecryptor({
cipher: 'aes-256-cbc',
key: process.env.APP_KEY ?? '',
previousKeys: [],
});
SecurePayload.registerDecryptor('default-envelope', decryptor);Full example with coercion and validation
import { Schema, SecurePayload } from '@zintrust/core';
const schema = Schema.create()
.required('amount')
.number('amount')
.required('currency')
.string('currency')
.required('active')
.boolean('active');
const payload = await SecurePayload.decode(rawPayload, {
decryptor: 'default-envelope',
})
.decrypt()
.json()
.coerce({
amount: 'number',
active: 'boolean',
})
.validate(schema)
.typed<{
amount: number;
currency: string;
active: boolean;
}>();Supported coercions
SecurePayload.coerce(...) currently supports:
stringnumberintegerboolean
Example:
.coerce({
amount: 'number',
retries: 'integer',
active: 'boolean',
currency: 'string',
})Failure semantics
Failures are normalized by pipeline stage. The thrown error message includes the failing stage:
SecurePayload decrypt failed: ...SecurePayload json failed: ...SecurePayload coerce failed: ...SecurePayload validate failed: ...
That makes logging, monitoring, and test assertions consistent across services.
Runtime safety
SecurePayload is designed to avoid common product-fit failures:
- pipeline state exists only inside one decode call
- there is no retained per-request cache
- decryptor registration is explicit and manageable
- inline decryptors keep request-scoped secrets out of global registries
Recommended usage rules
- register long-lived decryptors during startup only
- use inline decryptors for request-specific keys or tenant-specific context
- validate after coercion, not before
- keep decrypted payload handling local to the action that needs it
- avoid logging decrypted secrets