chainlesschain 0.161.12 → 0.162.1

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.
Files changed (145) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-B1dwBvzW.js → AIOps-5A54O3wF.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-DfR4oLvh.js → ActionButton-epuY2GkZ.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-D3ZYGXjr.js → Analytics-CIdxw7T5.js} +1 -1
  6. package/src/assets/web-panel/assets/{AppLayout-CsmOoh-7.js → AppLayout-xR6YUHsS.js} +2 -2
  7. package/src/assets/web-panel/assets/{Audit-B4gwDm63.js → Audit-D95nRcdZ.js} +1 -1
  8. package/src/assets/web-panel/assets/{Backup-T42uSArV.js → Backup-Cmw7ZTJD.js} +1 -1
  9. package/src/assets/web-panel/assets/{BaseInput-CFi52nMs.js → BaseInput-DgCEAL_E.js} +1 -1
  10. package/src/assets/web-panel/assets/{Chat-D7Vvok1V.js → Chat-DAQAzZsA.js} +1 -1
  11. package/src/assets/web-panel/assets/{Checkbox-Dwaflpww.js → Checkbox-GRxUANnE.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-BIqx3G0b.js → Codegen--6KIDt1W.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-DzIUYUNu.js → Col-3s6dxuMx.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-DKePtzhk.js → Community-D60pkEBz.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-CirVV9Wq.js → Compact-CQR4QsWP.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-CrXOr0sy.js → Compliance-KySNEhMK.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-BEBP6Tht.js → Cowork-Yrrf_Vuu.js} +1 -1
  18. package/src/assets/web-panel/assets/{Cron-k7nNUuqh.js → Cron-DSuuSKIQ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Crosschain-CBnX0Dhq.js → Crosschain-Dub0G9i4.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-B48FszWS.js → DID-CbpCGV51.js} +1 -1
  21. package/src/assets/web-panel/assets/{Dashboard-Pb3qfFpp.js → Dashboard-D29e0y01.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-pUHy4CQ2.js → Dropdown-DZY7X5v2.js} +1 -1
  23. package/src/assets/web-panel/assets/{Federation-CT0Qs-kR.js → Federation-dmrLzzMB.js} +1 -1
  24. package/src/assets/web-panel/assets/{FormItemContext-CbJJp5BR.js → FormItemContext-EopsCGez.js} +1 -1
  25. package/src/assets/web-panel/assets/{Git-B3mGNLQe.js → Git-D3mb4RhK.js} +1 -1
  26. package/src/assets/web-panel/assets/{Governance-BKf4733q.js → Governance-8v034-Nr.js} +1 -1
  27. package/src/assets/web-panel/assets/{Inference-DZjU541G.js → Inference-Ddl-HQwQ.js} +1 -1
  28. package/src/assets/web-panel/assets/{KnowledgeGraph-C8L-7Dd1.js → KnowledgeGraph-DrRruyud.js} +1 -1
  29. package/src/assets/web-panel/assets/{Logs-CwDwOiKv.js → Logs-Bsu6lYQe.js} +1 -1
  30. package/src/assets/web-panel/assets/{Marketplace-C2YWWU0M.js → Marketplace-Iyf2Xc23.js} +1 -1
  31. package/src/assets/web-panel/assets/{McpTools-dIbkOypF.js → McpTools-ljVE1lIN.js} +1 -1
  32. package/src/assets/web-panel/assets/{Memory-7eF8WzcY.js → Memory-Ca48TtDU.js} +1 -1
  33. package/src/assets/web-panel/assets/{MobileBridge-C74GHLbX.js → MobileBridge-DZ3Ar6da.js} +1 -1
  34. package/src/assets/web-panel/assets/{Mtc-CSEDo5Fo.js → Mtc-CRjBnVAT.js} +1 -1
  35. package/src/assets/web-panel/assets/{MtcAudit-DiJXxOrB.js → MtcAudit-CEC2eKez.js} +1 -1
  36. package/src/assets/web-panel/assets/Multisig-BHoQvNeL.js +7 -0
  37. package/src/assets/web-panel/assets/Multisig-kwPDnXnl.css +1 -0
  38. package/src/assets/web-panel/assets/{NLProgramming-DjF-gIUw.js → NLProgramming-9e5hIFvL.js} +1 -1
  39. package/src/assets/web-panel/assets/{Notes-BUE5CvMO.js → Notes-BEhZRadw.js} +1 -1
  40. package/src/assets/web-panel/assets/{NotificationSettings-Dfbrobje.js → NotificationSettings-CzPbT9UE.js} +1 -1
  41. package/src/assets/web-panel/assets/{Organization-C6YvqjQB.js → Organization-CvFg4VAr.js} +1 -1
  42. package/src/assets/web-panel/assets/{Overflow-BvHNhdMR.js → Overflow-Cm7ISeJl.js} +1 -1
  43. package/src/assets/web-panel/assets/{P2P-BO0hQHFS.js → P2P-BWN-wdfH.js} +1 -1
  44. package/src/assets/web-panel/assets/{Permissions-CCPlrJeP.js → Permissions-C4vKkP_t.js} +1 -1
  45. package/src/assets/web-panel/assets/{Pipeline-DTCL3FjJ.js → Pipeline-DQCShDeT.js} +1 -1
  46. package/src/assets/web-panel/assets/{Privacy-08DYgOe_.js → Privacy-Nc7LlSr3.js} +1 -1
  47. package/src/assets/web-panel/assets/{ProjectInit-B7j-Z8sa.js → ProjectInit-DDkqXAED.js} +1 -1
  48. package/src/assets/web-panel/assets/{ProjectSettings-CFqLhV1w.js → ProjectSettings-ia6GQr6A.js} +1 -1
  49. package/src/assets/web-panel/assets/{Projects-BPlpx2UN.js → Projects-CNqXa5XY.js} +1 -1
  50. package/src/assets/web-panel/assets/{Providers-BCBPbVbF.js → Providers-EwarbUb8.js} +1 -1
  51. package/src/assets/web-panel/assets/{QuickAsk-C8Job6zl.js → QuickAsk-BAHQBw0d.js} +1 -1
  52. package/src/assets/web-panel/assets/{Recommend-DOV_Aje-.js → Recommend-Ccgu6elV.js} +1 -1
  53. package/src/assets/web-panel/assets/{Reputation-DOgK_dIK.js → Reputation-ai0gRNpj.js} +1 -1
  54. package/src/assets/web-panel/assets/{Row-8jdU1xYg.js → Row-BWLfAhHC.js} +1 -1
  55. package/src/assets/web-panel/assets/{RssFeed-B289OgNZ.js → RssFeed-CnuRtiQH.js} +1 -1
  56. package/src/assets/web-panel/assets/{Search-cr0AndFE.js → Search-Da0CvI_2.js} +1 -1
  57. package/src/assets/web-panel/assets/{Security-w4diykaE.js → Security-DEFeOeUv.js} +1 -1
  58. package/src/assets/web-panel/assets/{Services-dTxAI5cG.js → Services-CsfvCqCH.js} +1 -1
  59. package/src/assets/web-panel/assets/{Skeleton-B3FiUiRo.js → Skeleton-DlRaGj_n.js} +1 -1
  60. package/src/assets/web-panel/assets/{Skills-DgtBTAFC.js → Skills-fjlZrsYq.js} +1 -1
  61. package/src/assets/web-panel/assets/{Sla-B4Y6bvbL.js → Sla-DNSDuEt2.js} +1 -1
  62. package/src/assets/web-panel/assets/{SpeechSettings-C8FEvArc.js → SpeechSettings-Bj0t-JCf.js} +1 -1
  63. package/src/assets/web-panel/assets/{SyncSettings-CHvV5L0m.js → SyncSettings-uO7Gy-BB.js} +1 -1
  64. package/src/assets/web-panel/assets/{Tasks-B4Py5ORS.js → Tasks-BSjsO-m8.js} +1 -1
  65. package/src/assets/web-panel/assets/{Templates-MeTyXM5u.js → Templates-B03SSuXn.js} +1 -1
  66. package/src/assets/web-panel/assets/{Tenant-Doiz7wyQ.js → Tenant-CTCPIBzq.js} +1 -1
  67. package/src/assets/web-panel/assets/{Terminal-DSEAdwWN.js → Terminal-CQZcdArx.js} +1 -1
  68. package/src/assets/web-panel/assets/{Tokens-BBrtaEKt.js → Tokens-CsNJIdNl.js} +1 -1
  69. package/src/assets/web-panel/assets/{Trigger-nPvjho27.js → Trigger-kYPQmm6P.js} +1 -1
  70. package/src/assets/web-panel/assets/{Trust-BFwPyS_6.js → Trust-mt-MiBeI.js} +1 -1
  71. package/src/assets/web-panel/assets/{UkeySign-DFsHoY1J.js → UkeySign-DBi8jgXw.js} +1 -1
  72. package/src/assets/web-panel/assets/{VideoEditing-FkSKqHE9.js → VideoEditing-CINSMEf5.js} +1 -1
  73. package/src/assets/web-panel/assets/{Wallet-AXo-_4-w.js → Wallet-DCVMgW0U.js} +1 -1
  74. package/src/assets/web-panel/assets/{WebAuthn-DoScnQ-4.js → WebAuthn-C9OjbTBs.js} +1 -1
  75. package/src/assets/web-panel/assets/{WorkflowEditor-IEMfIax-.js → WorkflowEditor-LM1HJs-S.js} +1 -1
  76. package/src/assets/web-panel/assets/{chat-pc1ciH6T.js → chat-Dz68Ixdp.js} +1 -1
  77. package/src/assets/web-panel/assets/{colors-CjkIkB0e.js → colors-qf63Eax9.js} +1 -1
  78. package/src/assets/web-panel/assets/{compact-item-CQZnkP5p.js → compact-item-r4YWRFGQ.js} +1 -1
  79. package/src/assets/web-panel/assets/{createContext-B1McGnV-.js → createContext-C8Lwrn-C.js} +1 -1
  80. package/src/assets/web-panel/assets/{hasIn-CbkA6peP.js → hasIn-DsPi2kPP.js} +1 -1
  81. package/src/assets/web-panel/assets/{index-B8r6OBuk.js → index-1xqIvVM5.js} +1 -1
  82. package/src/assets/web-panel/assets/{index-Dn7SHV2e.js → index-5nOMJaLt.js} +1 -1
  83. package/src/assets/web-panel/assets/{index-ChCCGHwz.js → index-B0KxtBNy.js} +1 -1
  84. package/src/assets/web-panel/assets/index-B3blziag.js +1 -0
  85. package/src/assets/web-panel/assets/{index-CGke5qZp.js → index-B8gs4g1v.js} +1 -1
  86. package/src/assets/web-panel/assets/{index-DHpwwlmv.js → index-BFV6aAoX.js} +1 -1
  87. package/src/assets/web-panel/assets/{index-DWq64zmv.js → index-BKEl9Ahm.js} +1 -1
  88. package/src/assets/web-panel/assets/{index-NRWBOo4F.js → index-BfdIkZnT.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-fqI1KnP_.js → index-BjPHuhxG.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-DHtiecyM.js → index-BtCSUUKm.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-DjxVsyn_.js → index-BxNXO95B.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-DGb36exe.js → index-Bz1O2KhE.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-DcSe5y5O.js → index-BzzFMBIM.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-uWvLy_3T.js → index-C2OFaKmx.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BTyhXFDW.js → index-C3VVwJcn.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-C052wi94.js → index-C8SY0_S8.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-wwQ_ZkWN.js → index-CCCX2ZSH.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DPzT5L14.js → index-CEbbycgJ.js} +3 -3
  99. package/src/assets/web-panel/assets/{index-CzZ3LxPK.js → index-CINVudo7.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CKwkP66s.js → index-CKRXnUTD.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-D4rhVRSV.js → index-CSCb-EY9.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DMjPzhsp.js → index-CY5X4uXO.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-5c4JzwY3.js → index-CqmyLTa_.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CEENN81t.js → index-Cw48kkkH.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-C0Lj7Yl0.js → index-D0mFFS7Y.js} +1 -1
  106. package/src/assets/web-panel/assets/index-D1XYuPHf.js +1 -0
  107. package/src/assets/web-panel/assets/{index-CKledOCh.js → index-D2BR3WQ-.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DsChgzu2.js → index-D4XgKgOF.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-C5mpCgak.js → index-DBpVVgRI.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Bs-romIz.js → index-DGXVjiyF.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BNLeseG1.js → index-DQSweK5-.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CJr12ypE.js → index-DXBc_lyR.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CFCQl1hk.js → index-DycrmZ_r.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Di5tuS0d.js → index-I55BAmVG.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CJbqE5Sw.js → index-LQYz2tRO.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DYAWIXFt.js → index-cTFVNgoS.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Dzfj71tf.js → index-qLyVEtRM.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DJWYN-AT.js → index-qcVx0s-M.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DjM0jsm1.js → index-wVpfPJXE.js} +1 -1
  120. package/src/assets/web-panel/assets/{initDefaultProps-BTloFqyf.js → initDefaultProps-CAgx2aHm.js} +1 -1
  121. package/src/assets/web-panel/assets/{motion-44iMBc1o.js → motion-DFuREFOd.js} +1 -1
  122. package/src/assets/web-panel/assets/{move-CyWQI5eW.js → move-DfcQIXuy.js} +1 -1
  123. package/src/assets/web-panel/assets/{omit-CXfJtuFy.js → omit-BFZV2hmZ.js} +1 -1
  124. package/src/assets/web-panel/assets/{pickAttrs-ANFpSZes.js → pickAttrs-D98WQSur.js} +1 -1
  125. package/src/assets/web-panel/assets/{placementArrow-CigX-url.js → placementArrow-BlP2AN9q.js} +1 -1
  126. package/src/assets/web-panel/assets/{responsiveObserve-Ch48RA0m.js → responsiveObserve-DuI2xGQd.js} +1 -1
  127. package/src/assets/web-panel/assets/{slide-BEz3LORF.js → slide-DSvqah_S.js} +1 -1
  128. package/src/assets/web-panel/assets/{statusUtils-D9EniG8V.js → statusUtils-B-96ZLWF.js} +1 -1
  129. package/src/assets/web-panel/assets/{styleChecker-C1IvuY3L.js → styleChecker-Br0YrCOO.js} +1 -1
  130. package/src/assets/web-panel/assets/{useFlexGapSupport-C92oHeXB.js → useFlexGapSupport-BD4Xr9XU.js} +1 -1
  131. package/src/assets/web-panel/assets/{useFs-CJhvFpgB.js → useFs-Ch5L3BnA.js} +1 -1
  132. package/src/assets/web-panel/assets/{vnode-BPkyunN_.js → vnode-BQi3uWRC.js} +1 -1
  133. package/src/assets/web-panel/assets/{zoom-B6Ipb64r.js → zoom-DaW-jbu7.js} +1 -1
  134. package/src/assets/web-panel/index.html +1 -1
  135. package/src/commands/crosschain.js +403 -1
  136. package/src/commands/pair.js +291 -0
  137. package/src/index.js +2 -0
  138. package/src/lib/cross-chain-mtc.js +275 -6
  139. package/src/lib/cross-chain.js +48 -2
  140. package/src/lib/lan-pairing-preflight.js +425 -0
  141. package/src/lib/lan-pairing-tokens.js +264 -0
  142. package/src/assets/web-panel/assets/Multisig-D-IuEDLa.css +0 -1
  143. package/src/assets/web-panel/assets/Multisig-FZTU5ri6.js +0 -1
  144. package/src/assets/web-panel/assets/index-BXfePRef.js +0 -1
  145. package/src/assets/web-panel/assets/index-Dc4fj_Ys.js +0 -1
@@ -117,6 +117,17 @@ function _strip(row) {
117
117
 
118
118
  /* ── Schema ────────────────────────────────────────────── */
119
119
 
120
+ /**
121
+ * Idempotent column add — PRAGMA table_info is widely supported on both
122
+ * better-sqlite3 and sql.js. Skips when column exists; runs ALTER otherwise.
123
+ */
124
+ function _addColumnIfMissing(db, table, column, def) {
125
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all();
126
+ if (!cols.some((c) => c.name === column)) {
127
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${def}`);
128
+ }
129
+ }
130
+
120
131
  export function ensureCrossChainTables(db) {
121
132
  db.exec(`CREATE TABLE IF NOT EXISTS cc_bridges (
122
133
  id TEXT PRIMARY KEY,
@@ -138,6 +149,16 @@ export function ensureCrossChainTables(db) {
138
149
  db.exec("CREATE INDEX IF NOT EXISTS idx_ccb_status ON cc_bridges(status)");
139
150
  db.exec("CREATE INDEX IF NOT EXISTS idx_ccb_from ON cc_bridges(from_chain)");
140
151
 
152
+ // #21 B.5 Layer 2 PR1 — m-of-n provenance columns. Idempotent for existing
153
+ // DBs via ALTER TABLE. NULL on rows created before this migration or via
154
+ // the legacy direct-bridge path (no --require-multisig).
155
+ _addColumnIfMissing(db, "cc_bridges", "multisig_proposal_id", "TEXT");
156
+ _addColumnIfMissing(db, "cc_bridges", "signers_did_json", "TEXT");
157
+ _addColumnIfMissing(db, "cc_bridges", "partial_sigs_json", "TEXT");
158
+ db.exec(
159
+ "CREATE INDEX IF NOT EXISTS idx_ccb_proposal ON cc_bridges(multisig_proposal_id)",
160
+ );
161
+
141
162
  db.exec(`CREATE TABLE IF NOT EXISTS cc_swaps (
142
163
  id TEXT PRIMARY KEY,
143
164
  from_chain TEXT NOT NULL,
@@ -217,6 +238,7 @@ function _validateChain(chainId) {
217
238
  export function bridgeAsset(
218
239
  db,
219
240
  { fromChain, toChain, asset, amount, senderAddress, recipientAddress },
241
+ multisigContext = null,
220
242
  ) {
221
243
  if (!_validateChain(fromChain))
222
244
  return { bridgeId: null, reason: "unsupported_chain", chain: fromChain };
@@ -232,6 +254,23 @@ export function bridgeAsset(
232
254
  const now = _now();
233
255
  const fee = Math.round(amount * DEFAULT_CONFIG.feePercentage * 10) / 1000;
234
256
 
257
+ // #21 B.5 Layer 2 PR1 — optional multisig provenance. caller passes
258
+ // { proposalId, signers, partialSigs } extracted from a reached/consumed
259
+ // multisig proposal. We persist all three so an onchain verifier can later
260
+ // check m-of-n. Legacy direct-bridge calls leave all three NULL.
261
+ let multisigProposalId = null;
262
+ let signersDidJson = null;
263
+ let partialSigsJson = null;
264
+ if (multisigContext && typeof multisigContext === "object") {
265
+ multisigProposalId = multisigContext.proposalId || null;
266
+ if (Array.isArray(multisigContext.signers)) {
267
+ signersDidJson = JSON.stringify(multisigContext.signers);
268
+ }
269
+ if (Array.isArray(multisigContext.partialSigs)) {
270
+ partialSigsJson = JSON.stringify(multisigContext.partialSigs);
271
+ }
272
+ }
273
+
235
274
  const bridge = {
236
275
  id,
237
276
  from_chain: fromChain,
@@ -248,12 +287,16 @@ export function bridgeAsset(
248
287
  error_message: null,
249
288
  created_at: now,
250
289
  completed_at: null,
290
+ multisig_proposal_id: multisigProposalId,
291
+ signers_did_json: signersDidJson,
292
+ partial_sigs_json: partialSigsJson,
251
293
  };
252
294
 
253
295
  db.prepare(
254
296
  `INSERT INTO cc_bridges (id, from_chain, to_chain, asset, amount, sender_address, recipient_address,
255
- lock_tx_hash, mint_tx_hash, status, fee_amount, fee_chain, error_message, created_at, completed_at)
256
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
297
+ lock_tx_hash, mint_tx_hash, status, fee_amount, fee_chain, error_message, created_at, completed_at,
298
+ multisig_proposal_id, signers_did_json, partial_sigs_json)
299
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
257
300
  ).run(
258
301
  id,
259
302
  fromChain,
@@ -270,6 +313,9 @@ export function bridgeAsset(
270
313
  null,
271
314
  now,
272
315
  null,
316
+ multisigProposalId,
317
+ signersDidJson,
318
+ partialSigsJson,
273
319
  );
274
320
 
275
321
  _bridges.set(id, bridge);
@@ -0,0 +1,425 @@
1
+ /**
2
+ * #21 A.1 PR1 — Linux LAN pairing preflight diagnostics.
3
+ *
4
+ * Pure-JS read-only checks for "why doesn't mobile→desktop QR pairing work
5
+ * on my Linux box". Five inspections:
6
+ *
7
+ * 1. interfaces — non-loopback IPv4 NICs available
8
+ * 2. multicastBind— can we bind UDP 5353 + join 224.0.0.251?
9
+ * 3. port5353Holders — on Linux, who else is listening on 5353 (avahi etc.)
10
+ * 4. platform — OS / distro context (Linux: /etc/os-release)
11
+ * 5. firewallHint — print actionable commands for the detected firewall tool
12
+ *
13
+ * Read-only — never modifies firewall config, never starts daemons. Outputs
14
+ * suggested commands; user runs them.
15
+ *
16
+ * Spike doc: docs/design/A1_Linux_Native_Pairing_spike.md §3.2
17
+ *
18
+ * @module lib/lan-pairing-preflight
19
+ * @version 0.1.0 (#21 A.1 PR1)
20
+ */
21
+
22
+ import os from "node:os";
23
+ import fs from "node:fs";
24
+ import { execSync } from "node:child_process";
25
+ import dgram from "node:dgram";
26
+
27
+ const MDNS_PORT = 5353;
28
+ const MDNS_GROUP = "224.0.0.251";
29
+
30
+ const STATUS = Object.freeze({
31
+ OK: "ok",
32
+ WARNING: "warning",
33
+ BLOCKER: "blocker",
34
+ SKIP: "skip",
35
+ });
36
+
37
+ // ─── 1. interfaces ──────────────────────────────────────────
38
+
39
+ /**
40
+ * List non-loopback IPv4 interfaces. Returns an array of
41
+ * `{ name, address, mac, internal:false, family: 'IPv4' }`.
42
+ *
43
+ * Exported for unit tests so caller can verify filter logic.
44
+ */
45
+ export function listInterfaces() {
46
+ const all = os.networkInterfaces();
47
+ const out = [];
48
+ for (const [name, addrs] of Object.entries(all)) {
49
+ if (!Array.isArray(addrs)) continue;
50
+ for (const a of addrs) {
51
+ if (a.family !== "IPv4") continue;
52
+ if (a.internal) continue;
53
+ out.push({
54
+ name,
55
+ address: a.address,
56
+ mac: a.mac,
57
+ netmask: a.netmask,
58
+ cidr: a.cidr,
59
+ });
60
+ }
61
+ }
62
+ return out;
63
+ }
64
+
65
+ function checkInterfaces() {
66
+ const ifs = listInterfaces();
67
+ if (ifs.length === 0) {
68
+ return {
69
+ name: "interfaces",
70
+ status: STATUS.WARNING,
71
+ detail: "no non-loopback IPv4 interface — check Wi-Fi / Ethernet",
72
+ data: { interfaces: [] },
73
+ };
74
+ }
75
+ return {
76
+ name: "interfaces",
77
+ status: STATUS.OK,
78
+ detail: `${ifs.length} active IPv4 interface(s)`,
79
+ data: {
80
+ interfaces: ifs.map((i) => ({
81
+ name: i.name,
82
+ address: i.address,
83
+ cidr: i.cidr,
84
+ })),
85
+ },
86
+ };
87
+ }
88
+
89
+ // ─── 2. multicastBind ───────────────────────────────────────
90
+
91
+ /**
92
+ * Try to bind UDP 5353 with multicast membership. Returns a promise so the
93
+ * test harness can race a timeout. Failure case usually means firewall (ufw
94
+ * default-deny) or kernel multicast disabled.
95
+ *
96
+ * Uses `reuseAddr: true` so we don't fight with system mDNS daemons that
97
+ * may already be on the port — what we're actually testing is whether the
98
+ * kernel accepts the membership join.
99
+ */
100
+ export async function checkMulticastBind(timeoutMs = 1500) {
101
+ return new Promise((resolve) => {
102
+ let socket;
103
+ let timer;
104
+ const cleanup = () => {
105
+ if (timer) clearTimeout(timer);
106
+ if (socket) {
107
+ try {
108
+ socket.close();
109
+ } catch (_e) {
110
+ /* already closed */
111
+ }
112
+ }
113
+ };
114
+ try {
115
+ socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
116
+ } catch (err) {
117
+ return resolve({
118
+ name: "multicast_bind",
119
+ status: STATUS.BLOCKER,
120
+ detail: `dgram.createSocket failed: ${err.message}`,
121
+ data: { errno: err.errno, code: err.code },
122
+ });
123
+ }
124
+ socket.once("error", (err) => {
125
+ cleanup();
126
+ resolve({
127
+ name: "multicast_bind",
128
+ status: STATUS.BLOCKER,
129
+ detail: `socket bind/join failed: ${err.code || err.message}`,
130
+ data: {
131
+ errno: err.errno,
132
+ code: err.code,
133
+ // Most common Linux culprits:
134
+ // EADDRINUSE — already bound w/o reuseAddr (unlikely with reuseAddr:true)
135
+ // EACCES — firewall (ufw/firewalld) blocking
136
+ // ENODEV — interface gone / multicast disabled on iface
137
+ },
138
+ });
139
+ });
140
+ timer = setTimeout(() => {
141
+ cleanup();
142
+ resolve({
143
+ name: "multicast_bind",
144
+ status: STATUS.BLOCKER,
145
+ detail: `bind+addMembership timed out after ${timeoutMs}ms`,
146
+ data: { timeoutMs },
147
+ });
148
+ }, timeoutMs);
149
+ try {
150
+ socket.bind(0, "0.0.0.0", () => {
151
+ try {
152
+ socket.addMembership(MDNS_GROUP);
153
+ cleanup();
154
+ resolve({
155
+ name: "multicast_bind",
156
+ status: STATUS.OK,
157
+ detail: `bound + joined ${MDNS_GROUP} on ephemeral port`,
158
+ data: { group: MDNS_GROUP },
159
+ });
160
+ } catch (err) {
161
+ cleanup();
162
+ resolve({
163
+ name: "multicast_bind",
164
+ status: STATUS.BLOCKER,
165
+ detail: `addMembership(${MDNS_GROUP}) failed: ${err.code || err.message}`,
166
+ data: { errno: err.errno, code: err.code },
167
+ });
168
+ }
169
+ });
170
+ } catch (err) {
171
+ cleanup();
172
+ resolve({
173
+ name: "multicast_bind",
174
+ status: STATUS.BLOCKER,
175
+ detail: `socket.bind sync threw: ${err.message}`,
176
+ data: { errno: err.errno, code: err.code },
177
+ });
178
+ }
179
+ });
180
+ }
181
+
182
+ // ─── 3. port5353Holders (Linux only) ─────────────────────────
183
+
184
+ /**
185
+ * Parse /proc/net/udp to find what's holding port 5353. Linux-only —
186
+ * non-Linux platforms return SKIP. Output: list of `{ uid, inode }`
187
+ * placeholders (mapping inode→process needs root, so we report inode
188
+ * + hint to use `ss -lup` for resolution).
189
+ */
190
+ export function checkPort5353Holders(parseProcText) {
191
+ if (os.platform() !== "linux") {
192
+ return {
193
+ name: "port_5353_holders",
194
+ status: STATUS.SKIP,
195
+ detail: "/proc/net/udp parser is Linux-only",
196
+ data: {},
197
+ };
198
+ }
199
+ let raw;
200
+ try {
201
+ if (typeof parseProcText === "string") {
202
+ // Injected for tests.
203
+ raw = parseProcText;
204
+ } else {
205
+ raw = fs.readFileSync("/proc/net/udp", "utf-8");
206
+ }
207
+ } catch (err) {
208
+ return {
209
+ name: "port_5353_holders",
210
+ status: STATUS.WARNING,
211
+ detail: `read /proc/net/udp failed: ${err.message}`,
212
+ data: {},
213
+ };
214
+ }
215
+ // /proc/net/udp format:
216
+ // sl local_address rem_address st tx_q rx_q tr tm uid timeout inode ref ...
217
+ // local_address column 2 is "AABBCCDD:PPPP" hex (little-endian addr + port).
218
+ const lines = raw.split("\n").slice(1);
219
+ const holders = [];
220
+ const portHex = MDNS_PORT.toString(16).toUpperCase().padStart(4, "0");
221
+ for (const line of lines) {
222
+ const trimmed = line.trim();
223
+ if (!trimmed) continue;
224
+ const cols = trimmed.split(/\s+/);
225
+ if (cols.length < 11) continue;
226
+ const localAddr = cols[1]; // "AABBCCDD:PPPP"
227
+ const parts = localAddr.split(":");
228
+ if (parts.length !== 2) continue;
229
+ if (parts[1] !== portHex) continue;
230
+ const uid = parseInt(cols[7], 10);
231
+ const inode = cols[9];
232
+ holders.push({ uid, inode });
233
+ }
234
+ if (holders.length === 0) {
235
+ return {
236
+ name: "port_5353_holders",
237
+ status: STATUS.OK,
238
+ detail: "port 5353 unbound — bonjour-service can advertise freely",
239
+ data: { holders: [] },
240
+ };
241
+ }
242
+ // Holders ≠ blocker because `reuseAddr:true` lets multiple peers bind.
243
+ // But it's a warning because if avahi-daemon owns the port with a TXT
244
+ // record schema we don't match, there may be advertise conflicts.
245
+ return {
246
+ name: "port_5353_holders",
247
+ status: STATUS.WARNING,
248
+ detail: `${holders.length} other process holding port 5353 (likely avahi-daemon). bonjour-service should still work via SO_REUSEADDR; run \`ss -lup sport = :5353\` to identify`,
249
+ data: { holders },
250
+ };
251
+ }
252
+
253
+ // ─── 4. platform / linux release ─────────────────────────────
254
+
255
+ export function parseLinuxOsRelease(text) {
256
+ const out = {};
257
+ for (const line of String(text || "").split("\n")) {
258
+ const trimmed = line.trim();
259
+ if (!trimmed || trimmed.startsWith("#")) continue;
260
+ const eq = trimmed.indexOf("=");
261
+ if (eq < 0) continue;
262
+ const key = trimmed.slice(0, eq).trim();
263
+ let val = trimmed.slice(eq + 1).trim();
264
+ if (val.startsWith('"') && val.endsWith('"')) {
265
+ val = val.slice(1, -1);
266
+ }
267
+ out[key] = val;
268
+ }
269
+ return out;
270
+ }
271
+
272
+ function checkPlatform() {
273
+ const platform = os.platform();
274
+ const data = { platform, arch: os.arch(), release: os.release() };
275
+ if (platform === "linux") {
276
+ try {
277
+ const text = fs.readFileSync("/etc/os-release", "utf-8");
278
+ const parsed = parseLinuxOsRelease(text);
279
+ data.distro = {
280
+ id: parsed.ID || null,
281
+ idLike: parsed.ID_LIKE || null,
282
+ name: parsed.PRETTY_NAME || parsed.NAME || null,
283
+ version: parsed.VERSION_ID || null,
284
+ };
285
+ } catch (_err) {
286
+ data.distro = null;
287
+ }
288
+ }
289
+ return {
290
+ name: "platform",
291
+ status: STATUS.OK,
292
+ detail: data.distro?.name || `${platform} ${data.release}`,
293
+ data,
294
+ };
295
+ }
296
+
297
+ // ─── 5. firewall hint ────────────────────────────────────────
298
+
299
+ /**
300
+ * Detect which Linux firewall tool is installed. Returns the FIRST tool
301
+ * found — most distros have only one in PATH. Defensive: never errors
302
+ * (returns null when none found).
303
+ */
304
+ export function detectFirewallTool(whichRunner = which) {
305
+ const candidates = ["ufw", "firewall-cmd", "nft", "iptables"];
306
+ for (const tool of candidates) {
307
+ if (whichRunner(tool)) return tool;
308
+ }
309
+ return null;
310
+ }
311
+
312
+ function which(cmd) {
313
+ try {
314
+ const platform = os.platform();
315
+ const probe = platform === "win32" ? "where" : "which";
316
+ execSync(`${probe} ${cmd}`, { stdio: "pipe", windowsHide: true });
317
+ return true;
318
+ } catch (_err) {
319
+ return false;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Build distro-appropriate command template for opening UDP 5353. Static
325
+ * strings — caller copies and runs. Never executes.
326
+ */
327
+ export function firewallCommandTemplate(tool) {
328
+ switch (tool) {
329
+ case "ufw":
330
+ return [
331
+ "# Open UDP 5353 (mDNS) on ufw:",
332
+ "sudo ufw allow 5353/udp",
333
+ "# Verify:",
334
+ "sudo ufw status numbered",
335
+ ].join("\n");
336
+ case "firewall-cmd":
337
+ return [
338
+ "# Open UDP 5353 (mDNS) on firewalld:",
339
+ "sudo firewall-cmd --add-port=5353/udp --permanent",
340
+ "sudo firewall-cmd --reload",
341
+ "# Verify:",
342
+ "sudo firewall-cmd --list-ports",
343
+ ].join("\n");
344
+ case "nft":
345
+ return [
346
+ "# Open UDP 5353 (mDNS) on nftables:",
347
+ "# Edit /etc/nftables.conf, add to inet filter input chain:",
348
+ "# udp dport 5353 accept",
349
+ "sudo systemctl reload nftables",
350
+ ].join("\n");
351
+ case "iptables":
352
+ return [
353
+ "# Open UDP 5353 (mDNS) on iptables:",
354
+ "sudo iptables -A INPUT -p udp --dport 5353 -j ACCEPT",
355
+ "# Persist (varies by distro):",
356
+ "# Debian/Ubuntu: sudo iptables-save > /etc/iptables/rules.v4",
357
+ "# RHEL/CentOS: sudo iptables-save > /etc/sysconfig/iptables",
358
+ ].join("\n");
359
+ default:
360
+ return null;
361
+ }
362
+ }
363
+
364
+ function checkFirewall() {
365
+ if (os.platform() !== "linux") {
366
+ return {
367
+ name: "firewall_hint",
368
+ status: STATUS.SKIP,
369
+ detail: "firewall hint only generated for Linux",
370
+ data: {},
371
+ };
372
+ }
373
+ const tool = detectFirewallTool();
374
+ if (!tool) {
375
+ return {
376
+ name: "firewall_hint",
377
+ status: STATUS.WARNING,
378
+ detail:
379
+ "no firewall tool detected in PATH (ufw/firewall-cmd/nft/iptables) — check your distro's firewall guide",
380
+ data: { tool: null },
381
+ };
382
+ }
383
+ return {
384
+ name: "firewall_hint",
385
+ status: STATUS.OK,
386
+ detail: `detected ${tool} — run \`cc pair preflight\` with --show-firewall to see commands`,
387
+ data: { tool, commands: firewallCommandTemplate(tool) },
388
+ };
389
+ }
390
+
391
+ // ─── orchestrator ────────────────────────────────────────────
392
+
393
+ /**
394
+ * Run all 5 checks. Returns `{ checks, summary, exitCode }`.
395
+ *
396
+ * exitCode 0 — all OK (or only SKIP)
397
+ * exitCode 1 — any WARNING
398
+ * exitCode 2 — any BLOCKER
399
+ */
400
+ export async function runPreflight(options = {}) {
401
+ const checks = [];
402
+ checks.push(checkPlatform());
403
+ checks.push(checkInterfaces());
404
+ checks.push(await checkMulticastBind(options.multicastBindTimeoutMs));
405
+ checks.push(checkPort5353Holders());
406
+ checks.push(checkFirewall());
407
+
408
+ let exitCode = 0;
409
+ let warnings = 0;
410
+ let blockers = 0;
411
+ for (const c of checks) {
412
+ if (c.status === STATUS.BLOCKER) blockers += 1;
413
+ else if (c.status === STATUS.WARNING) warnings += 1;
414
+ }
415
+ if (blockers > 0) exitCode = 2;
416
+ else if (warnings > 0) exitCode = 1;
417
+
418
+ return {
419
+ checks,
420
+ summary: { blockers, warnings, ok: checks.length - blockers - warnings },
421
+ exitCode,
422
+ };
423
+ }
424
+
425
+ export { STATUS };