TypeScript library for encoding and decoding data using ASN.1 PER (Packed Encoding Rules) unaligned variant (ITU-T X.691).
- Bit-level buffer with MSB-first encoding and automatic growth
- Primitive codecs: BOOLEAN, INTEGER, ENUMERATED, BIT STRING, OCTET STRING, OBJECT IDENTIFIER, IA5String, VisibleString, UTF8String, NULL
- Composite codecs: CHOICE, SEQUENCE, SEQUENCE OF
- Constraint support: value ranges, size constraints, extensibility markers, default values
- Schema-driven encoding: define types as JSON, encode/decode plain objects
- Typed from the schema:
decode()returns the type the schema describes andencode()rejects values that do not fit — at compile time, with no runtime validation and no extra dependency - Three ways to say it: an inline
SchemaNode, theasnbuilder DSL, or TypeScript generated from a.asnfile — all sharing one interchange format and one codec interface - Metadata decoding:
decodeWithMetadatareturns a tree ofDecodedNodeobjects with bit positions, raw bytes, and codec references for every field - Pre-encoded passthrough: embed already-encoded bits verbatim with
RawBytes, viacodec.raw
npm install asn1-per-tsThe package also installs an asn1-per-ts command:
npx asn1-per-ts types ticket.asn src/generated/ticket.ts # TypeScript types + codecs
npx asn1-per-ts schema ticket.asn ticket.schema.json # SchemaNode JSONimport { BitBuffer, IntegerCodec, BooleanCodec, SequenceCodec } from 'asn1-per-ts';
// Constrained integer (0..255) uses 8 bits
const intCodec = new IntegerCodec({ min: 0, max: 255 });
const buf = BitBuffer.alloc();
intCodec.encode(buf, 42);
buf.reset();
console.log(intCodec.decode(buf)); // 42import { SchemaCodec } from 'asn1-per-ts';
const codec = new SchemaCodec({
type: 'SEQUENCE',
fields: [
{ name: 'id', schema: { type: 'INTEGER', min: 0, max: 255 } },
{ name: 'active', schema: { type: 'BOOLEAN' } },
{ name: 'status', schema: { type: 'ENUMERATED', values: ['pending', 'approved', 'rejected'] } },
],
});
const hex = codec.encodeToHex({ id: 42, active: true, status: 'approved' });
console.log(hex);
const decoded = codec.decodeFromHex(hex);
console.log(decoded);The schema is read as a literal type, so the decoded type is derived from it — no interface to write, no cast to make:
const decoded = codec.decodeFromHex(hex);
// ^? { id: number; active: boolean; status: 'pending' | 'approved' | 'rejected' }
decoded.status; // 'pending' | 'approved' | 'rejected'
codec.encodeToHex({ id: 42, active: true, status: 'unknown' });
// ~~~~~~~~~ not one of the ENUMERATED valuesOPTIONAL fields become optional properties, DEFAULT fields are always present
after decoding, CHOICE becomes a discriminated union, and $ref resolves
through createCodecs. When the schema is only known at runtime (parsed from
JSON or from ASN.1 text), everything degrades to unknown exactly as before.
SchemaNode is the interchange format; the three ways of describing a type all
produce it and all hand back the same TypedCodec.
import { SchemaCodec, asn } from 'asn1-per-ts';
// 1. inline SchemaNode — the schema is data, types are read from the literal
const fromLiteral = new SchemaCodec({ type: 'SEQUENCE', fields: [/* … */] });
// 2. the asn DSL — types ride in the value, no widening traps, short errors
const fromDsl = asn.codec(
asn.sequence({
id: asn.integer({ min: 0, max: 255 }),
status: asn.enumerated(['pending', 'approved']),
nickname: asn.ia5String().optional(),
}),
);
// 3. generated from ASN.1 — named types, cheapest to compile, best errors
// npx asn1-per-ts types ticket.asn src/generated/ticket.ts
import { codecs } from './generated/ticket.js';Pick by where the schema comes from: generate from .asn files, use the DSL for
hand-written schemas, use SchemaNode when the schema itself is data.
- examples/typed-api.md — inline schemas and the
Infertypes - examples/dsl.md — the
asnbuilder DSL - examples/codegen.md — generating TypeScript from ASN.1
decodeWithMetadata returns a DecodedNode tree with full encoding metadata (bit offsets, bit lengths, raw bytes, codec references) for every field. Use stripMetadata to convert back to a plain object identical to decode().
import { SchemaCodec, stripMetadata } from 'asn1-per-ts';
const codec = new SchemaCodec({
type: 'SEQUENCE',
fields: [
{ name: 'id', schema: { type: 'INTEGER', min: 0, max: 255 } },
{ name: 'active', schema: { type: 'BOOLEAN' } },
],
});
const hex = codec.encodeToHex({ id: 42, active: true });
const node = codec.decodeFromHexWithMetadata(hex);
// The tree is typed from the schema — no cast needed
console.log(node.value.id.value); // 42 (number)
console.log(node.value.id.meta.bitOffset); // 0
console.log(node.value.id.meta.bitLength); // 8
console.log(node.value.id.meta.rawBytes); // Uint8Array([0x2a])
// Strip metadata to get the plain object, with the same type as decode()
const plain = stripMetadata(node);
// plain === { id: 42, active: true }Extension markers (...) indicate that a type may be extended in future versions, providing forward compatibility. When present, PER encoding includes a 1-bit extension marker prefix (0 = not extended, 1 = extended).
Use ... to mark a type as extensible in ASN.1 schema text parsed by the built-in parser:
-- SEQUENCE: extension marker separates root fields from extension additions
MessageV1 ::= SEQUENCE {
id INTEGER (0..255),
name IA5String,
... -- extensible, no extensions yet
}
MessageV2 ::= SEQUENCE {
id INTEGER (0..255),
name IA5String,
..., -- extension marker
email IA5String -- extension addition
}
-- ENUMERATED: extension marker separates root values from extension values
Color ::= ENUMERATED { red, green, blue, ... }
ColorV2 ::= ENUMERATED { red, green, blue, ..., yellow, purple }
-- CHOICE: extension marker separates root alternatives from extension alternatives
Shape ::= CHOICE { circle BOOLEAN, ..., polygon INTEGER }
-- Constraints: extensible constraints allow values outside the root range
FlexInt ::= INTEGER (0..100, ...)
FlexStr ::= OCTET STRING (SIZE (1..50, ...))When building schemas directly as JSON SchemaNode objects, indicate extensibility with these properties:
// SEQUENCE: provide extensionFields (even empty [] to mark as extensible)
const schema: SchemaNode = {
type: 'SEQUENCE',
fields: [
{ name: 'id', schema: { type: 'INTEGER', min: 0, max: 255 } },
],
extensionFields: [], // extensible with no additions yet
};
// ENUMERATED: provide extensionValues
{ type: 'ENUMERATED', values: ['red', 'green'], extensionValues: [] }
// CHOICE: provide extensionAlternatives
{ type: 'CHOICE', alternatives: [...], extensionAlternatives: [] }
// Constrained types: set extensible: true
{ type: 'INTEGER', min: 0, max: 100, extensible: true }
{ type: 'BIT STRING', minSize: 1, maxSize: 50, extensible: true }Key distinction: providing an empty array (extensionFields: []) marks the type as extensible, while omitting the property entirely (extensionFields: undefined) means the type is not extensible. This matters because extensible types include the 1-bit extension marker in the encoding.
| Type | Description |
|---|---|
BOOLEAN |
Single bit |
INTEGER |
Constrained, semi-constrained, or unconstrained |
ENUMERATED |
Indexed enumeration with optional extensions |
BIT STRING |
Bit sequences with size constraints |
OCTET STRING |
Byte sequences with size constraints |
OBJECT IDENTIFIER |
Dot-notation OIDs |
IA5String |
ASCII strings with optional alphabet constraints |
VisibleString |
Printable strings with optional alphabet constraints |
UTF8String |
UTF-8 encoded strings |
NULL |
Zero-bit placeholder |
CHOICE |
Tagged union of alternatives |
SEQUENCE |
Ordered fields with OPTIONAL/DEFAULT support |
SEQUENCE OF |
Homogeneous list with size constraints |
The examples/ directory contains detailed usage guides with code samples:
- Typed API - Types inferred from an inline schema:
Infer,InferInput,InferMetadata,$refregistries - Builder DSL - The
asnDSL, where types ride in the value instead of being read from literals - Code generation - Generating TypeScript types and codecs from a
.asnfile - Schema Parser - Parse ASN.1 text notation into
SchemaNodedefinitions, constraint options, extension markers, CLI usage - Encoding - Encode JavaScript objects to PER unaligned binary using
SchemaCodecor low-level codecs - Decoding - Decode PER unaligned binary data back into objects
dosipas-ts — a TypeScript library built on top of asn1-per-ts for encoding and decoding DOSIPAS / ERA electronic ticket data.
npm test # Run tests
npm run build # Build library to dist/
npx tsc --noEmit # Type-check without emitting
# Run the CLI from source, without building
npx tsx src/cli/main.ts types <in.asn> [out.ts]Changes are recorded in CHANGELOG.md; releasing is described in PUBLISH.md.
The website/ directory contains a React + TailwindCSS demo app for interactive ASN.1 PER encoding and decoding.
cd website
npm install
npm run build # Build for GitHub Pages (uses ./ asset path)
npm run dev # Development serverMIT