Skip to content

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

Open

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

Activity

  1. AnneAlbert-wt commented on Jul 31, 2026

    @AnneAlbert-wt

    Confirming the provisioning failure from the "Why we cannot just build it ourselves" section, from a second app — with one detail that makes it more urgent: it now reproduces from a plain dependency install, no self-building involved.

    5.3.4 was published to npm on 2026-07-30 with no corresponding release here (latest tag is still 5.3.3). Diffing the two tarballs, the only files that differ are package.json and the two .node binaries — the JS, the type definitions and Setapp.xcframework are byte-identical. The binding was rebuilt:

    npm package binding __scv__ SDK balances symbols
    5.3.3 5.2.1 26.0 0
    5.3.4 5.3.3 26.5 24

    That matches the two rows in your table exactly — published-and-working, and built-from-source-and-failing — on both the version stamp and the SDK.

    Our results, same machine, same application code, same signing identity, only the dependency moved:

    binding in the app result
    5.3.3 (__scv__5.2.1) works
    5.3.4 (__scv__5.3.3) "This copy of ... requires an active Setapp account installed on your Mac"

    Environment: Electron 43, universal (arm64 + x86_64), Developer ID signed and notarized, hardened runtime, distributed through Setapp.

    The practical impact: the range in the integration docs is a caret range, so any integrator who reinstalls dependencies after 2026-07-30 silently picks up the rebuilt binding and their app stops provisioning — with no version change on their side. We found it only because a routine release build broke. Pinning to exactly 5.3.3 restores the stale binding and works around it.

    So the two halves of this issue appear to trade off against each other: the binding that carries the correct library is the one that fails the handshake. It would help to know whether the handshake side is understood, because at the moment there is no published version that has both.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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