From 80dd5fdaa9000a32ba2247682de8f3d69deef611 Mon Sep 17 00:00:00 2001 From: Utkal Singh Date: Sat, 1 Aug 2026 17:01:41 +0000 Subject: [PATCH 1/3] fix(sdk): select gRPC native library by platform and arch The JavaScript SDK's UniFFI loader resolves natives from generated/-/, but the gRPC loader still selected on process.platform alone and read a flat generated/ path, so it had no way to pick between architectures. Route both loaders through one resolver and stage the gRPC native under the same per-arch directory. When no matching binary is bundled, the resolver now reports the target it looked for and the targets the package actually ships, instead of letting dlopen fail with a "wrong ELF class" message that does not say which architecture was expected. --- .../templates/javascript/grpc_client.ts.j2 | 5 ++- sdk/javascript/Makefile | 11 ++--- .../src/payments/_generated_grpc_client.ts | 5 ++- sdk/javascript/src/payments/native_lib.ts | 43 +++++++++++++++++++ sdk/javascript/src/payments/uniffi_client.ts | 9 ++-- 5 files changed, 58 insertions(+), 15 deletions(-) create mode 100644 sdk/javascript/src/payments/native_lib.ts diff --git a/scripts/generators/code/templates/javascript/grpc_client.ts.j2 b/scripts/generators/code/templates/javascript/grpc_client.ts.j2 index ac3010af5f..2e81f0d6e1 100644 --- a/scripts/generators/code/templates/javascript/grpc_client.ts.j2 +++ b/scripts/generators/code/templates/javascript/grpc_client.ts.j2 @@ -5,6 +5,7 @@ import koffi from "koffi"; import path from "path"; // @ts-ignore - generated CommonJS module import { types } from "./generated/proto.js"; +import { resolveNativeLib } from "./native_lib"; // Standard Node.js __dirname declare const __dirname: string; @@ -43,8 +44,8 @@ interface GrpcFfi { function loadGrpcFfi(libPath?: string): GrpcFfi { if (!libPath) { - const ext = process.platform === "darwin" ? "dylib" : "so"; - libPath = path.join(_dirname, "generated", `libhyperswitch_grpc_ffi.${ext}`); + // Bundled per platform under generated/-/; see native_lib.ts. + libPath = resolveNativeLib(path.join(_dirname, "generated"), "libhyperswitch_grpc_ffi"); } const lib = koffi.load(libPath); diff --git a/sdk/javascript/Makefile b/sdk/javascript/Makefile index 93c1420046..15dec2dc76 100644 --- a/sdk/javascript/Makefile +++ b/sdk/javascript/Makefile @@ -89,16 +89,17 @@ generate-proto: install-deps # --------------------------------------------------------------------------- # generate-grpc-bindings # Copies the pre-built gRPC FFI native library into the generated output -# directory so it is bundled with the npm package. +# directory so it is bundled with the npm package. Staged under +# generated/-/ to match how the loader resolves it. # Requires: build-grpc-ffi-lib to have been run first. # --------------------------------------------------------------------------- generate-grpc-bindings: @[ -f "$(GRPC_FFI_LIBRARY)" ] || \ (echo "Error: gRPC FFI library not found at $(GRPC_FFI_LIBRARY). Run 'make build-grpc-ffi-lib' first." && exit 1) - @echo "Copying gRPC FFI library to $(GENERATED_OUT)/..." - @mkdir -p $(GENERATED_OUT) - @cp -f $(GRPC_FFI_LIBRARY) $(GENERATED_OUT)/ - @echo "gRPC library copied to $(GENERATED_OUT)/" + @echo "Copying gRPC FFI library to $(GENERATED_OUT)/$(NODE_TARGET)/..." + @mkdir -p $(GENERATED_OUT)/$(NODE_TARGET) + @cp -f $(GRPC_FFI_LIBRARY) $(GENERATED_OUT)/$(NODE_TARGET)/ + @echo "gRPC library copied to $(GENERATED_OUT)/$(NODE_TARGET)/" # --------------------------------------------------------------------------- # generate-all diff --git a/sdk/javascript/src/payments/_generated_grpc_client.ts b/sdk/javascript/src/payments/_generated_grpc_client.ts index 1f85fda32c..5e0a59dabe 100644 --- a/sdk/javascript/src/payments/_generated_grpc_client.ts +++ b/sdk/javascript/src/payments/_generated_grpc_client.ts @@ -5,6 +5,7 @@ import koffi from "koffi"; import path from "path"; // @ts-ignore - generated CommonJS module import { types } from "./generated/proto.js"; +import { resolveNativeLib } from "./native_lib"; // Standard Node.js __dirname declare const __dirname: string; @@ -43,8 +44,8 @@ interface GrpcFfi { function loadGrpcFfi(libPath?: string): GrpcFfi { if (!libPath) { - const ext = process.platform === "darwin" ? "dylib" : "so"; - libPath = path.join(_dirname, "generated", `libhyperswitch_grpc_ffi.${ext}`); + // Bundled per platform under generated/-/; see native_lib.ts. + libPath = resolveNativeLib(path.join(_dirname, "generated"), "libhyperswitch_grpc_ffi"); } const lib = koffi.load(libPath); diff --git a/sdk/javascript/src/payments/native_lib.ts b/sdk/javascript/src/payments/native_lib.ts new file mode 100644 index 0000000000..8684f652d7 --- /dev/null +++ b/sdk/javascript/src/payments/native_lib.ts @@ -0,0 +1,43 @@ +/** + * Locates the native libraries bundled with this package. + * + * Natives are staged under generated/-/ (matching Node's + * process.platform + process.arch — e.g. linux-x64, linux-arm64, darwin-arm64) + * so a single package serves every supported runtime. Selecting on platform + * alone picks the x86-64 binary on aarch64 hosts, so both parts are needed. + */ + +import fs from "fs"; +import path from "path"; + +/** + * Absolute path to `libName` for the platform and architecture we are running on. + * + * When nothing matches, reports the target we looked for and the ones this + * package does carry — otherwise the failure surfaces from dlopen as a "wrong + * ELF class" message that never names the architecture it expected. + */ +export function resolveNativeLib(generatedDir: string, libName: string): string { + const target = `${process.platform}-${process.arch}`; + const ext = process.platform === "darwin" ? "dylib" : "so"; + const libPath = path.join(generatedDir, target, `${libName}.${ext}`); + if (fs.existsSync(libPath)) return libPath; + + let bundled: string[] = []; + try { + bundled = fs + .readdirSync(generatedDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + } catch { + // generated/ absent entirely; reported as "bundles no native libraries" below. + } + + throw new Error( + `hyperswitch-prism: ${libName} is not bundled for ${target} (expected ${libPath}). ` + + (bundled.length > 0 + ? `This package bundles: ${bundled.join(", ")}.` + : "This package bundles no native libraries.") + ); +} diff --git a/sdk/javascript/src/payments/uniffi_client.ts b/sdk/javascript/src/payments/uniffi_client.ts index bd8fd9a0c0..193997a00c 100644 --- a/sdk/javascript/src/payments/uniffi_client.ts +++ b/sdk/javascript/src/payments/uniffi_client.ts @@ -16,6 +16,7 @@ import { FLOWS, SINGLE_FLOWS } from "./_generated_flows.js"; // @ts-ignore - generated protobuf types import { types } from "./generated/proto.js"; import { IntegrationError, ConnectorError } from "./errors"; +import { resolveNativeLib } from "./native_lib"; // Standard Node.js __dirname declare const __dirname: string; @@ -60,12 +61,8 @@ interface FfiFunctions { function loadLib(libPath?: string): FfiFunctions { if (!libPath) { - const ext = process.platform === "darwin" ? "dylib" : "so"; - // Native libs are bundled per platform under generated/-/ - // (e.g. linux-x64, linux-arm64, darwin-arm64) so one package serves every - // architecture; select the one matching this runtime. - const target = `${process.platform}-${process.arch}`; - libPath = path.join(_dirname, "generated", target, `libconnector_service_ffi.${ext}`); + // Bundled per platform under generated/-/; see native_lib.ts. + libPath = resolveNativeLib(path.join(_dirname, "generated"), "libconnector_service_ffi"); } const lib = koffi.load(libPath); From c3545d751ed16b1f5787d56eb2a28bd8b33dd915 Mon Sep 17 00:00:00 2001 From: Utkal Singh Date: Sat, 1 Aug 2026 17:24:19 +0000 Subject: [PATCH 2/3] fix(sdk): name the Windows library extension in native lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK requirements list Windows (x64), but the extension mapping only distinguished darwin from everything else, so a win32 runtime reported a .so path it would never have loaded. Map win32 to .dll so the "not bundled" diagnostic names the file the platform actually uses. No behaviour change on Linux or macOS, and no Windows native is built today — this only affects which filename the error reports. --- sdk/javascript/src/payments/native_lib.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sdk/javascript/src/payments/native_lib.ts b/sdk/javascript/src/payments/native_lib.ts index 8684f652d7..381967a006 100644 --- a/sdk/javascript/src/payments/native_lib.ts +++ b/sdk/javascript/src/payments/native_lib.ts @@ -10,6 +10,12 @@ import fs from "fs"; import path from "path"; +/** Shared library extension per platform; anything else follows ELF naming. */ +const LIB_EXTENSION: Record = { + darwin: "dylib", + win32: "dll", +}; + /** * Absolute path to `libName` for the platform and architecture we are running on. * @@ -19,7 +25,7 @@ import path from "path"; */ export function resolveNativeLib(generatedDir: string, libName: string): string { const target = `${process.platform}-${process.arch}`; - const ext = process.platform === "darwin" ? "dylib" : "so"; + const ext = LIB_EXTENSION[process.platform] ?? "so"; const libPath = path.join(generatedDir, target, `${libName}.${ext}`); if (fs.existsSync(libPath)) return libPath; From fc83e72d579fc922493479aaa16835723ab57ae0 Mon Sep 17 00:00:00 2001 From: Utkal Singh Date: Sun, 2 Aug 2026 11:11:05 +0000 Subject: [PATCH 3/3] fix(sdk): bundle the gRPC native for every published target dist staged only libconnector_service_ffi, so the gRPC client resolved a path no published tarball ever contained. Stage both natives per target via JS_NATIVE_LIBS. The release workflow built only libconnector_service_ffi, so staging the gRPC native without touching it would have tripped the existing fail-closed check and aborted every release. Build hyperswitch-grpc-ffi and upload libhyperswitch_grpc_ffi.* alongside it. package.json needs no change: npm-packlist includes a matched directory recursively, so the existing generated/* entry already ships the per-arch subdirectories. Verified against a real dist tarball. --- .github/workflows/release-sdks.yml | 7 +++++++ sdk/javascript/Makefile | 22 +++++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-sdks.yml b/.github/workflows/release-sdks.yml index f0fd6a383a..7d78f9e2f2 100644 --- a/.github/workflows/release-sdks.yml +++ b/.github/workflows/release-sdks.yml @@ -206,12 +206,19 @@ jobs: - name: Build binary (${{ matrix.target }}) run: make -C sdk/java build-ffi-lib PROFILE=release + # The JavaScript package bundles this alongside libconnector_service_ffi — + # its gRPC client loads it from generated/-/ at runtime, so + # `make -C sdk/javascript dist` needs it staged for every target here. + - name: Build gRPC FFI binary (${{ matrix.target }}) + run: make -C sdk/java build-grpc-ffi-lib PROFILE=release + - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: binary-${{ matrix.target }} path: | target/${{ matrix.target }}/release/libconnector_service_ffi.* + target/${{ matrix.target }}/release/libhyperswitch_grpc_ffi.* - name: Verify FFI library has UniFFI scaffolding if: matrix.target == 'aarch64-apple-darwin' diff --git a/sdk/javascript/Makefile b/sdk/javascript/Makefile index 15dec2dc76..40bc488c5a 100644 --- a/sdk/javascript/Makefile +++ b/sdk/javascript/Makefile @@ -164,6 +164,12 @@ JS_PLATFORMS := \ aarch64-unknown-linux-gnu:linux-arm64 \ aarch64-apple-darwin:darwin-arm64 +# Native libraries bundled with the package. Both are resolved at runtime by +# resolveNativeLib() in src/payments/native_lib.ts — libconnector_service_ffi by +# the UniFFI client, libhyperswitch_grpc_ffi by the gRPC client — so both have to +# be staged for every target, or the tarball loads one and throws on the other. +JS_NATIVE_LIBS := libconnector_service_ffi libhyperswitch_grpc_ffi + # node platform-arch dir for the CURRENT build platform (used by generate-bindings). NODE_TARGET := $(strip \ $(if $(filter x86_64-unknown-linux-gnu,$(PLATFORM)),linux-x64,\ @@ -175,7 +181,7 @@ NODE_TARGET := $(strip \ dist: @echo "Building JavaScript SDK distribution package..." @mkdir -p $(GENERATED_OUT) - @# The published tarball bundles each platform's native lib under + @# The published tarball bundles each platform's native libs under @# generated/-/ so a single package works on every platform @# and the loader selects by process.arch. Fail closed if a required binary @# is missing unless ALLOW_PARTIAL_DIST=1 (deliberate single-platform build; @@ -184,12 +190,14 @@ dist: for entry in $(JS_PLATFORMS); do \ triple=$${entry%%:*}; target=$${entry##*:}; \ ext=so; case "$$triple" in *apple-darwin) ext=dylib;; esac; \ - lib="$(REPO_ROOT)/target/$$triple/release/libconnector_service_ffi.$$ext"; \ - if [ ! -f "$$lib" ]; then echo " missing $$triple: $$lib"; missing=1; continue; fi; \ - mkdir -p "$(GENERATED_OUT)/$$target"; \ - cp -f "$$lib" "$(GENERATED_OUT)/$$target/"; \ - echo " staged $$target/libconnector_service_ffi.$$ext"; \ - built=$$((built+1)); \ + for name in $(JS_NATIVE_LIBS); do \ + lib="$(REPO_ROOT)/target/$$triple/release/$$name.$$ext"; \ + if [ ! -f "$$lib" ]; then echo " missing $$triple: $$lib"; missing=1; continue; fi; \ + mkdir -p "$(GENERATED_OUT)/$$target"; \ + cp -f "$$lib" "$(GENERATED_OUT)/$$target/"; \ + echo " staged $$target/$$name.$$ext"; \ + built=$$((built+1)); \ + done; \ done; \ if [ "$$missing" = "1" ]; then \ if [ "$(ALLOW_PARTIAL_DIST)" = "1" ]; then \