node-rtc-connection 1.0.18 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +94 -85
  2. package/dist/index.cjs +20 -5421
  3. package/dist/index.mjs +25 -5413
  4. package/dist/types/crypto/der.d.ts +107 -0
  5. package/dist/types/crypto/x509.d.ts +56 -0
  6. package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
  7. package/dist/types/dtls/RTCCertificate.d.ts +163 -0
  8. package/dist/types/dtls/cipher.d.ts +81 -0
  9. package/dist/types/dtls/connection.d.ts +81 -0
  10. package/dist/types/dtls/prf.d.ts +29 -0
  11. package/dist/types/dtls/protocol.d.ts +127 -0
  12. package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
  13. package/dist/types/foundation/RTCError.d.ts +152 -0
  14. package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
  15. package/dist/types/ice/ice-agent.d.ts +154 -0
  16. package/dist/types/ice/stun-message.d.ts +92 -0
  17. package/dist/types/index.d.ts +29 -0
  18. package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
  19. package/dist/types/sctp/association.d.ts +77 -0
  20. package/dist/types/sctp/chunks.d.ts +200 -0
  21. package/dist/types/sctp/crc32c.d.ts +24 -0
  22. package/dist/types/sctp/datachannel-manager.d.ts +51 -0
  23. package/dist/types/sctp/dcep.d.ts +56 -0
  24. package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
  25. package/dist/types/sdp/sdp-utils.d.ts +103 -0
  26. package/dist/types/stun/stun-client.d.ts +119 -0
  27. package/dist/types/transport-stack.d.ts +68 -0
  28. package/package.json +26 -21
  29. package/src/crypto/der.ts +205 -0
  30. package/src/crypto/x509.ts +146 -0
  31. package/src/datachannel/RTCDataChannel.ts +388 -0
  32. package/src/dtls/RTCCertificate.ts +396 -0
  33. package/src/dtls/cipher.ts +198 -0
  34. package/src/dtls/connection.ts +974 -0
  35. package/src/dtls/prf.ts +62 -0
  36. package/src/dtls/protocol.ts +204 -0
  37. package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
  38. package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
  39. package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
  40. package/src/ice/ice-agent.ts +609 -0
  41. package/src/ice/stun-message.ts +260 -0
  42. package/src/index.ts +72 -0
  43. package/src/peerconnection/RTCPeerConnection.ts +430 -0
  44. package/src/sctp/association.ts +523 -0
  45. package/src/sctp/chunks.ts +350 -0
  46. package/src/sctp/crc32c.ts +57 -0
  47. package/src/sctp/datachannel-manager.ts +187 -0
  48. package/src/sctp/dcep.ts +94 -0
  49. package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
  50. package/src/sdp/sdp-utils.ts +229 -0
  51. package/src/stun/stun-client.ts +936 -0
  52. package/src/transport-stack.ts +165 -0
  53. package/dist/index.cjs.map +0 -1
  54. package/dist/index.mjs.map +0 -1
  55. package/src/datachannel/RTCDataChannel.js +0 -354
  56. package/src/dtls/RTCCertificate.js +0 -310
  57. package/src/dtls/RTCDtlsTransport.js +0 -247
  58. package/src/ice/RTCIceTransport.js +0 -998
  59. package/src/index.d.ts +0 -400
  60. package/src/index.js +0 -92
  61. package/src/network/network-transport.js +0 -478
  62. package/src/peerconnection/RTCPeerConnection.js +0 -851
  63. package/src/sctp/RTCSctpTransport.js +0 -253
  64. package/src/sdp/sdp-utils.js +0 -224
  65. package/src/stun/stun-client.js +0 -643
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @file sdp-utils.ts
3
+ * @description SDP generation/parsing for a WebRTC data-channel m-section.
4
+ * @module sdp/sdp-utils
5
+ *
6
+ * Emits the standard application m-line with the SCTP-over-DTLS profile and
7
+ * conveys transport addresses through ICE candidates (not the c=/m= lines, per
8
+ * the WebRTC JSEP/SDP rules). This is what browsers expect.
9
+ */
10
+ /** ICE credentials. */
11
+ export interface IceCredentials {
12
+ usernameFragment: string;
13
+ password: string;
14
+ }
15
+ /** DTLS certificate fingerprint. */
16
+ export interface Fingerprint {
17
+ algorithm: string;
18
+ value: string;
19
+ }
20
+ /** A candidate descriptor as accepted by {@link buildSdp}. */
21
+ export interface CandidateInput {
22
+ sdp?: string;
23
+ candidate?: string;
24
+ }
25
+ /** Options accepted by {@link buildSdp}. */
26
+ export interface BuildSdpOptions {
27
+ kind?: 'offer' | 'answer';
28
+ iceUfrag: string;
29
+ icePwd: string;
30
+ fingerprint?: Fingerprint;
31
+ setup?: string;
32
+ candidates?: CandidateInput[];
33
+ sctpPort?: number;
34
+ maxMessageSize?: number;
35
+ }
36
+ /** Options accepted by {@link generateOffer} / {@link generateAnswer}. */
37
+ export interface GenerateOptions extends BuildSdpOptions {
38
+ }
39
+ /** Parsed ICE parameters. */
40
+ export interface IceParameters {
41
+ usernameFragment: string | null;
42
+ password: string | null;
43
+ }
44
+ /** Parsed DTLS parameters. */
45
+ export interface DtlsParameters {
46
+ role: string;
47
+ fingerprints: Fingerprint[];
48
+ setup?: string;
49
+ }
50
+ /** Parsed SCTP parameters. */
51
+ export interface SctpParameters {
52
+ port: number;
53
+ maxMessageSize: number;
54
+ }
55
+ /** A parsed ICE candidate. */
56
+ export interface ParsedCandidate {
57
+ candidate: string;
58
+ foundation: string;
59
+ component: number;
60
+ protocol: string;
61
+ priority: number;
62
+ address: string;
63
+ port: number;
64
+ type: string;
65
+ }
66
+ /**
67
+ * Generate ICE credentials (ufrag >= 4 chars, pwd >= 22 chars per RFC 8445).
68
+ * @returns {{usernameFragment:string, password:string}}
69
+ */
70
+ export declare function generateIceCredentials(): IceCredentials;
71
+ /**
72
+ * Build the SDP for a data-channel-only session.
73
+ * @param {Object} o
74
+ * @param {'offer'|'answer'} o.kind
75
+ * @param {string} o.iceUfrag
76
+ * @param {string} o.icePwd
77
+ * @param {{algorithm:string,value:string}} o.fingerprint - DTLS cert fingerprint
78
+ * @param {string} o.setup - 'actpass' | 'active' | 'passive'
79
+ * @param {Array<{sdp?:string,candidate?:string}>} [o.candidates]
80
+ * @param {number} [o.sctpPort=5000]
81
+ * @param {number} [o.maxMessageSize=262144]
82
+ * @returns {string}
83
+ */
84
+ export declare function buildSdp(o: BuildSdpOptions): string;
85
+ export declare function generateOffer(opts: GenerateOptions): string;
86
+ export declare function generateAnswer(opts: GenerateOptions): string;
87
+ /** Parse ICE ufrag/pwd. */
88
+ export declare function parseIceParameters(sdp: string): IceParameters;
89
+ /** Parse DTLS setup role + fingerprints. */
90
+ export declare function parseDtlsParameters(sdp: string): DtlsParameters;
91
+ /** Parse SCTP port / max message size. */
92
+ export declare function parseSctpParameters(sdp: string): SctpParameters;
93
+ /**
94
+ * Parse ICE candidate lines into structured objects.
95
+ * @param {string} sdp
96
+ * @returns {Array<{candidate:string,foundation:string,component:number,protocol:string,priority:number,address:string,port:number,type:string}>}
97
+ */
98
+ export declare function parseCandidates(sdp: string): ParsedCandidate[];
99
+ /**
100
+ * Parse a single "candidate:..." string.
101
+ * @param {string} str
102
+ */
103
+ export declare function parseCandidateLine(str: string): ParsedCandidate | null;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @file stun-client.ts
3
+ * @description STUN (Session Traversal Utilities for NAT) client implementation
4
+ * @module stun/stun-client
5
+ *
6
+ * STUN Protocol: RFC 5389
7
+ * TURN Protocol: RFC 5766
8
+ */
9
+ import { EventEmitter } from 'events';
10
+ /**
11
+ * Constructor options for {@link STUNClient}.
12
+ */
13
+ interface STUNClientOptions {
14
+ /** STUN/TURN server address */
15
+ server: string;
16
+ /** Server port */
17
+ port: number;
18
+ /** TURN username */
19
+ username?: string;
20
+ /** TURN password */
21
+ credential?: string;
22
+ /** Transport protocol (udp/tcp) */
23
+ transport?: string;
24
+ /** Additional query parameters from URL */
25
+ params?: Record<string, unknown>;
26
+ }
27
+ /**
28
+ * Reflexive address info resolved from a STUN Binding response.
29
+ */
30
+ interface ReflexiveAddress {
31
+ address: string;
32
+ port: number;
33
+ family: string;
34
+ }
35
+ /**
36
+ * Relay address info resolved from a TURN Allocate response.
37
+ */
38
+ interface RelayAddress {
39
+ relayedAddress: string;
40
+ relayedPort: number;
41
+ lifetime: number;
42
+ type: 'relay';
43
+ }
44
+ /**
45
+ * Result of a TURN Refresh response.
46
+ */
47
+ interface RefreshResult {
48
+ lifetime: number;
49
+ }
50
+ /**
51
+ * Result of a generic success response (CreatePermission / ChannelBind).
52
+ */
53
+ interface OkResult {
54
+ ok: true;
55
+ }
56
+ /**
57
+ * Union of every result shape a transaction may resolve with.
58
+ */
59
+ type TransactionResult = ReflexiveAddress | RelayAddress | RefreshResult | OkResult;
60
+ /**
61
+ * @class STUNClient
62
+ * @description STUN/TURN client for NAT traversal
63
+ */
64
+ declare class STUNClient extends EventEmitter {
65
+ #private;
66
+ /**
67
+ * Create a STUN client
68
+ * @param {Object} options - Client options
69
+ * @param {string} options.server - STUN/TURN server address
70
+ * @param {number} options.port - Server port
71
+ * @param {string} [options.username] - TURN username
72
+ * @param {string} [options.credential] - TURN password
73
+ * @param {string} [options.transport='udp'] - Transport protocol (udp/tcp)
74
+ * @param {Object} [options.params={}] - Additional query parameters from URL
75
+ */
76
+ constructor(options: STUNClientOptions);
77
+ /**
78
+ * Connect to the STUN/TURN server
79
+ * @returns {Promise<void>}
80
+ */
81
+ connect(): Promise<void>;
82
+ /**
83
+ * Send a STUN Binding Request to get reflexive address
84
+ * @returns {Promise<Object>} Reflexive address info
85
+ */
86
+ getReflexiveAddress(): Promise<TransactionResult>;
87
+ /**
88
+ * Send a TURN Allocate Request to get relay address
89
+ * @param {number} [lifetime=600] - Allocation lifetime in seconds
90
+ * @returns {Promise<Object>} Relay address info
91
+ */
92
+ allocateRelay(lifetime?: number): Promise<TransactionResult>;
93
+ /**
94
+ * Send a TURN Refresh Request to keep allocation alive
95
+ * @param {number} [lifetime=600] - Allocation lifetime in seconds
96
+ * @returns {Promise<Object>} Updated allocation info
97
+ */
98
+ refreshAllocation(lifetime?: number): Promise<TransactionResult>;
99
+ /**
100
+ * Create a TURN Permission for a peer
101
+ * @param {string} peerAddress - Peer IP address
102
+ * @returns {Promise<void>}
103
+ */
104
+ createPermission(peerAddress: string): Promise<void>;
105
+ /**
106
+ * Send data to a peer via TURN Send Indication
107
+ * @param {string} peerAddress - Peer IP address
108
+ * @param {number} peerPort - Peer port
109
+ * @param {Buffer} data - Data to send
110
+ * @returns {Promise<void>}
111
+ */
112
+ sendIndication(peerAddress: string, peerPort: number, data: Buffer): Promise<void>;
113
+ /**
114
+ * Close the client
115
+ */
116
+ close(): void;
117
+ }
118
+ export default STUNClient;
119
+ export { STUNClient };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @file transport-stack.ts
3
+ * @description Composes ICE -> DTLS -> SCTP -> DCEP into one transport, the
4
+ * real WebRTC data-channel pipeline.
5
+ * @module transport-stack
6
+ *
7
+ * Wiring:
8
+ * IceAgent emits 'data' (non-STUN datagrams) -> DtlsConnection.handlePacket
9
+ * DtlsConnection.output -> IceAgent.send
10
+ * DtlsConnection 'data' (app records) -> SctpAssociation.receivePacket
11
+ * SctpAssociation 'output' -> DtlsConnection.send
12
+ * SctpAssociation + DataChannelManager -> RTCDataChannel
13
+ *
14
+ * The DTLS client/server role follows the negotiated a=setup; the SCTP client
15
+ * (INIT initiator) is the DTLS client, per RFC 8832.
16
+ */
17
+ import { EventEmitter } from 'events';
18
+ import { IceAgent } from './ice/ice-agent';
19
+ import { DtlsConnection } from './dtls/connection';
20
+ import { SctpAssociation } from './sctp/association';
21
+ import { DataChannelManager, OpenRequestInfo } from './sctp/datachannel-manager';
22
+ import type { RTCDataChannel, RTCDataChannelInit } from './datachannel/RTCDataChannel';
23
+ import type { KeyObject } from 'crypto';
24
+ export interface TransportStackOptions {
25
+ /** ICE controlling vs controlled (offerer is controlling). */
26
+ iceRole: 'controlling' | 'controlled';
27
+ /** DTLS role from a=setup (active=client). */
28
+ dtlsRole: 'client' | 'server';
29
+ localUfrag: string;
30
+ localPwd: string;
31
+ certDer: Buffer;
32
+ privateKey: KeyObject;
33
+ verifyFingerprint?: (fp: {
34
+ algorithm: string;
35
+ value: string;
36
+ }) => boolean;
37
+ }
38
+ export declare class TransportStack extends EventEmitter {
39
+ #private;
40
+ ice: IceAgent;
41
+ dtls: DtlsConnection | null;
42
+ sctp: SctpAssociation | null;
43
+ dcm: DataChannelManager | null;
44
+ constructor(opts: TransportStackOptions);
45
+ /**
46
+ * Begin gathering local candidates.
47
+ * @param opts - { iceServers, iceTransportPolicy } forwarded to ICE.
48
+ */
49
+ gather(opts?: {
50
+ iceServers?: unknown[];
51
+ iceTransportPolicy?: 'all' | 'relay';
52
+ }): Promise<void>;
53
+ getLocalCandidates(): ReturnType<IceAgent['getLocalCandidates']>;
54
+ /** Provide the peer's ICE credentials and start checks when ready. */
55
+ setRemote(ufrag: string, pwd: string): void;
56
+ addRemoteCandidate(cand: {
57
+ address: string;
58
+ port: number;
59
+ type?: string;
60
+ priority?: number;
61
+ }): void;
62
+ /** Open a locally-initiated data channel once SCTP is established. */
63
+ openChannel(channel: RTCDataChannel, init: RTCDataChannelInit): void;
64
+ /** Accept an inbound channel created from a 'datachannel-request'. */
65
+ acceptChannel(channel: RTCDataChannel, info: OpenRequestInfo): void;
66
+ isReady(): boolean;
67
+ close(): void;
68
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "node-rtc-connection",
3
- "version": "1.0.18",
3
+ "version": "2.0.4",
4
4
  "description": "WebRTC DataChannel implementation for Node.js with STUN, TURN, NAT traversal, and encryption. Pure Node.js, no native dependencies.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
7
- "types": "src/index.d.ts",
7
+ "types": "dist/types/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./src/index.d.ts",
10
+ "types": "./dist/types/index.d.ts",
11
11
  "require": "./dist/index.cjs",
12
12
  "import": "./dist/index.mjs"
13
13
  }
@@ -20,20 +20,17 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "build": "rollup -c",
23
+ "typecheck": "tsc -p tsconfig.test.json",
23
24
  "prepublishOnly": "npm run build && npm test",
24
- "test": "node test/run-all-tests.js",
25
- "test:watch": "node --test --watch test/*.test.js",
26
- "test:unit": "SKIP_INTEGRATION=1 node --test test/*.test.js",
27
- "test:integration": "node --test test/integration.test.js",
28
- "test:all": "SKIP_INTEGRATION=0 node test/run-all-tests.js",
29
- "test:stun": "node --test test/STUN.test.js",
30
- "test:turn": "node --test test/TURN.test.js",
31
- "test:turn-integration": "node --test test/turn-integration.test.js",
32
- "test:turn-all": "node --test test/TURN.test.js test/turn-integration.test.js",
33
- "example": "node examples/real-networking.js",
34
- "example:simple": "node examples/simple-datachannel.js",
35
- "example:stun": "node examples/with-stun-encryption.js",
36
- "example:turn": "node examples/with-turn-relay.js",
25
+ "test": "node --import tsx test/run-all-tests.ts",
26
+ "test:ci": "bash scripts/ci-local.sh",
27
+ "test:unit": "SKIP_INTEGRATION=1 node --import tsx --test test/*.test.ts",
28
+ "test:coverage": "c8 node --import tsx test/run-all-tests.ts",
29
+ "test:coverage:check": "c8 --check-coverage node --import tsx test/run-all-tests.ts",
30
+ "test:watch": "node --import tsx --test --watch test/*.test.ts",
31
+ "example": "node --import tsx examples/browser-server.ts",
32
+ "example:browser": "node --import tsx examples/browser-server.ts",
33
+ "example:node": "node --import tsx examples/node-to-node.ts",
37
34
  "release:patch": "npm version patch && git push && git push --tags",
38
35
  "release:minor": "npm version minor && git push && git push --tags",
39
36
  "release:major": "npm version major && git push && git push --tags"
@@ -59,20 +56,28 @@
59
56
  "license": "MIT",
60
57
  "type": "commonjs",
61
58
  "engines": {
62
- "node": ">=14.0.0"
59
+ "node": ">=18.0.0"
63
60
  },
64
61
  "devDependencies": {
65
62
  "@rollup/plugin-commonjs": "^29.0.0",
66
63
  "@rollup/plugin-json": "^6.1.0",
67
64
  "@rollup/plugin-node-resolve": "^16.0.3",
68
- "rollup": "^4.54.0"
65
+ "@rollup/plugin-terser": "^1.0.0",
66
+ "@rollup/plugin-typescript": "^12.3.0",
67
+ "@types/node": "^25.9.1",
68
+ "c8": "^11.0.0",
69
+ "playwright": "^1.60.0",
70
+ "rollup": "^4.54.0",
71
+ "tslib": "^2.8.1",
72
+ "tsx": "^4.22.3",
73
+ "typescript": "^6.0.3"
69
74
  },
70
75
  "repository": {
71
76
  "type": "git",
72
- "url": "git+https://github.com/nmhung1210/nodertc.git"
77
+ "url": "git+https://github.com/nmhung1210/node-rtc-connection.git"
73
78
  },
74
79
  "bugs": {
75
- "url": "https://github.com/nmhung1210/nodertc/issues"
80
+ "url": "https://github.com/nmhung1210/node-rtc-connection/issues"
76
81
  },
77
- "homepage": "https://github.com/nmhung1210/nodertc#readme"
82
+ "homepage": "https://github.com/nmhung1210/node-rtc-connection#readme"
78
83
  }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * @file der.ts
3
+ * @description Minimal ASN.1 DER encoder/decoder for X.509 certificate generation.
4
+ * @module crypto/der
5
+ *
6
+ * Implements just enough of ITU-T X.690 DER to build and read the structures
7
+ * WebRTC needs: self-signed ECDSA certificates and SubjectPublicKeyInfo.
8
+ *
9
+ * All encoders return Buffers. The TLV length is always encoded in the
10
+ * minimal (definite, shortest-form) representation required by DER.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ // ASN.1 universal tag numbers (class 0, primitive/constructed as noted).
16
+ export const TAG = Object.freeze({
17
+ BOOLEAN: 0x01,
18
+ INTEGER: 0x02,
19
+ BIT_STRING: 0x03,
20
+ OCTET_STRING: 0x04,
21
+ NULL: 0x05,
22
+ OID: 0x06,
23
+ UTF8_STRING: 0x0c,
24
+ PRINTABLE_STRING: 0x13,
25
+ IA5_STRING: 0x16,
26
+ UTC_TIME: 0x17,
27
+ GENERALIZED_TIME: 0x18,
28
+ SEQUENCE: 0x30, // constructed
29
+ SET: 0x31, // constructed
30
+ });
31
+
32
+ /**
33
+ * Encode a DER length in definite, shortest form.
34
+ * @param {number} len
35
+ * @returns {Buffer}
36
+ */
37
+ export function encodeLength(len: number): Buffer {
38
+ if (len < 0x80) {
39
+ return Buffer.from([len]);
40
+ }
41
+ const bytes: number[] = [];
42
+ let n = len;
43
+ while (n > 0) {
44
+ bytes.unshift(n & 0xff);
45
+ n >>>= 8;
46
+ }
47
+ return Buffer.from([0x80 | bytes.length, ...bytes]);
48
+ }
49
+
50
+ /**
51
+ * Wrap a body in a TLV with the given tag.
52
+ * @param {number} tag
53
+ * @param {Buffer} body
54
+ * @returns {Buffer}
55
+ */
56
+ export function tlv(tag: number, body: Buffer): Buffer {
57
+ return Buffer.concat([Buffer.from([tag]), encodeLength(body.length), body]);
58
+ }
59
+
60
+ /**
61
+ * Encode an unsigned big-endian integer (from a Buffer) as a DER INTEGER,
62
+ * adding a leading 0x00 when the high bit is set so it stays positive.
63
+ * @param {Buffer} buf - Big-endian magnitude.
64
+ * @returns {Buffer}
65
+ */
66
+ export function encodeIntegerFromBuffer(buf: Buffer): Buffer {
67
+ let start = 0;
68
+ while (start < buf.length - 1 && buf[start] === 0x00) {
69
+ start++; // strip leading zeros (keep at least one byte)
70
+ }
71
+ let body = buf.slice(start);
72
+ if (body[0]! & 0x80) {
73
+ body = Buffer.concat([Buffer.from([0x00]), body]);
74
+ }
75
+ return tlv(TAG.INTEGER, body);
76
+ }
77
+
78
+ /**
79
+ * Encode a small non-negative JS integer as a DER INTEGER.
80
+ * @param {number} value
81
+ * @returns {Buffer}
82
+ */
83
+ export function encodeInteger(value: number): Buffer {
84
+ if (value === 0) {
85
+ return tlv(TAG.INTEGER, Buffer.from([0x00]));
86
+ }
87
+ const bytes: number[] = [];
88
+ let n = value;
89
+ while (n > 0) {
90
+ bytes.unshift(n & 0xff);
91
+ n = Math.floor(n / 256);
92
+ }
93
+ if (bytes[0]! & 0x80) {
94
+ bytes.unshift(0x00);
95
+ }
96
+ return tlv(TAG.INTEGER, Buffer.from(bytes));
97
+ }
98
+
99
+ /**
100
+ * Encode an OBJECT IDENTIFIER from its dotted-decimal string.
101
+ * @param {string} oid - e.g. "1.2.840.10045.2.1"
102
+ * @returns {Buffer}
103
+ */
104
+ export function encodeOID(oid: string): Buffer {
105
+ const parts = oid.split('.').map(Number);
106
+ if (parts.length < 2) {
107
+ throw new Error(`Invalid OID: ${oid}`);
108
+ }
109
+ const bytes = [40 * parts[0]! + parts[1]!];
110
+ for (let i = 2; i < parts.length; i++) {
111
+ let v = parts[i]!;
112
+ const stack = [v & 0x7f];
113
+ v = Math.floor(v / 128);
114
+ while (v > 0) {
115
+ stack.unshift((v & 0x7f) | 0x80);
116
+ v = Math.floor(v / 128);
117
+ }
118
+ bytes.push(...stack);
119
+ }
120
+ return tlv(TAG.OID, Buffer.from(bytes));
121
+ }
122
+
123
+ /**
124
+ * Encode a BIT STRING with zero unused bits.
125
+ * @param {Buffer} data
126
+ * @returns {Buffer}
127
+ */
128
+ export function encodeBitString(data: Buffer): Buffer {
129
+ return tlv(TAG.BIT_STRING, Buffer.concat([Buffer.from([0x00]), data]));
130
+ }
131
+
132
+ /**
133
+ * Encode an OCTET STRING.
134
+ * @param {Buffer} data
135
+ * @returns {Buffer}
136
+ */
137
+ export function encodeOctetString(data: Buffer): Buffer {
138
+ return tlv(TAG.OCTET_STRING, data);
139
+ }
140
+
141
+ /**
142
+ * Encode a SEQUENCE from already-encoded components.
143
+ * @param {Buffer[]} components
144
+ * @returns {Buffer}
145
+ */
146
+ export function encodeSequence(components: Buffer[]): Buffer {
147
+ return tlv(TAG.SEQUENCE, Buffer.concat(components));
148
+ }
149
+
150
+ /**
151
+ * Encode a SET from already-encoded components.
152
+ * @param {Buffer[]} components
153
+ * @returns {Buffer}
154
+ */
155
+ export function encodeSet(components: Buffer[]): Buffer {
156
+ return tlv(TAG.SET, Buffer.concat(components));
157
+ }
158
+
159
+ /**
160
+ * Encode NULL.
161
+ * @returns {Buffer}
162
+ */
163
+ export function encodeNull(): Buffer {
164
+ return tlv(TAG.NULL, Buffer.alloc(0));
165
+ }
166
+
167
+ /**
168
+ * Encode a UTF8String.
169
+ * @param {string} str
170
+ * @returns {Buffer}
171
+ */
172
+ export function encodeUTF8String(str: string): Buffer {
173
+ return tlv(TAG.UTF8_STRING, Buffer.from(str, 'utf8'));
174
+ }
175
+
176
+ /**
177
+ * Encode a context-specific [n] explicit wrapper (constructed).
178
+ * @param {number} n - context tag number
179
+ * @param {Buffer} body
180
+ * @returns {Buffer}
181
+ */
182
+ export function encodeExplicit(n: number, body: Buffer): Buffer {
183
+ return tlv(0xa0 | n, body);
184
+ }
185
+
186
+ /**
187
+ * Encode an X.509 time. Uses UTCTime for years < 2050, else GeneralizedTime,
188
+ * per RFC 5280 §4.1.2.5.
189
+ * @param {Date} date
190
+ * @returns {Buffer}
191
+ */
192
+ export function encodeTime(date: Date): Buffer {
193
+ const yyyy = date.getUTCFullYear();
194
+ const pad = (v: number, n = 2): string => String(v).padStart(n, '0');
195
+ const mm = pad(date.getUTCMonth() + 1);
196
+ const dd = pad(date.getUTCDate());
197
+ const hh = pad(date.getUTCHours());
198
+ const mi = pad(date.getUTCMinutes());
199
+ const ss = pad(date.getUTCSeconds());
200
+ if (yyyy < 2050) {
201
+ const yy = pad(yyyy % 100);
202
+ return tlv(TAG.UTC_TIME, Buffer.from(`${yy}${mm}${dd}${hh}${mi}${ss}Z`, 'ascii'));
203
+ }
204
+ return tlv(TAG.GENERALIZED_TIME, Buffer.from(`${yyyy}${mm}${dd}${hh}${mi}${ss}Z`, 'ascii'));
205
+ }