- name: Check latest TS files are in git
run: |
git checkout ts/package.json
+ git checkout node-net/package.json
git diff --exit-code
java_bindings:
and `WeakRef` (Chrome 84, Firefox 79, Safari 14.1/iOS 14.5 and Node 14.6) and WASM BigInt support
(Chrome 85, Firefox 78, Safari 14.1/iOS 14.5, and Node 15.0).
+For users of Node.JS environments you may wish to use the `lightningdevkit-node-net` package as
+well to implement the required network handling to bridge the `lightningdevkit` package's
+`SocketDescriptor` interface to Node.JS TCP Sockets. For those wishing to run a lightning node in
+the browser you will need to provide your own bridge from `SocketDescriptor` to a WebSocket proxy.
+
## General
The only known issue resulting in a use-after-free bug requires custom a custom ChannelKeys instance
popd
LDK_LIB="tmp/libldk.bc tmp/libldk.a"
fi
- $COMPILE -o liblightningjni_release$LDK_TARGET_SUFFIX.so -flto -O3 -I"$1"/lightning-c-bindings/include/ $2 src/main/jni/bindings.c $LDK_LIB -lm
+ $COMPILE -o liblightningjni_release$LDK_TARGET_SUFFIX.so -s -flto -O3 -I"$1"/lightning-c-bindings/include/ $2 src/main/jni/bindings.c $LDK_LIB -lm
if [ "$IS_MAC" = "false" -a "$4" = "false" ]; then
GLIBC_SYMBS="$(objdump -T liblightningjni_release$LDK_TARGET_SUFFIX.so | grep GLIBC_ | grep -v "GLIBC_2\.2\." | grep -v "GLIBC_2\.3\(\.\| \)" | grep -v "GLIBC_2.\(14\|17\) " || echo)"
if [ "$GLIBC_SYMBS" != "" ]; then
fi
rm -f ts/bindings.c
sed -i 's/^ "version": .*/ "version": "'${LDK_GARBAGECOLLECTED_GIT_OVERRIDE:1:100}'",/g' ts/package.json
+ sed -i 's/^ "version": .*/ "version": "'${LDK_GARBAGECOLLECTED_GIT_OVERRIDE:1:100}'",/g' node-net/package.json
+ sed -i 's/^ "lightningdevkit": .*/ "lightningdevkit": "'${LDK_GARBAGECOLLECTED_GIT_OVERRIDE:1:100}'"/g' node-net/package.json
if [ "$3" = "true" ]; then
echo "#define LDK_DEBUG_BUILD" > ts/bindings.c
elif [ "$3" = "leaks" ]; then
tsc --types node --typeRoots .
cp ../$WASM_FILE liblightningjs.wasm
cp ../README.md README.md
+ cd ../node-net
+ tsc --types node --typeRoots .
echo Ready to publish!
if [ -x "$(which node)" ]; then
NODE_V="$(node --version)"
if [ "${NODE_V:1:2}" -gt 14 ]; then
+ cd ../ts
node test/node.mjs
+ cd ../node-net
+ node test/test.mjs
fi
fi
fi
--- /dev/null
+LDK Node.JS TypeScript Network Implementation
+=============================================
+
+This module bridges the LDK `SocketDescriptor` and `PeerManager` interfaces to Node.JS's `net`
+TCP sockets. See the `lightningdevkit` module for more info.
--- /dev/null
+import * as ldk from "lightningdevkit";
+import * as net from "net";
+
+/**
+ * Handles TCP connections using Node.JS's 'net' module given an `ldk.PeerManager`.
+ */
+export class NodeLDKNet {
+ private ping_timer;
+ private servers: net.Server[];
+ public constructor(public peer_manager: ldk.PeerManager) {
+ this.ping_timer = setInterval(function() {
+ peer_manager.timer_tick_occurred();
+ peer_manager.process_events();
+ }, 10_000);
+ this.servers = [];
+ }
+
+ /**
+ * Disconnects all connections and releases all resources for this net handler.
+ */
+ public stop() {
+ clearInterval(this.ping_timer);
+ for (const server of this.servers) {
+ server.close();
+ }
+ this.peer_manager.disconnect_all_peers();
+ }
+
+ /**
+ * Processes any pending events for the PeerManager, sending queued messages.
+ * You should call this (or peer_manager.process_events()) any time you take an action which
+ * is likely to generate messages to send (eg send a payment, processing payment forwards,
+ * etc).
+ */
+ public process_events() { this.peer_manager.process_events(); }
+
+ private descriptor_count = BigInt(0);
+ private get_descriptor(socket: net.Socket): ldk.SocketDescriptor {
+ const this_index = this.descriptor_count;
+ this.descriptor_count += BigInt(1);
+
+ socket.setNoDelay(true);
+
+ const this_pm = this.peer_manager;
+ var sock_write_waiting = false;
+
+ let descriptor = ldk.SocketDescriptor.new_impl ({
+ send_data(data: Uint8Array, resume_read: boolean): number {
+ if (resume_read) socket.resume();
+
+ if (sock_write_waiting) return 0;
+ const written = socket.write(data);
+ if (!written) sock_write_waiting = true;
+ return data.length;
+ },
+ disconnect_socket(): void {
+ socket.destroy();
+ },
+ eq(other: ldk.SocketDescriptor): boolean {
+ return other.hash() == this.hash();
+ },
+ hash(): bigint {
+ return this_index;
+ }
+ } as ldk.SocketDescriptorInterface);
+
+ socket.on("drain", function() {
+ if (sock_write_waiting) {
+ if (!this_pm.write_buffer_space_avail(descriptor).is_ok()) {
+ descriptor.disconnect_socket();
+ }
+ }
+ });
+
+ socket.on("data", function(data) {
+ const res = this_pm.read_event(descriptor, data);
+ if (!res.is_ok()) descriptor.disconnect_socket();
+ else if ((res as ldk.Result_boolPeerHandleErrorZ_OK).res) socket.pause();
+ this_pm.process_events();
+ });
+
+ socket.on("close", function() {
+ this_pm.socket_disconnected(descriptor);
+ });
+
+ return descriptor;
+ }
+
+ private static v4_addr_from_ip(ip: string, port: number): ldk.NetAddress {
+ const sockaddr = ip.split(".").map(parseFloat);
+ return ldk.NetAddress.constructor_ipv4(new Uint8Array(sockaddr), port);
+ }
+ private static v6_addr_from_ip(ip: string, port: number): ldk.NetAddress {
+ const sockaddr = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ const halves = ip.split("::"); // either one or two elements
+ const first_half = halves[0].split(":");
+ for (var idx = 0; idx < first_half.length; idx++) {
+ const v = parseInt(first_half[idx], 16);
+ sockaddr[idx*2] = v >> 8;
+ sockaddr[idx*2 + 1] = v & 0xff;
+ }
+ if (halves.length == 2) {
+ const second_half = halves[1].split(":");
+ for (var idx = 0; idx < second_half.length; idx++) {
+ const v = parseInt(second_half[second_half.length - idx - 1], 16);
+ sockaddr[14 - idx*2] = v >> 8;
+ sockaddr[15 - idx*2] = v & 0xff;
+ }
+ }
+ return ldk.NetAddress.constructor_ipv6(new Uint8Array(sockaddr), port);
+ }
+
+ private static get_addr_from_socket(socket: net.Socket): ldk.Option_NetAddressZ {
+ const addr = socket.remoteAddress;
+ if (addr === undefined)
+ return ldk.Option_NetAddressZ.constructor_none();
+ if (net.isIPv4(addr)) {
+ return ldk.Option_NetAddressZ.constructor_some(NodeLDKNet.v4_addr_from_ip(addr, socket.remotePort));
+ }
+ if (net.isIPv6(addr)) {
+ return ldk.Option_NetAddressZ.constructor_some(NodeLDKNet.v6_addr_from_ip(addr, socket.remotePort));
+ }
+ return ldk.Option_NetAddressZ.constructor_none();
+ }
+
+ /**
+ * Binds a listener on the given host and port, accepting incoming connections.
+ */
+ public async bind_listener(host: string, port: number) {
+ const this_handler = this;
+ const server = net.createServer(function(incoming_sock: net.Socket) {
+ const descriptor = this_handler.get_descriptor(incoming_sock);
+ const res = this_handler.peer_manager
+ .new_inbound_connection(descriptor, NodeLDKNet.get_addr_from_socket(incoming_sock));
+ if (!res.is_ok()) descriptor.disconnect_socket();
+ });
+ const servers_list = this.servers;
+ return new Promise<void>((resolve, reject) => {
+ server.on("error", function() {
+ reject();
+ server.close();
+ });
+ server.on("listening", function() {
+ servers_list.push(server);
+ resolve();
+ });
+ server.listen(port, host);
+ });
+ }
+
+ /**
+ * Establishes an outgoing connection to the given peer at the given host and port.
+ *
+ * Note that the peer will not appear in the PeerManager peers list until the socket has
+ * connected and the initial handshake completes.
+ */
+ public async connect_peer(host: string, port: number, peer_node_id: Uint8Array) {
+ const this_handler = this;
+ const sock = new net.Socket();
+ const res = new Promise<void>((resolve, reject) => {
+ sock.on("connect", function() { resolve(); });
+ sock.on("error", function() { reject(); });
+ });
+ sock.connect(port, host, function() {
+ const descriptor = this_handler.get_descriptor(sock);
+ const res = this_handler.peer_manager
+ .new_outbound_connection(peer_node_id, descriptor, NodeLDKNet.get_addr_from_socket(sock));
+ if (!res.is_ok()) descriptor.disconnect_socket();
+ else {
+ const bytes = (res as ldk.Result_CVec_u8ZPeerHandleErrorZ_OK).res;
+ const send_res = descriptor.send_data(bytes, true);
+ console.assert(send_res == bytes.length);
+ }
+ });
+ return res;
+ }
+}
--- /dev/null
+../../../ts/node/
\ No newline at end of file
--- /dev/null
+../../ts
\ No newline at end of file
--- /dev/null
+{
+ "name": "lightningdevkit-node-net",
+ "version": "Set in genbindings.sh automagically",
+ "description": "Lightning Development Kit Net Implementation for Node.JS",
+ "main": "net.mjs",
+ "types": "net.d.mts",
+ "type": "module",
+ "files": [
+ "net.mjs",
+ "net.d.mts",
+ "tsconfig.json",
+ "README.md"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/lightningdevkit/ldk-garbagecollected.git"
+ },
+ "keywords": [
+ "lightning",
+ "bitcoin"
+ ],
+ "author": "LDK Developers",
+ "license": "MIT OR Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/lightningdevkit/ldk-garbagecollected/issues"
+ },
+ "homepage": "https://github.com/lightningdevkit/ldk-garbagecollected#readme",
+ "dependencies": {
+ "lightningdevkit": "Set in genbindings.sh automagically"
+ },
+ "devDependencies": {
+ "@types/node": "^17.0.8"
+ }
+}
--- /dev/null
+import * as ldk from "../../ts/index.mjs";
+import * as node_net from '../net.mjs';
+
+import * as fs from 'fs';
+
+const wasm_file = fs.readFileSync('../ts/liblightningjs.wasm');
+await ldk.initializeWasmFromBinary(wasm_file);
+
+const logger_a = ldk.Logger.new_impl({
+ log(record: ldk.Record): void {
+ console.log(record.get_module_path() + ": " + record.get_args());
+ }
+} as ldk.LoggerInterface);
+const logger_b = logger_a;
+
+const node_a_secret = new Uint8Array(32);
+for (var i = 0; i < 32; i++) node_a_secret[i] = 42;
+// The public key for a secret key of all 42s:
+const node_a_pk = new Uint8Array([3, 91, 229, 233, 71, 130, 9, 103, 74, 150, 230, 15, 31, 3, 127, 97, 118, 84, 15, 208, 1, 250, 29, 100, 105, 71, 112, 197, 106, 119, 9, 196, 44]);
+
+const node_b_secret = new Uint8Array(32);
+for (var i = 0; i < 32; i++) node_b_secret[i] = 43;
+
+const rng_seed = new Uint8Array(32);
+const routing_handler = ldk.IgnoringMessageHandler.constructor_new().as_RoutingMessageHandler();
+const chan_handler = ldk.ErroringMessageHandler.constructor_new().as_ChannelMessageHandler();
+const cust_handler = ldk.IgnoringMessageHandler.constructor_new().as_CustomMessageHandler();
+
+const a_pm = ldk.PeerManager.constructor_new(chan_handler, routing_handler, node_a_secret, rng_seed, logger_a, cust_handler);
+const a_net_handler = new node_net.NodeLDKNet(a_pm);
+var port = 10000;
+for (; port < 11000; port++) {
+ try {
+ // Try ports until we find one we can bind to.
+ await a_net_handler.bind_listener("127.0.0.1", port);
+ break;
+ } catch(_) {}
+}
+
+const b_pm = ldk.PeerManager.constructor_new(chan_handler, routing_handler, node_b_secret, rng_seed, logger_b, cust_handler);
+const b_net_handler = new node_net.NodeLDKNet(b_pm);
+await b_net_handler.connect_peer("127.0.0.1", port, node_a_pk);
+
+try {
+ // Ensure we get an error if we try to bind the same port twice.
+ await a_net_handler.bind_listener("127.0.0.1", port);
+ console.assert(false);
+} catch(_) {}
+
+await new Promise<void>(resolve => {
+ // Wait until the peers are connected and have exchanged the initial handshake
+ var timer: ReturnType<typeof setInterval>;
+ timer = setInterval(function() {
+ if (a_pm.get_peer_node_ids().length == 1 && b_pm.get_peer_node_ids().length == 1) {
+ resolve();
+ clearInterval(timer);
+ }
+ }, 500);
+});
+
+b_pm.disconnect_by_node_id(node_a_pk, false);
+await new Promise<void>(resolve => {
+ // Wait until A learns the connection is closed from the socket closure
+ var timer: ReturnType<typeof setInterval>;
+ timer = setInterval(function() {
+ if (a_pm.get_peer_node_ids().length == 0 && b_pm.get_peer_node_ids().length == 0) {
+ resolve();
+ clearInterval(timer);
+ }
+ }, 500);
+});
+
+a_net_handler.stop();
+b_net_handler.stop();
+
+function arr_eq(a: number[]|Uint8Array, b: number[]|Uint8Array): boolean {
+ return a.length == b.length && a.every((val, idx) => val == b[idx]);
+}
+
+const v4_parse = node_net.NodeLDKNet["v4_addr_from_ip"];
+console.assert((v4_parse("127.0.0.1", 4242) as ldk.NetAddress_IPv4).port == 4242);
+console.assert(arr_eq((v4_parse("127.0.0.1", 4242) as ldk.NetAddress_IPv4).addr, [127,0,0,1]));
+console.assert(arr_eq((v4_parse("0.0.0.0", 4242) as ldk.NetAddress_IPv4).addr, [0,0,0,0]));
+
+const v6_parse = node_net.NodeLDKNet["v6_addr_from_ip"];
+console.assert((v6_parse("::", 4242) as ldk.NetAddress_IPv4).port == 4242);
+console.assert(arr_eq((v6_parse("::", 4242) as ldk.NetAddress_IPv6).addr,
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]));
+console.assert(arr_eq((v6_parse("fe80::", 4242) as ldk.NetAddress_IPv6).addr,
+ [0xfe,0x80,0,0,0,0,0,0,0,0,0,0,0,0,0,0]));
+console.assert(arr_eq((v6_parse("fe80::42", 4242) as ldk.NetAddress_IPv6).addr,
+ [0xfe,0x80,0,0,0,0,0,0,0,0,0,0,0,0,0,0x42]));
+console.assert(arr_eq((v6_parse("fe80:A:b::", 4242) as ldk.NetAddress_IPv6).addr,
+ [0xfe,0x80,0,0xa,0,0xb,0,0,0,0,0,0,0,0,0,0]));
+console.assert(arr_eq((v6_parse("2001:1:bad::beef:cafe", 4242) as ldk.NetAddress_IPv6).addr,
+ [0x20, 0x01, 0, 1, 0xb, 0xad, 0, 0, 0, 0, 0, 0, 0xbe, 0xef, 0xca, 0xfe]));
--- /dev/null
+{
+ "compilerOptions": {
+ "target": "es2021",
+ "module": "es2022",
+ "sourceMap": true,
+ "esModuleInterop": false,
+ "stripInternal": true,
+ "moduleResolution": "node",
+
+ "paths": {
+ "lightningdevkit": [ "../ts/index.mts" ]
+ },
+
+ "allowSyntheticDefaultImports": false,
+ "forceConsistentCasingInFileNames": true,
+ "declaration": true,
+ "strict": true,
+ "noImplicitAny": true,
+ "strictNullChecks": false,
+ "strictFunctionTypes": true,
+ "strictBindCallApply": true,
+ "strictPropertyInitialization": false,
+ "noImplicitThis": true,
+ "useUnknownInCatchVariables": true,
+ "alwaysStrict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "exactOptionalPropertyTypes": false,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "allowUnusedLabels": false,
+ "allowUnreachableCode": false
+ }
+}
/* @internal */
export function check_arr_len(arr: Uint8Array, len: number): Uint8Array {
- if (arr.length != len) { throw new Error("Expected array of length " + len + "got " + arr.length); }
+ if (arr.length != len) { throw new Error("Expected array of length " + len + " got " + arr.length); }
return arr;
}
/* @internal */ export function getRemainingAllocationCount(): number { return 0; }
/* @internal */ export function debugPrintRemainingAllocs() { }
-/* @internal */
+/**
+ * An error when accessing the chain via [`Access`].
+ */
export enum AccessError {
/**
* The requested chain is unknown.
}
-/* @internal */
+/**
+ * An enum which can either contain a or not
+ */
export enum COption_NoneZ {
/**
* When we're in this state, this COption_NoneZ contains a
}
-/* @internal */
+/**
+ * An error enum representing a failure to persist a channel monitor update.
+ */
export enum ChannelMonitorUpdateErr {
/**
* Used to indicate a temporary failure (eg connection to a watchtower or remote backup of
}
-/* @internal */
+/**
+ * An enum that represents the speed at which we want a transaction to confirm used for feerate
+ * estimation.
+ */
export enum ConfirmationTarget {
/**
* We are happy with this transaction confirming slowly when feerate drops some.
}
-/* @internal */
+/**
+ * Errors that may occur when constructing a new `RawInvoice` or `Invoice`
+ */
export enum CreationError {
/**
* The supplied description string was longer than 639 __bytes__ (see [`Description::new(...)`](./struct.Description.html#method.new))
}
-/* @internal */
+/**
+ * Enum representing the crypto currencies (or networks) supported by this library
+ */
export enum Currency {
/**
* Bitcoin mainnet
}
-/* @internal */
+/**
+ * Represents an IO Error. Note that some information is lost in the conversion from Rust.
+ */
export enum IOError {
LDKIOError_NotFound,
LDKIOError_PermissionDenied,
}
-/* @internal */
+/**
+ * An enum representing the available verbosity levels of the logger.
+ */
export enum Level {
/**
* Designates extremely verbose information, including gossip-induced messages
}
-/* @internal */
+/**
+ * An enum representing the possible Bitcoin or test networks which we can run on
+ */
export enum Network {
/**
* The main Bitcoin blockchain.
}
-/* @internal */
+/**
+ * Specifies the recipient of an invoice, to indicate to [`KeysInterface::sign_invoice`] what node
+ * secret key should be used to sign the invoice.
+ */
export enum Recipient {
/**
* The invoice should be signed with the local node secret key.
}
-/* @internal */
+/**
+ * Represents an error returned from libsecp256k1 during validation of some secp256k1 data
+ */
export enum Secp256k1Error {
/**
* Signature failed verification
}
-/* @internal */
+/**
+ * Errors that may occur when converting a `RawInvoice` to an `Invoice`. They relate to the
+ * requirements sections in BOLT #11
+ */
export enum SemanticError {
/**
* The invoice is missing the mandatory payment hash
}
-/* @internal */
+/**
+ * SI prefixes for the human readable part
+ */
export enum SiPrefix {
/**
* 10^-3
--- /dev/null
+// Minimal part of the Node API which we depend on.
+// May be (c) Microsoft licensed under the MIT license, however API's are not generally copyrightable per recent precedent.
+declare module 'buffer' {
+ global {
+ interface Buffer extends Uint8Array {}
+ }
+}
+declare module 'node:buffer' {
+ export * from 'buffer';
+}
// May be (c) Microsoft licensed under the MIT license, however API's are not generally copyrightable per recent precedent.
declare module 'crypto' {
namespace webcrypto {
- function getRandomValues(TypedArray): void;
+ function getRandomValues(out: Uint8Array): void;
}
}
declare module 'node:crypto' {
// Minimal part of the Node fs API which we depend on.
// May be (c) Microsoft licensed under the MIT license, however API's are not generally copyrightable per recent precedent.
declare module 'fs' {
- export type PathLike = string | Buffer | URL;
+ export type PathLike = string | URL;
export type PathOrFileDescriptor = PathLike | number;
export function readFileSync(
path: PathOrFileDescriptor,
// Minimal part of the Node API which we depend on.
// May be (c) Microsoft licensed under the MIT license, however API's are not generally copyrightable per recent precedent.
+/// <reference path="buffer.d.ts" />
/// <reference path="crypto.d.ts" />
/// <reference path="fs.d.ts" />
+/// <reference path="stream.d.ts" />
+/// <reference path="net.d.ts" />
--- /dev/null
+// Minimal part of the Node fs API which we depend on.
+// May be (c) Microsoft licensed under the MIT license, however API's are not generally copyrightable per recent precedent.
+declare module 'net' {
+ import * as stream from 'node:stream';
+ class Socket extends stream.Duplex {
+ constructor();
+ write(buffer: Uint8Array | string, cb?: (err?: Error) => void): boolean;
+ connect(port: number, host: string, connectionListener?: () => void): this;
+ pause(): this;
+ resume(): this;
+ setNoDelay(noDelay?: boolean): this;
+ readonly remoteAddress?: string | undefined;
+ readonly remotePort?: number | undefined;
+ on(event: 'close', listener: (hadError: boolean) => void): this;
+ on(event: 'connect', listener: () => void): this;
+ on(event: 'data', listener: (data: Buffer) => void): this;
+ on(event: 'drain', listener: () => void): this;
+ on(event: 'error', listener: (err: Error) => void): this;
+ }
+ class Server {
+ listen(port?: number, hostname?: string, listeningListener?: () => void): this;
+ close(callback?: (err?: Error) => void): this;
+ on(event: 'error', listener: (err: Error) => void): this;
+ on(event: 'listening', listener: () => void): this;
+ }
+ function createServer(connectionListener?: (socket: Socket) => void): Server;
+ function isIPv4(input: string): boolean;
+ function isIPv6(input: string): boolean;
+}
+declare module 'node:net' {
+ export * from 'net';
+}
--- /dev/null
+/**
+ * A stream is an abstract interface for working with streaming data in Node.js.
+ * The `stream` module provides an API for implementing the stream interface.
+ *
+ * There are many stream objects provided by Node.js. For instance, a `request to an HTTP server` and `process.stdout` are both stream instances.
+ *
+ * Streams can be readable, writable, or both. All streams are instances of `EventEmitter`.
+ *
+ * To access the `stream` module:
+ *
+ * ```js
+ * const stream = require('stream');
+ * ```
+ *
+ * The `stream` module is useful for creating new types of stream instances. It is
+ * usually not necessary to use the `stream` module to consume streams.
+ * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/stream.js)
+ */
+declare module 'stream' {
+ namespace internal {
+ class Stream {}
+ class Readable extends Stream {
+ destroy(error?: Error): this;
+ }
+ class Writable extends Stream {
+ destroy(error?: Error): this;
+ }
+ class Duplex extends Readable implements Writable {}
+ }
+ export = internal;
+}
+declare module 'node:stream' {
+ import stream = require('stream');
+ export = stream;
+}
/* @internal */
export function check_arr_len(arr: Uint8Array, len: number): Uint8Array {
- if (arr.length != len) { throw new Error("Expected array of length " + len + "got " + arr.length); }
+ if (arr.length != len) { throw new Error("Expected array of length " + len + " got " + arr.length); }
return arr;
}
out_c = out_c + "\t}\n"
out_c = out_c + "}\n"
+ # Note that this is *not* marked /* @internal */ as we re-expose it directly in enums/
+ enum_comment_formatted = enum_doc_comment.replace("\n", "\n * ")
out_typescript = f"""
-/* @internal */
+/**
+ * {enum_comment_formatted}
+ */
export enum {struct_name} {{
{out_typescript_enum_fields}
}}