Skip to content

Commit 29ae22c

Browse files
committed
WIP: Prebuild (needs to be 2nd publish, after setup)
1 parent 1305eab commit 29ae22c

6 files changed

Lines changed: 728 additions & 25 deletions

File tree

.github/workflows/ci.yml

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ name: CI
33
on:
44
push:
55
pull_request:
6+
workflow_dispatch:
67
schedule:
78
- cron: '0 6 * * 1' # Weekly test to check new Node versions
89

910
jobs:
10-
build-and-test:
11+
test:
1112
runs-on: ubuntu-latest
1213

1314
strategy:
@@ -26,11 +27,109 @@ jobs:
2627
cache: npm
2728
check-latest: true
2829

29-
- name: Node version
30-
run: node --version
31-
- name: OpenSSL version
32-
run: node -e "console.log('OpenSSL', process.versions.openssl)"
30+
- run: node -e "console.log('Node', process.version, '| OpenSSL', process.versions.openssl)"
3331

34-
- run: npm ci
35-
- run: npm run build
32+
- run: npm ci --ignore-scripts
33+
34+
# Build the shipped artifact (a --napi prebuild) and prove it loads and
35+
# works before running the suite against it.
36+
- run: npm run prebuild
37+
- run: npm run test:smoke
38+
39+
- run: npm run build:ts
3640
- run: npm test
41+
42+
prebuild:
43+
name: prebuild (${{ matrix.name }})
44+
runs-on: ${{ matrix.runs-on }}
45+
container: ${{ matrix.container }}
46+
strategy:
47+
fail-fast: false
48+
matrix:
49+
include:
50+
- { name: linux-x64-glibc, runs-on: ubuntu-latest }
51+
- { name: linux-x64-musl, runs-on: ubuntu-latest, container: node:24-alpine }
52+
- { name: linux-arm64-glibc, runs-on: ubuntu-24.04-arm }
53+
- { name: linux-arm64-musl, runs-on: ubuntu-24.04-arm, container: node:24-alpine }
54+
steps:
55+
# Alpine has no toolchain or git; install before checkout, which needs git.
56+
- if: ${{ matrix.container }}
57+
run: apk add --no-cache build-base python3 git tar
58+
- uses: actions/checkout@v4
59+
- uses: actions/setup-node@v4
60+
if: ${{ !matrix.container }}
61+
with:
62+
node-version: 24
63+
- run: npm ci --ignore-scripts
64+
- run: npm run prebuild
65+
- run: npm run test:smoke
66+
- run: tar -czf prebuilds-${{ matrix.name }}.tar.gz prebuilds
67+
- uses: actions/upload-artifact@v4
68+
with:
69+
name: prebuilds-${{ matrix.name }}
70+
path: prebuilds-${{ matrix.name }}.tar.gz
71+
72+
# Prove the single linux-x64 prebuild loads across the whole supported Node
73+
# range (24.15+), not just the Node it was built with.
74+
smoke-across-node:
75+
name: smoke (node ${{ matrix.node-version }}, reuse prebuild)
76+
needs: prebuild
77+
runs-on: ubuntu-latest
78+
strategy:
79+
fail-fast: false
80+
matrix:
81+
node-version: [24, 26, latest]
82+
steps:
83+
- uses: actions/checkout@v4
84+
- uses: actions/setup-node@v4
85+
with:
86+
node-version: ${{ matrix.node-version }}
87+
- run: npm ci --ignore-scripts
88+
- uses: actions/download-artifact@v4
89+
with:
90+
name: prebuilds-linux-x64-glibc
91+
- run: tar -xzf prebuilds-linux-x64-glibc.tar.gz
92+
- run: npm run test:smoke
93+
94+
publish:
95+
name: Publish to npm
96+
needs: [test, prebuild, smoke-across-node]
97+
if: startsWith(github.ref, 'refs/tags/v')
98+
runs-on: ubuntu-latest
99+
environment:
100+
name: npm
101+
url: https://www.npmjs.com/package/tls-impersonate
102+
permissions:
103+
contents: read
104+
id-token: write
105+
steps:
106+
- uses: actions/checkout@v4
107+
- uses: actions/setup-node@v4
108+
with:
109+
node-version: 24
110+
registry-url: 'https://registry.npmjs.org'
111+
112+
# Trusted publishing (tokenless OIDC) needs npm >= 11.5.1.
113+
- run: npm install -g npm@latest
114+
115+
- run: npm ci --ignore-scripts
116+
117+
- uses: actions/download-artifact@v4
118+
with:
119+
pattern: prebuilds-*
120+
path: artifacts
121+
merge-multiple: true
122+
- run: for f in artifacts/*.tar.gz; do tar -xzf "$f"; done
123+
- run: ls -R prebuilds
124+
125+
- name: Verify tag matches package.json version
126+
run: |
127+
TAG_VERSION=${GITHUB_REF#refs/tags/v}
128+
PKG_VERSION=$(node -p "require('./package.json').version")
129+
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
130+
echo "Tag v$TAG_VERSION does not match package.json version $PKG_VERSION"
131+
exit 1
132+
fi
133+
echo "Publishing $PKG_VERSION"
134+
135+
- run: npm publish --provenance --access public

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ dist/
33
*.tsbuildinfo
44
*.tgz
55
build/
6+
prebuilds/

native/binding.cc

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,38 @@
33
//
44
// Registered as a NAPI module, so a single prebuilt binary loads across all
55
// Node ABIs (no NODE_MODULE_VERSION gate). The one Node-internal dependency,
6-
// node::crypto::GetSSLCtx, is resolved with dlsym and passed V8 handles bridged
7-
// from napi_value; both the bridge and that symbol are ABI-stable, so the same
8-
// binary works on any Node >= 24.15.0 that exports GetSSLCtx.
6+
// node::crypto::GetSSLCtx, is declared and linked directly (weak on GCC/Clang, so
7+
// its address is null and we throw cleanly on a Node that lacks it); V8 handles
8+
// are bridged from napi_value. Both the bridge and that symbol are ABI-stable and
9+
// the mangled name depends only on the signature, so one binary works on any Node
10+
// >= 24.15.0 that exports GetSSLCtx - no per-version build.
911

1012
#include <node_api.h>
1113
#include <v8.h>
1214
#include <openssl/ssl.h>
1315
#include <openssl/tls1.h>
14-
#include <dlfcn.h>
1516
#include <cmath>
1617
#include <cstring>
1718
#include <vector>
1819

20+
// node::crypto::GetSSLCtx (a NODE_EXTERN since Node 24.15) unwraps a SecureContext
21+
// to its SSL_CTX. We declare the prototype ourselves so the compiler emits the
22+
// correctly mangled reference on every platform - no hardcoded symbol string.
23+
// Marked weak where the toolchain supports it, so on a Node that does not export
24+
// it the address is null and we throw cleanly rather than failing to load.
25+
#ifdef _WIN32
26+
#define TLS_IMPERSONATE_WEAK
27+
#else
28+
#define TLS_IMPERSONATE_WEAK __attribute__((weak))
29+
#endif
30+
31+
namespace node {
32+
namespace crypto {
33+
TLS_IMPERSONATE_WEAK SSL_CTX* GetSSLCtx(v8::Local<v8::Context> context,
34+
v8::Local<v8::Value> value);
35+
} // namespace crypto
36+
} // namespace node
37+
1938
namespace {
2039

2140
// ─── napi_value <-> v8::Local bridge ─────────────────────────────────────────
@@ -33,29 +52,20 @@ static v8::Local<v8::Value> V8LocalFromNapi(napi_value v) {
3352

3453
// ─── SSL_CTX resolution ──────────────────────────────────────────────────────
3554

36-
using GetSSLCtxFunc = SSL_CTX* (*)(v8::Local<v8::Context>, v8::Local<v8::Value>);
37-
38-
static GetSSLCtxFunc ResolveGetSSLCtx() {
39-
// Mangled: node::crypto::GetSSLCtx(v8::Local<v8::Context>, v8::Local<v8::Value>)
40-
return reinterpret_cast<GetSSLCtxFunc>(
41-
dlsym(RTLD_DEFAULT,
42-
"_ZN4node6crypto9GetSSLCtxEN2v85LocalINS1_7ContextEEENS2_INS1_5ValueEEE"));
43-
}
44-
4555
// Resolve the SSL_CTX* behind a SecureContext value, throwing a JS exception
4656
// (and returning nullptr) if the runtime is unsupported or the value is invalid.
47-
// GetSSLCtx unwraps the outer tls.createSecureContext() wrapper itself.
57+
// node::crypto::GetSSLCtx unwraps the outer tls.createSecureContext() wrapper.
4858
static SSL_CTX* GetSSLCtx(napi_env env, napi_value secure_context) {
49-
static GetSSLCtxFunc fn = ResolveGetSSLCtx();
50-
if (fn == nullptr) {
59+
auto* get_ssl_ctx = node::crypto::GetSSLCtx; // null iff Node lacks the export
60+
if (get_ssl_ctx == nullptr) {
5161
napi_throw_error(env, nullptr,
5262
"tls-impersonate requires Node.js >= 24.15.0 "
5363
"(node::crypto::GetSSLCtx is unavailable in this runtime)");
5464
return nullptr;
5565
}
5666
v8::Isolate* isolate = v8::Isolate::GetCurrent();
5767
v8::Local<v8::Context> context = isolate->GetCurrentContext();
58-
SSL_CTX* ssl_ctx = fn(context, V8LocalFromNapi(secure_context));
68+
SSL_CTX* ssl_ctx = get_ssl_ctx(context, V8LocalFromNapi(secure_context));
5969
if (ssl_ctx == nullptr) {
6070
napi_throw_type_error(env, nullptr,
6171
"Argument must be a TLS SecureContext");

0 commit comments

Comments
 (0)