@lazyneoaz/testfca 1.0.4 → 1.0.6

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.
package/index.js CHANGED
@@ -1,2 +1,9 @@
1
1
  "use strict";
2
+
3
+ // Inject Node's Mozilla CA bundle into SSL_CERT_FILE *before* any module loads
4
+ // the Go native binary (meta-messenger.js / messagix.so). Go reads SSL_CERT_FILE
5
+ // lazily on the first TLS dial — setting it here, at library entry, guarantees
6
+ // it is always in place in every hosting environment (Railway, Docker, Render …).
7
+ require('./src/utils/goCerts').ensureGoCerts();
8
+
2
9
  module.exports = require('./src/engine/client');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyneoaz/testfca",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "commonjs",
5
5
  "description": "Advanced Facebook Chat API client for building Messenger bots — supports real-time messaging, thread management, MQTT, session stability, anti-automation protection, and real E2EE via Signal Protocol.",
6
6
  "main": "index.js",
@@ -1,24 +1,24 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * Postinstall: replace meta-messenger.js's binary with our TLS-fixed build.
4
+ * Postinstall: ensure meta-messenger.js has a working native binary.
5
5
  *
6
- * WHY this is needed:
7
- * The meta-messenger.js binary lacks InsecureTLS=true in its messagix HTTP
8
- * client. In sandboxed/containerised environments (Replit, Docker, CI) the
9
- * Go runtime cannot locate system CA certificates, so every prekey-bundle
10
- * HTTPS request to Facebook fails silently. Without prekeys, Signal sessions
11
- * are never established and sendE2EEMessage always times out.
6
+ * meta-messenger.js's own postinstall runs first (npm guarantees dependency
7
+ * scripts run before the dependent's postinstall). It places the binary via:
8
+ * 1. A prebuilt shipped in its npm tarball (if present)
9
+ * 2. A download from GitHub Releases
10
+ * 3. A local `go build` (if MESSAGIX_BUILD_FROM_SOURCE=true)
12
11
  *
13
- * Our prebuilt/ ships the same binary compiled with InsecureTLS=true.
12
+ * This script validates that a binary was placed. If for any reason it was
13
+ * not (e.g. the GitHub release download failed, npm ci --ignore-scripts was
14
+ * used), we copy the prebuilt binary we ship in prebuilt/ as a fallback.
14
15
  *
15
- * INSTALL ORDER (guaranteed by npm):
16
- * 1. meta-messenger.js postinstall runs first (copies/downloads their binary)
17
- * 2. Our postinstall runs second and overwrites it with ours.
18
- *
19
- * PATH RESOLUTION:
20
- * Uses require.resolve() so the path is correct whether meta-messenger.js is
21
- * at the project root node_modules/ or nested — works in all npm versions.
16
+ * TLS NOTE:
17
+ * TLS certificate errors in containerized environments (Railway, Docker, etc.)
18
+ * are fixed at runtime by src/utils/goCerts.js not by a patched binary.
19
+ * Go reads SSL_CERT_FILE lazily on the first TLS dial. We set it to Node's
20
+ * built-in Mozilla CA bundle before any connection is attempted.
21
+ * No binary modification is required for this fix.
22
22
  */
23
23
 
24
24
  const fs = require('fs');
@@ -35,7 +35,7 @@ function platformKey() {
35
35
  return null;
36
36
  }
37
37
 
38
- function ext() {
38
+ function binaryExt() {
39
39
  if (process.platform === 'win32') return '.dll';
40
40
  if (process.platform === 'darwin') return '.dylib';
41
41
  return '.so';
@@ -43,7 +43,6 @@ function ext() {
43
43
 
44
44
  function findMetaMessengerBuildDir() {
45
45
  try {
46
- // resolve() finds the package regardless of hoisting depth
47
46
  const pkgJson = require.resolve('meta-messenger.js/package.json');
48
47
  return path.join(path.dirname(pkgJson), 'build');
49
48
  } catch (_) {
@@ -59,39 +58,43 @@ function main() {
59
58
 
60
59
  const key = platformKey();
61
60
  if (!key) {
62
- console.log('[nkxfca] postinstall: unsupported platform (' + process.platform + '/' + process.arch + '), skipping binary patch.');
61
+ console.log('[nkxfca] postinstall: unsupported platform (' + process.platform + '/' + process.arch + '), skipping.');
63
62
  return;
64
63
  }
65
64
 
66
- const binaryExt = ext();
67
- const ourBin = path.join(__dirname, '..', 'prebuilt', key, 'messagix' + binaryExt);
65
+ const ext = binaryExt();
66
+ const mmBuildDir = findMetaMessengerBuildDir();
68
67
 
69
- if (!fs.existsSync(ourBin)) {
70
- console.log('[nkxfca] postinstall: no prebuilt for ' + key + ', skipping binary patch.');
68
+ if (!mmBuildDir) {
69
+ console.log('[nkxfca] postinstall: meta-messenger.js not found skipping binary check.');
71
70
  return;
72
71
  }
73
72
 
74
- const mmBuildDir = findMetaMessengerBuildDir();
75
- if (!mmBuildDir) {
76
- console.log('[nkxfca] postinstall: meta-messenger.js not found via require.resolve, skipping patch.');
73
+ const mmBin = path.join(mmBuildDir, 'messagix' + ext);
74
+
75
+ // Happy path: meta-messenger.js's own postinstall already placed the binary
76
+ if (fs.existsSync(mmBin) && fs.statSync(mmBin).size > 0) {
77
+ console.log('[nkxfca] postinstall: meta-messenger.js binary present — OK.');
77
78
  return;
78
79
  }
79
80
 
80
- const mmBin = path.join(mmBuildDir, 'messagix' + binaryExt);
81
+ // Fallback: meta-messenger.js postinstall failed (download error, no internet, etc.)
82
+ // Use the prebuilt we ship as insurance.
83
+ const ourBin = path.join(__dirname, '..', 'prebuilt', key, 'messagix' + ext);
84
+ if (!fs.existsSync(ourBin)) {
85
+ console.warn('[nkxfca] postinstall: no prebuilt for ' + key + ' and meta-messenger.js binary is missing.');
86
+ console.warn('[nkxfca] E2EE will not work. Try: MESSAGIX_BUILD_FROM_SOURCE=true npm install');
87
+ return;
88
+ }
81
89
 
82
- // Ensure the build directory exists (meta-messenger.js postinstall creates it,
83
- // but guard against edge cases where it hasn't run yet)
84
90
  try {
85
91
  fs.mkdirSync(mmBuildDir, { recursive: true });
86
- } catch (_) {}
87
-
88
- try {
89
92
  fs.copyFileSync(ourBin, mmBin);
90
- console.log('[nkxfca] postinstall: patched meta-messenger.js binary with TLS-fixed build (' + key + ')');
93
+ fs.chmodSync(mmBin, 0o755);
94
+ console.log('[nkxfca] postinstall: installed fallback prebuilt binary for ' + key);
91
95
  } catch (err) {
92
- // Non-fatal: warn but don't fail the install
93
- console.warn('[nkxfca] postinstall: could not patch binary: ' + err.message);
94
- console.warn('[nkxfca] E2EE messaging may not work in sandboxed environments.');
96
+ console.warn('[nkxfca] postinstall: could not copy fallback binary: ' + err.message);
97
+ console.warn('[nkxfca] E2EE may not work. Try reinstalling meta-messenger.js manually.');
95
98
  }
96
99
  }
97
100
 
@@ -592,130 +592,134 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
592
592
  }
593
593
  while (ctx._e2eePollingActive) {
594
594
  try {
595
- // Yield to event loop before blocking poll (same pattern as yumi-team)
595
+ // Yield to event loop before blocking poll
596
596
  await new Promise(resolve => setImmediate(resolve));
597
597
  if (!ctx._e2eePollingActive) break;
598
598
 
599
- // Poll with 1 s timeoutreturns a typed event object, NOT an array
600
- const ev = await api.e2ee.pollEvents(1000);
599
+ // pollEvents returns { events: [...] } iterate the array
600
+ const result = await api.e2ee.pollEvents(1000);
601
601
  if (!ctx._e2eePollingActive) break;
602
602
 
603
- // No event / empty result
604
- if (!ev) continue;
603
+ // No events returned (timeout or empty)
604
+ if (!result || !Array.isArray(result.events) || result.events.length === 0) continue;
605
605
 
606
- const evType = (ev.type || '').toLowerCase();
606
+ for (const ev of result.events) {
607
+ if (!ctx._e2eePollingActive) break;
607
608
 
608
- // ── timeout: no event arrived during the poll window ──
609
- if (evType === 'timeout') continue;
609
+ const evType = (ev.type || '').toLowerCase();
610
610
 
611
- // ── closed: Go bridge shut down ──
612
- if (evType === 'closed') {
613
- utils.warn("E2EE", "Bridge closed. Stopping DM poll loop.");
614
- ctx._e2eePollingActive = false;
615
- break;
616
- }
611
+ // ── timeout: no event arrived during the poll window ──
612
+ if (evType === 'timeout') continue;
617
613
 
618
- // ── permanent error (e.g. session invalid) ──
619
- if (evType === 'error') {
620
- const msg = ev.data && ev.data.message ? ev.data.message : JSON.stringify(ev.data);
621
- utils.warn("E2EE", "Bridge error event:", msg);
622
- if (ev.data && ev.data.code === 1) {
614
+ // ── closed: bridge shut down ──
615
+ if (evType === 'closed') {
616
+ utils.warn("E2EE", "Bridge closed. Stopping DM poll loop.");
623
617
  ctx._e2eePollingActive = false;
624
618
  break;
625
619
  }
626
- continue;
627
- }
628
620
 
629
- // ── E2EE connection confirmed by the bridge ──
630
- if (evType === 'e2eeconnected') {
631
- utils.log("E2EE", "E2EE fully connected (bridge event).");
632
- continue;
633
- }
621
+ // ── permanent error (e.g. session invalid) ──
622
+ if (evType === 'error') {
623
+ const msg = ev.data && ev.data.message ? ev.data.message : JSON.stringify(ev.data);
624
+ utils.warn("E2EE", "Bridge error event:", msg);
625
+ if (ev.data && ev.data.code === 1) {
626
+ ctx._e2eePollingActive = false;
627
+ break;
628
+ }
629
+ continue;
630
+ }
634
631
 
635
- // ── Incoming E2EE DM ─────────────────────────────────
636
- if (evType === 'e2eemessage') {
637
- const d = ev.data;
638
- if (!d) continue;
639
- // threadId may be 0 for pure-JID threads — fall back to chatJid
640
- const chatJid = d.chatJid || '';
641
- const senderJid = d.senderJid || '';
642
- const threadID = String(d.threadId || chatJid.replace(/@.*/, '') || '');
643
- const senderID = String(d.senderId || senderJid.replace(/@.*/, '') || '');
644
- if (!threadID || !senderID) {
645
- utils.log("E2EE", "e2eeMessage missing IDs — raw:", JSON.stringify(d).slice(0, 300));
632
+ // ── E2EE connection confirmed by the bridge ──
633
+ if (evType === 'e2eeconnected') {
634
+ utils.log("E2EE", "E2EE fully connected (bridge event).");
646
635
  continue;
647
636
  }
648
- if (senderID === ctx.userID.toString() && !ctx.globalOptions.selfListen) continue;
649
-
650
- utils.log("E2EE", "Incoming E2EE DM from", senderID, "→ thread", threadID, "body:", JSON.stringify(d.text || '').slice(0, 80));
651
-
652
- const fmtMsg = {
653
- type: 'message',
654
- senderID: utils.formatID(senderID),
655
- body: d.text || '',
656
- threadID: utils.formatID(threadID),
657
- messageID: d.id || '',
658
- isGroup: false,
659
- attachments: [],
660
- mentions: {},
661
- timestamp: Number(d.timestampMs || Date.now()),
662
- isUnread: true,
663
- };
664
- if (ctx.globalOptions.autoMarkDelivery) {
665
- try { api.markAsDelivered(fmtMsg.threadID, fmtMsg.messageID); } catch (_) {}
637
+
638
+ // ── Incoming E2EE DM ─────────────────────────────────
639
+ if (evType === 'e2eemessage') {
640
+ const d = ev.data;
641
+ if (!d) continue;
642
+ // threadId may be 0 for pure-JID threads — fall back to chatJid
643
+ const chatJid = d.chatJid || '';
644
+ const senderJid = d.senderJid || '';
645
+ const threadID = String(d.threadId || chatJid.replace(/@.*/, '') || '');
646
+ const senderID = String(d.senderId || senderJid.replace(/@.*/, '') || '');
647
+ if (!threadID || !senderID) {
648
+ utils.log("E2EE", "e2eeMessage missing IDs — raw:", JSON.stringify(d).slice(0, 300));
649
+ continue;
650
+ }
651
+ if (senderID === ctx.userID.toString() && !ctx.globalOptions.selfListen) continue;
652
+
653
+ utils.log("E2EE", "Incoming E2EE DM from", senderID, "→ thread", threadID, "body:", JSON.stringify(d.text || '').slice(0, 80));
654
+
655
+ const fmtMsg = {
656
+ type: 'message',
657
+ senderID: utils.formatID(senderID),
658
+ body: d.text || '',
659
+ threadID: utils.formatID(threadID),
660
+ messageID: d.id || '',
661
+ isGroup: false,
662
+ attachments: [],
663
+ mentions: {},
664
+ timestamp: Number(d.timestampMs || Date.now()),
665
+ isUnread: true,
666
+ };
667
+ if (ctx.globalOptions.autoMarkDelivery) {
668
+ try { api.markAsDelivered(fmtMsg.threadID, fmtMsg.messageID); } catch (_) {}
669
+ }
670
+ if (ctx.globalOptions.autoMarkRead) {
671
+ try { api.markAsRead(fmtMsg.threadID); } catch (_) {}
672
+ }
673
+ globalCallback(null, fmtMsg);
674
+ continue;
666
675
  }
667
- if (ctx.globalOptions.autoMarkRead) {
668
- try { api.markAsRead(fmtMsg.threadID); } catch (_) {}
676
+
677
+ // ── Incoming regular (non-E2EE) message via bridge ───
678
+ if (evType === 'message') {
679
+ const d = ev.data;
680
+ if (!d) continue;
681
+ const threadID = String(d.threadId || '');
682
+ const senderID = String(d.senderId || '');
683
+ if (!threadID || !senderID) continue;
684
+ if (senderID === ctx.userID.toString() && !ctx.globalOptions.selfListen) continue;
685
+ globalCallback(null, {
686
+ type: 'message',
687
+ senderID: utils.formatID(senderID),
688
+ body: d.text || '',
689
+ threadID: utils.formatID(threadID),
690
+ messageID: d.id || '',
691
+ isGroup: !!(d.isGroup),
692
+ attachments: [],
693
+ mentions: {},
694
+ timestamp: Number(d.timestampMs || Date.now()),
695
+ isUnread: true,
696
+ });
697
+ continue;
669
698
  }
670
- globalCallback(null, fmtMsg);
671
- continue;
672
- }
673
699
 
674
- // ── Incoming regular (non-E2EE) message via bridge ───
675
- if (evType === 'message') {
676
- const d = ev.data;
677
- if (!d) continue;
678
- const threadID = String(d.threadId || '');
679
- const senderID = String(d.senderId || '');
680
- if (!threadID || !senderID) continue;
681
- if (senderID === ctx.userID.toString() && !ctx.globalOptions.selfListen) continue;
682
- globalCallback(null, {
683
- type: 'message',
684
- senderID: utils.formatID(senderID),
685
- body: d.text || '',
686
- threadID: utils.formatID(threadID),
687
- messageID: d.id || '',
688
- isGroup: !!(d.isGroup),
689
- attachments: [],
690
- mentions: {},
691
- timestamp: Number(d.timestampMs || Date.now()),
692
- isUnread: true,
693
- });
694
- continue;
695
- }
700
+ // ── E2EE reaction ────────────────────────────────────
701
+ if (evType === 'e2eereaction') {
702
+ if (!ctx.globalOptions.listenEvents) continue;
703
+ const d = ev.data;
704
+ if (!d) continue;
705
+ const chatJid = d.chatJid || '';
706
+ const senderJid = d.senderJid || '';
707
+ globalCallback(null, {
708
+ type: 'message_reaction',
709
+ threadID: utils.formatID(chatJid.replace(/@.*/, '')),
710
+ messageID: d.messageId || '',
711
+ reaction: d.reaction || '',
712
+ senderID: utils.formatID(senderJid.replace(/@.*/, '')),
713
+ offlineThreadingId: null,
714
+ timestamp: Date.now(),
715
+ isGroup: false,
716
+ });
717
+ continue;
718
+ }
696
719
 
697
- // ── E2EE reaction ────────────────────────────────────
698
- if (evType === 'e2eereaction') {
699
- if (!ctx.globalOptions.listenEvents) continue;
700
- const d = ev.data;
701
- if (!d) continue;
702
- const chatJid = d.chatJid || '';
703
- const senderJid = d.senderJid || '';
704
- globalCallback(null, {
705
- type: 'message_reaction',
706
- threadID: utils.formatID(chatJid.replace(/@.*/, '')),
707
- messageID: d.messageId || '',
708
- reaction: d.reaction || '',
709
- senderID: utils.formatID(senderJid.replace(/@.*/, '')),
710
- offlineThreadingId: null,
711
- timestamp: Date.now(),
712
- isGroup: false,
713
- });
714
- continue;
720
+ // ready / reconnected / disconnected / deviceDataChanged / raw → ignore silently
715
721
  }
716
722
 
717
- // ready / reconnected / disconnected / deviceDataChanged / raw → ignore silently
718
-
719
723
  } catch (pollErr) {
720
724
  if (!ctx._e2eePollingActive) break;
721
725
  utils.warn("E2EE", "pollEvents error:", pollErr && pollErr.message ? pollErr.message : pollErr);
@@ -3,16 +3,34 @@
3
3
  /**
4
4
  * Lightweight E2EE bridge — delegates to the `meta-messenger.js` npm package.
5
5
  *
6
- * Our postinstall replaces meta-messenger.js's binary with our TLS-fixed build
7
- * so that prekey HTTPS requests succeed in sandboxed environments (Replit, Docker).
6
+ * meta-messenger.js ships a native Go shared library (messagix.so) compiled
7
+ * from bridge-go/ Go source. The library uses Go's standard TLS stack
8
+ * (x509.SystemCertPool) which requires OS CA certificates to verify Facebook's
9
+ * TLS chain. Many hosting environments (Railway, Docker Alpine, Render, etc.)
10
+ * ship no CA bundle, causing every outbound TLS connection to fail with:
11
+ * "x509: certificate signed by unknown authority"
12
+ *
13
+ * THE FIX — SSL_CERT_FILE (see src/utils/goCerts.js):
14
+ * We set SSL_CERT_FILE to a temp file containing Node's built-in Mozilla CA
15
+ * bundle BEFORE the Go binary is ever loaded. ensureGoCerts() is called from
16
+ * index.js (library entry point) so it fires on the very first require().
17
+ * It is also called here at module evaluation time as a second guarantee.
8
18
  *
9
19
  * Key design decisions:
10
20
  * - enableE2EE: false → we call connectE2EE() explicitly; no auto-start race
11
- * - e2eeMemoryOnly: false → Signal sessions are persisted to devicePath on disk
21
+ * - e2eeMemoryOnly: false → Signal sessions persisted to devicePath on disk
12
22
  * - devicePath defaults to ".nkxfca_e2ee_device.json" in cwd when not provided
13
23
  */
14
24
 
15
- let _Client = null;
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+
28
+ // Ensure Go TLS certs are ready — second guarantee (index.js is the primary)
29
+ require('../utils/goCerts').ensureGoCerts();
30
+
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+
33
+ let _Client = null;
16
34
  let _loadError = null;
17
35
 
18
36
  function loadClient() {
@@ -49,9 +67,9 @@ function newClient(cfg) {
49
67
  const cookies = cfg.cookies || {};
50
68
 
51
69
  const opts = {
52
- platform: cfg.platform || 'facebook',
53
- logLevel: cfg.logLevel || 'warn',
54
- enableE2EE: false, // we call connectE2EE() explicitly
70
+ platform: cfg.platform || 'facebook',
71
+ logLevel: cfg.logLevel || 'warn',
72
+ enableE2EE: false, // we call connectE2EE() explicitly
55
73
  e2eeMemoryOnly: false, // always persist sessions to disk
56
74
  autoReconnect: cfg.autoReconnect !== false,
57
75
  };
@@ -65,7 +83,7 @@ function newClient(cfg) {
65
83
 
66
84
  const client = new Client(cookies, opts);
67
85
  const handle = String(seq++);
68
- const entry = { client, eventBuffer: [], resolveWaiter: null, lastDeviceData: null };
86
+ const entry = { client, eventBuffer: [], resolveWaiter: null, lastDeviceData: null };
69
87
 
70
88
  for (const type of EVENT_TYPES) {
71
89
  client.on(type, (data) => {
@@ -154,7 +172,7 @@ function toBigInt(val) {
154
172
  return BigInt(String(val).split('@')[0]);
155
173
  }
156
174
 
157
- // ── E2EE operations ────────────────────────────────────────────────────────────
175
+ // ── E2EE operations ───────────────────────────────────────────────────────────
158
176
 
159
177
  async function sendE2EEMessage(handle, chatJid, text, replyToId, replyToSenderJid) {
160
178
  const opts = replyToId ? { replyToId, replyToSenderJid } : undefined;
@@ -218,7 +236,7 @@ async function uploadMedia(handle, options) {
218
236
  async function sendImage(handle, options) {
219
237
  const { chatJid, data, filename, caption, replyToId } = options;
220
238
  const opts = {};
221
- if (caption) opts.caption = caption;
239
+ if (caption) opts.caption = caption;
222
240
  if (replyToId) opts.replyToId = replyToId;
223
241
  return getEntry(handle).client.sendImage(
224
242
  toBigInt(chatJid), toBuffer(data), filename || 'image.jpg', Object.keys(opts).length ? opts : undefined
@@ -228,7 +246,7 @@ async function sendImage(handle, options) {
228
246
  async function sendVideo(handle, options) {
229
247
  const { chatJid, data, filename, caption, replyToId } = options;
230
248
  const opts = {};
231
- if (caption) opts.caption = caption;
249
+ if (caption) opts.caption = caption;
232
250
  if (replyToId) opts.replyToId = replyToId;
233
251
  return getEntry(handle).client.sendVideo(
234
252
  toBigInt(chatJid), toBuffer(data), filename || 'video.mp4', Object.keys(opts).length ? opts : undefined
@@ -246,7 +264,7 @@ async function sendVoice(handle, options) {
246
264
  async function sendFile(handle, options) {
247
265
  const { chatJid, data, filename, mimeType, caption, replyToId } = options;
248
266
  const opts = {};
249
- if (caption) opts.caption = caption;
267
+ if (caption) opts.caption = caption;
250
268
  if (replyToId) opts.replyToId = replyToId;
251
269
  return getEntry(handle).client.sendFile(
252
270
  toBigInt(chatJid), toBuffer(data), filename || 'file', mimeType || 'application/octet-stream',
@@ -287,7 +305,7 @@ function unload() {
287
305
  try { entry.client.disconnect(); } catch (_) {}
288
306
  }
289
307
  registry.clear();
290
- _Client = null;
308
+ _Client = null;
291
309
  _loadError = null;
292
310
  }
293
311
 
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ensureGoCerts — inject Node's built-in Mozilla CA bundle into the process
5
+ * environment so that the Go binary inside meta-messenger.js can verify TLS
6
+ * certificates in any hosting environment.
7
+ *
8
+ * WHY this is needed:
9
+ * The Go binary (messagix.so) uses Go's standard TLS stack, which calls
10
+ * x509.SystemCertPool() to verify server certificates. In many hosting
11
+ * environments — Railway, Render, Docker Alpine, Heroku, etc. — the OS
12
+ * ships no CA certificate bundle (no /etc/ssl/certs), so SystemCertPool()
13
+ * returns an empty pool. Every outbound TLS connection the binary makes
14
+ * (Facebook's E2EE WebSocket, prekey HTTPS requests) then fails with:
15
+ * "x509: certificate signed by unknown authority"
16
+ *
17
+ * THE FIX:
18
+ * Go reads the SSL_CERT_FILE environment variable when calling
19
+ * x509.SystemCertPool(). If we set it to a valid PEM file before the first
20
+ * TLS connection, Go finds a full CA bundle. Node.js ships Mozilla's CA
21
+ * bundle as tls.rootCertificates — we write that to a temp file and point
22
+ * SSL_CERT_FILE at it. DigiCert (Facebook), Let's Encrypt, and all major
23
+ * CAs are included.
24
+ *
25
+ * TIMING:
26
+ * Must run before any TLS dial in the Go binary. We call it from index.js
27
+ * (library entry point) so it fires the moment anyone requires nkxfca,
28
+ * long before the Go binary is loaded or any connection is attempted.
29
+ *
30
+ * IDEMPOTENT:
31
+ * Safe to call multiple times — exits immediately on subsequent calls or
32
+ * when the host already provides SSL_CERT_FILE / SSL_CERT_DIR.
33
+ */
34
+
35
+ const fs = require('fs');
36
+ const path = require('path');
37
+ const os = require('os');
38
+
39
+ let _done = false;
40
+
41
+ function ensureGoCerts() {
42
+ if (_done) return;
43
+
44
+ // Respect host-provided cert configuration — don't override it
45
+ if (process.env.SSL_CERT_FILE || process.env.SSL_CERT_DIR) {
46
+ _done = true;
47
+ return;
48
+ }
49
+
50
+ try {
51
+ const { rootCertificates } = require('tls');
52
+ if (!rootCertificates || !rootCertificates.length) return;
53
+
54
+ const certFile = path.join(os.tmpdir(), '.nkxfca-cacert.pem');
55
+
56
+ // Write the bundle once per container/OS session (not per process start)
57
+ if (!fs.existsSync(certFile) || fs.statSync(certFile).size === 0) {
58
+ fs.writeFileSync(certFile, rootCertificates.join('\n'), 'utf8');
59
+ }
60
+
61
+ process.env.SSL_CERT_FILE = certFile;
62
+ _done = true;
63
+ } catch (_) {
64
+ // Never crash — worst case, Go falls back to the system cert store
65
+ }
66
+ }
67
+
68
+ module.exports = { ensureGoCerts };
@@ -1,61 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { existsSync, mkdirSync } from "node:fs";
3
- import { dirname, join } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- import { detectPlatform } from "./detect-platform.mjs";
7
- import { packageJson } from "./package.mjs";
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
10
- const { ext } = detectPlatform();
11
- const { name } = packageJson;
12
- const bridgeDir = join(__dirname, "..", "bridge-e2ee", "bridge-go");
13
- const vendorDir = join(bridgeDir, "vendor");
14
- const hasVendor = existsSync(vendorDir);
15
-
16
- function runGo(args) {
17
- const res = spawnSync(process.env.GO_BIN || "go", args, {
18
- cwd: bridgeDir,
19
- stdio: "inherit",
20
- env: {
21
- ...process.env,
22
- CGO_ENABLED: "1",
23
- // Prevent Go from downloading a newer toolchain when go.mod specifies
24
- // a version higher than what is locally installed. The local compiler
25
- // should always be used in sandboxed / offline environments.
26
- GOTOOLCHAIN: "local",
27
- },
28
- });
29
- if (res.error) {
30
- console.error(`[${name}] Failed to spawn Go: ${res.error.message}`);
31
- process.exit(1);
32
- }
33
- if (res.status !== 0) process.exit(res.status || 1);
34
- }
35
-
36
- const buildDir = join(__dirname, "..", "build");
37
- if (!existsSync(buildDir)) mkdirSync(buildDir, { recursive: true });
38
-
39
- // Skip mod tidy when vendor directory is present — tidy can break vendor/go.mod consistency
40
- // and would overwrite the patched prekeys.go with the unpatched upstream version.
41
- if (!hasVendor) {
42
- console.log(`[${name}] Tidying Go modules...`);
43
- runGo(["mod", "tidy"]);
44
- } else {
45
- console.log(`[${name}] Vendor directory present — skipping mod tidy to preserve patches.`);
46
- }
47
-
48
- const buildArgs = [
49
- "build",
50
- ...(hasVendor ? ["-mod=vendor"] : []),
51
- "-buildmode=c-shared",
52
- "-ldflags=-s -w",
53
- "-o",
54
- join("..", "..", "build", `messagix.${ext}`),
55
- ".",
56
- ];
57
-
58
- console.log(`[${name}] Building native library (release mode)...`);
59
- runGo(buildArgs);
60
-
61
- console.log(`[${name}] Built native: build/messagix.${ext}`);
@@ -1,36 +0,0 @@
1
- import { execSync } from "node:child_process";
2
-
3
- export function detectPlatform() {
4
- const { platform } = process;
5
- const { arch } = process;
6
- const isMusl = detectMusl();
7
-
8
- const libc = platform === "linux" ? (isMusl ? "musl" : "gnu") : "";
9
- const triplet = platform === "linux" ? `${platform}-${arch}-${libc}` : `${platform}-${arch}`;
10
-
11
- const ext = platform === "win32" ? "dll" : platform === "darwin" ? "dylib" : "so";
12
-
13
- return { platform, arch, libc, triplet, ext };
14
- }
15
-
16
- function detectMusl() {
17
- try {
18
- if (process.platform !== "linux") return false;
19
- if (process.report && typeof process.report.getReport === "function") {
20
- const rep = process.report.getReport();
21
- const glibc = rep.header && rep.header.glibcVersionRuntime;
22
- return !glibc;
23
- }
24
- } catch {
25
- //
26
- }
27
- try {
28
- const out = execSync("ldd --version 2>&1 || true", { encoding: "utf8" });
29
- return /musl/i.test(out);
30
- } catch {
31
- //
32
- }
33
- return false;
34
- }
35
-
36
- export default detectPlatform;
@@ -1,108 +0,0 @@
1
- import { createWriteStream } from "node:fs";
2
- import { mkdir, rename, unlink } from "node:fs/promises";
3
- import https from "node:https";
4
- import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
-
7
- import { detectPlatform } from "./detect-platform.mjs";
8
- import { packageJson as pkg } from "./package.mjs";
9
-
10
- const __dirname = dirname(fileURLToPath(import.meta.url));
11
-
12
- function buildBaseURL() {
13
- // yumi-team/meta-messenger.js hosts prebuilt messagix binaries for all
14
- // platforms except linux-x64-gnu, which ships directly in this package's
15
- // prebuilt/ directory and is therefore never fetched from here.
16
- const repo = "yumi-team/meta-messenger.js";
17
- const tag = "v1.1.3";
18
- return `https://github.com/${repo}/releases/download/${tag}`;
19
- }
20
-
21
- function httpGet(url, redirectCount = 0) {
22
- console.log(`[${pkg.name}] HTTP GET: ${url}${redirectCount > 0 ? ` (redirect #${redirectCount})` : ""}`);
23
- const reqStart = Date.now();
24
- return new Promise((resolve, reject) => {
25
- https
26
- .get(url, res => {
27
- console.log(`[${pkg.name}] Response: HTTP ${res.statusCode} in ${Date.now() - reqStart}ms`);
28
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
29
- console.log(`[${pkg.name}] Following redirect to: ${res.headers.location}`);
30
- return resolve(httpGet(res.headers.location, redirectCount + 1));
31
- }
32
- if (res.statusCode !== 200) {
33
- return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
34
- }
35
- resolve(res);
36
- })
37
- .on("error", err => {
38
- console.log(`[${pkg.name}] HTTP error after ${Date.now() - reqStart}ms:`, err?.message);
39
- reject(err);
40
- });
41
- });
42
- }
43
-
44
- async function downloadTo(url, dstPath) {
45
- console.log(`[${pkg.name}] Downloading to: ${dstPath}`);
46
- await mkdir(dirname(dstPath), { recursive: true });
47
- const tmp = `${dstPath}.download`;
48
- try {
49
- await unlink(tmp);
50
- } catch {
51
- //
52
- }
53
- const res = await httpGet(url);
54
- console.log(`[${pkg.name}] Starting file write...`);
55
- const writeStart = Date.now();
56
- let bytesWritten = 0;
57
- await new Promise((resolve, reject) => {
58
- const out = createWriteStream(tmp);
59
- res.on("data", chunk => {
60
- bytesWritten += chunk.length;
61
- });
62
- res.pipe(out);
63
- res.on("error", reject);
64
- out.on("error", reject);
65
- out.on("finish", () => {
66
- console.log(`[${pkg.name}] File write completed: ${bytesWritten} bytes in ${Date.now() - writeStart}ms`);
67
- res.destroy();
68
- resolve();
69
- });
70
- });
71
- await rename(tmp, dstPath);
72
- console.log(`[${pkg.name}] File renamed to final destination`);
73
- }
74
-
75
- export async function downloadPrebuilt() {
76
- console.log(`[${pkg.name}] downloadPrebuilt() started`);
77
- const { triplet, ext } = detectPlatform();
78
- const baseURL = buildBaseURL();
79
- const filename = `messagix-${triplet}.${ext}`;
80
- const url = `${baseURL}/${filename}`;
81
- console.log(`[${pkg.name}] Target URL: ${url}`);
82
-
83
- const out = join(__dirname, "..", "build", `messagix.${ext}`);
84
- const totalStart = Date.now();
85
- try {
86
- await downloadTo(url, out);
87
- console.log(`[${pkg.name}] Downloaded prebuilt from ${url} in ${Date.now() - totalStart}ms`);
88
- return true;
89
- } catch (err) {
90
- console.warn(
91
- `[${pkg.name}] No remote prebuilt found at ${url} (after ${Date.now() - totalStart}ms):`,
92
- err?.message || String(err),
93
- );
94
- return false;
95
- }
96
- }
97
-
98
- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
99
- if (isMain) {
100
- downloadPrebuilt()
101
- .then(ok => {
102
- if (!ok) process.exit(1);
103
- })
104
- .catch(err => {
105
- console.error(`[${pkg.name}] download-prebuilt failed:`, err?.message || String(err));
106
- process.exit(1);
107
- });
108
- }
@@ -1,6 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- const __dirname = dirname(fileURLToPath(import.meta.url));
6
- export const packageJson = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
@@ -1,95 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { copyFile, mkdir } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- import { detectPlatform } from "./detect-platform.mjs";
7
- import { downloadPrebuilt } from "./download-prebuilt.mjs";
8
- import { packageJson as pkg } from "./package.mjs";
9
-
10
- const __dirname = dirname(fileURLToPath(import.meta.url));
11
-
12
- async function copyIfExists(src, dst) {
13
- try {
14
- await mkdir(dirname(dst), { recursive: true });
15
- await copyFile(src, dst);
16
- return true;
17
- } catch (err) {
18
- if (err?.code === "ENOENT") return false;
19
- throw err;
20
- }
21
- }
22
-
23
- async function run() {
24
- console.log(`[${pkg.name}] postinstall started`);
25
- const startTime = Date.now();
26
-
27
- if (process.env.MESSAGIX_SKIP_POSTINSTALL === "true") {
28
- console.log(`[${pkg.name}] Skipping postinstall (MESSAGIX_SKIP_POSTINSTALL=true)`);
29
- return;
30
- }
31
-
32
- console.log(`[${pkg.name}] Detecting platform...`);
33
- const { triplet, ext } = detectPlatform();
34
- console.log(`[${pkg.name}] Platform: ${triplet}, ext: ${ext}`);
35
-
36
- const buildOut = join(__dirname, "..", "build", `messagix.${ext}`);
37
- console.log(`[${pkg.name}] Build output path: ${buildOut}`);
38
-
39
- if (existsSync(buildOut)) {
40
- console.log(`[${pkg.name}] Native library already present at build/messagix.${ext}`);
41
- console.log(`[${pkg.name}] postinstall completed in ${Date.now() - startTime}ms`);
42
- return;
43
- }
44
-
45
- // 1) Prefer local prebuilt shipped in npm tarball
46
- const prebuiltDir = join(__dirname, "..", "prebuilt", triplet);
47
- const prebuilt = join(prebuiltDir, `messagix.${ext}`);
48
- console.log(`[${pkg.name}] Checking local prebuilt at: ${prebuilt}`);
49
- if (await copyIfExists(prebuilt, buildOut)) {
50
- console.log(`[${pkg.name}] Using local prebuilt for ${triplet}`);
51
- // Keep the prebuilt copy so reinstalls (e.g. npm rebuild, CI caches) don't
52
- // lose the patched binary and fall back to downloading an unpatched one.
53
- console.log(`[${pkg.name}] postinstall completed in ${Date.now() - startTime}ms`);
54
- return;
55
- }
56
- console.log(`[${pkg.name}] No local prebuilt found`);
57
-
58
- // 2) Try remote prebuilt from GitHub Releases
59
- console.log(`[${pkg.name}] Attempting to download remote prebuilt...`);
60
- const downloadStart = Date.now();
61
- try {
62
- if (await downloadPrebuilt()) {
63
- console.log(`[${pkg.name}] Download completed in ${Date.now() - downloadStart}ms`);
64
- console.log(`[${pkg.name}] postinstall completed in ${Date.now() - startTime}ms`);
65
- return;
66
- }
67
- } catch (err) {
68
- console.log(
69
- `[${pkg.name}] Download failed after ${Date.now() - downloadStart}ms:`,
70
- err?.message || String(err),
71
- );
72
- }
73
-
74
- console.warn(`[${pkg.name}] ──────────────────────────────────────────────────────────`);
75
- console.warn(`[${pkg.name}] No prebuilt E2EE bridge found for your platform.`);
76
- console.warn(`[${pkg.name}] Platform detected: ${triplet}`);
77
- console.warn(`[${pkg.name}]`);
78
- console.warn(`[${pkg.name}] Supported out-of-the-box:`);
79
- console.warn(`[${pkg.name}] linux-x64-gnu (ships inside the npm package)`);
80
- console.warn(`[${pkg.name}] linux-x64-musl / darwin-x64 / darwin-arm64 / win32-x64`);
81
- console.warn(`[${pkg.name}] (downloaded automatically from yumi-team/meta-messenger.js)`);
82
- console.warn(`[${pkg.name}]`);
83
- console.warn(`[${pkg.name}] If the download failed, check your internet connection and retry:`);
84
- console.warn(`[${pkg.name}] npm install`);
85
- console.warn(`[${pkg.name}]`);
86
- console.warn(`[${pkg.name}] Non-E2EE features (sendMessage, listen, etc.) work without the bridge.`);
87
- console.warn(`[${pkg.name}] ──────────────────────────────────────────────────────────`);
88
- }
89
-
90
- run()
91
- .then(() => process.exit(0))
92
- .catch(err => {
93
- console.error(`[${pkg.name}] postinstall failed:`, err?.message || String(err));
94
- process.exit(1);
95
- });