@spotpatch/dev-server 0.3.0 → 0.5.0
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/dist/index.cjs +393 -114
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -2
- package/dist/index.d.ts +44 -2
- package/dist/index.js +390 -102
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -204,7 +204,11 @@ function createAgentJobManager(options) {
|
|
|
204
204
|
}
|
|
205
205
|
};
|
|
206
206
|
const applyChange = async (job, preparedChange) => {
|
|
207
|
-
transition(
|
|
207
|
+
transition(
|
|
208
|
+
job,
|
|
209
|
+
"applying",
|
|
210
|
+
job.applyMode === "trusted-auto" ? "Applying trusted change directly to the project." : "Applying validated changes to the project."
|
|
211
|
+
);
|
|
208
212
|
try {
|
|
209
213
|
await dependencies.applyChange(preparedChange);
|
|
210
214
|
transition(job, "applied", "Changes were applied to local project files.");
|
|
@@ -243,11 +247,15 @@ function createAgentJobManager(options) {
|
|
|
243
247
|
);
|
|
244
248
|
}
|
|
245
249
|
};
|
|
250
|
+
const execution = Object.freeze({
|
|
251
|
+
...options.ai.execution,
|
|
252
|
+
applyMode: job.applyMode
|
|
253
|
+
});
|
|
246
254
|
const preparedChange = await dependencies.executeChange({
|
|
247
255
|
annotation: job.annotation,
|
|
248
256
|
callbacks,
|
|
249
257
|
credential: job.credential,
|
|
250
|
-
execution
|
|
258
|
+
execution,
|
|
251
259
|
jobId: job.id,
|
|
252
260
|
model: job.model,
|
|
253
261
|
provider: job.provider,
|
|
@@ -734,7 +742,8 @@ import {
|
|
|
734
742
|
DEFAULT_AGENT_LIMITS,
|
|
735
743
|
MAX_ANNOTATION_TARGETS,
|
|
736
744
|
SPOTPATCH_EDITOR_PREFERENCES,
|
|
737
|
-
SPOTPATCH_LOCALE_PREFERENCES
|
|
745
|
+
SPOTPATCH_LOCALE_PREFERENCES,
|
|
746
|
+
DEFAULT_DATA_FLOW_LIMITS
|
|
738
747
|
} from "@spotpatch/shared";
|
|
739
748
|
import { z } from "zod";
|
|
740
749
|
var DEFAULT_EXCLUDE = Object.freeze([
|
|
@@ -745,7 +754,7 @@ var DEFAULT_EXCLUDE = Object.freeze([
|
|
|
745
754
|
/(?:^|\/)dist(?:\/|$)/,
|
|
746
755
|
/(?:^|\/)coverage(?:\/|$)/
|
|
747
756
|
]);
|
|
748
|
-
var DEFAULT_INCLUDE = Object.freeze([/(?:^|[/\\])src[/\\].+\.(?:jsx|tsx)$/]);
|
|
757
|
+
var DEFAULT_INCLUDE = Object.freeze([/(?:^|[/\\])src[/\\].+\.(?:js|jsx|ts|tsx)$/]);
|
|
749
758
|
var DEFAULT_BUDGET = Object.freeze({
|
|
750
759
|
totalCharacters: 16e3,
|
|
751
760
|
domCharacters: 3e3,
|
|
@@ -766,7 +775,12 @@ var DEFAULT_OPTIONS = Object.freeze({
|
|
|
766
775
|
debug: false,
|
|
767
776
|
locale: "auto",
|
|
768
777
|
maxTargets: 8,
|
|
769
|
-
ai: false
|
|
778
|
+
ai: false,
|
|
779
|
+
dataFlow: Object.freeze({
|
|
780
|
+
enabled: false,
|
|
781
|
+
runtime: "dispatch",
|
|
782
|
+
limits: DEFAULT_DATA_FLOW_LIMITS
|
|
783
|
+
})
|
|
770
784
|
});
|
|
771
785
|
var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
772
786
|
var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
@@ -1058,6 +1072,36 @@ function assertPositiveBudget(budget) {
|
|
|
1058
1072
|
}
|
|
1059
1073
|
}
|
|
1060
1074
|
}
|
|
1075
|
+
function resolveDataFlowOptions(options) {
|
|
1076
|
+
if (options === void 0 || options === false) {
|
|
1077
|
+
return DEFAULT_OPTIONS.dataFlow;
|
|
1078
|
+
}
|
|
1079
|
+
const candidate = options;
|
|
1080
|
+
if (typeof candidate !== "object" || candidate === null) {
|
|
1081
|
+
throw new RangeError("SpotPatch dataFlow configuration is invalid.");
|
|
1082
|
+
}
|
|
1083
|
+
const runtime = options.runtime ?? "dispatch";
|
|
1084
|
+
if (runtime !== "dispatch") {
|
|
1085
|
+
throw new RangeError("SpotPatch dataFlow runtime mode is invalid.");
|
|
1086
|
+
}
|
|
1087
|
+
return Object.freeze({
|
|
1088
|
+
enabled: true,
|
|
1089
|
+
runtime,
|
|
1090
|
+
limits: DEFAULT_DATA_FLOW_LIMITS
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
function createRuntimeDataFlowConfig(options) {
|
|
1094
|
+
return Object.freeze({
|
|
1095
|
+
enabled: options.enabled,
|
|
1096
|
+
runtime: options.runtime,
|
|
1097
|
+
limits: Object.freeze({
|
|
1098
|
+
observationMaxEntries: options.limits.observationMaxEntries,
|
|
1099
|
+
observationMaxBytes: options.limits.observationMaxBytes,
|
|
1100
|
+
observationTtlMs: options.limits.observationTtlMs,
|
|
1101
|
+
reportMaxBytes: options.limits.reportMaxBytes
|
|
1102
|
+
})
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1061
1105
|
function resolveOptions(options = {}, environmentAi) {
|
|
1062
1106
|
if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
|
|
1063
1107
|
throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
|
|
@@ -1093,7 +1137,8 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1093
1137
|
debug: options.debug ?? DEFAULT_OPTIONS.debug,
|
|
1094
1138
|
locale,
|
|
1095
1139
|
maxTargets,
|
|
1096
|
-
ai: resolveAiOptions(options.ai ?? environmentAi)
|
|
1140
|
+
ai: resolveAiOptions(options.ai ?? environmentAi),
|
|
1141
|
+
dataFlow: resolveDataFlowOptions(options.dataFlow)
|
|
1097
1142
|
};
|
|
1098
1143
|
if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
|
|
1099
1144
|
throw new RangeError("SpotPatch shortcut is invalid.");
|
|
@@ -1279,37 +1324,62 @@ function createSourceRegistry(options = {}) {
|
|
|
1279
1324
|
const createId = options.createId ?? createRandomSourceId;
|
|
1280
1325
|
const pathToId = /* @__PURE__ */ new Map();
|
|
1281
1326
|
const idToPath = /* @__PURE__ */ new Map();
|
|
1327
|
+
const componentAnchors = /* @__PURE__ */ new Map();
|
|
1328
|
+
const componentIdsByPath = /* @__PURE__ */ new Map();
|
|
1329
|
+
function registerSourcePath(absolutePath) {
|
|
1330
|
+
const normalizedPath = normalizeAbsolutePath(absolutePath);
|
|
1331
|
+
const existingId = pathToId.get(normalizedPath);
|
|
1332
|
+
if (existingId !== void 0) {
|
|
1333
|
+
return existingId;
|
|
1334
|
+
}
|
|
1335
|
+
let fileId = createId();
|
|
1336
|
+
while (idToPath.has(fileId)) fileId = createId();
|
|
1337
|
+
pathToId.set(normalizedPath, fileId);
|
|
1338
|
+
idToPath.set(fileId, normalizedPath);
|
|
1339
|
+
return fileId;
|
|
1340
|
+
}
|
|
1282
1341
|
return Object.freeze({
|
|
1283
1342
|
register(absolutePath) {
|
|
1343
|
+
return registerSourcePath(absolutePath);
|
|
1344
|
+
},
|
|
1345
|
+
registerDataFlowComponents(absolutePath, sourceVersion, components) {
|
|
1284
1346
|
const normalizedPath = normalizeAbsolutePath(absolutePath);
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1347
|
+
const previousIds = componentIdsByPath.get(normalizedPath);
|
|
1348
|
+
for (const componentSourceId of previousIds ?? []) {
|
|
1349
|
+
componentAnchors.delete(componentSourceId);
|
|
1288
1350
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1351
|
+
const fileId = registerSourcePath(normalizedPath);
|
|
1352
|
+
const currentIds = /* @__PURE__ */ new Set();
|
|
1353
|
+
for (const component of components) {
|
|
1354
|
+
currentIds.add(component.componentSourceId);
|
|
1355
|
+
componentAnchors.set(
|
|
1356
|
+
component.componentSourceId,
|
|
1357
|
+
Object.freeze({ ...component, fileId, sourceVersion })
|
|
1358
|
+
);
|
|
1292
1359
|
}
|
|
1293
|
-
|
|
1294
|
-
idToPath.set(fileId, normalizedPath);
|
|
1295
|
-
return fileId;
|
|
1360
|
+
componentIdsByPath.set(normalizedPath, currentIds);
|
|
1296
1361
|
},
|
|
1297
1362
|
resolve(fileId) {
|
|
1298
1363
|
return idToPath.get(fileId);
|
|
1299
1364
|
},
|
|
1365
|
+
resolveDataFlowComponent(componentSourceId) {
|
|
1366
|
+
return componentAnchors.get(componentSourceId);
|
|
1367
|
+
},
|
|
1300
1368
|
clear() {
|
|
1301
1369
|
pathToId.clear();
|
|
1302
1370
|
idToPath.clear();
|
|
1371
|
+
componentAnchors.clear();
|
|
1372
|
+
componentIdsByPath.clear();
|
|
1303
1373
|
}
|
|
1304
1374
|
});
|
|
1305
1375
|
}
|
|
1306
1376
|
|
|
1307
1377
|
// src/server/middleware.ts
|
|
1308
1378
|
import {
|
|
1309
|
-
ERROR_CODES as
|
|
1379
|
+
ERROR_CODES as ERROR_CODES10,
|
|
1310
1380
|
SPOTPATCH_API_BASE,
|
|
1311
1381
|
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
|
|
1312
|
-
SpotPatchError as
|
|
1382
|
+
SpotPatchError as SpotPatchError10,
|
|
1313
1383
|
openEditorRequestSchema,
|
|
1314
1384
|
sourceContextRequestSchema
|
|
1315
1385
|
} from "@spotpatch/shared";
|
|
@@ -2142,10 +2212,186 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
2142
2212
|
}
|
|
2143
2213
|
var launchConfiguredEditor = createEditorLauncher();
|
|
2144
2214
|
|
|
2215
|
+
// src/server/data-flow-http.ts
|
|
2216
|
+
import { createHash as createHash2 } from "crypto";
|
|
2217
|
+
import {
|
|
2218
|
+
createStaticDataFlowAnalyzer
|
|
2219
|
+
} from "@spotpatch/analyzer";
|
|
2220
|
+
import {
|
|
2221
|
+
DATA_FLOW_SCHEMA_VERSION,
|
|
2222
|
+
ERROR_CODES as ERROR_CODES7,
|
|
2223
|
+
SpotPatchError as SpotPatchError7,
|
|
2224
|
+
dataFlowComponentReportRequestSchema,
|
|
2225
|
+
dataFlowPageReportRequestSchema,
|
|
2226
|
+
limitDataFlowReportCollections
|
|
2227
|
+
} from "@spotpatch/shared";
|
|
2228
|
+
function envelopeBytes(report) {
|
|
2229
|
+
return Buffer.byteLength(JSON.stringify({ ok: true, data: report }), "utf8");
|
|
2230
|
+
}
|
|
2231
|
+
function limitDataFlowReportToBytes(report, maximumBytes) {
|
|
2232
|
+
const structurallyLimited = limitDataFlowReportCollections(report);
|
|
2233
|
+
if (envelopeBytes(structurallyLimited) <= maximumBytes) {
|
|
2234
|
+
return structurallyLimited;
|
|
2235
|
+
}
|
|
2236
|
+
let limited = limitDataFlowReportCollections(structurallyLimited, {
|
|
2237
|
+
forceTruncation: true,
|
|
2238
|
+
maximumDependencies: 0,
|
|
2239
|
+
truncatedBy: "bytes"
|
|
2240
|
+
});
|
|
2241
|
+
if (envelopeBytes(limited) > maximumBytes) {
|
|
2242
|
+
throw new SpotPatchError7(ERROR_CODES7.INTERNAL_ERROR);
|
|
2243
|
+
}
|
|
2244
|
+
for (let maximumDependencies = 1; maximumDependencies <= structurallyLimited.dependencies.length; maximumDependencies += 1) {
|
|
2245
|
+
const candidate = limitDataFlowReportCollections(structurallyLimited, {
|
|
2246
|
+
forceTruncation: true,
|
|
2247
|
+
maximumDependencies,
|
|
2248
|
+
truncatedBy: "bytes"
|
|
2249
|
+
});
|
|
2250
|
+
if (envelopeBytes(candidate) > maximumBytes) break;
|
|
2251
|
+
limited = candidate;
|
|
2252
|
+
}
|
|
2253
|
+
return limited;
|
|
2254
|
+
}
|
|
2255
|
+
function createDataFlowAnalyzer(options) {
|
|
2256
|
+
if (!options.options.dataFlow.enabled) return void 0;
|
|
2257
|
+
return createStaticDataFlowAnalyzer({
|
|
2258
|
+
root: options.root,
|
|
2259
|
+
registryEpoch: options.session.id,
|
|
2260
|
+
registerSource: (absolutePath) => options.registry.register(absolutePath),
|
|
2261
|
+
limits: options.options.dataFlow.limits
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
async function analyzeTarget(request, analyzer, options) {
|
|
2265
|
+
const resolvedRequest = (() => {
|
|
2266
|
+
if ("componentSourceId" in request) {
|
|
2267
|
+
const anchor = options.registry.resolveDataFlowComponent(
|
|
2268
|
+
request.componentSourceId
|
|
2269
|
+
);
|
|
2270
|
+
if (anchor?.sourceVersion !== request.sourceVersion) {
|
|
2271
|
+
throw new SpotPatchError7(ERROR_CODES7.DATA_FLOW_SOURCE_STALE);
|
|
2272
|
+
}
|
|
2273
|
+
return anchor;
|
|
2274
|
+
}
|
|
2275
|
+
return request;
|
|
2276
|
+
})();
|
|
2277
|
+
const absolutePath = await resolveSourceFile({
|
|
2278
|
+
fileId: resolvedRequest.fileId,
|
|
2279
|
+
registry: options.registry,
|
|
2280
|
+
root: options.root
|
|
2281
|
+
});
|
|
2282
|
+
const report = analyzer.analyzeComponent({
|
|
2283
|
+
absolutePath,
|
|
2284
|
+
line: resolvedRequest.line,
|
|
2285
|
+
column: resolvedRequest.column
|
|
2286
|
+
});
|
|
2287
|
+
if (resolvedRequest.sourceVersion !== void 0 && resolvedRequest.sourceVersion !== report.component.source.sourceVersion) {
|
|
2288
|
+
throw new SpotPatchError7(ERROR_CODES7.DATA_FLOW_SOURCE_STALE);
|
|
2289
|
+
}
|
|
2290
|
+
return limitDataFlowReportToBytes(
|
|
2291
|
+
report,
|
|
2292
|
+
options.options.dataFlow.limits.reportMaxBytes
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
function requireAnalyzer(analyzer) {
|
|
2296
|
+
if (analyzer === void 0) {
|
|
2297
|
+
throw new SpotPatchError7(ERROR_CODES7.DATA_FLOW_DISABLED);
|
|
2298
|
+
}
|
|
2299
|
+
return analyzer;
|
|
2300
|
+
}
|
|
2301
|
+
async function handleComponentDataFlowReport(request, analyzer, options) {
|
|
2302
|
+
const parsed = dataFlowComponentReportRequestSchema.safeParse(
|
|
2303
|
+
await readJsonRequestBody(
|
|
2304
|
+
request,
|
|
2305
|
+
options.options.dataFlow.limits.protocolRequestMaxBytes
|
|
2306
|
+
)
|
|
2307
|
+
);
|
|
2308
|
+
if (!parsed.success) {
|
|
2309
|
+
throw new SpotPatchError7(ERROR_CODES7.INVALID_REQUEST);
|
|
2310
|
+
}
|
|
2311
|
+
return analyzeTarget(parsed.data, requireAnalyzer(analyzer), options);
|
|
2312
|
+
}
|
|
2313
|
+
async function handlePageDataFlowReport(request, analyzer, options) {
|
|
2314
|
+
const parsed = dataFlowPageReportRequestSchema.safeParse(
|
|
2315
|
+
await readJsonRequestBody(
|
|
2316
|
+
request,
|
|
2317
|
+
options.options.dataFlow.limits.protocolRequestMaxBytes
|
|
2318
|
+
)
|
|
2319
|
+
);
|
|
2320
|
+
if (!parsed.success) {
|
|
2321
|
+
throw new SpotPatchError7(ERROR_CODES7.INVALID_REQUEST);
|
|
2322
|
+
}
|
|
2323
|
+
const activeAnalyzer = requireAnalyzer(analyzer);
|
|
2324
|
+
const componentReports = await Promise.all(
|
|
2325
|
+
parsed.data.targets.map((target) => analyzeTarget(target, activeAnalyzer, options))
|
|
2326
|
+
);
|
|
2327
|
+
const dependencies = new Map(
|
|
2328
|
+
componentReports.flatMap(
|
|
2329
|
+
(report2) => report2.dependencies.map((dependency) => [dependency.id, dependency])
|
|
2330
|
+
)
|
|
2331
|
+
);
|
|
2332
|
+
const evidence = new Map(
|
|
2333
|
+
componentReports.flatMap(
|
|
2334
|
+
(report2) => report2.evidence.map((entry) => [entry.id, entry])
|
|
2335
|
+
)
|
|
2336
|
+
);
|
|
2337
|
+
const diagnostics = new Map(
|
|
2338
|
+
componentReports.flatMap(
|
|
2339
|
+
(report2) => report2.diagnostics.map((entry) => [entry.id, entry])
|
|
2340
|
+
)
|
|
2341
|
+
);
|
|
2342
|
+
const analyzedVersions = new Set(
|
|
2343
|
+
componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
|
|
2344
|
+
);
|
|
2345
|
+
const reportId = `page_${createHash2("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
|
|
2346
|
+
const complete = componentReports.every((report2) => report2.completeness.complete);
|
|
2347
|
+
const report = Object.freeze({
|
|
2348
|
+
schemaVersion: DATA_FLOW_SCHEMA_VERSION,
|
|
2349
|
+
reportId,
|
|
2350
|
+
baseline: Object.freeze({
|
|
2351
|
+
registryEpoch: options.session.id,
|
|
2352
|
+
analyzerVersion: componentReports[0]?.baseline.analyzerVersion ?? "unavailable",
|
|
2353
|
+
adapterSetHash: componentReports[0]?.baseline.adapterSetHash ?? "unavailable",
|
|
2354
|
+
analyzedSourceVersions: Object.freeze([...analyzedVersions].sort())
|
|
2355
|
+
}),
|
|
2356
|
+
capability: Object.freeze({
|
|
2357
|
+
enabled: true,
|
|
2358
|
+
staticAnalysis: complete ? "available" : "partial",
|
|
2359
|
+
runtimeObservation: "dispatch-only",
|
|
2360
|
+
responseShape: "consumed-fields-only",
|
|
2361
|
+
aiAssistance: "disabled",
|
|
2362
|
+
reasons: Object.freeze(
|
|
2363
|
+
componentReports.flatMap((report2) => report2.capability.reasons)
|
|
2364
|
+
)
|
|
2365
|
+
}),
|
|
2366
|
+
dependencies: Object.freeze([...dependencies.values()]),
|
|
2367
|
+
evidence: Object.freeze([...evidence.values()]),
|
|
2368
|
+
diagnostics: Object.freeze([...diagnostics.values()]),
|
|
2369
|
+
completeness: Object.freeze({
|
|
2370
|
+
complete,
|
|
2371
|
+
visitedModules: componentReports.reduce(
|
|
2372
|
+
(total, report2) => total + report2.completeness.visitedModules,
|
|
2373
|
+
0
|
|
2374
|
+
),
|
|
2375
|
+
visitedCallsites: componentReports.reduce(
|
|
2376
|
+
(total, report2) => total + report2.completeness.visitedCallsites,
|
|
2377
|
+
0
|
|
2378
|
+
),
|
|
2379
|
+
frontierCount: componentReports.reduce(
|
|
2380
|
+
(total, report2) => total + report2.completeness.frontierCount,
|
|
2381
|
+
0
|
|
2382
|
+
)
|
|
2383
|
+
})
|
|
2384
|
+
});
|
|
2385
|
+
return limitDataFlowReportToBytes(
|
|
2386
|
+
report,
|
|
2387
|
+
options.options.dataFlow.limits.reportMaxBytes
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2145
2391
|
// src/server/request-security.ts
|
|
2146
2392
|
import { timingSafeEqual } from "crypto";
|
|
2147
2393
|
import { isIP } from "net";
|
|
2148
|
-
import { ERROR_CODES as
|
|
2394
|
+
import { ERROR_CODES as ERROR_CODES8, SPOTPATCH_TOKEN_HEADER, SpotPatchError as SpotPatchError8 } from "@spotpatch/shared";
|
|
2149
2395
|
function getSingleHeader(request, name) {
|
|
2150
2396
|
const value = request.headers[name.toLowerCase()];
|
|
2151
2397
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -2192,32 +2438,32 @@ function parseOrigin(value) {
|
|
|
2192
2438
|
function assertRequestAuthorized(request, options) {
|
|
2193
2439
|
const actualToken = getSingleHeader(request, SPOTPATCH_TOKEN_HEADER);
|
|
2194
2440
|
if (!tokensMatch(actualToken, options.sessionToken)) {
|
|
2195
|
-
throw new
|
|
2441
|
+
throw new SpotPatchError8(ERROR_CODES8.INVALID_TOKEN);
|
|
2196
2442
|
}
|
|
2197
2443
|
const hostHeader = getSingleHeader(request, "host");
|
|
2198
2444
|
const originHeader = getSingleHeader(request, "origin");
|
|
2199
2445
|
const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
|
|
2200
2446
|
const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
|
|
2201
2447
|
if (host === void 0 || origin === void 0) {
|
|
2202
|
-
throw new
|
|
2448
|
+
throw new SpotPatchError8(ERROR_CODES8.ORIGIN_NOT_ALLOWED);
|
|
2203
2449
|
}
|
|
2204
2450
|
const hostIsLoopback = isLoopbackHostname(host.hostname);
|
|
2205
2451
|
const originIsLoopback = isLoopbackHostname(origin.hostname);
|
|
2206
2452
|
if (!options.allowLan) {
|
|
2207
2453
|
if (!hostIsLoopback || !originIsLoopback) {
|
|
2208
|
-
throw new
|
|
2454
|
+
throw new SpotPatchError8(ERROR_CODES8.ORIGIN_NOT_ALLOWED);
|
|
2209
2455
|
}
|
|
2210
2456
|
return;
|
|
2211
2457
|
}
|
|
2212
2458
|
if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
|
|
2213
|
-
throw new
|
|
2459
|
+
throw new SpotPatchError8(ERROR_CODES8.ORIGIN_NOT_ALLOWED);
|
|
2214
2460
|
}
|
|
2215
2461
|
}
|
|
2216
2462
|
|
|
2217
2463
|
// src/server/runtime-bootstrap.ts
|
|
2218
2464
|
import {
|
|
2219
|
-
ERROR_CODES as
|
|
2220
|
-
SpotPatchError as
|
|
2465
|
+
ERROR_CODES as ERROR_CODES9,
|
|
2466
|
+
SpotPatchError as SpotPatchError9,
|
|
2221
2467
|
runtimeBootstrapRequestSchema,
|
|
2222
2468
|
runtimeConfigSchema
|
|
2223
2469
|
} from "@spotpatch/shared";
|
|
@@ -2247,7 +2493,7 @@ function resolveRuntimeBootstrapOptions(options) {
|
|
|
2247
2493
|
function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
2248
2494
|
const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
2249
2495
|
if (request.method !== "POST" || contentType !== "application/json") {
|
|
2250
|
-
throw new
|
|
2496
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
2251
2497
|
}
|
|
2252
2498
|
const host = getSingleHeader2(request, "host");
|
|
2253
2499
|
let hostIsLoopback = false;
|
|
@@ -2259,7 +2505,7 @@ function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
|
2259
2505
|
}
|
|
2260
2506
|
}
|
|
2261
2507
|
if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
|
|
2262
|
-
throw new
|
|
2508
|
+
throw new SpotPatchError9(ERROR_CODES9.ORIGIN_NOT_ALLOWED);
|
|
2263
2509
|
}
|
|
2264
2510
|
}
|
|
2265
2511
|
async function readRuntimeBootstrap(request, options) {
|
|
@@ -2268,81 +2514,87 @@ async function readRuntimeBootstrap(request, options) {
|
|
|
2268
2514
|
await readJsonRequestBody(request)
|
|
2269
2515
|
);
|
|
2270
2516
|
if (!parsedBody.success) {
|
|
2271
|
-
throw new
|
|
2517
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
2272
2518
|
}
|
|
2273
2519
|
return options.runtimeConfig;
|
|
2274
2520
|
}
|
|
2275
2521
|
|
|
2276
2522
|
// src/server/middleware.ts
|
|
2277
2523
|
var STATUS_BY_ERROR = Object.freeze({
|
|
2278
|
-
[
|
|
2279
|
-
[
|
|
2280
|
-
[
|
|
2281
|
-
[
|
|
2282
|
-
[
|
|
2283
|
-
[
|
|
2284
|
-
[
|
|
2285
|
-
[
|
|
2286
|
-
[
|
|
2287
|
-
[
|
|
2288
|
-
[
|
|
2289
|
-
[
|
|
2290
|
-
[
|
|
2291
|
-
[
|
|
2292
|
-
[
|
|
2293
|
-
[
|
|
2294
|
-
[
|
|
2295
|
-
[
|
|
2296
|
-
[
|
|
2297
|
-
[
|
|
2298
|
-
[
|
|
2299
|
-
[
|
|
2300
|
-
[
|
|
2301
|
-
[
|
|
2302
|
-
[
|
|
2303
|
-
[
|
|
2304
|
-
[
|
|
2305
|
-
[
|
|
2306
|
-
[
|
|
2307
|
-
[
|
|
2308
|
-
[
|
|
2309
|
-
[
|
|
2310
|
-
[
|
|
2524
|
+
[ERROR_CODES10.INVALID_REQUEST]: 400,
|
|
2525
|
+
[ERROR_CODES10.INVALID_TOKEN]: 401,
|
|
2526
|
+
[ERROR_CODES10.ORIGIN_NOT_ALLOWED]: 403,
|
|
2527
|
+
[ERROR_CODES10.SOURCE_NOT_FOUND]: 404,
|
|
2528
|
+
[ERROR_CODES10.SOURCE_OUTSIDE_ROOT]: 403,
|
|
2529
|
+
[ERROR_CODES10.SOURCE_TOO_LARGE]: 413,
|
|
2530
|
+
[ERROR_CODES10.EDITOR_OPEN_FAILED]: 500,
|
|
2531
|
+
[ERROR_CODES10.DATA_FLOW_DISABLED]: 404,
|
|
2532
|
+
[ERROR_CODES10.DATA_FLOW_SOURCE_STALE]: 409,
|
|
2533
|
+
[ERROR_CODES10.DATA_FLOW_ANALYSIS_CANCELLED]: 409,
|
|
2534
|
+
[ERROR_CODES10.AI_DISABLED]: 404,
|
|
2535
|
+
[ERROR_CODES10.PROVIDER_NOT_CONFIGURED]: 503,
|
|
2536
|
+
[ERROR_CODES10.PROVIDER_AUTH_FAILED]: 502,
|
|
2537
|
+
[ERROR_CODES10.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
|
|
2538
|
+
[ERROR_CODES10.MODEL_NOT_ALLOWED]: 400,
|
|
2539
|
+
[ERROR_CODES10.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
|
|
2540
|
+
[ERROR_CODES10.PROVIDER_RATE_LIMITED]: 429,
|
|
2541
|
+
[ERROR_CODES10.AGENT_BUSY]: 409,
|
|
2542
|
+
[ERROR_CODES10.AGENT_LIMIT_EXCEEDED]: 413,
|
|
2543
|
+
[ERROR_CODES10.AGENT_CANCELLED]: 409,
|
|
2544
|
+
[ERROR_CODES10.WORKTREE_DIRTY]: 409,
|
|
2545
|
+
[ERROR_CODES10.WORKTREE_NOT_REPOSITORY]: 409,
|
|
2546
|
+
[ERROR_CODES10.WORKTREE_OPERATION_IN_PROGRESS]: 409,
|
|
2547
|
+
[ERROR_CODES10.WORKTREE_CONFLICTED]: 409,
|
|
2548
|
+
[ERROR_CODES10.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
|
|
2549
|
+
[ERROR_CODES10.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
|
|
2550
|
+
[ERROR_CODES10.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
|
|
2551
|
+
[ERROR_CODES10.TOOL_DENIED]: 403,
|
|
2552
|
+
[ERROR_CODES10.TOOL_INPUT_INVALID]: 422,
|
|
2553
|
+
[ERROR_CODES10.TOOL_ARGUMENTS_INVALID]: 422,
|
|
2554
|
+
[ERROR_CODES10.TOOL_CALL_ID_CONFLICT]: 422,
|
|
2555
|
+
[ERROR_CODES10.TOOL_PATH_DENIED]: 403,
|
|
2556
|
+
[ERROR_CODES10.PATCH_REJECTED]: 422,
|
|
2557
|
+
[ERROR_CODES10.VALIDATION_FAILED]: 422,
|
|
2558
|
+
[ERROR_CODES10.APPLY_CONFLICT]: 409,
|
|
2559
|
+
[ERROR_CODES10.INTERNAL_ERROR]: 500
|
|
2311
2560
|
});
|
|
2312
2561
|
var PUBLIC_MESSAGES = Object.freeze({
|
|
2313
|
-
[
|
|
2314
|
-
[
|
|
2315
|
-
[
|
|
2316
|
-
[
|
|
2317
|
-
[
|
|
2318
|
-
[
|
|
2319
|
-
[
|
|
2320
|
-
[
|
|
2321
|
-
[
|
|
2322
|
-
[
|
|
2323
|
-
[
|
|
2324
|
-
[
|
|
2325
|
-
[
|
|
2326
|
-
[
|
|
2327
|
-
[
|
|
2328
|
-
[
|
|
2329
|
-
[
|
|
2330
|
-
[
|
|
2331
|
-
[
|
|
2332
|
-
[
|
|
2333
|
-
[
|
|
2334
|
-
[
|
|
2335
|
-
[
|
|
2336
|
-
[
|
|
2337
|
-
[
|
|
2338
|
-
[
|
|
2339
|
-
[
|
|
2340
|
-
[
|
|
2341
|
-
[
|
|
2342
|
-
[
|
|
2343
|
-
[
|
|
2344
|
-
[
|
|
2345
|
-
[
|
|
2562
|
+
[ERROR_CODES10.INVALID_REQUEST]: "The request is invalid.",
|
|
2563
|
+
[ERROR_CODES10.INVALID_TOKEN]: "The session token is invalid.",
|
|
2564
|
+
[ERROR_CODES10.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
|
|
2565
|
+
[ERROR_CODES10.SOURCE_NOT_FOUND]: "The source file is unavailable.",
|
|
2566
|
+
[ERROR_CODES10.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
|
|
2567
|
+
[ERROR_CODES10.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
|
|
2568
|
+
[ERROR_CODES10.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
|
|
2569
|
+
[ERROR_CODES10.DATA_FLOW_DISABLED]: "Component data-flow analysis is not enabled.",
|
|
2570
|
+
[ERROR_CODES10.DATA_FLOW_SOURCE_STALE]: "The selected source version is stale.",
|
|
2571
|
+
[ERROR_CODES10.DATA_FLOW_ANALYSIS_CANCELLED]: "The data-flow analysis was cancelled.",
|
|
2572
|
+
[ERROR_CODES10.AI_DISABLED]: "AI execution is not enabled.",
|
|
2573
|
+
[ERROR_CODES10.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
|
|
2574
|
+
[ERROR_CODES10.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
|
|
2575
|
+
[ERROR_CODES10.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
|
|
2576
|
+
[ERROR_CODES10.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
|
|
2577
|
+
[ERROR_CODES10.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
|
|
2578
|
+
[ERROR_CODES10.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
|
|
2579
|
+
[ERROR_CODES10.AGENT_BUSY]: "Another Agent job is already running.",
|
|
2580
|
+
[ERROR_CODES10.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
|
|
2581
|
+
[ERROR_CODES10.AGENT_CANCELLED]: "The Agent job was cancelled.",
|
|
2582
|
+
[ERROR_CODES10.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
|
|
2583
|
+
[ERROR_CODES10.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
|
|
2584
|
+
[ERROR_CODES10.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
|
|
2585
|
+
[ERROR_CODES10.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
|
|
2586
|
+
[ERROR_CODES10.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
|
|
2587
|
+
[ERROR_CODES10.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
|
|
2588
|
+
[ERROR_CODES10.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
|
|
2589
|
+
[ERROR_CODES10.TOOL_DENIED]: "The Agent tool request was denied.",
|
|
2590
|
+
[ERROR_CODES10.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
|
|
2591
|
+
[ERROR_CODES10.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
|
|
2592
|
+
[ERROR_CODES10.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
|
|
2593
|
+
[ERROR_CODES10.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
|
|
2594
|
+
[ERROR_CODES10.PATCH_REJECTED]: "The proposed patch was rejected.",
|
|
2595
|
+
[ERROR_CODES10.VALIDATION_FAILED]: "The proposed change failed validation.",
|
|
2596
|
+
[ERROR_CODES10.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
|
|
2597
|
+
[ERROR_CODES10.INTERNAL_ERROR]: "The request could not be completed."
|
|
2346
2598
|
});
|
|
2347
2599
|
function writeJson(response, status, payload) {
|
|
2348
2600
|
response.statusCode = status;
|
|
@@ -2351,11 +2603,11 @@ function writeJson(response, status, payload) {
|
|
|
2351
2603
|
response.end(JSON.stringify(payload));
|
|
2352
2604
|
}
|
|
2353
2605
|
function asSpotPatchError(error) {
|
|
2354
|
-
return error instanceof
|
|
2606
|
+
return error instanceof SpotPatchError10 ? error : new SpotPatchError10(ERROR_CODES10.INTERNAL_ERROR, void 0, { cause: error });
|
|
2355
2607
|
}
|
|
2356
2608
|
function writeError(response, error, logger) {
|
|
2357
2609
|
const normalized = asSpotPatchError(error);
|
|
2358
|
-
if (normalized.code ===
|
|
2610
|
+
if (normalized.code === ERROR_CODES10.INTERNAL_ERROR) {
|
|
2359
2611
|
logger?.warn("[spotpatch:server] Internal request failure.");
|
|
2360
2612
|
}
|
|
2361
2613
|
writeJson(response, STATUS_BY_ERROR[normalized.code], {
|
|
@@ -2378,7 +2630,7 @@ async function handleSourceContext(request, options) {
|
|
|
2378
2630
|
await readJsonRequestBody(request)
|
|
2379
2631
|
);
|
|
2380
2632
|
if (!parsed.success) {
|
|
2381
|
-
throw new
|
|
2633
|
+
throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
2382
2634
|
}
|
|
2383
2635
|
return readSourceContext({
|
|
2384
2636
|
request: parsed.data,
|
|
@@ -2391,7 +2643,7 @@ async function handleSourceContext(request, options) {
|
|
|
2391
2643
|
async function handleOpenEditor(request, options) {
|
|
2392
2644
|
const parsed = openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
|
|
2393
2645
|
if (!parsed.success) {
|
|
2394
|
-
throw new
|
|
2646
|
+
throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
2395
2647
|
}
|
|
2396
2648
|
const body = parsed.data;
|
|
2397
2649
|
const sourcePath = await resolveSourceFile({
|
|
@@ -2408,17 +2660,18 @@ async function handleOpenEditor(request, options) {
|
|
|
2408
2660
|
options.logger?.warn(
|
|
2409
2661
|
`[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
|
|
2410
2662
|
);
|
|
2411
|
-
throw new
|
|
2663
|
+
throw new SpotPatchError10(ERROR_CODES10.EDITOR_OPEN_FAILED, void 0, {
|
|
2412
2664
|
cause: error
|
|
2413
2665
|
});
|
|
2414
2666
|
}
|
|
2415
2667
|
}
|
|
2416
2668
|
function createSpotPatchMiddleware(options) {
|
|
2417
2669
|
const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
|
|
2670
|
+
const dataFlowAnalyzer = createDataFlowAnalyzer(options);
|
|
2418
2671
|
return (request, response, next) => {
|
|
2419
2672
|
const path8 = requestPath(request);
|
|
2420
2673
|
const agentRoute = matchAgentRequestPath(path8);
|
|
2421
|
-
if (path8 !== SPOTPATCH_ENDPOINTS2.sourceContext && path8 !== SPOTPATCH_ENDPOINTS2.openEditor && agentRoute === void 0 && !path8.startsWith(`${SPOTPATCH_API_BASE}/`)) {
|
|
2674
|
+
if (path8 !== SPOTPATCH_ENDPOINTS2.sourceContext && path8 !== SPOTPATCH_ENDPOINTS2.openEditor && path8 !== SPOTPATCH_ENDPOINTS2.dataFlowComponentReport && path8 !== SPOTPATCH_ENDPOINTS2.dataFlowPageReport && agentRoute === void 0 && !path8.startsWith(`${SPOTPATCH_API_BASE}/`)) {
|
|
2422
2675
|
next();
|
|
2423
2676
|
return;
|
|
2424
2677
|
}
|
|
@@ -2437,7 +2690,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
2437
2690
|
});
|
|
2438
2691
|
if (path8 === SPOTPATCH_ENDPOINTS2.sourceContext) {
|
|
2439
2692
|
if (request.method !== "POST") {
|
|
2440
|
-
throw new
|
|
2693
|
+
throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
2441
2694
|
}
|
|
2442
2695
|
const data = await handleSourceContext(request, options);
|
|
2443
2696
|
writeJson(response, 200, { ok: true, data });
|
|
@@ -2445,14 +2698,34 @@ function createSpotPatchMiddleware(options) {
|
|
|
2445
2698
|
}
|
|
2446
2699
|
if (path8 === SPOTPATCH_ENDPOINTS2.openEditor) {
|
|
2447
2700
|
if (request.method !== "POST") {
|
|
2448
|
-
throw new
|
|
2701
|
+
throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
2449
2702
|
}
|
|
2450
2703
|
const data = await handleOpenEditor(request, options);
|
|
2451
2704
|
writeJson(response, 200, { ok: true, data });
|
|
2452
2705
|
return;
|
|
2453
2706
|
}
|
|
2707
|
+
if (path8 === SPOTPATCH_ENDPOINTS2.dataFlowComponentReport) {
|
|
2708
|
+
if (request.method !== "POST") {
|
|
2709
|
+
throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
2710
|
+
}
|
|
2711
|
+
const data = await handleComponentDataFlowReport(
|
|
2712
|
+
request,
|
|
2713
|
+
dataFlowAnalyzer,
|
|
2714
|
+
options
|
|
2715
|
+
);
|
|
2716
|
+
writeJson(response, 200, { ok: true, data });
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
if (path8 === SPOTPATCH_ENDPOINTS2.dataFlowPageReport) {
|
|
2720
|
+
if (request.method !== "POST") {
|
|
2721
|
+
throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
2722
|
+
}
|
|
2723
|
+
const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
|
|
2724
|
+
writeJson(response, 200, { ok: true, data });
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2454
2727
|
if (agentRoute === void 0) {
|
|
2455
|
-
throw new
|
|
2728
|
+
throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
2456
2729
|
}
|
|
2457
2730
|
await handleAgentRequest(
|
|
2458
2731
|
request,
|
|
@@ -2603,6 +2876,7 @@ var OPTION_KEYS = Object.freeze([
|
|
|
2603
2876
|
"allowLan",
|
|
2604
2877
|
"budget",
|
|
2605
2878
|
"debug",
|
|
2879
|
+
"dataFlow",
|
|
2606
2880
|
"editor",
|
|
2607
2881
|
"enabled",
|
|
2608
2882
|
"exclude",
|
|
@@ -2690,6 +2964,9 @@ function serializeResolvedSpotPatchOptions(options) {
|
|
|
2690
2964
|
allowLan: options.allowLan,
|
|
2691
2965
|
budget: options.budget,
|
|
2692
2966
|
debug: options.debug,
|
|
2967
|
+
dataFlow: options.dataFlow.enabled ? Object.freeze({
|
|
2968
|
+
runtime: options.dataFlow.runtime
|
|
2969
|
+
}) : false,
|
|
2693
2970
|
editor: options.editor,
|
|
2694
2971
|
enabled: options.enabled,
|
|
2695
2972
|
exclude: Object.freeze(options.exclude.map(serializeFilter)),
|
|
@@ -2732,6 +3009,15 @@ function parseBudget(value) {
|
|
|
2732
3009
|
);
|
|
2733
3010
|
return Object.freeze(budget);
|
|
2734
3011
|
}
|
|
3012
|
+
function parseDataFlow(value) {
|
|
3013
|
+
if (value === false) return false;
|
|
3014
|
+
if (!isRecord2(value) || !hasExactKeys(value, ["runtime"]) || value.runtime !== "dispatch") {
|
|
3015
|
+
throw new TypeError("The SpotPatch data-flow transport is invalid.");
|
|
3016
|
+
}
|
|
3017
|
+
return Object.freeze({
|
|
3018
|
+
runtime: value.runtime
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
2735
3021
|
function parseSerializedSpotPatchOptions(value) {
|
|
2736
3022
|
if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
|
|
2737
3023
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
@@ -2745,6 +3031,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
2745
3031
|
allowLan: value.allowLan,
|
|
2746
3032
|
budget: parseBudget(value.budget),
|
|
2747
3033
|
debug: value.debug,
|
|
3034
|
+
dataFlow: parseDataFlow(value.dataFlow),
|
|
2748
3035
|
editor: value.editor,
|
|
2749
3036
|
enabled: value.enabled,
|
|
2750
3037
|
exclude: parseFilterList(value.exclude),
|
|
@@ -2767,6 +3054,7 @@ export {
|
|
|
2767
3054
|
createAgentJobManager,
|
|
2768
3055
|
createIntegrationFileChange,
|
|
2769
3056
|
createRuntimeAiConfig,
|
|
3057
|
+
createRuntimeDataFlowConfig,
|
|
2770
3058
|
createSession,
|
|
2771
3059
|
createSourceRegistrationService,
|
|
2772
3060
|
createSourceRegistry,
|