@aokiapp/tlv
Version:
Tag-Length-Value (TLV) parser and builder library with schema support. Provides both parsing and building APIs as submodules.
615 lines (448 loc) • 15.5 kB
Markdown
Tag-Length-Value (TLV) parser and builder library with schema support. Provides both parsing and building APIs as submodules.
- **Schema-based TLV parsing and building**: Define structured schemas for complex TLV data
- **DER encoding support**: Full compliance with Distinguished Encoding Rules
- **Modular API**: Separate parser and builder modules for focused usage
- **TypeScript-first**: Complete type safety with inferred types from schemas
- **ASN.1 compatible**: Support for various ASN.1 constructs (SEQUENCE, SET, primitives, etc.)
- **Real-world examples**: Includes CMS (RFC 5652) and CRCL implementations
## Installation
### From npm
```bash
npm install @aokiapp/tlv
```
### From GitHub Packages
**Note**: GitHub Packages requires authentication even for public packages.
First, authenticate with GitHub:
```bash
npm login --registry=https://npm.pkg.github.com
```
Then install:
```bash
npm install @aokiapp/tlv --registry=https://npm.pkg.github.com
```
Or configure your `.npmrc`:
```
@aokiapp:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
```
```typescript
import { BasicTLVParser } from "@aokiapp/tlv/parser";
// Parse raw TLV data
const buffer = new Uint8Array([0x04, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
.buffer;
const tlv = BasicTLVParser.parse(buffer);
console.log(tlv);
// {
// tag: { tagClass: 0, constructed: false, tagNumber: 4 },
// length: 5,
// value: ArrayBuffer(...),
// endOffset: 7
// }
```
```typescript
import { SchemaParser, Schema, TagClass } from "@aokiapp/tlv/parser";
import { decodeUtf8, decodeInteger } from "@aokiapp/tlv/common";
// Define a schema for a person record
const PersonSchema = Schema.constructed("person", { tagNumber: 16 }, [
Schema.primitive("id", { tagNumber: 2 }, decodeInteger),
Schema.primitive("name", { tagNumber: 12 }, decodeUtf8),
Schema.primitive("email", { tagNumber: 12, optional: true }, decodeUtf8),
]);
// Parse TLV data according to schema
const parser = new SchemaParser(PersonSchema);
const result = parser.parse(tlvBuffer);
// Result is fully typed:
// { id: number, name: string, email?: string }
```
```typescript
import { SchemaBuilder, Schema } from "@aokiapp/tlv/builder";
import { encodeUtf8, encodeInteger } from "@aokiapp/tlv/common";
// Define builder schema
const PersonSchema = Schema.constructed("person", { tagNumber: 16 }, [
Schema.primitive("id", { tagNumber: 2 }, encodeInteger),
Schema.primitive("name", { tagNumber: 12 }, encodeUtf8),
]);
// Build TLV data from structured input
const builder = new SchemaBuilder(PersonSchema);
const tlvBuffer = builder.build({
id: 123,
name: "John Doe",
});
```
Low-level TLV parsing for raw DER/BER data.
```typescript
class BasicTLVParser {
static parse(buffer: ArrayBuffer): TLVResult;
}
```
**Parameters:**
- `buffer: ArrayBuffer` - Raw TLV data to parse
**Returns:**
- [`TLVResult`](src/common/types.ts:15) - Parsed structure with tag, length, value, and endOffset
**Throws:**
- Error if indefinite length (0x80) is encountered (DER compliance)
- Error if declared length exceeds available bytes
**Example:**
```typescript
const buffer = new Uint8Array([0x04, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
.buffer;
const result = BasicTLVParser.parse(buffer);
// Returns: {
// tag: { tagClass: 0, constructed: false, tagNumber: 4 },
// length: 5,
// value: ArrayBuffer(5), // "Hello"
// endOffset: 7
// }
```
Schema-driven TLV parsing with full type inference.
```typescript
class SchemaParser {
constructor(schema: TLVSchema, options?: { strict?: boolean });
parse(buffer: ArrayBuffer): any; // Fully typed based on schema
}
```
**Constructor Parameters:**
- `schema: TLVSchema` - TLV schema definition
- `options.strict?: boolean` - Enable strict validation (default: true)
**Methods:**
##### `parse(buffer: ArrayBuffer): any`
Parse TLV data according to schema with full type inference.
**Parameters:**
- `buffer: ArrayBuffer` - DER-encoded TLV data
**Returns:**
- Fully typed result based on schema definition (TypeScript infers the exact type)
**Behavior:**
- **Strict mode (default)**: Enforces exact schema compliance, canonical SET ordering
- **Non-strict mode**: More permissive parsing, preserves original SET order
- **SEQUENCE**: Children must appear in exact schema order
- **SET**: Children can appear in any order, validated for canonical ordering in strict mode
- **Repeated fields**: Automatically collect multiple consecutive matching children
**Throws:**
- Error on tag class/number mismatches
- Error on missing required fields
- Error on unknown children (immediate failure regardless of strict mode)
- Error on DER canonical order violations in SET (strict mode only)
#### `Schema` (Parser)
Factory class for creating parser schemas.
```typescript
class Schema {
static primitive(
name: string,
options: SchemaOptions,
decode?: (buffer: ArrayBuffer) => any,
): TLVSchema;
static constructed(
name: string,
options: SchemaOptions,
fields: TLVSchema[],
): TLVSchema;
static repeated(
name: string,
options: SchemaOptions,
item: TLVSchema,
): TLVSchema;
}
```
Low-level TLV building for constructing DER-encoded data.
```typescript
class BasicTLVBuilder {
static build(tlv: TLVResult): ArrayBuffer;
}
```
**Parameters:**
- `tlv: TLVResult` - TLV structure to encode
**Returns:**
- `ArrayBuffer` - DER-encoded TLV data
**Behavior:**
- Supports high tag numbers (>= 31) with multi-byte encoding
- Supports long-form length encoding (>= 128 bytes)
- Enforces DER canonical encoding rules
- Maximum length field: 126 bytes (BER/DER limit)
**Throws:**
- Error for invalid tag numbers (negative, non-finite)
- Error for invalid tag classes (outside 0-3 range)
- Error for values too large to encode (> 126-byte length field)
**Example:**
```typescript
const tlv: TLVResult = {
tag: { tagClass: TagClass.Universal, constructed: false, tagNumber: 4 },
length: 5,
value: new TextEncoder().encode("Hello").buffer,
endOffset: 0,
};
const encoded = BasicTLVBuilder.build(tlv);
```
Schema-driven TLV building with type validation.
```typescript
class SchemaBuilder {
constructor(schema: TLVSchema, options?: { strict?: boolean });
build(data: any): ArrayBuffer; // Input type inferred from schema
}
```
**Constructor Parameters:**
- `schema: TLVSchema` - TLV schema definition
- `options.strict?: boolean` - Enable strict validation (default: true)
**Methods:**
##### `build(data: any): ArrayBuffer`
Build DER-encoded TLV data from structured input.
**Parameters:**
- `data: any` - Input data matching schema structure (TypeScript infers exact type)
**Returns:**
- `ArrayBuffer` - DER-encoded TLV data
**Behavior:**
- **Strict mode (default)**: Validates all required fields, sorts SET children canonically
- **Non-strict mode**: Permits missing fields, preserves SET child order
- **Type safety**: Input data type is inferred from schema definition
- **SET ordering**: Applies DER canonical lexicographic sorting in strict mode
**Throws:**
- Error on missing required properties (strict mode)
- Error on type validation failures
- Error on top-level repeated schemas (must be wrapped in constructed container)
#### `Schema` (Builder)
Factory class for creating builder schemas.
```typescript
class Schema {
static primitive(
name: string,
options: SchemaOptions,
encode?: (data: any) => ArrayBuffer,
): TLVSchema;
static constructed(
name: string,
options: SchemaOptions,
fields: TLVSchema[],
): TLVSchema;
static repeated(
name: string,
options: SchemaOptions,
item: TLVSchema,
): TLVSchema;
}
```
All schema factory methods accept common options:
```typescript
interface SchemaOptions {
readonly tagClass?: TagClass; // Default: TagClass.Universal
readonly tagNumber?: number; // Required for primitives
readonly optional?: boolean; // Default: false
readonly isSet?: boolean; // Auto-inferred for UNIVERSAL 16/17
}
```
**Tag Class Values:**
- `TagClass.Universal` (0) - Standard ASN.1 types
- `TagClass.Application` (1) - Application-specific types
- `TagClass.ContextSpecific` (2) - Context-specific types
- `TagClass.Private` (3) - Private types
**Tag Number Inference:**
- **SEQUENCE**: `tagNumber: 16` (UNIVERSAL, constructed)
- **SET**: `tagNumber: 17` (UNIVERSAL, constructed)
- **Custom tags**: Explicit `tagNumber` required
```typescript
// Text encoding
function encodeUtf8(str: string): ArrayBuffer;
function encodeAscii(str: string): ArrayBuffer;
// Number encoding
function encodeInteger(n: number): ArrayBuffer; // DER INTEGER encoding
function encodeOID(oid: string): ArrayBuffer; // OBJECT IDENTIFIER encoding
// Binary encoding
function encodeBitString(bits: {
unusedBits: number;
data: Uint8Array;
}): ArrayBuffer;
// Buffer utilities
function toArrayBuffer(u8: Uint8Array): ArrayBuffer;
```
```typescript
// Text decoding
function decodeUtf8(buffer: ArrayBuffer): string;
function decodeAscii(buffer: ArrayBuffer): string;
function decodeShiftJis(buffer: ArrayBuffer): string;
// Number decoding
function decodeInteger(buffer: ArrayBuffer): number;
function decodeOID(buffer: ArrayBuffer): string;
// Binary decoding
function decodeBitStringHex(buffer: ArrayBuffer): {
unusedBits: number;
hex: string;
};
// Buffer utilities
function toHex(input: ArrayBuffer | Uint8Array): string;
function bufferToArrayBuffer(buf: Buffer): ArrayBuffer;
```
```typescript
// Primitive field
Schema.primitive("fieldName", { tagNumber: 4 }, decodeUtf8);
// Optional field
Schema.primitive("optional", { tagNumber: 5, optional: true }, decodeUtf8);
// Context-specific tag [0], [1], etc.
Schema.primitive(
"contextTag",
{
tagClass: TagClass.ContextSpecific,
tagNumber: 0,
},
decodeInteger,
);
// SEQUENCE (ordered container)
Schema.constructed("sequence", { tagNumber: 16 }, [
Schema.primitive("field1", { tagNumber: 2 }, decodeInteger),
Schema.primitive("field2", { tagNumber: 12 }, decodeUtf8),
]);
// SET (unordered container)
Schema.constructed("set", { tagNumber: 17, isSet: true }, [
Schema.primitive("a", { tagNumber: 2 }, decodeInteger),
Schema.primitive("b", { tagNumber: 12 }, decodeUtf8),
]);
// SEQUENCE OF (repeated items)
Schema.constructed("container", { tagNumber: 16 }, [
Schema.repeated(
"items",
{},
Schema.primitive("item", { tagNumber: 4 }, decodeUtf8),
),
]);
```
```typescript
const DocumentSchema = Schema.constructed("document", { tagNumber: 16 }, [
Schema.constructed("header", { tagNumber: 16 }, [
Schema.primitive("version", { tagNumber: 2 }, decodeInteger),
Schema.primitive("timestamp", { tagNumber: 24 }, decodeUtf8),
]),
Schema.constructed("content", { tagNumber: 16 }, [
Schema.primitive("title", { tagNumber: 12 }, decodeUtf8),
Schema.primitive(
"description",
{ tagNumber: 12, optional: true },
decodeUtf8,
),
Schema.repeated(
"tags",
{},
Schema.primitive("tag", { tagNumber: 12 }, decodeUtf8),
),
]),
]);
// TypeScript infers:
// {
// header: { version: number; timestamp: string };
// content: { title: string; description?: string; tags: string[] }
// }
```
```typescript
new SchemaParser(schema, { strict: true }); // Default
new SchemaBuilder(schema, { strict: true });
```
- **Validation**: Enforces exact schema compliance
- **SET ordering**: Validates/applies DER canonical ordering
- **Error handling**: Fails fast on any schema violations
```typescript
new SchemaParser(schema, { strict: false });
new SchemaBuilder(schema, { strict: false });
```
- **Flexibility**: More permissive parsing/building
- **SET ordering**: Preserves original order
- **Performance**: Faster with less validation overhead
```typescript
try {
const result = new SchemaParser(schema).parse(buffer);
} catch (error) {
// Detailed error messages for debugging:
// - "TLV tag mismatch for primitive 'fieldName'"
// - "Missing required property 'fieldName'"
// - "DER canonical order violation in SET 'setName'"
console.error("Parse failed:", error.message);
}
```
```typescript
// Custom timestamp codec
function decodeTimestamp(buffer: ArrayBuffer): Date {
const iso = new TextDecoder().decode(buffer);
return new Date(iso);
}
const schema = Schema.primitive("created", { tagNumber: 24 }, decodeTimestamp);
// Result type automatically inferred as Date
```
```bash
npm run build
npm run typecheck
```
```bash
npm test
```
```bash
npm run lint
npm run format
```
This library is automatically published to both npm and GitHub Packages via GitHub Actions.
```bash
npm run changelog
npm run version
npm run publish
```
When changes are pushed to the `main` branch with a changeset, the GitHub Actions workflow will:
1. Create a release PR or publish to npm
2. Automatically publish to GitHub Packages if npm publish succeeds
```
├── src/
│ ├── parser/
│ ├── builder/
│ ├── common/
│ └── utils/
├── examples/
│ ├── cms/
│ └── crcl/
├── tests/
└── dist/
```
The library includes various built-in codecs:
- **Text**: UTF-8, ASCII, Shift-JIS
- **Numbers**: INTEGER (DER encoding)
- **Identifiers**: OBJECT IDENTIFIER
- **Binary**: BIT STRING, OCTET STRING
- **Utility**: Hex conversion, buffer operations
Full TypeScript support with:
- Schema type inference for parsing results
- Compile-time validation of builder input data
- Optional/required field type safety
- Generic schema composition
This project is licensed under the AokiApp Normative Application License - Tight. See [`LICENSE.md`](LICENSE.md:1) for details.
**Note**: This is NOT an open source license. The source code is made publicly visible for transparency only. Commercial use requires explicit written permission from AokiApp Inc.
This project is currently under restrictive licensing. For contribution guidelines or commercial licensing inquiries, please contact AokiApp Inc. at hello+github@aoki.app.
See [`CHANGELOG.md`](CHANGELOG.md:1) for version history and changes.