A robust encoder/decoder for structured metadata within Git commit messages.
- Standard Compliant: Follows the Git "trailer" convention (RFC 822 / Email headers)
- DoS Protection: Built-in 5MB message size limit to prevent attacks
- Structured Domain: Formalized entities and value objects for type safety
- Zod Validation: Schema-driven validation with helpful error messages
- Case Normalization: Trailer keys normalized to lowercase for consistency
- Pure Domain Logic: No I/O, no Git subprocess execution
- Domain Purity: Core logic independent of infrastructure
- Type Safety: Value Objects ensure data validity at instantiation
- Immutability: All entities are immutable
- Separation of Concerns: Encoding/decoding in dedicated service
- Node.js: >= 20.0.0
npm install @git-stunts/trailer-codec- Node.js ≥ 20 matches the
enginesfield inpackage.jsonand is required for Vitest/ESM support. npm testruns the Vitest suite,npm run lintvalidates the code with ESLint, andnpm run formatformats files with Prettier; all scripts target the entire repo root.- Consult
TESTING.mdfor run modes, test filters, and tips for extending the suite before submitting contributions.
import { createDefaultTrailerCodec } from '@git-stunts/trailer-codec';
const codec = createDefaultTrailerCodec();
const message = codec.encode({
title: 'feat: add user authentication',
body: 'Implemented OAuth2 flow with JWT tokens.',
trailers: [
{ key: 'Signed-off-by', value: 'James Ross' },
{ key: 'Reviewed-by', value: 'Alice Smith' },
],
});
console.log(message);
// feat: add user authentication
//
// Implemented OAuth2 flow with JWT tokens.
//
// signed-off-by: James Ross
// reviewed-by: Alice Smith
const decoded = codec.decode(message);
console.log(decoded.title); // "feat: add user authentication"
console.log(decoded.trailers); // { 'signed-off-by': 'James Ross', 'reviewed-by': 'Alice Smith' }- Primary entry point:
createDefaultTrailerCodec()returns aTrailerCodecwired with a freshTrailerCodecService; use.encode()/.decode()(or.encodeMessage()/.decodeMessage()) to keep configuration in one place. - Facade:
TrailerCodeckeeps configuration near instantiation while still leveragingcreateMessageHelpers()under the hood (pass your own service when you need control). - Advanced:
createConfiguredCodec()and directTrailerCodecServiceusage let you swap schema bundles, parsers, formatters, or helper overrides when you need custom validation or formatting behavior. The standalone helpersencodeMessage()/decodeMessage()remain available as deprecated convenience wrappers.
decodeMessage()now trims trailing newlines in the versionv0.2.0+runtime, so plain string inputs will no longer include a final\nunless you opt into it.- To preserve the trailing newline you rely on (e.g., when round-tripping commit templates), either instantiate
TrailerCodecwithbodyFormatOptions: { keepTrailingNewline: true }, callformatBodySegment(body, { keepTrailingNewline: true })yourself, or pass the same option throughcreateConfiguredCodec. - See
docs/MIGRATION.md#v020for the full migration checklist and decoding behavior rationale.
decodeMessage now trims the decoded body by default, returning the content exactly as stored; no extra newline is appended automatically. If you still need the trailing newline (for example when writing the decoded body back into a commit template), instantiate the helpers or facade with bodyFormatOptions: { keepTrailingNewline: true }:
import TrailerCodec from '@git-stunts/trailer-codec';
const codec = new TrailerCodec({ bodyFormatOptions: { keepTrailingNewline: true } });
const payload = codec.decode('Title\n\nBody\n');
console.log(payload.body); // 'Body\n'You can also call the exported formatBodySegment(body, { keepTrailingNewline: true }) helper directly when you need the formatting logic elsewhere.
import { formatBodySegment } from '@git-stunts/trailer-codec';
const trimmed = formatBodySegment('Body\n', { keepTrailingNewline: true });
console.log(trimmed); // 'Body\n'When you need a prewired codec (custom key patterns, parser tweaks, formatter hooks), use createConfiguredCodec({ keyPattern, keyMaxLength, parserOptions }). It builds a schema bundle, parser, and service for you, and returns helpers so you can immediately call decodeMessage/encodeMessage:
import { createConfiguredCodec } from '@git-stunts/trailer-codec';
const { decodeMessage, encodeMessage } = createConfiguredCodec({
keyPattern: '[A-Za-z._-]+',
keyMaxLength: 120,
parserOptions: {},
});
const payload = { title: 'feat: cli docs', trailers: { 'Custom.Key': 'value' } };
const encoded = encodeMessage(payload);
const decoded = decodeMessage(encoded);
console.log(decoded.title); // 'feat: cli docs'import { GitCommitMessage } from '@git-stunts/trailer-codec';
const msg = new GitCommitMessage({
title: 'fix: resolve memory leak',
body: 'Fixed WeakMap reference cycle.',
trailers: [
{ key: 'Issue', value: 'GH-123' },
{ key: 'Signed-off-by', value: 'James Ross' }
]
});
console.log(msg.toString());formatBodySegment(body, { keepTrailingNewline = false })mirrors the helper poweringdecodeMessage, trimming whitespace while optionally preserving the trailing newline when you plan to write the body back into a template.createMessageHelpers({ service, bodyFormatOptions })returns{ decodeMessage, encodeMessage }bound to the providedTrailerCodecService; passbodyFormatOptionsto control whether decoded bodies keep their trailing newline.TrailerCodecwrapscreateMessageHelpers()so you can instantiate a codec class with customserviceorbodyFormatOptionsand still leverage the helper contract viaencode()/decode().createConfiguredCodec({ keyPattern, keyMaxLength, parserOptions, formatters, bodyFormatOptions })wires togethercreateGitTrailerSchemaBundle,TrailerParser,TrailerCodecService, and the helper pair, letting you configure key validation, parser heuristics, formatting hooks, and body formatting in a single call.TrailerCodecServiceexposes the schema bundle, parser, trailer factory, formatter hooks, and helper utilities (MessageNormalizer,extractTitle,composeBody); seedocs/SERVICE.mdfor a deeper explanation of how to customize each stage without touching the core service.
Trailer codec enforces strict validation via the concrete subclasses of TrailerCodecError:
| Rule | Constraint | Thrown Error |
|---|---|---|
| Message Size | ≤ 5MB | TrailerTooLargeError |
| Title | Must be a non-empty string | CommitMessageInvalidError (during entity construction) |
| Trailer Key | Alphanumeric, hyphens, underscores only (/^[A-Za-z0-9_-]+$/) and ≤ 100 characters (prevents ReDoS) |
TrailerInvalidError |
| Trailer Value | Cannot contain carriage returns or line feeds and must not be empty | TrailerValueInvalidError |
Key Normalization: All trailer keys are automatically normalized to lowercase (e.g., Signed-Off-By → signed-off-by).
Blank-Line Guard: Trailers must be separated from the body by a blank line; omitting the separator throws TrailerNoSeparatorError.
When TrailerCodecService or the exported helpers throw, they surface one of the following classes so you can recover with instanceof checks:
| Error | Trigger | Suggested Fix |
|---|---|---|
TrailerTooLargeError |
Message exceeds 5MB while MessageNormalizer.guardMessageSize() runs |
Split the commit or remove content until the payload fits. |
TrailerNoSeparatorError |
Missing blank line before trailers when TrailerParser.split() runs |
Insert the required empty line between body and trailers. |
TrailerValueInvalidError |
Trailer value includes newline characters or fails the schema value rules | Remove or escape newline characters before encoding. |
TrailerInvalidError |
Trailer key/value pair fails the schema validation (GitTrailerSchema) |
Adjust the key/value or supply a custom schema bundle via TrailerCodecService. |
CommitMessageInvalidError |
GitCommitMessageSchema rejects the full payload (title/body/trailers) |
Fix the invalid field or pass a conforming payload; use formatters if needed. |
All of the above inherit from TrailerCodecError (src/domain/errors/TrailerCodecError.js) and expose meta for diagnostics; prefer checking the specific class instead of inspecting code.
- No Code Execution: Pure string manipulation, no
eval()or dynamic execution - DoS Protection: Rejects messages > 5MB
- ReDoS Prevention: Max key length limits regex execution time
- No Git Subprocess: Library performs no I/O operations
- Line Injection Guard: Trailer values omit newline characters so no unexpected trailers can be injected
See SECURITY.md for details.
docs/ADVANCED.md— Custom schema injection, validation overrides, and advanced integration patterns.docs/PARSER.md— Step-by-step explanation of the backward-walk parser.docs/INTEGRATION.md— Git log scripting, streaming decoder, and Git-CMS filtering recipes.docs/SERVICE.md— HowTrailerCodecServicewires schema, parser, and formatter helpers for customization.API_REFERENCE.md— Complete catalog of the public exports, their inputs/outputs, and notable knobs.TESTING.md— How to run/extend the Vitest, lint, and format scripts plus contributor tips.- Git hooks: Run
npm run setuphooksonce per clone to pointcore.hooksPathatscripts/. The hook now runs justnpm run lintandnpm run formatbefore each commit.
Apache-2.0 Copyright © 2026 James Ross