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.
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-B1dwBvzW.js → AIOps-5A54O3wF.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DfR4oLvh.js → ActionButton-epuY2GkZ.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D3ZYGXjr.js → Analytics-CIdxw7T5.js} +1 -1
- package/src/assets/web-panel/assets/{AppLayout-CsmOoh-7.js → AppLayout-xR6YUHsS.js} +2 -2
- package/src/assets/web-panel/assets/{Audit-B4gwDm63.js → Audit-D95nRcdZ.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-T42uSArV.js → Backup-Cmw7ZTJD.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CFi52nMs.js → BaseInput-DgCEAL_E.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-D7Vvok1V.js → Chat-DAQAzZsA.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Dwaflpww.js → Checkbox-GRxUANnE.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BIqx3G0b.js → Codegen--6KIDt1W.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DzIUYUNu.js → Col-3s6dxuMx.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DKePtzhk.js → Community-D60pkEBz.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CirVV9Wq.js → Compact-CQR4QsWP.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CrXOr0sy.js → Compliance-KySNEhMK.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BEBP6Tht.js → Cowork-Yrrf_Vuu.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-k7nNUuqh.js → Cron-DSuuSKIQ.js} +1 -1
- package/src/assets/web-panel/assets/{Crosschain-CBnX0Dhq.js → Crosschain-Dub0G9i4.js} +1 -1
- package/src/assets/web-panel/assets/{DID-B48FszWS.js → DID-CbpCGV51.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-Pb3qfFpp.js → Dashboard-D29e0y01.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-pUHy4CQ2.js → Dropdown-DZY7X5v2.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CT0Qs-kR.js → Federation-dmrLzzMB.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CbJJp5BR.js → FormItemContext-EopsCGez.js} +1 -1
- package/src/assets/web-panel/assets/{Git-B3mGNLQe.js → Git-D3mb4RhK.js} +1 -1
- package/src/assets/web-panel/assets/{Governance-BKf4733q.js → Governance-8v034-Nr.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DZjU541G.js → Inference-Ddl-HQwQ.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-C8L-7Dd1.js → KnowledgeGraph-DrRruyud.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CwDwOiKv.js → Logs-Bsu6lYQe.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-C2YWWU0M.js → Marketplace-Iyf2Xc23.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-dIbkOypF.js → McpTools-ljVE1lIN.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-7eF8WzcY.js → Memory-Ca48TtDU.js} +1 -1
- package/src/assets/web-panel/assets/{MobileBridge-C74GHLbX.js → MobileBridge-DZ3Ar6da.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CSEDo5Fo.js → Mtc-CRjBnVAT.js} +1 -1
- package/src/assets/web-panel/assets/{MtcAudit-DiJXxOrB.js → MtcAudit-CEC2eKez.js} +1 -1
- package/src/assets/web-panel/assets/Multisig-BHoQvNeL.js +7 -0
- package/src/assets/web-panel/assets/Multisig-kwPDnXnl.css +1 -0
- package/src/assets/web-panel/assets/{NLProgramming-DjF-gIUw.js → NLProgramming-9e5hIFvL.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BUE5CvMO.js → Notes-BEhZRadw.js} +1 -1
- package/src/assets/web-panel/assets/{NotificationSettings-Dfbrobje.js → NotificationSettings-CzPbT9UE.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-C6YvqjQB.js → Organization-CvFg4VAr.js} +1 -1
- package/src/assets/web-panel/assets/{Overflow-BvHNhdMR.js → Overflow-Cm7ISeJl.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BO0hQHFS.js → P2P-BWN-wdfH.js} +1 -1
- package/src/assets/web-panel/assets/{Permissions-CCPlrJeP.js → Permissions-C4vKkP_t.js} +1 -1
- package/src/assets/web-panel/assets/{Pipeline-DTCL3FjJ.js → Pipeline-DQCShDeT.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-08DYgOe_.js → Privacy-Nc7LlSr3.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-B7j-Z8sa.js → ProjectInit-DDkqXAED.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectSettings-CFqLhV1w.js → ProjectSettings-ia6GQr6A.js} +1 -1
- package/src/assets/web-panel/assets/{Projects-BPlpx2UN.js → Projects-CNqXa5XY.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BCBPbVbF.js → Providers-EwarbUb8.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-C8Job6zl.js → QuickAsk-BAHQBw0d.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DOV_Aje-.js → Recommend-Ccgu6elV.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-DOgK_dIK.js → Reputation-ai0gRNpj.js} +1 -1
- package/src/assets/web-panel/assets/{Row-8jdU1xYg.js → Row-BWLfAhHC.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B289OgNZ.js → RssFeed-CnuRtiQH.js} +1 -1
- package/src/assets/web-panel/assets/{Search-cr0AndFE.js → Search-Da0CvI_2.js} +1 -1
- package/src/assets/web-panel/assets/{Security-w4diykaE.js → Security-DEFeOeUv.js} +1 -1
- package/src/assets/web-panel/assets/{Services-dTxAI5cG.js → Services-CsfvCqCH.js} +1 -1
- package/src/assets/web-panel/assets/{Skeleton-B3FiUiRo.js → Skeleton-DlRaGj_n.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DgtBTAFC.js → Skills-fjlZrsYq.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-B4Y6bvbL.js → Sla-DNSDuEt2.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C8FEvArc.js → SpeechSettings-Bj0t-JCf.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CHvV5L0m.js → SyncSettings-uO7Gy-BB.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-B4Py5ORS.js → Tasks-BSjsO-m8.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-MeTyXM5u.js → Templates-B03SSuXn.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-Doiz7wyQ.js → Tenant-CTCPIBzq.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DSEAdwWN.js → Terminal-CQZcdArx.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BBrtaEKt.js → Tokens-CsNJIdNl.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-nPvjho27.js → Trigger-kYPQmm6P.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BFwPyS_6.js → Trust-mt-MiBeI.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DFsHoY1J.js → UkeySign-DBi8jgXw.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-FkSKqHE9.js → VideoEditing-CINSMEf5.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-AXo-_4-w.js → Wallet-DCVMgW0U.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-DoScnQ-4.js → WebAuthn-C9OjbTBs.js} +1 -1
- package/src/assets/web-panel/assets/{WorkflowEditor-IEMfIax-.js → WorkflowEditor-LM1HJs-S.js} +1 -1
- package/src/assets/web-panel/assets/{chat-pc1ciH6T.js → chat-Dz68Ixdp.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CjkIkB0e.js → colors-qf63Eax9.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CQZnkP5p.js → compact-item-r4YWRFGQ.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-B1McGnV-.js → createContext-C8Lwrn-C.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-CbkA6peP.js → hasIn-DsPi2kPP.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8r6OBuk.js → index-1xqIvVM5.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dn7SHV2e.js → index-5nOMJaLt.js} +1 -1
- package/src/assets/web-panel/assets/{index-ChCCGHwz.js → index-B0KxtBNy.js} +1 -1
- package/src/assets/web-panel/assets/index-B3blziag.js +1 -0
- package/src/assets/web-panel/assets/{index-CGke5qZp.js → index-B8gs4g1v.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHpwwlmv.js → index-BFV6aAoX.js} +1 -1
- package/src/assets/web-panel/assets/{index-DWq64zmv.js → index-BKEl9Ahm.js} +1 -1
- package/src/assets/web-panel/assets/{index-NRWBOo4F.js → index-BfdIkZnT.js} +1 -1
- package/src/assets/web-panel/assets/{index-fqI1KnP_.js → index-BjPHuhxG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHtiecyM.js → index-BtCSUUKm.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjxVsyn_.js → index-BxNXO95B.js} +1 -1
- package/src/assets/web-panel/assets/{index-DGb36exe.js → index-Bz1O2KhE.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcSe5y5O.js → index-BzzFMBIM.js} +1 -1
- package/src/assets/web-panel/assets/{index-uWvLy_3T.js → index-C2OFaKmx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTyhXFDW.js → index-C3VVwJcn.js} +1 -1
- package/src/assets/web-panel/assets/{index-C052wi94.js → index-C8SY0_S8.js} +1 -1
- package/src/assets/web-panel/assets/{index-wwQ_ZkWN.js → index-CCCX2ZSH.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPzT5L14.js → index-CEbbycgJ.js} +3 -3
- package/src/assets/web-panel/assets/{index-CzZ3LxPK.js → index-CINVudo7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKwkP66s.js → index-CKRXnUTD.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4rhVRSV.js → index-CSCb-EY9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMjPzhsp.js → index-CY5X4uXO.js} +1 -1
- package/src/assets/web-panel/assets/{index-5c4JzwY3.js → index-CqmyLTa_.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEENN81t.js → index-Cw48kkkH.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0Lj7Yl0.js → index-D0mFFS7Y.js} +1 -1
- package/src/assets/web-panel/assets/index-D1XYuPHf.js +1 -0
- package/src/assets/web-panel/assets/{index-CKledOCh.js → index-D2BR3WQ-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsChgzu2.js → index-D4XgKgOF.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5mpCgak.js → index-DBpVVgRI.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bs-romIz.js → index-DGXVjiyF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNLeseG1.js → index-DQSweK5-.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJr12ypE.js → index-DXBc_lyR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFCQl1hk.js → index-DycrmZ_r.js} +1 -1
- package/src/assets/web-panel/assets/{index-Di5tuS0d.js → index-I55BAmVG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJbqE5Sw.js → index-LQYz2tRO.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYAWIXFt.js → index-cTFVNgoS.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dzfj71tf.js → index-qLyVEtRM.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJWYN-AT.js → index-qcVx0s-M.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjM0jsm1.js → index-wVpfPJXE.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BTloFqyf.js → initDefaultProps-CAgx2aHm.js} +1 -1
- package/src/assets/web-panel/assets/{motion-44iMBc1o.js → motion-DFuREFOd.js} +1 -1
- package/src/assets/web-panel/assets/{move-CyWQI5eW.js → move-DfcQIXuy.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CXfJtuFy.js → omit-BFZV2hmZ.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-ANFpSZes.js → pickAttrs-D98WQSur.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CigX-url.js → placementArrow-BlP2AN9q.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Ch48RA0m.js → responsiveObserve-DuI2xGQd.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BEz3LORF.js → slide-DSvqah_S.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-D9EniG8V.js → statusUtils-B-96ZLWF.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-C1IvuY3L.js → styleChecker-Br0YrCOO.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-C92oHeXB.js → useFlexGapSupport-BD4Xr9XU.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CJhvFpgB.js → useFs-Ch5L3BnA.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BPkyunN_.js → vnode-BQi3uWRC.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-B6Ipb64r.js → zoom-DaW-jbu7.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/crosschain.js +403 -1
- package/src/commands/pair.js +291 -0
- package/src/index.js +2 -0
- package/src/lib/cross-chain-mtc.js +275 -6
- package/src/lib/cross-chain.js +48 -2
- package/src/lib/lan-pairing-preflight.js +425 -0
- package/src/lib/lan-pairing-tokens.js +264 -0
- package/src/assets/web-panel/assets/Multisig-D-IuEDLa.css +0 -1
- package/src/assets/web-panel/assets/Multisig-FZTU5ri6.js +0 -1
- package/src/assets/web-panel/assets/index-BXfePRef.js +0 -1
- package/src/assets/web-panel/assets/index-Dc4fj_Ys.js +0 -1
package/src/lib/cross-chain.js
CHANGED
|
@@ -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
|
-
|
|
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 };
|