hs-playlib 0.1.0

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.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * UniFFI client for Node.js — calls the same shared library as Python/Kotlin.
3
+ *
4
+ * Uses koffi to call the UniFFI C ABI directly, replacing NAPI entirely.
5
+ * Handles RustBuffer serialization/deserialization for the UniFFI protocol.
6
+ *
7
+ * Flow dispatch is generic: callReq(flow, ...) and callRes(flow, ...) load
8
+ * the corresponding C symbol dynamically from the flow list in _generated_flows.js.
9
+ * No flow names are hardcoded here — add new flows to flows.yaml and run `make generate`.
10
+ */
11
+ export interface RustBuffer {
12
+ capacity: bigint;
13
+ len: bigint;
14
+ data: Buffer | null;
15
+ }
16
+ export interface RustCallStatus {
17
+ code: number;
18
+ error_buf: RustBuffer;
19
+ }
20
+ export declare class UniffiClient {
21
+ private _ffi;
22
+ constructor(libPath?: string);
23
+ /**
24
+ * Build the connector HTTP request for any flow.
25
+ * Returns protobuf-encoded FfiConnectorHttpRequest bytes.
26
+ */
27
+ callReq(flow: string, requestBytes: Buffer | Uint8Array, metadata: Record<string, string>, optionsBytes: Buffer | Uint8Array): Buffer;
28
+ /**
29
+ * Parse the connector HTTP response for any flow.
30
+ * responseBytes: protobuf-encoded FfiConnectorHttpResponse.
31
+ * Returns protobuf-encoded response bytes for the flow's response type.
32
+ */
33
+ callRes(flow: string, responseBytes: Buffer | Uint8Array, requestBytes: Buffer | Uint8Array, metadata: Record<string, string>, optionsBytes: Buffer | Uint8Array): Buffer;
34
+ }
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ /**
3
+ * UniFFI client for Node.js — calls the same shared library as Python/Kotlin.
4
+ *
5
+ * Uses koffi to call the UniFFI C ABI directly, replacing NAPI entirely.
6
+ * Handles RustBuffer serialization/deserialization for the UniFFI protocol.
7
+ *
8
+ * Flow dispatch is generic: callReq(flow, ...) and callRes(flow, ...) load
9
+ * the corresponding C symbol dynamically from the flow list in _generated_flows.js.
10
+ * No flow names are hardcoded here — add new flows to flows.yaml and run `make generate`.
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.UniffiClient = void 0;
17
+ const koffi_1 = __importDefault(require("koffi"));
18
+ const path_1 = __importDefault(require("path"));
19
+ // @ts-ignore - generated CommonJS module
20
+ const _generated_flows_js_1 = require("./_generated_flows.js");
21
+ const _dirname = __dirname;
22
+ const FLOW_NAMES = Object.keys(_generated_flows_js_1.FLOWS);
23
+ const RustBufferStruct = koffi_1.default.struct("RustBuffer", {
24
+ capacity: "uint64",
25
+ len: "uint64",
26
+ data: "void *",
27
+ });
28
+ const RustCallStatusStruct = koffi_1.default.struct("RustCallStatus", {
29
+ code: "int8",
30
+ error_buf: RustBufferStruct,
31
+ });
32
+ function loadLib(libPath) {
33
+ if (!libPath) {
34
+ const ext = process.platform === "darwin" ? "dylib" : "so";
35
+ libPath = path_1.default.join(_dirname, "generated", `libconnector_service_ffi.${ext}`);
36
+ }
37
+ const lib = koffi_1.default.load(libPath);
38
+ const fns = {
39
+ alloc: lib.func("ffi_connector_service_ffi_rustbuffer_alloc", RustBufferStruct, ["uint64", koffi_1.default.out(koffi_1.default.pointer(RustCallStatusStruct))]),
40
+ free: lib.func("ffi_connector_service_ffi_rustbuffer_free", "void", [RustBufferStruct, koffi_1.default.out(koffi_1.default.pointer(RustCallStatusStruct))]),
41
+ };
42
+ // Load req and res transformer symbols for every registered flow.
43
+ for (const flow of FLOW_NAMES) {
44
+ fns[`${flow}_req`] = lib.func(`uniffi_connector_service_ffi_fn_func_${flow}_req_transformer`, RustBufferStruct, [RustBufferStruct, RustBufferStruct, RustBufferStruct, koffi_1.default.out(koffi_1.default.pointer(RustCallStatusStruct))]);
45
+ fns[`${flow}_res`] = lib.func(`uniffi_connector_service_ffi_fn_func_${flow}_res_transformer`, RustBufferStruct, [RustBufferStruct, RustBufferStruct, RustBufferStruct, RustBufferStruct, koffi_1.default.out(koffi_1.default.pointer(RustCallStatusStruct))]);
46
+ }
47
+ return fns;
48
+ }
49
+ // ── Helpers ──────────────────────────────────────────────────────
50
+ function makeCallStatus() {
51
+ return { code: 0, error_buf: { capacity: 0n, len: 0n, data: null } };
52
+ }
53
+ function checkCallStatus(ffi, status) {
54
+ if (status.code === 0)
55
+ return;
56
+ if (status.code === 1) {
57
+ const errMsg = liftError(status.error_buf);
58
+ freeRustBuffer(ffi, status.error_buf);
59
+ throw new Error(errMsg);
60
+ }
61
+ if (status.error_buf.len > 0n) {
62
+ const msg = liftString(status.error_buf);
63
+ freeRustBuffer(ffi, status.error_buf);
64
+ throw new Error(`Rust panic: ${msg}`);
65
+ }
66
+ throw new Error("Unknown Rust panic");
67
+ }
68
+ function liftError(buf) {
69
+ if (!buf.data || buf.len === 0n)
70
+ return "Unknown error";
71
+ const raw = Buffer.from(koffi_1.default.decode(buf.data, "uint8", Number(buf.len)));
72
+ let offset = 0;
73
+ // UniFFI Error layout: [i32 variant] + [i32 len] + [bytes]
74
+ const variant = raw.readInt32BE(offset);
75
+ offset += 4;
76
+ const variantNames = {
77
+ 1: "DecodeError",
78
+ 2: "MissingMetadata",
79
+ 3: "MetadataParseError",
80
+ 4: "HandlerError",
81
+ 5: "NoConnectorRequest",
82
+ };
83
+ if (variant === 5)
84
+ return "NoConnectorRequest";
85
+ const strLen = raw.readInt32BE(offset);
86
+ offset += 4;
87
+ const msg = raw.subarray(offset, offset + strLen).toString("utf-8");
88
+ return `${variantNames[variant] || "UniffiError"}: ${msg}`;
89
+ }
90
+ /**
91
+ * UniFFI Strings are serialized as raw UTF8 bytes when top-level in RustBuffer.
92
+ */
93
+ function liftString(buf) {
94
+ if (!buf.data || buf.len === 0n)
95
+ return "";
96
+ const raw = Buffer.from(koffi_1.default.decode(buf.data, "uint8", Number(buf.len)));
97
+ return raw.toString("utf-8");
98
+ }
99
+ /**
100
+ * UniFFI Vec<u8> (Bytes) as return values are serialized as [i32 length] + [raw bytes]
101
+ */
102
+ function liftBytes(buf) {
103
+ if (!buf.data || buf.len === 0n)
104
+ return Buffer.alloc(0);
105
+ const raw = Buffer.from(koffi_1.default.decode(buf.data, "uint8", Number(buf.len)));
106
+ // UniFFI protocol for return values: first 4 bytes are the length of the actual payload
107
+ const len = raw.readInt32BE(0);
108
+ return raw.subarray(4, 4 + len);
109
+ }
110
+ function freeRustBuffer(ffi, buf) {
111
+ if (buf.data && buf.len > 0n) {
112
+ ffi.free(buf, makeCallStatus());
113
+ }
114
+ }
115
+ function allocRustBuffer(ffi, data) {
116
+ const status = makeCallStatus();
117
+ const buf = ffi.alloc(BigInt(data.length), status);
118
+ checkCallStatus(ffi, status);
119
+ koffi_1.default.encode(buf.data, "uint8", Array.from(data), data.length);
120
+ buf.len = BigInt(data.length);
121
+ return buf;
122
+ }
123
+ /**
124
+ * Lowers raw bytes into a UniFFI-compliant buffer for top-level arguments.
125
+ * Protocol: [i32 length prefix] + [raw bytes]
126
+ */
127
+ function lowerBytes(ffi, data) {
128
+ const buf = Buffer.alloc(4 + data.length);
129
+ buf.writeInt32BE(data.length, 0);
130
+ Buffer.from(data).copy(buf, 4);
131
+ return allocRustBuffer(ffi, buf);
132
+ }
133
+ /**
134
+ * Lowers a Map into a UniFFI-compliant serialized buffer.
135
+ * Protocol: [i32 count] + [ [i32 key_len]+[key_bytes] + [i32 val_len]+[val_bytes] ] * count
136
+ */
137
+ function lowerMap(ffi, map) {
138
+ const entries = Object.entries(map);
139
+ let totalSize = 4; // count
140
+ const encoded = entries.map(([k, v]) => {
141
+ const kBuf = Buffer.from(k, "utf-8");
142
+ const vBuf = Buffer.from(v, "utf-8");
143
+ totalSize += 4 + kBuf.length + 4 + vBuf.length;
144
+ return { kBuf, vBuf };
145
+ });
146
+ const buf = Buffer.alloc(totalSize);
147
+ let offset = 0;
148
+ buf.writeInt32BE(entries.length, offset);
149
+ offset += 4;
150
+ for (const { kBuf, vBuf } of encoded) {
151
+ buf.writeInt32BE(kBuf.length, offset);
152
+ offset += 4;
153
+ kBuf.copy(buf, offset);
154
+ offset += kBuf.length;
155
+ buf.writeInt32BE(vBuf.length, offset);
156
+ offset += 4;
157
+ vBuf.copy(buf, offset);
158
+ offset += vBuf.length;
159
+ }
160
+ return allocRustBuffer(ffi, buf);
161
+ }
162
+ class UniffiClient {
163
+ _ffi;
164
+ constructor(libPath) {
165
+ this._ffi = loadLib(libPath);
166
+ }
167
+ /**
168
+ * Build the connector HTTP request for any flow.
169
+ * Returns protobuf-encoded FfiConnectorHttpRequest bytes.
170
+ */
171
+ callReq(flow, requestBytes, metadata, optionsBytes) {
172
+ const fn = this._ffi[`${flow}_req`];
173
+ if (!fn)
174
+ throw new Error(`Unknown flow: '${flow}'. Supported: ${FLOW_NAMES.join(", ")}`);
175
+ const rbReq = lowerBytes(this._ffi, requestBytes);
176
+ const rbMeta = lowerMap(this._ffi, metadata);
177
+ const rbOpts = lowerBytes(this._ffi, optionsBytes);
178
+ const status = makeCallStatus();
179
+ const result = fn(rbReq, rbMeta, rbOpts, status);
180
+ try {
181
+ checkCallStatus(this._ffi, status);
182
+ return liftBytes(result);
183
+ }
184
+ finally {
185
+ freeRustBuffer(this._ffi, result);
186
+ }
187
+ }
188
+ /**
189
+ * Parse the connector HTTP response for any flow.
190
+ * responseBytes: protobuf-encoded FfiConnectorHttpResponse.
191
+ * Returns protobuf-encoded response bytes for the flow's response type.
192
+ */
193
+ callRes(flow, responseBytes, requestBytes, metadata, optionsBytes) {
194
+ const fn = this._ffi[`${flow}_res`];
195
+ if (!fn)
196
+ throw new Error(`Unknown flow: '${flow}'. Supported: ${FLOW_NAMES.join(", ")}`);
197
+ const rbRes = lowerBytes(this._ffi, responseBytes);
198
+ const rbReq = lowerBytes(this._ffi, requestBytes);
199
+ const rbMeta = lowerMap(this._ffi, metadata);
200
+ const rbOpts = lowerBytes(this._ffi, optionsBytes);
201
+ const status = makeCallStatus();
202
+ const result = fn(rbRes, rbReq, rbMeta, rbOpts, status);
203
+ try {
204
+ checkCallStatus(this._ffi, status);
205
+ return liftBytes(result);
206
+ }
207
+ finally {
208
+ freeRustBuffer(this._ffi, result);
209
+ }
210
+ }
211
+ }
212
+ exports.UniffiClient = UniffiClient;
213
+ //# sourceMappingURL=uniffi_client.js.map
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "hs-playlib",
3
+ "version": "0.1.0",
4
+ "description": "Hyperswitch Payments SDK — Node.js client for connector integrations via UniFFI FFI",
5
+ "author": "Juspay <hyperswitch@juspay.in>",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/juspay/connector-service.git",
10
+ "directory": "sdk/javascript"
11
+ },
12
+ "keywords": ["payments", "hyperswitch", "juspay", "ffi"],
13
+ "bugs": {
14
+ "url": "https://github.com/juspay/connector-service/issues"
15
+ },
16
+ "homepage": "https://github.com/juspay/connector-service#readme",
17
+ "main": "dist/src/index.js",
18
+ "types": "dist/src/index.d.ts",
19
+ "files": [
20
+ "dist/src/**/*.js",
21
+ "dist/src/**/*.d.ts",
22
+ "dist/src/payments/generated/*"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc && cp -r src/payments/generated dist/src/payments/ && cp src/payments/_generated_flows.js dist/src/payments/",
26
+ "start": "node dist/src/index.js",
27
+ "test": "node test_smoke.js"
28
+ },
29
+ "dependencies": {
30
+ "koffi": "^2.15.1",
31
+ "protobufjs": "^7.4.0",
32
+ "undici": "^6.23.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^25.3.3",
36
+ "protobufjs-cli": "^1.1.3",
37
+ "ts-node": "^10.9.2",
38
+ "tsconfig-paths": "^4.2.0",
39
+ "typescript": "^5.9.3"
40
+ }
41
+ }