lurqrun 0.1.7 → 0.1.9
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/README.md +312 -148
- package/dist/{auditHandler-JCOJSUYW.js → auditHandler-G7PHKMZR.js} +12 -11
- package/dist/autopilotInit-JO2SH5K3.js +266 -0
- package/dist/bin/lurq.js +238 -83
- package/dist/{browserAuth-B6G27USY.js → browserAuth-PVYODBNR.js} +4 -3
- package/dist/{cache-DKI34SVS.js → cache-ND4VOUVY.js} +2 -2
- package/dist/check-A3FJE62G.js +2613 -0
- package/dist/{check-2FDGUOV7.js → check-PQSQ2MMA.js} +1 -1
- package/dist/checkUpgrade-Q4CV7GXG.js +24 -0
- package/dist/{chunk-BCANRN5B.js → chunk-2GAYNIHG.js} +8 -1
- package/dist/chunk-2LXYRDUN.js +92 -0
- package/dist/chunk-2XMD4H2I.js +243 -0
- package/dist/chunk-3D2MPGOQ.js +181 -0
- package/dist/chunk-53SG6XWA.js +141 -0
- package/dist/chunk-5ABNDB3I.js +37 -0
- package/dist/chunk-5ALIJFDA.js +114 -0
- package/dist/{chunk-LYMPYETA.js → chunk-6353IJJ5.js} +270 -6
- package/dist/{chunk-M6NYEXCW.js → chunk-64DPQSXV.js} +1 -1
- package/dist/chunk-64YOP5BQ.js +320 -0
- package/dist/{chunk-2VMOWSRY.js → chunk-6DM6UQFD.js} +19 -1
- package/dist/{chunk-NH5I4KHH.js → chunk-75LMEMJ3.js} +1 -5
- package/dist/{chunk-FW6BD2NO.js → chunk-ABH4BTK4.js} +458 -121
- package/dist/chunk-AHRDDKNO.js +90 -0
- package/dist/chunk-AQ6IVSFB.js +119 -0
- package/dist/chunk-BM3LF5T6.js +132 -0
- package/dist/chunk-D3K6IZVR.js +12845 -0
- package/dist/chunk-DONTW3NE.js +171 -0
- package/dist/{chunk-UDVNA7EA.js → chunk-ETY4FSOW.js} +161 -8
- package/dist/{chunk-VL2E5T2A.js → chunk-FHRT32JH.js} +6 -3
- package/dist/{chunk-DWAGCNEL.js → chunk-GM5Y555O.js} +36 -103
- package/dist/{chunk-6L3U7DZ5.js → chunk-GNNGXVCG.js} +1 -1
- package/dist/{chunk-PSX3GIAN.js → chunk-HWFX4BC4.js} +2 -1
- package/dist/{checkUpgrade-F2WESBPM.js → chunk-ILKYIVYV.js} +72 -10
- package/dist/{chunk-HWBBVLOT.js → chunk-J3NUERU7.js} +1 -1
- package/dist/chunk-J3U2CPSK.js +136 -0
- package/dist/{chunk-Y5GI7RL3.js → chunk-J6XQ7OPZ.js} +9 -1
- package/dist/{chunk-3N3Q3DSW.js → chunk-JGIJZJ3X.js} +8 -8
- package/dist/{chunk-DB2VF5UD.js → chunk-JJCQVOC6.js} +16 -11
- package/dist/{chunk-LBFGSSF2.js → chunk-KMV4Y6R3.js} +2 -1
- package/dist/{chunk-L6N4JFW2.js → chunk-KP5ZDDPE.js} +81 -4
- package/dist/{chunk-CW5HJ3LX.js → chunk-KT7PWDNV.js} +1 -1
- package/dist/{chunk-SJOVFF7B.js → chunk-KYKSZ3XZ.js} +3 -2
- package/dist/{chunk-GPTKSNR6.js → chunk-L3V7AVM6.js} +2 -1
- package/dist/{chunk-CUP36ND3.js → chunk-M2CYYBAH.js} +145 -36
- package/dist/{chunk-IQ4SDKKY.js → chunk-N2NYPTRU.js} +31 -228
- package/dist/chunk-N4TJO44E.js +603 -0
- package/dist/chunk-NEZNAPPN.js +41 -0
- package/dist/{chunk-WQ23REYT.js → chunk-NGMJQBOV.js} +1 -1
- package/dist/{chunk-BULZAX2U.js → chunk-POHTPT37.js} +55 -38
- package/dist/{chunk-EA3DP5HG.js → chunk-PZZU3FQY.js} +155 -25
- package/dist/{chunk-4F7ZESPO.js → chunk-Q7CMOU5C.js} +3 -5
- package/dist/{chunk-LRD4GBYE.js → chunk-QUIMQ7YL.js} +1 -1
- package/dist/chunk-RG3YLU6E.js +49 -0
- package/dist/{chunk-4ZYCIMY4.js → chunk-SFY5MPJE.js} +25 -4
- package/dist/{chunk-ST2X2GYN.js → chunk-SHZFOSXC.js} +109 -20
- package/dist/{chunk-CIDWNUAP.js → chunk-SOXFMMXC.js} +8 -2
- package/dist/{chunk-B43LCXBM.js → chunk-SWMWVK2R.js} +81 -16
- package/dist/{chunk-J5FZYN4A.js → chunk-TVOCVIM7.js} +1 -1
- package/dist/{chunk-U5AT3TD4.js → chunk-X4W64MHU.js} +1735 -1858
- package/dist/{chunk-BV32UNLL.js → chunk-XA2SLNBR.js} +2 -2
- package/dist/{chunk-AHBP533R.js → chunk-XBTYJQEK.js} +7 -5
- package/dist/chunk-XGOV6KV4.js +413 -0
- package/dist/{chunk-OQS4GUSI.js → chunk-XIFQIFSZ.js} +210 -661
- package/dist/{chunk-XBIDMHSK.js → chunk-XLXPVN3Y.js} +2 -34
- package/dist/{chunk-LO35HLQZ.js → chunk-ZJ5XHA77.js} +25 -4
- package/dist/{client-IRKAMDYZ.js → client-SJIWK4CK.js} +3 -3
- package/dist/{commands-FIL5ET6P.js → commands-6BBLN5N5.js} +145 -33
- package/dist/{compat-R3UTFQ5Q.js → compat-6B3CRT5F.js} +6 -6
- package/dist/constants-HX3RTD6Q.js +24 -0
- package/dist/{dashboardIssue-WYYMBFIB.js → dashboardIssue-KBKLQMGC.js} +105 -23
- package/dist/{diff-GZA2774U.js → diff-I4SNUHU3.js} +1 -1
- package/dist/domains-RKRPVP5A.js +14 -0
- package/dist/env-UN6KGZ7S.js +15 -0
- package/dist/envCheck-IL22OTSC.js +71 -0
- package/dist/{extract-NKGKLIVT.js → extract-6WG265BL.js} +14 -3
- package/dist/{fetch-QYTUJVG2.js → fetch-GHIP3VHN.js} +4 -4
- package/dist/{firstTouch-QQTJH4QX.js → firstTouch-M64T2MWF.js} +1 -1
- package/dist/fix-Z7ZY55HB.js +220 -0
- package/dist/{handlers-GEJAVZDK.js → handlers-GOXW2GWZ.js} +19 -17
- package/dist/hook-TWFLIDK3.js +412 -0
- package/dist/{http-ETPYMBGD.js → http-7VF2RE2D.js} +2097 -509
- package/dist/{ingestQueue-VKN4KBIF.js → ingestQueue-RSEPMORK.js} +14 -12
- package/dist/{install-EK3W2COX.js → install-55EPX34C.js} +6 -6
- package/dist/{installSkill-VQJWJVCL.js → installSkill-A7KKPDMS.js} +21 -3
- package/dist/manifest-ENMIC54B.js +13 -0
- package/dist/{mcp-3JHD5SUZ.js → mcp-CM6CDLHM.js} +60 -32
- package/dist/mcpConfig-FENV6NI7.js +80 -0
- package/dist/{mcpHandlers-QFN4MCFL.js → mcpHandlers-2UCLKMWN.js} +7 -7
- package/dist/{mcpScan-QZHWENIP.js → mcpScan-CKBQNK5H.js} +306 -442
- package/dist/{mcpScanWorkflow-JPYCBGC4.js → mcpScanWorkflow-NFUD3S7U.js} +29 -12
- package/dist/{mcpStack-BYJCZNIV.js → mcpStack-FLQ6XY2P.js} +1 -1
- package/dist/model-ASMZWLM7.js +214 -0
- package/dist/{packages-THVFMWFP.js → packages-2XRHNBJ7.js} +3 -5
- package/dist/{plan-V4OEI3J4.js → plan-XRZS6C7Z.js} +23 -21
- package/dist/{planView-JRXGCG2N.js → planView-ZAKRTGVS.js} +4 -1
- package/dist/{policy-3P6BGRDN.js → policy-EQT43P7S.js} +5 -5
- package/dist/probe-ZEZFNIQZ.js +610 -0
- package/dist/publicUpgrades-JZEOV24A.js +42 -0
- package/dist/{references-6N7VEPHT.js → references-I6Y4BSRR.js} +3 -1
- package/dist/{release-4U4ABNSJ.js → release-EUOM5I3R.js} +5 -5
- package/dist/{remote-YGCXYI4V.js → remote-I5O43WAY.js} +13 -3
- package/dist/rename-OS6JYBZQ.js +204 -0
- package/dist/{reportRuns-CKQQYLY4.js → reportRuns-O4X7ZFTX.js} +15 -2
- package/dist/safeFetch-COERXGFN.js +19 -0
- package/dist/{sandbox-MEZLAODF.js → sandbox-46TZNYZT.js} +4 -4
- package/dist/sarif-WLVJFJ7U.js +125 -0
- package/dist/server-LROMHS62.js +59 -0
- package/dist/{surface-YL6O67W3.js → surface-CZMHQLV3.js} +5 -3
- package/dist/{surface-V62GE554.js → surface-OH4TZR32.js} +18 -16
- package/dist/{surfaceHandlers-X2IZKA3E.js → surfaceHandlers-BDYRZMAC.js} +5 -5
- package/dist/{typecheck-X7U5ELMR.js → typecheck-NGOKNAJY.js} +23 -9
- package/dist/{uninstall-P7RGBX6G.js → uninstall-4E5YUDQO.js} +30 -4
- package/dist/{upgrade-NQ5IWFZB.js → upgrade-S72W7JVC.js} +86 -32
- package/dist/{upgradePlan-A4LRKLEG.js → upgradePlan-F3OUNL3E.js} +46 -11
- package/dist/upkeepHandler-7Q56A4UA.js +13 -0
- package/dist/{userConfig-Z3GQE776.js → userConfig-ICASHI47.js} +1 -1
- package/dist/{watch-DGFMWUDL.js → watch-LZPOKWCL.js} +24 -34
- package/package.json +3 -2
- package/templates/skill-instructions.md +22 -3
- package/dist/chunk-6AAEVQ7I.js +0 -6336
- package/dist/chunk-I2JUKN6Z.js +0 -24
- package/dist/server-KFUD5X57.js +0 -55
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createRequire as __lurqCreateRequire } from 'node:module'; const require = __lurqCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
SCAN_LIMIT,
|
|
4
|
+
fixableTargets,
|
|
5
|
+
modeDisagreement,
|
|
6
|
+
parseUpgradeSpec,
|
|
7
|
+
planMode,
|
|
8
|
+
planUpgrades,
|
|
9
|
+
runCheckUpgrade,
|
|
10
|
+
targetsFromPlanFile,
|
|
11
|
+
targetsFromUpgrades
|
|
12
|
+
} from "./chunk-ILKYIVYV.js";
|
|
13
|
+
import "./chunk-I3M2KJXB.js";
|
|
14
|
+
export {
|
|
15
|
+
SCAN_LIMIT,
|
|
16
|
+
fixableTargets,
|
|
17
|
+
modeDisagreement,
|
|
18
|
+
parseUpgradeSpec,
|
|
19
|
+
planMode,
|
|
20
|
+
planUpgrades,
|
|
21
|
+
runCheckUpgrade,
|
|
22
|
+
targetsFromPlanFile,
|
|
23
|
+
targetsFromUpgrades
|
|
24
|
+
};
|
|
@@ -8,8 +8,14 @@ init_esm_shims();
|
|
|
8
8
|
var DEFAULT_REPO_POLICY = {
|
|
9
9
|
enabled: false,
|
|
10
10
|
scope: "blocking",
|
|
11
|
-
autoMerge: false
|
|
11
|
+
autoMerge: false,
|
|
12
|
+
// Stated rather than relied upon. `permits()` already reads an absent
|
|
13
|
+
// `checks` as on, so a repo connected before this shipped behaves the same as
|
|
14
|
+
// a new one — which is the point of the inversion. Writing it here keeps the
|
|
15
|
+
// default visible in the policy the dashboard round-trips.
|
|
16
|
+
checks: { env: true }
|
|
12
17
|
};
|
|
18
|
+
var RUN_TRIGGERS = ["schedule", "dispatch", "push", "pull_request", "other"];
|
|
13
19
|
var UPGRADE_SEVERITIES = ["blocking", "warning", "ok", "unverified"];
|
|
14
20
|
var UPGRADE_RUN_STATUSES = [
|
|
15
21
|
"checked",
|
|
@@ -25,6 +31,7 @@ var TRANSITIVE_DETAIL_CAP = 100;
|
|
|
25
31
|
|
|
26
32
|
export {
|
|
27
33
|
DEFAULT_REPO_POLICY,
|
|
34
|
+
RUN_TRIGGERS,
|
|
28
35
|
UPGRADE_SEVERITIES,
|
|
29
36
|
UPGRADE_RUN_STATUSES,
|
|
30
37
|
REPO_MANIFEST_CAP,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createRequire as __lurqCreateRequire } from 'node:module'; const require = __lurqCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
init_esm_shims
|
|
4
|
+
} from "./chunk-I3M2KJXB.js";
|
|
5
|
+
|
|
6
|
+
// src/fix/domains.ts
|
|
7
|
+
init_esm_shims();
|
|
8
|
+
var DOMAIN_SCAN_LIMIT = 2e4;
|
|
9
|
+
var DOMAINS = {
|
|
10
|
+
/** Variables the code reads that no .env file declares. No key, no network. */
|
|
11
|
+
env: async (root, opts) => {
|
|
12
|
+
const { envFindings } = await import("./env-UN6KGZ7S.js");
|
|
13
|
+
const plan = envFindings(root, { limit: opts.limit ?? DOMAIN_SCAN_LIMIT });
|
|
14
|
+
return { findings: plan.findings, truncated: plan.truncated };
|
|
15
|
+
},
|
|
16
|
+
/**
|
|
17
|
+
* Model identifiers the provider has retired or put an end date on.
|
|
18
|
+
*
|
|
19
|
+
* Reads source only — no network, no key, no arguments. That is why it runs
|
|
20
|
+
* by default: the check costs nothing the scan is not already paying, and
|
|
21
|
+
* the failure it catches is one nothing else in the toolchain can see.
|
|
22
|
+
*/
|
|
23
|
+
model: async (root, opts) => {
|
|
24
|
+
const { modelFindings } = await import("./model-ASMZWLM7.js");
|
|
25
|
+
const plan = modelFindings(root, { limit: opts.limit ?? DOMAIN_SCAN_LIMIT });
|
|
26
|
+
return { findings: plan.findings, truncated: plan.truncated };
|
|
27
|
+
},
|
|
28
|
+
/**
|
|
29
|
+
* Renames the package itself proves, the manifest ranges an upgrade leaves
|
|
30
|
+
* stale, and a brief for everything that needs judgement.
|
|
31
|
+
*
|
|
32
|
+
* Reads the two published versions from npm, so it needs the network but no
|
|
33
|
+
* API key — the same contract `check-upgrade` advertises.
|
|
34
|
+
*/
|
|
35
|
+
package: async (root, opts) => {
|
|
36
|
+
const targets = opts.upgrade ?? [];
|
|
37
|
+
if (targets.length === 0) {
|
|
38
|
+
return {
|
|
39
|
+
findings: [],
|
|
40
|
+
skipped: "no upgrades given. Pass `upgrade` with the versions to assess, or run `lurq fix` in the project to have them worked out from the index."
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const { scanReferences } = await import("./references-I6Y4BSRR.js");
|
|
44
|
+
const { checkUpgrade } = await import("./upgrade-S72W7JVC.js");
|
|
45
|
+
const { renamePlan } = await import("./rename-OS6JYBZQ.js");
|
|
46
|
+
const { manifestFindings } = await import("./manifest-ENMIC54B.js");
|
|
47
|
+
const stats = { files: 0, truncated: false };
|
|
48
|
+
const refs = scanReferences(root, { limit: opts.limit ?? DOMAIN_SCAN_LIMIT, stats });
|
|
49
|
+
const report = await checkUpgrade(targets, refs, { rootDir: root });
|
|
50
|
+
return {
|
|
51
|
+
findings: [...renamePlan(report).findings, ...manifestFindings(root, targets).findings],
|
|
52
|
+
truncated: stats.truncated
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var AVAILABLE = ["env", "model", "package"];
|
|
57
|
+
var SOURCE_DOMAINS = AVAILABLE.filter((d) => d !== "package");
|
|
58
|
+
async function runDomains(opts = {}) {
|
|
59
|
+
const root = opts.dir?.trim() || process.cwd();
|
|
60
|
+
const wanted = opts.domains?.length ? opts.domains : AVAILABLE;
|
|
61
|
+
const findings = [];
|
|
62
|
+
const ran = [];
|
|
63
|
+
const skipped = [];
|
|
64
|
+
let truncated = false;
|
|
65
|
+
for (const domain of wanted) {
|
|
66
|
+
const run = DOMAINS[domain];
|
|
67
|
+
if (!run) {
|
|
68
|
+
skipped.push({ domain, reason: `lurq has no detector for the ${domain} domain yet` });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const result = await run(root, opts);
|
|
73
|
+
if (result.skipped) {
|
|
74
|
+
skipped.push({ domain, reason: result.skipped });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
findings.push(...result.findings);
|
|
78
|
+
if (result.truncated) truncated = true;
|
|
79
|
+
ran.push(domain);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
skipped.push({ domain, reason: err instanceof Error ? err.message : String(err) });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { root, ran, skipped, findings, truncated };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
DOMAIN_SCAN_LIMIT,
|
|
89
|
+
AVAILABLE,
|
|
90
|
+
SOURCE_DOMAINS,
|
|
91
|
+
runDomains
|
|
92
|
+
};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { createRequire as __lurqCreateRequire } from 'node:module'; const require = __lurqCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
mcpChangeEvents,
|
|
4
|
+
mcpContracts,
|
|
5
|
+
mcpDeployments,
|
|
6
|
+
mcpObservations,
|
|
7
|
+
mcpPublicReports
|
|
8
|
+
} from "./chunk-6353IJJ5.js";
|
|
9
|
+
import {
|
|
10
|
+
init_esm_shims
|
|
11
|
+
} from "./chunk-I3M2KJXB.js";
|
|
12
|
+
|
|
13
|
+
// src/db/mcpScans.ts
|
|
14
|
+
init_esm_shims();
|
|
15
|
+
import { and, desc, eq, inArray, isNull, sql } from "drizzle-orm";
|
|
16
|
+
async function putContract(db, row) {
|
|
17
|
+
await db.insert(mcpContracts).values(row).onConflictDoNothing();
|
|
18
|
+
}
|
|
19
|
+
async function getContract(db, contentHash) {
|
|
20
|
+
const [row] = await db.select().from(mcpContracts).where(eq(mcpContracts.contentHash, contentHash)).limit(1);
|
|
21
|
+
return row ?? null;
|
|
22
|
+
}
|
|
23
|
+
async function countDeployments(db, ownerId) {
|
|
24
|
+
const [row] = await db.select({ n: sql`count(*)::int` }).from(mcpDeployments).where(eq(mcpDeployments.ownerId, ownerId));
|
|
25
|
+
return row?.n ?? 0;
|
|
26
|
+
}
|
|
27
|
+
async function deploymentExists(db, ownerId, serverKey, configFingerprint) {
|
|
28
|
+
const [row] = await db.select({ id: mcpDeployments.id }).from(mcpDeployments).where(
|
|
29
|
+
and(
|
|
30
|
+
eq(mcpDeployments.ownerId, ownerId),
|
|
31
|
+
eq(mcpDeployments.serverKey, serverKey),
|
|
32
|
+
eq(mcpDeployments.configFingerprint, configFingerprint)
|
|
33
|
+
)
|
|
34
|
+
).limit(1);
|
|
35
|
+
return !!row;
|
|
36
|
+
}
|
|
37
|
+
async function recordScan(db, input, diff) {
|
|
38
|
+
return db.transaction(async (tx) => {
|
|
39
|
+
const now = /* @__PURE__ */ new Date();
|
|
40
|
+
const inserted = await tx.insert(mcpDeployments).values({
|
|
41
|
+
ownerId: input.ownerId,
|
|
42
|
+
serverKey: input.serverKey,
|
|
43
|
+
configFingerprint: input.configFingerprint,
|
|
44
|
+
alias: input.alias,
|
|
45
|
+
registry: input.registry,
|
|
46
|
+
packageName: input.packageName,
|
|
47
|
+
transport: input.transport,
|
|
48
|
+
lastStatus: input.status,
|
|
49
|
+
firstSeenAt: now,
|
|
50
|
+
lastScannedAt: now
|
|
51
|
+
}).onConflictDoNothing({
|
|
52
|
+
target: [
|
|
53
|
+
mcpDeployments.ownerId,
|
|
54
|
+
mcpDeployments.serverKey,
|
|
55
|
+
mcpDeployments.configFingerprint
|
|
56
|
+
]
|
|
57
|
+
}).returning({ id: mcpDeployments.id });
|
|
58
|
+
const isFirst = inserted.length > 0;
|
|
59
|
+
const [dep] = await tx.select().from(mcpDeployments).where(
|
|
60
|
+
and(
|
|
61
|
+
eq(mcpDeployments.ownerId, input.ownerId),
|
|
62
|
+
eq(mcpDeployments.serverKey, input.serverKey),
|
|
63
|
+
eq(mcpDeployments.configFingerprint, input.configFingerprint)
|
|
64
|
+
)
|
|
65
|
+
).for("update").limit(1);
|
|
66
|
+
if (!dep) throw new Error("deployment vanished inside its own transaction");
|
|
67
|
+
const [last] = await tx.select().from(mcpObservations).where(eq(mcpObservations.deploymentId, dep.id)).orderBy(desc(mcpObservations.id)).limit(1);
|
|
68
|
+
const same = last && last.status === input.status && last.contentHash === input.contentHash && last.serverVersion === input.serverVersion;
|
|
69
|
+
if (same) {
|
|
70
|
+
await tx.update(mcpObservations).set({ lastSeenAt: now, scanCount: sql`${mcpObservations.scanCount} + 1` }).where(eq(mcpObservations.id, last.id));
|
|
71
|
+
} else {
|
|
72
|
+
await tx.insert(mcpObservations).values({
|
|
73
|
+
deploymentId: dep.id,
|
|
74
|
+
ownerId: input.ownerId,
|
|
75
|
+
status: input.status,
|
|
76
|
+
contentHash: input.contentHash,
|
|
77
|
+
serverVersion: input.serverVersion,
|
|
78
|
+
error: input.error,
|
|
79
|
+
source: input.source,
|
|
80
|
+
firstSeenAt: now,
|
|
81
|
+
lastSeenAt: now
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
let change = isFirst ? "first" : "unchanged";
|
|
85
|
+
let event = null;
|
|
86
|
+
const moved = !isFirst && input.contentHash && dep.lastContentHash && input.contentHash !== dep.lastContentHash;
|
|
87
|
+
if (moved) {
|
|
88
|
+
const [previous] = await tx.select().from(mcpContracts).where(eq(mcpContracts.contentHash, dep.lastContentHash)).limit(1);
|
|
89
|
+
if (previous) {
|
|
90
|
+
const d = diff(previous);
|
|
91
|
+
await tx.insert(mcpChangeEvents).values({
|
|
92
|
+
deploymentId: dep.id,
|
|
93
|
+
ownerId: input.ownerId,
|
|
94
|
+
fromHash: dep.lastContentHash,
|
|
95
|
+
toHash: input.contentHash,
|
|
96
|
+
severity: d.severity,
|
|
97
|
+
summary: d.summary.slice(0, 1e3),
|
|
98
|
+
diff: d,
|
|
99
|
+
createdAt: now
|
|
100
|
+
}).onConflictDoUpdate({
|
|
101
|
+
target: [
|
|
102
|
+
mcpChangeEvents.deploymentId,
|
|
103
|
+
mcpChangeEvents.fromHash,
|
|
104
|
+
mcpChangeEvents.toHash
|
|
105
|
+
],
|
|
106
|
+
set: {
|
|
107
|
+
createdAt: now,
|
|
108
|
+
acknowledgedAt: null,
|
|
109
|
+
severity: d.severity,
|
|
110
|
+
summary: d.summary.slice(0, 1e3),
|
|
111
|
+
diff: d
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
change = "changed";
|
|
115
|
+
event = { at: now, severity: d.severity, summary: d.summary, rugPull: d.rugPull };
|
|
116
|
+
}
|
|
117
|
+
} else if (!isFirst && dep.lastStatus !== input.status) {
|
|
118
|
+
change = "status_changed";
|
|
119
|
+
}
|
|
120
|
+
await tx.update(mcpDeployments).set({
|
|
121
|
+
alias: input.alias,
|
|
122
|
+
transport: input.transport,
|
|
123
|
+
packageName: input.packageName,
|
|
124
|
+
lastStatus: input.status,
|
|
125
|
+
lastError: input.error,
|
|
126
|
+
lastScannedAt: now,
|
|
127
|
+
...input.serverName !== null ? { serverName: input.serverName } : {},
|
|
128
|
+
...input.serverVersion !== null ? { serverVersion: input.serverVersion } : {},
|
|
129
|
+
// A failed scan keeps the last contract that was read: "the server was
|
|
130
|
+
// unreachable today" must not erase what it exposed yesterday.
|
|
131
|
+
...input.contentHash ? { lastContentHash: input.contentHash, worstSeverity: input.worstSeverity } : {},
|
|
132
|
+
...moved || isFirst ? { lastChangedAt: now } : {}
|
|
133
|
+
}).where(eq(mcpDeployments.id, dep.id));
|
|
134
|
+
return { deploymentId: dep.id, change, event };
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async function reportPublic(db, row) {
|
|
138
|
+
await db.insert(mcpPublicReports).values(row).onConflictDoNothing();
|
|
139
|
+
const [agree] = await db.select({ n: sql`count(distinct ${mcpPublicReports.ownerId})::int` }).from(mcpPublicReports).where(
|
|
140
|
+
and(
|
|
141
|
+
eq(mcpPublicReports.registry, row.registry),
|
|
142
|
+
eq(mcpPublicReports.packageName, row.packageName),
|
|
143
|
+
eq(mcpPublicReports.version, row.version),
|
|
144
|
+
eq(mcpPublicReports.contentHash, row.contentHash)
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
return agree?.n ?? 0;
|
|
148
|
+
}
|
|
149
|
+
var RANK = { critical: 0, high: 1, moderate: 2, low: 3, info: 4 };
|
|
150
|
+
async function listDeployments(db, ownerId) {
|
|
151
|
+
const rows = await db.select({
|
|
152
|
+
d: mcpDeployments,
|
|
153
|
+
c: { toolCount: mcpContracts.toolCount, analysis: mcpContracts.analysis }
|
|
154
|
+
}).from(mcpDeployments).leftJoin(mcpContracts, eq(mcpContracts.contentHash, mcpDeployments.lastContentHash)).where(eq(mcpDeployments.ownerId, ownerId)).orderBy(desc(mcpDeployments.lastScannedAt)).limit(500);
|
|
155
|
+
if (rows.length === 0) return [];
|
|
156
|
+
const open = await db.select({ deploymentId: mcpChangeEvents.deploymentId, severity: mcpChangeEvents.severity }).from(mcpChangeEvents).where(
|
|
157
|
+
and(
|
|
158
|
+
eq(mcpChangeEvents.ownerId, ownerId),
|
|
159
|
+
isNull(mcpChangeEvents.acknowledgedAt),
|
|
160
|
+
inArray(
|
|
161
|
+
mcpChangeEvents.deploymentId,
|
|
162
|
+
rows.map((r) => r.d.id)
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
const byDep = /* @__PURE__ */ new Map();
|
|
167
|
+
for (const e of open) {
|
|
168
|
+
const cur = byDep.get(e.deploymentId) ?? { n: 0, worst: null };
|
|
169
|
+
cur.n++;
|
|
170
|
+
if (!cur.worst || RANK[e.severity] < RANK[cur.worst]) cur.worst = e.severity;
|
|
171
|
+
byDep.set(e.deploymentId, cur);
|
|
172
|
+
}
|
|
173
|
+
return rows.map(({ d, c }) => ({
|
|
174
|
+
...d,
|
|
175
|
+
toolCount: c?.toolCount ?? null,
|
|
176
|
+
writes: c?.analysis?.stats.writes ?? null,
|
|
177
|
+
destroys: c?.analysis?.stats.destroys ?? null,
|
|
178
|
+
capabilities: c?.analysis?.stats.capabilities ?? {},
|
|
179
|
+
findings: c?.analysis?.findings.filter((f) => f.severity !== "info").length ?? 0,
|
|
180
|
+
openEvents: byDep.get(d.id)?.n ?? 0,
|
|
181
|
+
openWorst: byDep.get(d.id)?.worst ?? null
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
async function getDeploymentDetail(db, ownerId, id) {
|
|
185
|
+
const [deployment] = await db.select().from(mcpDeployments).where(and(eq(mcpDeployments.id, id), eq(mcpDeployments.ownerId, ownerId))).limit(1);
|
|
186
|
+
if (!deployment) return null;
|
|
187
|
+
const [contract, observations, events] = await Promise.all([
|
|
188
|
+
deployment.lastContentHash ? getContract(db, deployment.lastContentHash) : Promise.resolve(null),
|
|
189
|
+
db.select().from(mcpObservations).where(and(eq(mcpObservations.deploymentId, id), eq(mcpObservations.ownerId, ownerId))).orderBy(desc(mcpObservations.id)).limit(60),
|
|
190
|
+
db.select().from(mcpChangeEvents).where(and(eq(mcpChangeEvents.deploymentId, id), eq(mcpChangeEvents.ownerId, ownerId))).orderBy(desc(mcpChangeEvents.createdAt)).limit(50)
|
|
191
|
+
]);
|
|
192
|
+
return { deployment, contract, observations, events };
|
|
193
|
+
}
|
|
194
|
+
async function listChangeEvents(db, ownerId, opts = {}) {
|
|
195
|
+
const rows = await db.select({
|
|
196
|
+
e: mcpChangeEvents,
|
|
197
|
+
alias: mcpDeployments.alias,
|
|
198
|
+
serverKey: mcpDeployments.serverKey
|
|
199
|
+
}).from(mcpChangeEvents).innerJoin(mcpDeployments, eq(mcpDeployments.id, mcpChangeEvents.deploymentId)).where(
|
|
200
|
+
and(
|
|
201
|
+
eq(mcpChangeEvents.ownerId, ownerId),
|
|
202
|
+
opts.openOnly ? isNull(mcpChangeEvents.acknowledgedAt) : void 0
|
|
203
|
+
)
|
|
204
|
+
).orderBy(desc(mcpChangeEvents.createdAt)).limit(Math.min(opts.limit ?? 50, 200));
|
|
205
|
+
return rows.map((r) => ({ ...r.e, alias: r.alias, serverKey: r.serverKey }));
|
|
206
|
+
}
|
|
207
|
+
async function acknowledgeEvent(db, ownerId, eventId) {
|
|
208
|
+
const rows = await db.update(mcpChangeEvents).set({ acknowledgedAt: /* @__PURE__ */ new Date() }).where(and(eq(mcpChangeEvents.id, eventId), eq(mcpChangeEvents.ownerId, ownerId))).returning({ id: mcpChangeEvents.id });
|
|
209
|
+
return rows.length > 0;
|
|
210
|
+
}
|
|
211
|
+
async function listAccountDeployments(db, ownerId) {
|
|
212
|
+
const summaries = await listDeployments(db, ownerId);
|
|
213
|
+
const hashes = [
|
|
214
|
+
...new Set(summaries.map((s) => s.lastContentHash).filter((h) => !!h))
|
|
215
|
+
];
|
|
216
|
+
const analyses = hashes.length ? await db.select({ hash: mcpContracts.contentHash, analysis: mcpContracts.analysis }).from(mcpContracts).where(inArray(mcpContracts.contentHash, hashes)) : [];
|
|
217
|
+
const byHash = new Map(analyses.map((a) => [a.hash, a.analysis]));
|
|
218
|
+
return summaries.map((s) => ({
|
|
219
|
+
serverKey: s.serverKey,
|
|
220
|
+
alias: s.alias,
|
|
221
|
+
lastStatus: s.lastStatus,
|
|
222
|
+
lastError: s.lastError,
|
|
223
|
+
lastContentHash: s.lastContentHash,
|
|
224
|
+
lastScannedAt: s.lastScannedAt,
|
|
225
|
+
analysis: s.lastContentHash ? byHash.get(s.lastContentHash) ?? null : null,
|
|
226
|
+
openEvents: s.openEvents,
|
|
227
|
+
openWorst: s.openWorst
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export {
|
|
232
|
+
putContract,
|
|
233
|
+
getContract,
|
|
234
|
+
countDeployments,
|
|
235
|
+
deploymentExists,
|
|
236
|
+
recordScan,
|
|
237
|
+
reportPublic,
|
|
238
|
+
listDeployments,
|
|
239
|
+
getDeploymentDetail,
|
|
240
|
+
listChangeEvents,
|
|
241
|
+
acknowledgeEvent,
|
|
242
|
+
listAccountDeployments
|
|
243
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { createRequire as __lurqCreateRequire } from 'node:module'; const require = __lurqCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
init_esm_shims
|
|
4
|
+
} from "./chunk-I3M2KJXB.js";
|
|
5
|
+
|
|
6
|
+
// src/notify/safeHttp.ts
|
|
7
|
+
init_esm_shims();
|
|
8
|
+
import { lookup as dnsLookup } from "dns";
|
|
9
|
+
import https from "https";
|
|
10
|
+
import { isIP } from "net";
|
|
11
|
+
var MAX_URL = 2e3;
|
|
12
|
+
var TIMEOUT_MS = 1e4;
|
|
13
|
+
var MAX_RESPONSE = 4e3;
|
|
14
|
+
function v4Private(ip) {
|
|
15
|
+
const [a = 0, b = 0] = ip.split(".").map(Number);
|
|
16
|
+
return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || // carrier-grade NAT
|
|
17
|
+
a === 169 && b === 254 || // link-local, cloud metadata
|
|
18
|
+
a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 || a === 198 && (b === 18 || b === 19) || // benchmarking
|
|
19
|
+
a >= 224;
|
|
20
|
+
}
|
|
21
|
+
function isPrivateAddress(ip) {
|
|
22
|
+
const kind = isIP(ip);
|
|
23
|
+
if (kind === 4) return v4Private(ip);
|
|
24
|
+
if (kind !== 6) return true;
|
|
25
|
+
const v = ip.toLowerCase();
|
|
26
|
+
if (v === "::" || v === "::1") return true;
|
|
27
|
+
const mapped = /^::ffff:(.+)$/.exec(v);
|
|
28
|
+
if (mapped) {
|
|
29
|
+
const rest = mapped[1];
|
|
30
|
+
if (isIP(rest) === 4) return v4Private(rest);
|
|
31
|
+
const [hi = "0", lo = "0"] = rest.split(":");
|
|
32
|
+
const n = parseInt(hi, 16) << 16 | parseInt(lo, 16);
|
|
33
|
+
return v4Private([n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255].join("."));
|
|
34
|
+
}
|
|
35
|
+
return /^f[cd]/.test(v) || /^fe[89ab]/.test(v) || v.startsWith("ff");
|
|
36
|
+
}
|
|
37
|
+
var SLACK_HOSTS = /* @__PURE__ */ new Set(["hooks.slack.com"]);
|
|
38
|
+
var DISCORD_HOSTS = /* @__PURE__ */ new Set([
|
|
39
|
+
"discord.com",
|
|
40
|
+
"discordapp.com",
|
|
41
|
+
"ptb.discord.com",
|
|
42
|
+
"canary.discord.com"
|
|
43
|
+
]);
|
|
44
|
+
var TEAMS_SUFFIXES = [".logic.azure.com", ".powerplatform.com", ".powerautomate.com"];
|
|
45
|
+
function validateChannelUrl(kind, raw) {
|
|
46
|
+
if (typeof raw !== "string" || !raw.trim() || raw.length > MAX_URL)
|
|
47
|
+
return { ok: false, error: "Paste the full webhook URL." };
|
|
48
|
+
let url;
|
|
49
|
+
try {
|
|
50
|
+
url = new URL(raw.trim());
|
|
51
|
+
} catch {
|
|
52
|
+
return { ok: false, error: "That is not a valid URL." };
|
|
53
|
+
}
|
|
54
|
+
if (url.protocol !== "https:") return { ok: false, error: "The URL must start with https://." };
|
|
55
|
+
if (url.username || url.password)
|
|
56
|
+
return { ok: false, error: "The URL must not contain a username or password." };
|
|
57
|
+
const host = url.hostname.toLowerCase();
|
|
58
|
+
if (kind === "slack") {
|
|
59
|
+
if (!SLACK_HOSTS.has(host) || !/^\/(services|triggers)\//.test(url.pathname)) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: "A Slack incoming webhook URL starts with https://hooks.slack.com/services/."
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
} else if (kind === "discord") {
|
|
66
|
+
if (!DISCORD_HOSTS.has(host) || !url.pathname.startsWith("/api/webhooks/")) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: "A Discord webhook URL starts with https://discord.com/api/webhooks/."
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
} else if (kind === "teams") {
|
|
73
|
+
if (host.endsWith(".webhook.office.com") || host === "outlook.office.com") {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
error: "Teams retired Office 365 connector webhooks in May 2026. Create one with the Workflows app instead."
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (!TEAMS_SUFFIXES.some((s) => host.endsWith(s))) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: "Use the webhook URL a Teams Workflows flow gives you (a logic.azure.com or powerplatform.com address)."
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) {
|
|
87
|
+
return { ok: false, error: "The URL must be reachable on the public internet." };
|
|
88
|
+
}
|
|
89
|
+
if (isIP(host.replace(/^\[|\]$/g, "")) && isPrivateAddress(host.replace(/^\[|\]$/g, ""))) {
|
|
90
|
+
return { ok: false, error: "The URL must be reachable on the public internet." };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, url };
|
|
94
|
+
}
|
|
95
|
+
function safeLookup(resolve = dnsLookup) {
|
|
96
|
+
return (hostname, options, callback) => {
|
|
97
|
+
resolve(hostname, { all: true }, (err, addresses) => {
|
|
98
|
+
if (err) return callback(err, []);
|
|
99
|
+
const list = addresses;
|
|
100
|
+
if (!list.length || list.some((a) => isPrivateAddress(a.address))) {
|
|
101
|
+
const e = Object.assign(new Error(`${hostname} resolves to a private network address`), {
|
|
102
|
+
code: "EPRIVATE"
|
|
103
|
+
});
|
|
104
|
+
return callback(e, []);
|
|
105
|
+
}
|
|
106
|
+
if (options?.all) return callback(null, list);
|
|
107
|
+
callback(null, list[0].address, list[0].family);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function postJson(url, payload, headers, opts = {}) {
|
|
112
|
+
return new Promise((resolvePost) => {
|
|
113
|
+
let settled = false;
|
|
114
|
+
const done = (o) => {
|
|
115
|
+
if (!settled) {
|
|
116
|
+
settled = true;
|
|
117
|
+
resolvePost(o);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
let req;
|
|
121
|
+
try {
|
|
122
|
+
req = https.request(
|
|
123
|
+
url,
|
|
124
|
+
{
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
"User-Agent": "lurq-alerts",
|
|
129
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
130
|
+
...headers
|
|
131
|
+
},
|
|
132
|
+
lookup: opts.lookup ?? safeLookup(),
|
|
133
|
+
timeout: opts.timeoutMs ?? TIMEOUT_MS
|
|
134
|
+
},
|
|
135
|
+
(res) => {
|
|
136
|
+
let body = "";
|
|
137
|
+
res.setEncoding("utf8");
|
|
138
|
+
res.on("data", (chunk) => {
|
|
139
|
+
if (body.length < MAX_RESPONSE) body += chunk;
|
|
140
|
+
});
|
|
141
|
+
res.on("end", () => {
|
|
142
|
+
const status = res.statusCode ?? 0;
|
|
143
|
+
done({
|
|
144
|
+
status,
|
|
145
|
+
error: status >= 200 && status < 300 ? null : `HTTP ${status}${body ? `: ${body.slice(0, 120).replace(/\s+/g, " ")}` : ""}`
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
res.on(
|
|
149
|
+
"error",
|
|
150
|
+
() => done({ status: res.statusCode ?? 0, error: "response interrupted" })
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
done({
|
|
156
|
+
status: 0,
|
|
157
|
+
error: err instanceof Error ? err.message : "request failed",
|
|
158
|
+
code: "EINVAL"
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
req.on("timeout", () => {
|
|
163
|
+
req.destroy(Object.assign(new Error("timed out"), { code: "ETIMEDOUT" }));
|
|
164
|
+
});
|
|
165
|
+
req.on("error", (err) => {
|
|
166
|
+
done({
|
|
167
|
+
status: 0,
|
|
168
|
+
error: err.code === "EPRIVATE" ? err.message : `could not connect (${err.code ?? err.message})`,
|
|
169
|
+
code: err.code ?? "ECONNECT"
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
req.end(payload);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export {
|
|
177
|
+
isPrivateAddress,
|
|
178
|
+
validateChannelUrl,
|
|
179
|
+
safeLookup,
|
|
180
|
+
postJson
|
|
181
|
+
};
|