Skip to content

npm package ships an N-API binding built against an older Setapp library (5.3.3 ships 5.2.1) #68

Description

@iampawan

Summary

Every published @setapp/framework-wrapper npm package ships a prebuilt N-API binding compiled against an older Setapp library than the package it is in. 5.3.3 ships a binding built against 5.2.1.

Because the binding statically embeds the Swift library, the embedded version is the library that runs. So an Electron app on @setapp/framework-wrapper@5.3.3 runs Setapp library 5.2.1, and cannot use anything added in 5.3.0–5.3.3.

This is reproducible from your repository alone, with no third-party code involved.

Evidence

Each Mach-O the framework produces carries an embedded __scv__<version> stamp. Comparing the two binaries inside a single published package, pulled straight from npm:

package Setapp.xcframework/…/libSetapp.a published N-API binding binding sha256
4.3.4 4.3.4 4.3.1 0fb32263502d…
4.3.5 4.3.5 4.3.4 9685e5662d00…
5.0.0 5.0.0 4.3.4 9685e5662d00…
5.0.1 5.0.0 4.3.3 cd6084928c16…
5.1.0 5.1.0 5.1.0 d79de25cd5b5…
5.2.0 5.2.0 5.1.0 d79de25cd5b5…
5.2.1 5.2.1 5.1.0 d79de25cd5b5…
5.3.0 5.3.0 5.2.1 0526114f9eb1…
5.3.1 5.3.1 5.2.1 0526114f9eb1…
5.3.2 5.3.2 5.2.1 0526114f9eb1…
5.3.3 5.3.3 5.2.1 0526114f9eb1…

The binding has been byte-identical across 5.3.0 → 5.3.3. 5.1.0 appears to be the only release where it matched.

The libSetapp.a in the package cannot compensate, because the binding does not link it dynamically:

$ nm -gU node_setapp_binding.node | grep -ci setapp     # 6525  (Swift symbols embedded)
$ otool -L node_setapp_binding.node | grep -ci libSetapp  # 0   (no dynamic reference)

And only that one copy can ever load — main is ./nodejs/dist/lib/setapp.js, and nodejs/dist/lib/setapp-binding.js:10 does a plain relative require with no search path:

exports.binding = binding = require('./binding/node_setapp_binding.node');

Concrete consequence: 5.3.x APIs are missing

The 5.3.3 changelog says:

New — [SetappAI] Added a new credits API — setappAI.credits.balances().

Symbol counts, published binding vs one built from this repo at the 5.3.3 tag:

symbol published binding built from source
credits 8 36
balances 0 12 (incl. CreditBalancesResponse)

The same applies to 5.3.0's text-to-video and 5.3.2's model context-window info. None of it is reachable from Electron.

Root cause

The publish chain never rebuilds the binding. From package.json:

"prebuild":        "rimraf ./nodejs/dist",
"build":           "tsc",
"postbuild":       "mkdir ./nodejs/dist/lib/binding && cp ./nodejs/lib/binding/node_setapp_binding.node ./nodejs/dist/lib/binding/node_setapp_binding.node",
"prepublishOnly":  "npm run build"

build is tsc only — it never invokes node-gyp. postbuild then copies whatever .node happens to be in nodejs/lib/binding/, and that directory is gitignored, so it is untracked build output on the publishing machine.

Running the chain on a clean checkout produces the correct binding:

git clone --depth 1 --branch 5.3.3 https://github.com/MacPaw/Setapp-framework.git
cd Setapp-framework && npm install && npm run build && npm pack
# dist binding: __scv__5.3.3   (published 5.3.3 tarball: __scv__5.2.1)

So the release process itself works. The artifact on npm was not produced by it on a clean tree.

The documented rebuild cannot work around it

Integration with an Electron app instructs:

"postinstall": "electron-builder install-app-deps"

That rebuild writes to nodejs/lib/binding, because binding.gyp declares both copy destinations as the same path:

"module_bindnings_path":    "nodejs/lib/binding",
"path_for_nodejs_package":  "nodejs/lib/binding",

main loads nodejs/dist/lib/binding. Only postbuild bridges them, and it runs from npm run build / prepublishOnly, never from npm install.

So the documented rebuild step is a no-op for the binary that actually loads. Following the guide exactly still ships the stale binding.

(Separately: that page still specifies "@setapp/framework-wrapper": "^3.1.2".)

Why we cannot just build it ourselves

We tried, and this is worth flagging because it may indicate a second issue.

A binding built from this repo at the 5.3.3 tag, via npm install && npm run build, carries the correct library — but a signed and notarized app using it does not complete the provisioning handshake. It shows:

This copy of LazyReel requires an active Setapp account installed on your Mac.

Verified on the same Mac, minutes apart, with SetappAgent -xpcServer running and Setapp client 3.54.0:

binding in the app result
published (__scv__5.2.1) works
built from source (__scv__5.3.3) dialog above

The failing build passed codesign --verify --deep --strict and spctl -a -t exec (accepted, Notarized Developer ID), so signing is not the explanation. Both binaries are platform 1, minos 12.0, differing only in SDK (26.0 vs 26.5).

We do not know why yours connects and ours does not — but it means integrators cannot self-serve a current library, so the fix has to come from the published package.

Suggested fix

Either make build rebuild the binding before postbuild copies it, or have postbuild fail when the copied binary's __scv__ stamp does not match the package version. The second is two lines and would have caught every row in the table above.

Environment

  • macOS 26.5, Apple Silicon
  • Electron 41, Node 24
  • @setapp/framework-wrapper 5.2.0 → 5.3.3 inspected; app shipping 5.3.3

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions