@spotpatch/dev-server 0.4.0 → 0.6.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 +1776 -331
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -2
- package/dist/index.d.ts +69 -2
- package/dist/index.js +1798 -295
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -34,8 +34,10 @@ __export(index_exports, {
|
|
|
34
34
|
DEFAULT_OPTIONS: () => DEFAULT_OPTIONS,
|
|
35
35
|
applyIntegrationPlan: () => applyIntegrationPlan,
|
|
36
36
|
createAgentJobManager: () => createAgentJobManager,
|
|
37
|
+
createExternalHandoffService: () => createExternalHandoffService,
|
|
37
38
|
createIntegrationFileChange: () => createIntegrationFileChange,
|
|
38
39
|
createRuntimeAiConfig: () => createRuntimeAiConfig,
|
|
40
|
+
createRuntimeDataFlowConfig: () => createRuntimeDataFlowConfig,
|
|
39
41
|
createSession: () => createSession,
|
|
40
42
|
createSourceRegistrationService: () => createSourceRegistrationService,
|
|
41
43
|
createSourceRegistry: () => createSourceRegistry,
|
|
@@ -520,6 +522,1064 @@ function createAgentJobManager(options) {
|
|
|
520
522
|
});
|
|
521
523
|
}
|
|
522
524
|
|
|
525
|
+
// src/external-handoff/service.ts
|
|
526
|
+
var import_shared7 = require("@spotpatch/shared");
|
|
527
|
+
var import_external_agent_node3 = require("@spotpatch/shared/external-agent-node");
|
|
528
|
+
|
|
529
|
+
// src/external-handoff/active-registry.ts
|
|
530
|
+
var import_node_crypto2 = require("crypto");
|
|
531
|
+
var import_shared2 = require("@spotpatch/shared");
|
|
532
|
+
|
|
533
|
+
// src/external-handoff/clock.ts
|
|
534
|
+
var import_node_perf_hooks = require("perf_hooks");
|
|
535
|
+
var SYSTEM_EXTERNAL_HANDOFF_CLOCK = Object.freeze({
|
|
536
|
+
monotonicNow: () => import_node_perf_hooks.performance.now(),
|
|
537
|
+
wallNow: () => Date.now()
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// src/external-handoff/active-registry.ts
|
|
541
|
+
var ALLOWED_TRANSITIONS = Object.freeze({
|
|
542
|
+
queued: ["dispatching", "failed"],
|
|
543
|
+
dispatching: ["dispatched", "working", "failed", "delivery-unknown"],
|
|
544
|
+
dispatched: ["working", "failed", "delivery-unknown"],
|
|
545
|
+
working: ["completed", "failed", "delivery-unknown"],
|
|
546
|
+
completed: [],
|
|
547
|
+
failed: [],
|
|
548
|
+
"delivery-unknown": []
|
|
549
|
+
});
|
|
550
|
+
var TERMINAL_PHASES = /* @__PURE__ */ new Set([
|
|
551
|
+
"completed",
|
|
552
|
+
"failed",
|
|
553
|
+
"delivery-unknown"
|
|
554
|
+
]);
|
|
555
|
+
function defaultRandomId() {
|
|
556
|
+
return (0, import_node_crypto2.randomBytes)(32).toString("base64url");
|
|
557
|
+
}
|
|
558
|
+
function requirePresent(value) {
|
|
559
|
+
if (value === null) throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.INTERNAL_ERROR);
|
|
560
|
+
return value;
|
|
561
|
+
}
|
|
562
|
+
function createActiveAdapterRegistry(options = {}) {
|
|
563
|
+
const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
|
|
564
|
+
const randomId = options.randomId ?? defaultRandomId;
|
|
565
|
+
let blocked;
|
|
566
|
+
let closed = false;
|
|
567
|
+
let dispatch;
|
|
568
|
+
let lastReleasedToken;
|
|
569
|
+
let lease;
|
|
570
|
+
const nowIso = () => new Date(clock.wallNow()).toISOString();
|
|
571
|
+
const requireOpen = () => {
|
|
572
|
+
if (closed) throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.SESSION_CLOSED);
|
|
573
|
+
};
|
|
574
|
+
const dispatchSummary = () => dispatch === void 0 ? null : Object.freeze({
|
|
575
|
+
adapterKind: dispatch.adapterKind,
|
|
576
|
+
revision: dispatch.revision,
|
|
577
|
+
phase: dispatch.phase,
|
|
578
|
+
updatedAt: dispatch.updatedAt
|
|
579
|
+
});
|
|
580
|
+
const activeSummary = () => {
|
|
581
|
+
if (blocked !== void 0) {
|
|
582
|
+
return Object.freeze({
|
|
583
|
+
kind: blocked.adapterKind,
|
|
584
|
+
state: "blocked",
|
|
585
|
+
canDispatch: false,
|
|
586
|
+
connectedAt: blocked.connectedAt,
|
|
587
|
+
updatedAt: blocked.updatedAt
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (lease === void 0) return null;
|
|
591
|
+
const busy = dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase);
|
|
592
|
+
return Object.freeze({
|
|
593
|
+
kind: lease.adapterKind,
|
|
594
|
+
state: busy ? "busy" : "ready",
|
|
595
|
+
canDispatch: !busy,
|
|
596
|
+
connectedAt: lease.connectedAt,
|
|
597
|
+
updatedAt: lease.updatedAt
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
const state = (cursor) => Object.freeze({
|
|
601
|
+
activeAdapter: activeSummary(),
|
|
602
|
+
dispatch: cursor === void 0 || dispatch?.cursor === cursor ? dispatchSummary() : null
|
|
603
|
+
});
|
|
604
|
+
const enterUnknown = (activeLease) => {
|
|
605
|
+
if (dispatch === void 0) return;
|
|
606
|
+
const updatedAt = nowIso();
|
|
607
|
+
dispatch.phase = "delivery-unknown";
|
|
608
|
+
dispatch.updatedAt = updatedAt;
|
|
609
|
+
blocked = Object.freeze({
|
|
610
|
+
adapterKind: activeLease.adapterKind,
|
|
611
|
+
connectedAt: activeLease.connectedAt,
|
|
612
|
+
updatedAt
|
|
613
|
+
});
|
|
614
|
+
};
|
|
615
|
+
const endLease = (activeLease) => {
|
|
616
|
+
if (dispatch?.phase === "queued") {
|
|
617
|
+
dispatch.phase = "failed";
|
|
618
|
+
dispatch.updatedAt = nowIso();
|
|
619
|
+
} else if (dispatch?.phase === "dispatching" || dispatch?.phase === "dispatched" || dispatch?.phase === "working") {
|
|
620
|
+
enterUnknown(activeLease);
|
|
621
|
+
}
|
|
622
|
+
lastReleasedToken = activeLease.token;
|
|
623
|
+
lease = void 0;
|
|
624
|
+
};
|
|
625
|
+
const sweep = () => {
|
|
626
|
+
if (lease === void 0) return;
|
|
627
|
+
const monotonicNow = clock.monotonicNow();
|
|
628
|
+
if (monotonicNow >= lease.expiresAtMonotonic || dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase) && monotonicNow >= dispatch.deadlineMonotonic) {
|
|
629
|
+
endLease(lease);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
const assertPublishable = () => {
|
|
633
|
+
requireOpen();
|
|
634
|
+
sweep();
|
|
635
|
+
if (blocked !== void 0 || lease !== void 0 && dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase)) {
|
|
636
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.EXTERNAL_AGENT_BUSY);
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
const requireLease = (leaseToken) => {
|
|
640
|
+
requireOpen();
|
|
641
|
+
sweep();
|
|
642
|
+
if (lease?.token !== leaseToken) {
|
|
643
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID);
|
|
644
|
+
}
|
|
645
|
+
return lease;
|
|
646
|
+
};
|
|
647
|
+
return Object.freeze({
|
|
648
|
+
assertPublishable,
|
|
649
|
+
claim(adapterKind, connectorInstanceId, baselineCursor) {
|
|
650
|
+
requireOpen();
|
|
651
|
+
sweep();
|
|
652
|
+
if (blocked !== void 0) {
|
|
653
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.EXTERNAL_AGENT_BUSY);
|
|
654
|
+
}
|
|
655
|
+
if (lease !== void 0) {
|
|
656
|
+
if (lease.adapterKind === adapterKind && lease.connectorInstanceId === connectorInstanceId) {
|
|
657
|
+
lease.expiresAtMonotonic = clock.monotonicNow() + import_shared2.EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
|
|
658
|
+
lease.updatedAt = nowIso();
|
|
659
|
+
return Object.freeze({
|
|
660
|
+
leaseToken: lease.token,
|
|
661
|
+
heartbeatIntervalMs: import_shared2.EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
|
|
662
|
+
baselineCursor: lease.baselineCursor,
|
|
663
|
+
activeAdapter: requirePresent(activeSummary())
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT);
|
|
667
|
+
}
|
|
668
|
+
const timestamp = nowIso();
|
|
669
|
+
lease = {
|
|
670
|
+
adapterKind,
|
|
671
|
+
baselineCursor,
|
|
672
|
+
connectedAt: timestamp,
|
|
673
|
+
connectorInstanceId,
|
|
674
|
+
token: randomId(),
|
|
675
|
+
expiresAtMonotonic: clock.monotonicNow() + import_shared2.EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs,
|
|
676
|
+
updatedAt: timestamp
|
|
677
|
+
};
|
|
678
|
+
lastReleasedToken = void 0;
|
|
679
|
+
return Object.freeze({
|
|
680
|
+
leaseToken: lease.token,
|
|
681
|
+
heartbeatIntervalMs: import_shared2.EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
|
|
682
|
+
baselineCursor,
|
|
683
|
+
activeAdapter: requirePresent(activeSummary())
|
|
684
|
+
});
|
|
685
|
+
},
|
|
686
|
+
heartbeat(leaseToken) {
|
|
687
|
+
const activeLease = requireLease(leaseToken);
|
|
688
|
+
activeLease.expiresAtMonotonic = clock.monotonicNow() + import_shared2.EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
|
|
689
|
+
activeLease.updatedAt = nowIso();
|
|
690
|
+
return state();
|
|
691
|
+
},
|
|
692
|
+
report(leaseToken, cursor, phase) {
|
|
693
|
+
const activeLease = requireLease(leaseToken);
|
|
694
|
+
if (dispatch?.cursor !== cursor || dispatch.adapterKind !== activeLease.adapterKind) {
|
|
695
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_DISPATCH_INVALID);
|
|
696
|
+
}
|
|
697
|
+
if (dispatch.phase === phase) return state(cursor);
|
|
698
|
+
const allowed = ALLOWED_TRANSITIONS[dispatch.phase];
|
|
699
|
+
if (!allowed.includes(phase)) {
|
|
700
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_DISPATCH_INVALID);
|
|
701
|
+
}
|
|
702
|
+
const updatedAt = nowIso();
|
|
703
|
+
dispatch.phase = phase;
|
|
704
|
+
dispatch.updatedAt = updatedAt;
|
|
705
|
+
activeLease.updatedAt = updatedAt;
|
|
706
|
+
if (phase === "delivery-unknown") {
|
|
707
|
+
blocked = Object.freeze({
|
|
708
|
+
adapterKind: activeLease.adapterKind,
|
|
709
|
+
connectedAt: activeLease.connectedAt,
|
|
710
|
+
updatedAt
|
|
711
|
+
});
|
|
712
|
+
lastReleasedToken = activeLease.token;
|
|
713
|
+
lease = void 0;
|
|
714
|
+
}
|
|
715
|
+
return state(cursor);
|
|
716
|
+
},
|
|
717
|
+
release(leaseToken) {
|
|
718
|
+
requireOpen();
|
|
719
|
+
sweep();
|
|
720
|
+
if (lease === void 0 && lastReleasedToken === leaseToken) return state();
|
|
721
|
+
const activeLease = lease;
|
|
722
|
+
if (activeLease?.token !== leaseToken) {
|
|
723
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID);
|
|
724
|
+
}
|
|
725
|
+
endLease(activeLease);
|
|
726
|
+
return state();
|
|
727
|
+
},
|
|
728
|
+
reserve(cursor, revision) {
|
|
729
|
+
assertPublishable();
|
|
730
|
+
if (lease === void 0) return Object.freeze({ mode: "inbox" });
|
|
731
|
+
const updatedAt = nowIso();
|
|
732
|
+
dispatch = {
|
|
733
|
+
adapterKind: lease.adapterKind,
|
|
734
|
+
cursor,
|
|
735
|
+
deadlineMonotonic: clock.monotonicNow() + import_shared2.EXTERNAL_HANDOFF_LIMITS.activeDispatchTimeoutMs,
|
|
736
|
+
revision,
|
|
737
|
+
phase: "queued",
|
|
738
|
+
updatedAt
|
|
739
|
+
};
|
|
740
|
+
lease.updatedAt = updatedAt;
|
|
741
|
+
return Object.freeze({
|
|
742
|
+
mode: "active",
|
|
743
|
+
adapter: requirePresent(activeSummary()),
|
|
744
|
+
dispatch: requirePresent(dispatchSummary())
|
|
745
|
+
});
|
|
746
|
+
},
|
|
747
|
+
resolveDelivery(cursor) {
|
|
748
|
+
requireOpen();
|
|
749
|
+
sweep();
|
|
750
|
+
const activeDispatch = dispatch;
|
|
751
|
+
if (blocked === void 0 || activeDispatch?.cursor !== cursor || activeDispatch.phase !== "delivery-unknown") {
|
|
752
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_DISPATCH_INVALID);
|
|
753
|
+
}
|
|
754
|
+
blocked = void 0;
|
|
755
|
+
return state(cursor);
|
|
756
|
+
},
|
|
757
|
+
snapshot(cursor) {
|
|
758
|
+
requireOpen();
|
|
759
|
+
sweep();
|
|
760
|
+
return state(cursor);
|
|
761
|
+
},
|
|
762
|
+
close() {
|
|
763
|
+
if (closed) return;
|
|
764
|
+
closed = true;
|
|
765
|
+
blocked = void 0;
|
|
766
|
+
dispatch = void 0;
|
|
767
|
+
lease = void 0;
|
|
768
|
+
lastReleasedToken = void 0;
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/external-handoff/broker.ts
|
|
774
|
+
var import_node_crypto3 = require("crypto");
|
|
775
|
+
var import_node_http = require("http");
|
|
776
|
+
var import_shared4 = require("@spotpatch/shared");
|
|
777
|
+
var import_external_agent_node = require("@spotpatch/shared/external-agent-node");
|
|
778
|
+
|
|
779
|
+
// src/server/request-body.ts
|
|
780
|
+
var import_shared3 = require("@spotpatch/shared");
|
|
781
|
+
|
|
782
|
+
// src/server/constants.ts
|
|
783
|
+
var MAX_REQUEST_BODY_BYTES = 32 * 1024;
|
|
784
|
+
var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
|
|
785
|
+
var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
|
786
|
+
|
|
787
|
+
// src/server/request-body.ts
|
|
788
|
+
function isJsonContentType(value) {
|
|
789
|
+
return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
790
|
+
}
|
|
791
|
+
async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
|
|
792
|
+
if (!isJsonContentType(request.headers["content-type"])) {
|
|
793
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST);
|
|
794
|
+
}
|
|
795
|
+
const declaredLength = Number(request.headers["content-length"]);
|
|
796
|
+
if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
|
|
797
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST);
|
|
798
|
+
}
|
|
799
|
+
const chunks = [];
|
|
800
|
+
let byteLength = 0;
|
|
801
|
+
let exceededLimit = false;
|
|
802
|
+
for await (const rawChunk of request) {
|
|
803
|
+
const chunk = rawChunk;
|
|
804
|
+
if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
|
|
805
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST);
|
|
806
|
+
}
|
|
807
|
+
const buffer = Buffer.from(chunk);
|
|
808
|
+
byteLength += buffer.byteLength;
|
|
809
|
+
if (byteLength > maximumBytes) {
|
|
810
|
+
exceededLimit = true;
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
chunks.push(buffer);
|
|
814
|
+
}
|
|
815
|
+
if (exceededLimit || byteLength === 0) {
|
|
816
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST);
|
|
817
|
+
}
|
|
818
|
+
try {
|
|
819
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
820
|
+
} catch (error) {
|
|
821
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST, void 0, {
|
|
822
|
+
cause: error
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/external-handoff/broker.ts
|
|
828
|
+
function singleHeader(request, name) {
|
|
829
|
+
const value = request.headers[name.toLowerCase()];
|
|
830
|
+
return Array.isArray(value) ? void 0 : value;
|
|
831
|
+
}
|
|
832
|
+
function tokensMatch(actual, expected) {
|
|
833
|
+
if (actual === void 0) return false;
|
|
834
|
+
const actualBytes = Buffer.from(actual);
|
|
835
|
+
const expectedBytes = Buffer.from(expected);
|
|
836
|
+
return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto3.timingSafeEqual)(actualBytes, expectedBytes);
|
|
837
|
+
}
|
|
838
|
+
function writeJson(response, status, payload) {
|
|
839
|
+
response.statusCode = status;
|
|
840
|
+
response.setHeader("Cache-Control", "no-store");
|
|
841
|
+
response.setHeader("Connection", "close");
|
|
842
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
843
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
844
|
+
response.end(JSON.stringify(payload));
|
|
845
|
+
}
|
|
846
|
+
function statusForError(code) {
|
|
847
|
+
if (code === import_shared4.ERROR_CODES.BRIDGE_UNAUTHORIZED) return 401;
|
|
848
|
+
if (code === import_shared4.ERROR_CODES.HANDOFF_NOT_FOUND) return 404;
|
|
849
|
+
if (code === import_shared4.ERROR_CODES.HANDOFF_EXPIRED || code === import_shared4.ERROR_CODES.SESSION_CLOSED) {
|
|
850
|
+
return 410;
|
|
851
|
+
}
|
|
852
|
+
if (code === import_shared4.ERROR_CODES.BRIDGE_BUSY) return 429;
|
|
853
|
+
if (code === import_shared4.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID) return 401;
|
|
854
|
+
if (code === import_shared4.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE) return 413;
|
|
855
|
+
if (code === import_shared4.ERROR_CODES.HANDOFF_CURSOR_INVALID || code === import_shared4.ERROR_CODES.BRIDGE_PROTOCOL_MISMATCH || code === import_shared4.ERROR_CODES.EXTERNAL_AGENT_BUSY || code === import_shared4.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT || code === import_shared4.ERROR_CODES.ACTIVE_DISPATCH_INVALID) {
|
|
856
|
+
return 409;
|
|
857
|
+
}
|
|
858
|
+
if (code === import_shared4.ERROR_CODES.INVALID_REQUEST) return 400;
|
|
859
|
+
return 500;
|
|
860
|
+
}
|
|
861
|
+
function normalizeError2(error) {
|
|
862
|
+
return error instanceof import_shared4.SpotPatchError ? error : new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INTERNAL_ERROR, void 0, { cause: error });
|
|
863
|
+
}
|
|
864
|
+
function assertAuthorized(request, expectedHost, bridgeToken) {
|
|
865
|
+
if (request.socket.remoteAddress !== "127.0.0.1" || singleHeader(request, "host") !== expectedHost || !tokensMatch(singleHeader(request, import_external_agent_node.SPOTPATCH_BRIDGE_TOKEN_HEADER), bridgeToken)) {
|
|
866
|
+
throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.BRIDGE_UNAUTHORIZED);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async function closeServer(server, sockets) {
|
|
870
|
+
await new Promise((resolve) => {
|
|
871
|
+
server.close(() => {
|
|
872
|
+
resolve();
|
|
873
|
+
});
|
|
874
|
+
for (const socket of sockets) {
|
|
875
|
+
socket.destroy();
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
async function createExternalHandoffBroker(options) {
|
|
880
|
+
const bridgeToken = (0, import_node_crypto3.randomBytes)(32).toString("base64url");
|
|
881
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
882
|
+
let expectedHost = "";
|
|
883
|
+
const server = (0, import_node_http.createServer)(
|
|
884
|
+
{ maxHeaderSize: import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumBrokerHeaderBytes },
|
|
885
|
+
(request, response) => {
|
|
886
|
+
const handle = async () => {
|
|
887
|
+
assertAuthorized(request, expectedHost, bridgeToken);
|
|
888
|
+
if (request.method !== "POST") {
|
|
889
|
+
throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
890
|
+
}
|
|
891
|
+
const body = await readJsonRequestBody(
|
|
892
|
+
request,
|
|
893
|
+
import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumBrokerRequestBytes
|
|
894
|
+
);
|
|
895
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.status) {
|
|
896
|
+
const parsed = import_external_agent_node.bridgeStatusRequestSchema.safeParse(body);
|
|
897
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
898
|
+
let current = null;
|
|
899
|
+
try {
|
|
900
|
+
current = options.store.status();
|
|
901
|
+
} catch (error) {
|
|
902
|
+
if (!(error instanceof import_shared4.SpotPatchError) || error.code !== import_shared4.ERROR_CODES.HANDOFF_NOT_FOUND) {
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
writeJson(response, 200, {
|
|
907
|
+
ok: true,
|
|
908
|
+
data: Object.freeze({
|
|
909
|
+
brokerProtocolVersion: import_shared4.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
|
|
910
|
+
projectKey: options.projectKey,
|
|
911
|
+
sessionId: options.sessionId,
|
|
912
|
+
framework: options.framework,
|
|
913
|
+
current
|
|
914
|
+
})
|
|
915
|
+
});
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.current) {
|
|
919
|
+
const parsed = import_external_agent_node.bridgeCurrentRequestSchema.safeParse(body);
|
|
920
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
921
|
+
const snapshot2 = options.store.current(parsed.data.cursor);
|
|
922
|
+
writeJson(response, 200, {
|
|
923
|
+
ok: true,
|
|
924
|
+
data: Object.freeze({ outcome: "handoff", snapshot: snapshot2 })
|
|
925
|
+
});
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.ack) {
|
|
929
|
+
const parsed = import_external_agent_node.bridgeAckRequestSchema.safeParse(body);
|
|
930
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
931
|
+
const summary = options.store.ack(
|
|
932
|
+
parsed.data.cursor,
|
|
933
|
+
parsed.data.connectorInstanceId
|
|
934
|
+
);
|
|
935
|
+
writeJson(response, 200, {
|
|
936
|
+
ok: true,
|
|
937
|
+
data: Object.freeze({ summary })
|
|
938
|
+
});
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.wait) {
|
|
942
|
+
const parsed = import_external_agent_node.bridgeWaitRequestSchema.safeParse(body);
|
|
943
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
944
|
+
const controller = new AbortController();
|
|
945
|
+
const abort = () => {
|
|
946
|
+
if (!response.writableEnded) controller.abort("bridge-client-closed");
|
|
947
|
+
};
|
|
948
|
+
response.once("close", abort);
|
|
949
|
+
try {
|
|
950
|
+
const data = await options.store.wait(
|
|
951
|
+
parsed.data.afterCursor,
|
|
952
|
+
parsed.data.timeoutMs,
|
|
953
|
+
controller.signal
|
|
954
|
+
);
|
|
955
|
+
writeJson(response, 200, { ok: true, data });
|
|
956
|
+
} finally {
|
|
957
|
+
response.removeListener("close", abort);
|
|
958
|
+
}
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.activeClaim) {
|
|
962
|
+
const parsed = import_external_agent_node.bridgeActiveClaimRequestSchema.safeParse(body);
|
|
963
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
964
|
+
const data = options.activeRegistry.claim(
|
|
965
|
+
parsed.data.adapterKind,
|
|
966
|
+
parsed.data.connectorInstanceId,
|
|
967
|
+
options.store.currentCursor()
|
|
968
|
+
);
|
|
969
|
+
writeJson(response, 200, { ok: true, data });
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.activeHeartbeat) {
|
|
973
|
+
const parsed = import_external_agent_node.bridgeActiveHeartbeatRequestSchema.safeParse(body);
|
|
974
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
975
|
+
const data = options.activeRegistry.heartbeat(parsed.data.leaseToken);
|
|
976
|
+
writeJson(response, 200, { ok: true, data });
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.activeReport) {
|
|
980
|
+
const parsed = import_external_agent_node.bridgeActiveReportRequestSchema.safeParse(body);
|
|
981
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
982
|
+
const data = options.activeRegistry.report(
|
|
983
|
+
parsed.data.leaseToken,
|
|
984
|
+
parsed.data.cursor,
|
|
985
|
+
parsed.data.phase
|
|
986
|
+
);
|
|
987
|
+
writeJson(response, 200, { ok: true, data });
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.activeRelease) {
|
|
991
|
+
const parsed = import_external_agent_node.bridgeActiveReleaseRequestSchema.safeParse(body);
|
|
992
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
993
|
+
const data = options.activeRegistry.release(parsed.data.leaseToken);
|
|
994
|
+
writeJson(response, 200, { ok: true, data });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
998
|
+
};
|
|
999
|
+
void handle().catch((error) => {
|
|
1000
|
+
if (response.writableEnded || response.destroyed) return;
|
|
1001
|
+
const normalized = normalizeError2(error);
|
|
1002
|
+
if (normalized.code === import_shared4.ERROR_CODES.BRIDGE_BUSY) {
|
|
1003
|
+
response.setHeader("Retry-After", "1");
|
|
1004
|
+
}
|
|
1005
|
+
writeJson(response, statusForError(normalized.code), {
|
|
1006
|
+
ok: false,
|
|
1007
|
+
error: {
|
|
1008
|
+
code: normalized.code,
|
|
1009
|
+
message: "The local SpotPatch bridge request failed."
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
);
|
|
1015
|
+
server.maxConnections = import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumBrokerSockets;
|
|
1016
|
+
server.headersTimeout = 5e3;
|
|
1017
|
+
server.requestTimeout = import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumWaitMs + 5e3;
|
|
1018
|
+
server.keepAliveTimeout = 1;
|
|
1019
|
+
server.on("connection", (socket) => {
|
|
1020
|
+
if (sockets.size >= import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumBrokerSockets) {
|
|
1021
|
+
socket.destroy();
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
sockets.add(socket);
|
|
1025
|
+
socket.once("close", () => sockets.delete(socket));
|
|
1026
|
+
});
|
|
1027
|
+
await new Promise((resolve, reject) => {
|
|
1028
|
+
server.once("error", reject);
|
|
1029
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
1030
|
+
});
|
|
1031
|
+
const address = server.address();
|
|
1032
|
+
if (address === null || typeof address === "string" || address.address !== "127.0.0.1") {
|
|
1033
|
+
await closeServer(server, sockets);
|
|
1034
|
+
throw new Error("SpotPatch external Agent broker did not bind IPv4 loopback.");
|
|
1035
|
+
}
|
|
1036
|
+
expectedHost = `127.0.0.1:${String(address.port)}`;
|
|
1037
|
+
let closed = false;
|
|
1038
|
+
let ready = true;
|
|
1039
|
+
server.removeAllListeners("error");
|
|
1040
|
+
server.on("error", () => {
|
|
1041
|
+
ready = false;
|
|
1042
|
+
for (const socket of sockets) socket.destroy();
|
|
1043
|
+
});
|
|
1044
|
+
return Object.freeze({
|
|
1045
|
+
bridgeToken,
|
|
1046
|
+
endpoint: `http://${expectedHost}`,
|
|
1047
|
+
isReady: () => ready && !closed,
|
|
1048
|
+
async close() {
|
|
1049
|
+
if (closed) return;
|
|
1050
|
+
closed = true;
|
|
1051
|
+
ready = false;
|
|
1052
|
+
await closeServer(server, sockets);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// src/external-handoff/discovery.ts
|
|
1058
|
+
var import_node_crypto4 = require("crypto");
|
|
1059
|
+
var import_promises = require("fs/promises");
|
|
1060
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
1061
|
+
var import_shared5 = require("@spotpatch/shared");
|
|
1062
|
+
var import_external_agent_node2 = require("@spotpatch/shared/external-agent-node");
|
|
1063
|
+
async function syncDirectory(directory) {
|
|
1064
|
+
const handle = await (0, import_promises.open)(directory, "r");
|
|
1065
|
+
try {
|
|
1066
|
+
await handle.sync();
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
const code = error.code;
|
|
1069
|
+
if (code !== "EINVAL" && code !== "ENOTSUP") {
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
} finally {
|
|
1073
|
+
await handle.close();
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
async function publishExternalHandoffDescriptor(options) {
|
|
1077
|
+
const directory = await (0, import_external_agent_node2.resolveExternalHandoffRuntimeDirectory)(true);
|
|
1078
|
+
const descriptor = import_external_agent_node2.externalHandoffDescriptorSchema.parse({
|
|
1079
|
+
schemaVersion: 1,
|
|
1080
|
+
brokerProtocolVersion: import_shared5.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
|
|
1081
|
+
projectKey: await (0, import_external_agent_node2.computeExternalHandoffProjectKey)(options.root),
|
|
1082
|
+
sessionId: options.sessionId,
|
|
1083
|
+
framework: options.framework,
|
|
1084
|
+
endpoint: options.endpoint,
|
|
1085
|
+
bridgeToken: options.bridgeToken,
|
|
1086
|
+
pid: process.pid,
|
|
1087
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1088
|
+
});
|
|
1089
|
+
const serialized = JSON.stringify(descriptor);
|
|
1090
|
+
if (Buffer.byteLength(serialized, "utf8") > import_shared5.EXTERNAL_HANDOFF_LIMITS.maximumDescriptorBytes) {
|
|
1091
|
+
throw new RangeError("SpotPatch external Agent descriptor exceeds its limit.");
|
|
1092
|
+
}
|
|
1093
|
+
const destination = import_node_path.default.join(directory, `${descriptor.sessionId}.json`);
|
|
1094
|
+
const temporary = import_node_path.default.join(
|
|
1095
|
+
directory,
|
|
1096
|
+
`.${descriptor.sessionId}.${(0, import_node_crypto4.randomBytes)(8).toString("hex")}.tmp`
|
|
1097
|
+
);
|
|
1098
|
+
let temporaryExists = false;
|
|
1099
|
+
let published = false;
|
|
1100
|
+
let descriptorIdentity = Object.freeze({
|
|
1101
|
+
device: -1,
|
|
1102
|
+
inode: -1
|
|
1103
|
+
});
|
|
1104
|
+
try {
|
|
1105
|
+
const handle = await (0, import_promises.open)(temporary, "wx", 384);
|
|
1106
|
+
temporaryExists = true;
|
|
1107
|
+
try {
|
|
1108
|
+
await handle.writeFile(serialized, "utf8");
|
|
1109
|
+
await handle.sync();
|
|
1110
|
+
} finally {
|
|
1111
|
+
await handle.close();
|
|
1112
|
+
}
|
|
1113
|
+
await (0, import_promises.rename)(temporary, destination);
|
|
1114
|
+
temporaryExists = false;
|
|
1115
|
+
published = true;
|
|
1116
|
+
const status = await (0, import_promises.lstat)(destination);
|
|
1117
|
+
const uid = process.getuid?.();
|
|
1118
|
+
if (!status.isFile() || status.isSymbolicLink() || uid === void 0 || status.uid !== uid || (status.mode & 63) !== 0) {
|
|
1119
|
+
throw new Error("SpotPatch external Agent descriptor is not private.");
|
|
1120
|
+
}
|
|
1121
|
+
descriptorIdentity = Object.freeze({ device: status.dev, inode: status.ino });
|
|
1122
|
+
await syncDirectory(directory);
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
if (temporaryExists) {
|
|
1125
|
+
await (0, import_promises.unlink)(temporary).catch(() => void 0);
|
|
1126
|
+
}
|
|
1127
|
+
if (published) {
|
|
1128
|
+
await (0, import_promises.unlink)(destination).catch(() => void 0);
|
|
1129
|
+
}
|
|
1130
|
+
throw error;
|
|
1131
|
+
}
|
|
1132
|
+
let closed = false;
|
|
1133
|
+
return Object.freeze({
|
|
1134
|
+
descriptor,
|
|
1135
|
+
async close() {
|
|
1136
|
+
if (closed) return;
|
|
1137
|
+
closed = true;
|
|
1138
|
+
await (0, import_promises.lstat)(destination).then(async (status) => {
|
|
1139
|
+
if (status.dev === descriptorIdentity.device && status.ino === descriptorIdentity.inode) {
|
|
1140
|
+
await (0, import_promises.unlink)(destination);
|
|
1141
|
+
}
|
|
1142
|
+
}).catch((error) => {
|
|
1143
|
+
if (error.code !== "ENOENT") {
|
|
1144
|
+
throw error;
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
await syncDirectory(directory);
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// src/external-handoff/fingerprint.ts
|
|
1153
|
+
var import_node_crypto5 = require("crypto");
|
|
1154
|
+
function canonicalJson(value) {
|
|
1155
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
1156
|
+
return JSON.stringify(value);
|
|
1157
|
+
}
|
|
1158
|
+
if (typeof value === "number") {
|
|
1159
|
+
if (!Number.isFinite(value)) throw new TypeError("Non-finite JSON number.");
|
|
1160
|
+
return JSON.stringify(value);
|
|
1161
|
+
}
|
|
1162
|
+
if (Array.isArray(value)) {
|
|
1163
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
1164
|
+
}
|
|
1165
|
+
if (typeof value === "object") {
|
|
1166
|
+
const record = value;
|
|
1167
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
1168
|
+
}
|
|
1169
|
+
throw new TypeError("Unsupported JSON value.");
|
|
1170
|
+
}
|
|
1171
|
+
function fingerprintExternalHandoffAnnotation(annotation) {
|
|
1172
|
+
return (0, import_node_crypto5.createHash)("sha256").update(canonicalJson(annotation)).digest("hex");
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/external-handoff/store.ts
|
|
1176
|
+
var import_node_crypto6 = require("crypto");
|
|
1177
|
+
var import_shared6 = require("@spotpatch/shared");
|
|
1178
|
+
function defaultRandomId2() {
|
|
1179
|
+
return (0, import_node_crypto6.randomBytes)(24).toString("base64url");
|
|
1180
|
+
}
|
|
1181
|
+
function pageSummary(annotation) {
|
|
1182
|
+
let origin = "[unavailable]";
|
|
1183
|
+
try {
|
|
1184
|
+
const url = new URL(annotation.page.url);
|
|
1185
|
+
origin = url.origin === "null" ? "[unavailable]" : url.origin;
|
|
1186
|
+
} catch {
|
|
1187
|
+
}
|
|
1188
|
+
return Object.freeze({ origin, pathname: annotation.page.pathname });
|
|
1189
|
+
}
|
|
1190
|
+
function summaryOf(current, state) {
|
|
1191
|
+
const snapshot2 = current.snapshot;
|
|
1192
|
+
return Object.freeze({
|
|
1193
|
+
sessionId: snapshot2.session.id,
|
|
1194
|
+
framework: snapshot2.session.framework,
|
|
1195
|
+
revision: snapshot2.revision,
|
|
1196
|
+
cursor: snapshot2.cursor,
|
|
1197
|
+
targetCount: snapshot2.annotation.targets.length,
|
|
1198
|
+
page: pageSummary(snapshot2.annotation),
|
|
1199
|
+
publishedAt: snapshot2.publishedAt,
|
|
1200
|
+
expiresAt: snapshot2.expiresAt,
|
|
1201
|
+
state,
|
|
1202
|
+
pickupCount: current.receipts.size,
|
|
1203
|
+
...current.pickedUpAt === void 0 ? {} : { pickedUpAt: current.pickedUpAt }
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
function replayResult(record) {
|
|
1207
|
+
return Object.freeze({ ...record.result, replayed: true });
|
|
1208
|
+
}
|
|
1209
|
+
function createExternalHandoffStore(options) {
|
|
1210
|
+
const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
|
|
1211
|
+
const randomId = options.randomId ?? defaultRandomId2;
|
|
1212
|
+
const history = [];
|
|
1213
|
+
const idempotency = /* @__PURE__ */ new Map();
|
|
1214
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
1215
|
+
let closed = false;
|
|
1216
|
+
let current;
|
|
1217
|
+
let revision = 0;
|
|
1218
|
+
const requireOpen = () => {
|
|
1219
|
+
if (closed) throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.SESSION_CLOSED);
|
|
1220
|
+
};
|
|
1221
|
+
const archive = (state) => {
|
|
1222
|
+
if (current === void 0) return;
|
|
1223
|
+
history.unshift(summaryOf(current, state));
|
|
1224
|
+
history.length = Math.min(
|
|
1225
|
+
history.length,
|
|
1226
|
+
import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumHistorySummaries
|
|
1227
|
+
);
|
|
1228
|
+
current = void 0;
|
|
1229
|
+
};
|
|
1230
|
+
const sweep = () => {
|
|
1231
|
+
const monotonicNow = clock.monotonicNow();
|
|
1232
|
+
if (current !== void 0 && monotonicNow >= current.expiresAtMonotonic) {
|
|
1233
|
+
archive("expired");
|
|
1234
|
+
}
|
|
1235
|
+
for (const [requestId, record] of idempotency) {
|
|
1236
|
+
if (monotonicNow >= record.expiresAtMonotonic) idempotency.delete(requestId);
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
const knownSummary = (cursor) => {
|
|
1240
|
+
sweep();
|
|
1241
|
+
if (current?.snapshot.cursor === cursor) {
|
|
1242
|
+
return summaryOf(current, "available");
|
|
1243
|
+
}
|
|
1244
|
+
return history.find((summary) => summary.cursor === cursor);
|
|
1245
|
+
};
|
|
1246
|
+
const readCurrent = (cursor) => {
|
|
1247
|
+
requireOpen();
|
|
1248
|
+
sweep();
|
|
1249
|
+
if (current === void 0) {
|
|
1250
|
+
const prior = cursor === void 0 ? void 0 : knownSummary(cursor);
|
|
1251
|
+
throw new import_shared6.SpotPatchError(
|
|
1252
|
+
prior?.state === "expired" ? import_shared6.ERROR_CODES.HANDOFF_EXPIRED : cursor === void 0 ? import_shared6.ERROR_CODES.HANDOFF_NOT_FOUND : import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
if (cursor !== void 0 && cursor !== current.snapshot.cursor) {
|
|
1256
|
+
const prior = knownSummary(cursor);
|
|
1257
|
+
throw new import_shared6.SpotPatchError(
|
|
1258
|
+
prior?.state === "expired" ? import_shared6.ERROR_CODES.HANDOFF_EXPIRED : import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
return current.snapshot;
|
|
1262
|
+
};
|
|
1263
|
+
const findReplay = (requestId, fingerprint) => {
|
|
1264
|
+
requireOpen();
|
|
1265
|
+
sweep();
|
|
1266
|
+
const record = idempotency.get(requestId);
|
|
1267
|
+
if (record === void 0) return void 0;
|
|
1268
|
+
if (record.fingerprint !== fingerprint) {
|
|
1269
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
|
|
1270
|
+
}
|
|
1271
|
+
return replayResult(record);
|
|
1272
|
+
};
|
|
1273
|
+
const settleWaiters = (result) => {
|
|
1274
|
+
const pending = [...waiters];
|
|
1275
|
+
waiters.clear();
|
|
1276
|
+
for (const waiter of pending) waiter.resolve(result);
|
|
1277
|
+
};
|
|
1278
|
+
return Object.freeze({
|
|
1279
|
+
activeWaitCount: () => waiters.size,
|
|
1280
|
+
replay: findReplay,
|
|
1281
|
+
publish(input) {
|
|
1282
|
+
requireOpen();
|
|
1283
|
+
const replayed = findReplay(input.requestId, input.fingerprint);
|
|
1284
|
+
if (replayed !== void 0) return replayed;
|
|
1285
|
+
if (idempotency.size >= import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumRequestIdRecords) {
|
|
1286
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE);
|
|
1287
|
+
}
|
|
1288
|
+
const nextRevision = revision + 1;
|
|
1289
|
+
const publishedAtMs = clock.wallNow();
|
|
1290
|
+
const publishedAtMonotonic = clock.monotonicNow();
|
|
1291
|
+
const snapshot2 = Object.freeze({
|
|
1292
|
+
schemaVersion: import_shared6.EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
|
|
1293
|
+
cursor: randomId(),
|
|
1294
|
+
session: Object.freeze({ id: options.sessionId, framework: options.framework }),
|
|
1295
|
+
revision: nextRevision,
|
|
1296
|
+
publishedAt: new Date(publishedAtMs).toISOString(),
|
|
1297
|
+
expiresAt: new Date(
|
|
1298
|
+
publishedAtMs + import_shared6.EXTERNAL_HANDOFF_LIMITS.handoffTtlMs
|
|
1299
|
+
).toISOString(),
|
|
1300
|
+
annotation: input.annotation
|
|
1301
|
+
});
|
|
1302
|
+
if (Buffer.byteLength(JSON.stringify(snapshot2), "utf8") > import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumSnapshotBytes) {
|
|
1303
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE);
|
|
1304
|
+
}
|
|
1305
|
+
const delivery = input.reserve(snapshot2.cursor, nextRevision);
|
|
1306
|
+
if (current !== void 0) archive("superseded");
|
|
1307
|
+
revision = nextRevision;
|
|
1308
|
+
current = {
|
|
1309
|
+
expiresAtMonotonic: publishedAtMonotonic + import_shared6.EXTERNAL_HANDOFF_LIMITS.handoffTtlMs,
|
|
1310
|
+
receipts: /* @__PURE__ */ new Set(),
|
|
1311
|
+
snapshot: snapshot2
|
|
1312
|
+
};
|
|
1313
|
+
const result = Object.freeze({
|
|
1314
|
+
handoff: summaryOf(current, "available"),
|
|
1315
|
+
delivery,
|
|
1316
|
+
replayed: false
|
|
1317
|
+
});
|
|
1318
|
+
idempotency.set(
|
|
1319
|
+
input.requestId,
|
|
1320
|
+
Object.freeze({
|
|
1321
|
+
expiresAtMonotonic: publishedAtMonotonic + import_shared6.EXTERNAL_HANDOFF_LIMITS.requestIdTtlMs,
|
|
1322
|
+
fingerprint: input.fingerprint,
|
|
1323
|
+
result
|
|
1324
|
+
})
|
|
1325
|
+
);
|
|
1326
|
+
settleWaiters(Object.freeze({ outcome: "handoff", snapshot: snapshot2 }));
|
|
1327
|
+
return result;
|
|
1328
|
+
},
|
|
1329
|
+
current: readCurrent,
|
|
1330
|
+
currentCursor() {
|
|
1331
|
+
requireOpen();
|
|
1332
|
+
sweep();
|
|
1333
|
+
return current?.snapshot.cursor ?? null;
|
|
1334
|
+
},
|
|
1335
|
+
status(cursor) {
|
|
1336
|
+
requireOpen();
|
|
1337
|
+
sweep();
|
|
1338
|
+
if (cursor === void 0) {
|
|
1339
|
+
if (current === void 0) {
|
|
1340
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_NOT_FOUND);
|
|
1341
|
+
}
|
|
1342
|
+
return summaryOf(current, "available");
|
|
1343
|
+
}
|
|
1344
|
+
const summary = knownSummary(cursor);
|
|
1345
|
+
if (summary === void 0) {
|
|
1346
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID);
|
|
1347
|
+
}
|
|
1348
|
+
return summary;
|
|
1349
|
+
},
|
|
1350
|
+
ack(cursor, connectorInstanceId) {
|
|
1351
|
+
const snapshot2 = readCurrent(cursor);
|
|
1352
|
+
if (snapshot2 !== current?.snapshot) {
|
|
1353
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID);
|
|
1354
|
+
}
|
|
1355
|
+
if (!current.receipts.has(connectorInstanceId)) {
|
|
1356
|
+
if (current.receipts.size >= import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumConnectorReceipts) {
|
|
1357
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.BRIDGE_BUSY);
|
|
1358
|
+
}
|
|
1359
|
+
current.receipts.add(connectorInstanceId);
|
|
1360
|
+
current.pickedUpAt = new Date(clock.wallNow()).toISOString();
|
|
1361
|
+
}
|
|
1362
|
+
return summaryOf(current, "available");
|
|
1363
|
+
},
|
|
1364
|
+
async wait(afterCursor, timeoutMs, signal) {
|
|
1365
|
+
requireOpen();
|
|
1366
|
+
sweep();
|
|
1367
|
+
if (afterCursor === void 0 && current !== void 0) {
|
|
1368
|
+
return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
|
|
1369
|
+
}
|
|
1370
|
+
if (afterCursor !== void 0) {
|
|
1371
|
+
const known = knownSummary(afterCursor);
|
|
1372
|
+
if (known === void 0) {
|
|
1373
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID);
|
|
1374
|
+
}
|
|
1375
|
+
if (current !== void 0 && current.snapshot.cursor !== afterCursor) {
|
|
1376
|
+
return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (waiters.size >= import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumWaiters) {
|
|
1380
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.BRIDGE_BUSY);
|
|
1381
|
+
}
|
|
1382
|
+
if (signal.aborted) throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.SESSION_CLOSED);
|
|
1383
|
+
return new Promise((resolve, reject) => {
|
|
1384
|
+
let settled = false;
|
|
1385
|
+
const finish = () => {
|
|
1386
|
+
if (settled) return false;
|
|
1387
|
+
settled = true;
|
|
1388
|
+
waiters.delete(waiter);
|
|
1389
|
+
clearTimeout(timer);
|
|
1390
|
+
signal.removeEventListener("abort", abort);
|
|
1391
|
+
return true;
|
|
1392
|
+
};
|
|
1393
|
+
const waiter = {
|
|
1394
|
+
reject(error) {
|
|
1395
|
+
if (finish()) reject(error);
|
|
1396
|
+
},
|
|
1397
|
+
resolve(result) {
|
|
1398
|
+
if (finish()) resolve(result);
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
const abort = () => {
|
|
1402
|
+
waiter.reject(new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.SESSION_CLOSED));
|
|
1403
|
+
};
|
|
1404
|
+
const timer = setTimeout(() => {
|
|
1405
|
+
waiter.resolve(Object.freeze({ outcome: "timeout" }));
|
|
1406
|
+
}, timeoutMs);
|
|
1407
|
+
timer.unref();
|
|
1408
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1409
|
+
waiters.add(waiter);
|
|
1410
|
+
});
|
|
1411
|
+
},
|
|
1412
|
+
close() {
|
|
1413
|
+
if (closed) return;
|
|
1414
|
+
closed = true;
|
|
1415
|
+
current = void 0;
|
|
1416
|
+
history.length = 0;
|
|
1417
|
+
idempotency.clear();
|
|
1418
|
+
const pending = [...waiters];
|
|
1419
|
+
waiters.clear();
|
|
1420
|
+
for (const waiter of pending) {
|
|
1421
|
+
waiter.reject(new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.SESSION_CLOSED));
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// src/external-handoff/service.ts
|
|
1428
|
+
function asReplay(result) {
|
|
1429
|
+
return Object.freeze({ ...result, replayed: true });
|
|
1430
|
+
}
|
|
1431
|
+
function createExternalHandoffService(options) {
|
|
1432
|
+
const activeRegistry = createActiveAdapterRegistry();
|
|
1433
|
+
const store = createExternalHandoffStore({
|
|
1434
|
+
framework: options.framework,
|
|
1435
|
+
sessionId: options.sessionId
|
|
1436
|
+
});
|
|
1437
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1438
|
+
let broker;
|
|
1439
|
+
let descriptor;
|
|
1440
|
+
let startPromise;
|
|
1441
|
+
let closePromise;
|
|
1442
|
+
let state = "idle";
|
|
1443
|
+
const isClosed = () => state === "closed";
|
|
1444
|
+
const requireReady = () => {
|
|
1445
|
+
if (state !== "ready" || broker?.isReady() !== true) {
|
|
1446
|
+
throw new import_shared7.SpotPatchError(
|
|
1447
|
+
state === "closed" ? import_shared7.ERROR_CODES.SESSION_CLOSED : import_shared7.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
const start = async () => {
|
|
1452
|
+
if (state === "ready") return;
|
|
1453
|
+
if (state === "closed") throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.SESSION_CLOSED);
|
|
1454
|
+
if (startPromise !== void 0) return startPromise;
|
|
1455
|
+
state = "starting";
|
|
1456
|
+
startPromise = (async () => {
|
|
1457
|
+
let createdBroker;
|
|
1458
|
+
let createdDescriptor;
|
|
1459
|
+
try {
|
|
1460
|
+
const projectKey = await (0, import_external_agent_node3.computeExternalHandoffProjectKey)(options.root);
|
|
1461
|
+
createdBroker = await createExternalHandoffBroker({
|
|
1462
|
+
activeRegistry,
|
|
1463
|
+
framework: options.framework,
|
|
1464
|
+
projectKey,
|
|
1465
|
+
sessionId: options.sessionId,
|
|
1466
|
+
store
|
|
1467
|
+
});
|
|
1468
|
+
createdDescriptor = await publishExternalHandoffDescriptor({
|
|
1469
|
+
bridgeToken: createdBroker.bridgeToken,
|
|
1470
|
+
endpoint: createdBroker.endpoint,
|
|
1471
|
+
framework: options.framework,
|
|
1472
|
+
root: options.root,
|
|
1473
|
+
sessionId: options.sessionId
|
|
1474
|
+
});
|
|
1475
|
+
if (isClosed()) {
|
|
1476
|
+
await createdDescriptor.close();
|
|
1477
|
+
await createdBroker.close();
|
|
1478
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.SESSION_CLOSED);
|
|
1479
|
+
}
|
|
1480
|
+
broker = createdBroker;
|
|
1481
|
+
descriptor = createdDescriptor;
|
|
1482
|
+
state = "ready";
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
if (createdDescriptor !== void 0 && descriptor !== createdDescriptor) {
|
|
1485
|
+
await createdDescriptor.close().catch(() => void 0);
|
|
1486
|
+
}
|
|
1487
|
+
if (createdBroker !== void 0 && broker !== createdBroker) {
|
|
1488
|
+
await createdBroker.close().catch(() => void 0);
|
|
1489
|
+
}
|
|
1490
|
+
if (!isClosed()) state = "failed";
|
|
1491
|
+
throw error;
|
|
1492
|
+
}
|
|
1493
|
+
})();
|
|
1494
|
+
return startPromise;
|
|
1495
|
+
};
|
|
1496
|
+
return Object.freeze({
|
|
1497
|
+
start,
|
|
1498
|
+
capability() {
|
|
1499
|
+
const currentCursor = store.currentCursor();
|
|
1500
|
+
const active = activeRegistry.snapshot(currentCursor ?? void 0);
|
|
1501
|
+
return Object.freeze({
|
|
1502
|
+
enabled: true,
|
|
1503
|
+
brokerReady: state === "ready" && broker?.isReady() === true,
|
|
1504
|
+
activeWaitCount: store.activeWaitCount(),
|
|
1505
|
+
snapshotSchemaVersion: import_shared7.EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
|
|
1506
|
+
brokerProtocolVersion: import_shared7.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
|
|
1507
|
+
activeAdapter: active.activeAdapter,
|
|
1508
|
+
dispatch: currentCursor === null ? null : active.dispatch
|
|
1509
|
+
});
|
|
1510
|
+
},
|
|
1511
|
+
async publish(request, authorize) {
|
|
1512
|
+
requireReady();
|
|
1513
|
+
const fingerprint = fingerprintExternalHandoffAnnotation(request.annotation);
|
|
1514
|
+
const replayed = store.replay(request.requestId, fingerprint);
|
|
1515
|
+
if (replayed !== void 0) return replayed;
|
|
1516
|
+
const pending = inFlight.get(request.requestId);
|
|
1517
|
+
if (pending !== void 0) {
|
|
1518
|
+
if (pending.fingerprint !== fingerprint) {
|
|
1519
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
|
|
1520
|
+
}
|
|
1521
|
+
return asReplay(await pending.promise);
|
|
1522
|
+
}
|
|
1523
|
+
activeRegistry.assertPublishable();
|
|
1524
|
+
const promise = (async () => {
|
|
1525
|
+
const annotation = await authorize(request.annotation);
|
|
1526
|
+
return store.publish({
|
|
1527
|
+
annotation,
|
|
1528
|
+
fingerprint,
|
|
1529
|
+
requestId: request.requestId,
|
|
1530
|
+
reserve: activeRegistry.reserve
|
|
1531
|
+
});
|
|
1532
|
+
})();
|
|
1533
|
+
const activePublish = Object.freeze({ fingerprint, promise });
|
|
1534
|
+
inFlight.set(request.requestId, activePublish);
|
|
1535
|
+
try {
|
|
1536
|
+
return await promise;
|
|
1537
|
+
} finally {
|
|
1538
|
+
if (inFlight.get(request.requestId) === activePublish) {
|
|
1539
|
+
inFlight.delete(request.requestId);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
},
|
|
1543
|
+
status(cursor) {
|
|
1544
|
+
requireReady();
|
|
1545
|
+
const handoff = store.status(cursor);
|
|
1546
|
+
const active = activeRegistry.snapshot(cursor ?? handoff.cursor);
|
|
1547
|
+
return Object.freeze({
|
|
1548
|
+
handoff,
|
|
1549
|
+
activeAdapter: active.activeAdapter,
|
|
1550
|
+
dispatch: active.dispatch
|
|
1551
|
+
});
|
|
1552
|
+
},
|
|
1553
|
+
resolveDelivery(cursor) {
|
|
1554
|
+
requireReady();
|
|
1555
|
+
const handoff = store.status(cursor);
|
|
1556
|
+
const active = activeRegistry.resolveDelivery(cursor);
|
|
1557
|
+
return Object.freeze({
|
|
1558
|
+
handoff,
|
|
1559
|
+
activeAdapter: active.activeAdapter,
|
|
1560
|
+
dispatch: active.dispatch
|
|
1561
|
+
});
|
|
1562
|
+
},
|
|
1563
|
+
close() {
|
|
1564
|
+
closePromise ??= (async () => {
|
|
1565
|
+
if (state === "closed") return;
|
|
1566
|
+
state = "closed";
|
|
1567
|
+
activeRegistry.close();
|
|
1568
|
+
store.close();
|
|
1569
|
+
inFlight.clear();
|
|
1570
|
+
await startPromise?.catch(() => void 0);
|
|
1571
|
+
const publishedDescriptor = descriptor;
|
|
1572
|
+
descriptor = void 0;
|
|
1573
|
+
const activeBroker = broker;
|
|
1574
|
+
broker = void 0;
|
|
1575
|
+
await publishedDescriptor?.close().catch(() => void 0);
|
|
1576
|
+
await activeBroker?.close().catch(() => void 0);
|
|
1577
|
+
})();
|
|
1578
|
+
return closePromise;
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
|
|
523
1583
|
// src/environment-ai.ts
|
|
524
1584
|
var AI_ENVIRONMENT_NAMES = Object.freeze({
|
|
525
1585
|
authentication: "SPOTPATCH_AI_AUTHENTICATION",
|
|
@@ -610,46 +1670,46 @@ function resolveEnvironmentAiConfiguration(environment) {
|
|
|
610
1670
|
}
|
|
611
1671
|
|
|
612
1672
|
// src/integration/file-plan.ts
|
|
613
|
-
var
|
|
614
|
-
var
|
|
615
|
-
var
|
|
1673
|
+
var import_node_crypto7 = require("crypto");
|
|
1674
|
+
var import_promises2 = require("fs/promises");
|
|
1675
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
616
1676
|
function isMissingPathError(error) {
|
|
617
1677
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
618
1678
|
}
|
|
619
1679
|
function isPathWithin(root, target) {
|
|
620
|
-
const relative =
|
|
621
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
1680
|
+
const relative = import_node_path2.default.relative(root, target);
|
|
1681
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path2.default.sep}`) && !import_node_path2.default.isAbsolute(relative);
|
|
622
1682
|
}
|
|
623
1683
|
function relativePathWithin(root, target) {
|
|
624
|
-
const relative =
|
|
1684
|
+
const relative = import_node_path2.default.relative(root, target);
|
|
625
1685
|
if (relative.length === 0 || !isPathWithin(root, target)) {
|
|
626
1686
|
throw new Error("SpotPatch init refuses to modify a path outside the app root.");
|
|
627
1687
|
}
|
|
628
|
-
return relative.split(
|
|
1688
|
+
return relative.split(import_node_path2.default.sep).join("/");
|
|
629
1689
|
}
|
|
630
1690
|
async function integrationPathExists(absolutePath) {
|
|
631
1691
|
try {
|
|
632
|
-
await (0,
|
|
1692
|
+
await (0, import_promises2.access)(absolutePath);
|
|
633
1693
|
return true;
|
|
634
1694
|
} catch {
|
|
635
1695
|
return false;
|
|
636
1696
|
}
|
|
637
1697
|
}
|
|
638
1698
|
async function readIntegrationFile(absolutePath) {
|
|
639
|
-
const metadata = await (0,
|
|
1699
|
+
const metadata = await (0, import_promises2.lstat)(absolutePath);
|
|
640
1700
|
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
641
1701
|
throw new Error(
|
|
642
|
-
`SpotPatch refuses to modify the non-regular file ${
|
|
1702
|
+
`SpotPatch refuses to modify the non-regular file ${import_node_path2.default.basename(absolutePath)}.`
|
|
643
1703
|
);
|
|
644
1704
|
}
|
|
645
|
-
return (0,
|
|
1705
|
+
return (0, import_promises2.readFile)(absolutePath, "utf8");
|
|
646
1706
|
}
|
|
647
1707
|
function createIntegrationFileChange(appRoot, absolutePath, nextContent, previousContent) {
|
|
648
1708
|
if (previousContent === nextContent) {
|
|
649
1709
|
return void 0;
|
|
650
1710
|
}
|
|
651
|
-
const root =
|
|
652
|
-
const target =
|
|
1711
|
+
const root = import_node_path2.default.resolve(appRoot);
|
|
1712
|
+
const target = import_node_path2.default.resolve(absolutePath);
|
|
653
1713
|
return Object.freeze({
|
|
654
1714
|
absolutePath: target,
|
|
655
1715
|
nextContent,
|
|
@@ -658,19 +1718,19 @@ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previou
|
|
|
658
1718
|
});
|
|
659
1719
|
}
|
|
660
1720
|
function temporaryPath(absolutePath, label) {
|
|
661
|
-
return
|
|
662
|
-
|
|
663
|
-
`.${
|
|
1721
|
+
return import_node_path2.default.join(
|
|
1722
|
+
import_node_path2.default.dirname(absolutePath),
|
|
1723
|
+
`.${import_node_path2.default.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${(0, import_node_crypto7.randomBytes)(8).toString("hex")}`
|
|
664
1724
|
);
|
|
665
1725
|
}
|
|
666
1726
|
async function writeAtomic(absolutePath, content, mode) {
|
|
667
|
-
await (0,
|
|
1727
|
+
await (0, import_promises2.mkdir)(import_node_path2.default.dirname(absolutePath), { recursive: true });
|
|
668
1728
|
const stagedPath = temporaryPath(absolutePath, "stage");
|
|
669
1729
|
try {
|
|
670
|
-
await (0,
|
|
671
|
-
await (0,
|
|
1730
|
+
await (0, import_promises2.writeFile)(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
|
|
1731
|
+
await (0, import_promises2.rename)(stagedPath, absolutePath);
|
|
672
1732
|
} catch (error) {
|
|
673
|
-
await (0,
|
|
1733
|
+
await (0, import_promises2.unlink)(stagedPath).catch(() => void 0);
|
|
674
1734
|
throw error;
|
|
675
1735
|
}
|
|
676
1736
|
}
|
|
@@ -682,21 +1742,21 @@ async function rollbackChange(change) {
|
|
|
682
1742
|
);
|
|
683
1743
|
}
|
|
684
1744
|
if (change.previousContent === void 0) {
|
|
685
|
-
await (0,
|
|
1745
|
+
await (0, import_promises2.unlink)(change.absolutePath);
|
|
686
1746
|
return;
|
|
687
1747
|
}
|
|
688
|
-
const mode = (await (0,
|
|
1748
|
+
const mode = (await (0, import_promises2.stat)(change.absolutePath)).mode & 511;
|
|
689
1749
|
await writeAtomic(change.absolutePath, change.previousContent, mode);
|
|
690
1750
|
}
|
|
691
1751
|
async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
692
|
-
const target =
|
|
1752
|
+
const target = import_node_path2.default.resolve(change.absolutePath);
|
|
693
1753
|
const relativePath = relativePathWithin(appRoot, target);
|
|
694
|
-
if (target !== change.absolutePath || relativePath !== change.relativePath ||
|
|
1754
|
+
if (target !== change.absolutePath || relativePath !== change.relativePath || import_node_path2.default.dirname(target) === target) {
|
|
695
1755
|
throw new Error("SpotPatch init received an invalid integration file plan.");
|
|
696
1756
|
}
|
|
697
1757
|
let targetMetadata;
|
|
698
1758
|
try {
|
|
699
|
-
targetMetadata = await (0,
|
|
1759
|
+
targetMetadata = await (0, import_promises2.lstat)(target);
|
|
700
1760
|
} catch (error) {
|
|
701
1761
|
if (!isMissingPathError(error)) {
|
|
702
1762
|
throw error;
|
|
@@ -707,8 +1767,8 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
|
707
1767
|
`SpotPatch refuses to modify the symbolic link ${change.relativePath}.`
|
|
708
1768
|
);
|
|
709
1769
|
}
|
|
710
|
-
const containmentAnchor = await (0,
|
|
711
|
-
targetMetadata === void 0 ?
|
|
1770
|
+
const containmentAnchor = await (0, import_promises2.realpath)(
|
|
1771
|
+
targetMetadata === void 0 ? import_node_path2.default.dirname(target) : target
|
|
712
1772
|
);
|
|
713
1773
|
if (!isPathWithin(realAppRoot, containmentAnchor)) {
|
|
714
1774
|
throw new Error("SpotPatch init refuses to modify a path outside the app root.");
|
|
@@ -717,7 +1777,7 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
|
717
1777
|
async function assertCurrentBaseline(change) {
|
|
718
1778
|
if (change.previousContent === void 0) {
|
|
719
1779
|
try {
|
|
720
|
-
await (0,
|
|
1780
|
+
await (0, import_promises2.lstat)(change.absolutePath);
|
|
721
1781
|
} catch (error) {
|
|
722
1782
|
if (isMissingPathError(error)) {
|
|
723
1783
|
return;
|
|
@@ -739,8 +1799,8 @@ async function applyIntegrationPlan(plan) {
|
|
|
739
1799
|
if (plan.changes.length === 0) {
|
|
740
1800
|
return;
|
|
741
1801
|
}
|
|
742
|
-
const appRoot =
|
|
743
|
-
const realAppRoot = await (0,
|
|
1802
|
+
const appRoot = import_node_path2.default.resolve(plan.appRoot);
|
|
1803
|
+
const realAppRoot = await (0, import_promises2.realpath)(appRoot);
|
|
744
1804
|
const targets = /* @__PURE__ */ new Set();
|
|
745
1805
|
for (const change of plan.changes) {
|
|
746
1806
|
if (targets.has(change.absolutePath)) {
|
|
@@ -754,7 +1814,7 @@ async function applyIntegrationPlan(plan) {
|
|
|
754
1814
|
try {
|
|
755
1815
|
for (const change of plan.changes) {
|
|
756
1816
|
await assertCurrentBaseline(change);
|
|
757
|
-
const mode = change.previousContent === void 0 ? 384 : (await (0,
|
|
1817
|
+
const mode = change.previousContent === void 0 ? 384 : (await (0, import_promises2.stat)(change.absolutePath)).mode & 511;
|
|
758
1818
|
await writeAtomic(change.absolutePath, change.nextContent, mode);
|
|
759
1819
|
applied.push(change);
|
|
760
1820
|
}
|
|
@@ -775,7 +1835,7 @@ async function applyIntegrationPlan(plan) {
|
|
|
775
1835
|
}
|
|
776
1836
|
|
|
777
1837
|
// src/options.ts
|
|
778
|
-
var
|
|
1838
|
+
var import_shared8 = require("@spotpatch/shared");
|
|
779
1839
|
var import_zod = require("zod");
|
|
780
1840
|
var DEFAULT_EXCLUDE = Object.freeze([
|
|
781
1841
|
/node_modules/,
|
|
@@ -785,7 +1845,7 @@ var DEFAULT_EXCLUDE = Object.freeze([
|
|
|
785
1845
|
/(?:^|\/)dist(?:\/|$)/,
|
|
786
1846
|
/(?:^|\/)coverage(?:\/|$)/
|
|
787
1847
|
]);
|
|
788
|
-
var DEFAULT_INCLUDE = Object.freeze([/(?:^|[/\\])src[/\\].+\.(?:jsx|tsx)$/]);
|
|
1848
|
+
var DEFAULT_INCLUDE = Object.freeze([/(?:^|[/\\])src[/\\].+\.(?:js|jsx|ts|tsx)$/]);
|
|
789
1849
|
var DEFAULT_BUDGET = Object.freeze({
|
|
790
1850
|
totalCharacters: 16e3,
|
|
791
1851
|
domCharacters: 3e3,
|
|
@@ -806,7 +1866,13 @@ var DEFAULT_OPTIONS = Object.freeze({
|
|
|
806
1866
|
debug: false,
|
|
807
1867
|
locale: "auto",
|
|
808
1868
|
maxTargets: 8,
|
|
809
|
-
ai: false
|
|
1869
|
+
ai: false,
|
|
1870
|
+
dataFlow: Object.freeze({
|
|
1871
|
+
enabled: false,
|
|
1872
|
+
runtime: "dispatch",
|
|
1873
|
+
limits: import_shared8.DEFAULT_DATA_FLOW_LIMITS
|
|
1874
|
+
}),
|
|
1875
|
+
externalAgent: Object.freeze({ enabled: false })
|
|
810
1876
|
});
|
|
811
1877
|
var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
812
1878
|
var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
@@ -851,7 +1917,7 @@ var aiOptionsSchema = import_zod.z.strictObject({
|
|
|
851
1917
|
defaultProvider: import_zod.z.string(),
|
|
852
1918
|
execution: import_zod.z.strictObject({
|
|
853
1919
|
isolation: import_zod.z.literal("git-worktree").optional(),
|
|
854
|
-
applyMode: import_zod.z.enum(
|
|
1920
|
+
applyMode: import_zod.z.enum(import_shared8.AGENT_APPLY_MODES).optional(),
|
|
855
1921
|
checks: import_zod.z.record(import_zod.z.string(), agentCheckSchema).optional(),
|
|
856
1922
|
limits: agentLimitsSchema
|
|
857
1923
|
}).optional()
|
|
@@ -897,18 +1963,18 @@ function normalizeProviderBaseURL(value) {
|
|
|
897
1963
|
}
|
|
898
1964
|
function resolveLimits(limits) {
|
|
899
1965
|
const resolved = Object.freeze({
|
|
900
|
-
maxTurns: limits?.maxTurns ??
|
|
901
|
-
maxToolCalls: limits?.maxToolCalls ??
|
|
902
|
-
maxChangedFiles: limits?.maxChangedFiles ??
|
|
903
|
-
maxDiffBytes: limits?.maxDiffBytes ??
|
|
904
|
-
maxReadBytesPerFile: limits?.maxReadBytesPerFile ??
|
|
905
|
-
maxToolOutputCharacters: limits?.maxToolOutputCharacters ??
|
|
906
|
-
maxProviderResponseBytes: limits?.maxProviderResponseBytes ??
|
|
907
|
-
providerConnectTimeoutMs: limits?.providerConnectTimeoutMs ??
|
|
908
|
-
providerFirstByteTimeoutMs: limits?.providerFirstByteTimeoutMs ??
|
|
909
|
-
providerIdleTimeoutMs: limits?.providerIdleTimeoutMs ??
|
|
910
|
-
checkTimeoutMs: limits?.checkTimeoutMs ??
|
|
911
|
-
jobTimeoutMs: limits?.jobTimeoutMs ??
|
|
1966
|
+
maxTurns: limits?.maxTurns ?? import_shared8.DEFAULT_AGENT_LIMITS.maxTurns,
|
|
1967
|
+
maxToolCalls: limits?.maxToolCalls ?? import_shared8.DEFAULT_AGENT_LIMITS.maxToolCalls,
|
|
1968
|
+
maxChangedFiles: limits?.maxChangedFiles ?? import_shared8.DEFAULT_AGENT_LIMITS.maxChangedFiles,
|
|
1969
|
+
maxDiffBytes: limits?.maxDiffBytes ?? import_shared8.DEFAULT_AGENT_LIMITS.maxDiffBytes,
|
|
1970
|
+
maxReadBytesPerFile: limits?.maxReadBytesPerFile ?? import_shared8.DEFAULT_AGENT_LIMITS.maxReadBytesPerFile,
|
|
1971
|
+
maxToolOutputCharacters: limits?.maxToolOutputCharacters ?? import_shared8.DEFAULT_AGENT_LIMITS.maxToolOutputCharacters,
|
|
1972
|
+
maxProviderResponseBytes: limits?.maxProviderResponseBytes ?? import_shared8.DEFAULT_AGENT_LIMITS.maxProviderResponseBytes,
|
|
1973
|
+
providerConnectTimeoutMs: limits?.providerConnectTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.providerConnectTimeoutMs,
|
|
1974
|
+
providerFirstByteTimeoutMs: limits?.providerFirstByteTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.providerFirstByteTimeoutMs,
|
|
1975
|
+
providerIdleTimeoutMs: limits?.providerIdleTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.providerIdleTimeoutMs,
|
|
1976
|
+
checkTimeoutMs: limits?.checkTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.checkTimeoutMs,
|
|
1977
|
+
jobTimeoutMs: limits?.jobTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.jobTimeoutMs
|
|
912
1978
|
});
|
|
913
1979
|
for (const [name, value] of Object.entries(resolved)) {
|
|
914
1980
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
@@ -1098,10 +2164,43 @@ function assertPositiveBudget(budget) {
|
|
|
1098
2164
|
}
|
|
1099
2165
|
}
|
|
1100
2166
|
}
|
|
2167
|
+
function resolveDataFlowOptions(options) {
|
|
2168
|
+
if (options === void 0 || options === false) {
|
|
2169
|
+
return DEFAULT_OPTIONS.dataFlow;
|
|
2170
|
+
}
|
|
2171
|
+
const candidate = options;
|
|
2172
|
+
if (typeof candidate !== "object" || candidate === null) {
|
|
2173
|
+
throw new RangeError("SpotPatch dataFlow configuration is invalid.");
|
|
2174
|
+
}
|
|
2175
|
+
const runtime = options.runtime ?? "dispatch";
|
|
2176
|
+
if (runtime !== "dispatch") {
|
|
2177
|
+
throw new RangeError("SpotPatch dataFlow runtime mode is invalid.");
|
|
2178
|
+
}
|
|
2179
|
+
return Object.freeze({
|
|
2180
|
+
enabled: true,
|
|
2181
|
+
runtime,
|
|
2182
|
+
limits: import_shared8.DEFAULT_DATA_FLOW_LIMITS
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
function createRuntimeDataFlowConfig(options) {
|
|
2186
|
+
return Object.freeze({
|
|
2187
|
+
enabled: options.enabled,
|
|
2188
|
+
runtime: options.runtime,
|
|
2189
|
+
limits: Object.freeze({
|
|
2190
|
+
observationMaxEntries: options.limits.observationMaxEntries,
|
|
2191
|
+
observationMaxBytes: options.limits.observationMaxBytes,
|
|
2192
|
+
observationTtlMs: options.limits.observationTtlMs,
|
|
2193
|
+
reportMaxBytes: options.limits.reportMaxBytes
|
|
2194
|
+
})
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
1101
2197
|
function resolveOptions(options = {}, environmentAi) {
|
|
1102
2198
|
if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
|
|
1103
2199
|
throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
|
|
1104
2200
|
}
|
|
2201
|
+
if (options.externalAgent !== void 0 && typeof options.externalAgent !== "boolean") {
|
|
2202
|
+
throw new RangeError("SpotPatch externalAgent must be a boolean.");
|
|
2203
|
+
}
|
|
1105
2204
|
const budget = Object.freeze({
|
|
1106
2205
|
...DEFAULT_OPTIONS.budget,
|
|
1107
2206
|
...options.budget
|
|
@@ -1110,15 +2209,15 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1110
2209
|
const maxTargets = options.maxTargets ?? DEFAULT_OPTIONS.maxTargets;
|
|
1111
2210
|
const locale = options.locale ?? DEFAULT_OPTIONS.locale;
|
|
1112
2211
|
const editor = options.editor ?? DEFAULT_OPTIONS.editor;
|
|
1113
|
-
if (!
|
|
2212
|
+
if (!import_shared8.SPOTPATCH_LOCALE_PREFERENCES.includes(locale)) {
|
|
1114
2213
|
throw new RangeError("SpotPatch locale must be auto, en-US, or zh-CN.");
|
|
1115
2214
|
}
|
|
1116
|
-
if (!
|
|
2215
|
+
if (!import_shared8.SPOTPATCH_EDITOR_PREFERENCES.includes(editor)) {
|
|
1117
2216
|
throw new RangeError("SpotPatch editor must be auto, vscode, or cursor.");
|
|
1118
2217
|
}
|
|
1119
|
-
if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets >
|
|
2218
|
+
if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets > import_shared8.MAX_ANNOTATION_TARGETS) {
|
|
1120
2219
|
throw new RangeError(
|
|
1121
|
-
`SpotPatch maxTargets must be an integer between 1 and ${String(
|
|
2220
|
+
`SpotPatch maxTargets must be an integer between 1 and ${String(import_shared8.MAX_ANNOTATION_TARGETS)}.`
|
|
1122
2221
|
);
|
|
1123
2222
|
}
|
|
1124
2223
|
const resolved = {
|
|
@@ -1133,7 +2232,11 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1133
2232
|
debug: options.debug ?? DEFAULT_OPTIONS.debug,
|
|
1134
2233
|
locale,
|
|
1135
2234
|
maxTargets,
|
|
1136
|
-
ai: resolveAiOptions(options.ai ?? environmentAi)
|
|
2235
|
+
ai: resolveAiOptions(options.ai ?? environmentAi),
|
|
2236
|
+
dataFlow: resolveDataFlowOptions(options.dataFlow),
|
|
2237
|
+
externalAgent: Object.freeze({
|
|
2238
|
+
enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
|
|
2239
|
+
})
|
|
1137
2240
|
};
|
|
1138
2241
|
if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
|
|
1139
2242
|
throw new RangeError("SpotPatch shortcut is invalid.");
|
|
@@ -1143,9 +2246,9 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1143
2246
|
|
|
1144
2247
|
// src/project-validation.ts
|
|
1145
2248
|
var import_node_child_process = require("child_process");
|
|
1146
|
-
var
|
|
2249
|
+
var import_promises3 = require("fs/promises");
|
|
1147
2250
|
var import_node_module = require("module");
|
|
1148
|
-
var
|
|
2251
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
1149
2252
|
var import_node_util = require("util");
|
|
1150
2253
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
1151
2254
|
var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
|
|
@@ -1155,19 +2258,19 @@ function isRecord(value) {
|
|
|
1155
2258
|
}
|
|
1156
2259
|
async function isRegularFile(absolutePath) {
|
|
1157
2260
|
try {
|
|
1158
|
-
const metadata = await (0,
|
|
2261
|
+
const metadata = await (0, import_promises3.lstat)(absolutePath);
|
|
1159
2262
|
return metadata.isFile() && !metadata.isSymbolicLink();
|
|
1160
2263
|
} catch {
|
|
1161
2264
|
return false;
|
|
1162
2265
|
}
|
|
1163
2266
|
}
|
|
1164
2267
|
async function readManifest(appRoot) {
|
|
1165
|
-
const manifestPath =
|
|
2268
|
+
const manifestPath = import_node_path3.default.join(appRoot, "package.json");
|
|
1166
2269
|
if (!await isRegularFile(manifestPath)) {
|
|
1167
2270
|
return void 0;
|
|
1168
2271
|
}
|
|
1169
2272
|
try {
|
|
1170
|
-
const value = JSON.parse(await (0,
|
|
2273
|
+
const value = JSON.parse(await (0, import_promises3.readFile)(manifestPath, "utf8"));
|
|
1171
2274
|
return isRecord(value) ? value : void 0;
|
|
1172
2275
|
} catch {
|
|
1173
2276
|
return void 0;
|
|
@@ -1190,9 +2293,9 @@ async function findGitRoot(appRoot) {
|
|
|
1190
2293
|
timeout: 5e3,
|
|
1191
2294
|
windowsHide: true
|
|
1192
2295
|
});
|
|
1193
|
-
const root = await (0,
|
|
1194
|
-
const relative =
|
|
1195
|
-
if (relative === "" || !relative.startsWith(`..${
|
|
2296
|
+
const root = await (0, import_promises3.realpath)(result.stdout.trim());
|
|
2297
|
+
const relative = import_node_path3.default.relative(root, appRoot);
|
|
2298
|
+
if (relative === "" || !relative.startsWith(`..${import_node_path3.default.sep}`) && relative !== ".." && !import_node_path3.default.isAbsolute(relative)) {
|
|
1196
2299
|
return root;
|
|
1197
2300
|
}
|
|
1198
2301
|
} catch {
|
|
@@ -1201,22 +2304,22 @@ async function findGitRoot(appRoot) {
|
|
|
1201
2304
|
return void 0;
|
|
1202
2305
|
}
|
|
1203
2306
|
async function resolveTypeScriptCli(appRoot) {
|
|
1204
|
-
const resolveFromApplication = (0, import_node_module.createRequire)(
|
|
2307
|
+
const resolveFromApplication = (0, import_node_module.createRequire)(import_node_path3.default.join(appRoot, "package.json"));
|
|
1205
2308
|
try {
|
|
1206
2309
|
const packagePath = resolveFromApplication.resolve("typescript/package.json");
|
|
1207
|
-
const cliPath =
|
|
1208
|
-
await (0,
|
|
1209
|
-
return await (0,
|
|
2310
|
+
const cliPath = import_node_path3.default.join(import_node_path3.default.dirname(packagePath), "bin", "tsc");
|
|
2311
|
+
await (0, import_promises3.access)(cliPath);
|
|
2312
|
+
return await (0, import_promises3.realpath)(cliPath);
|
|
1210
2313
|
} catch {
|
|
1211
2314
|
return void 0;
|
|
1212
2315
|
}
|
|
1213
2316
|
}
|
|
1214
2317
|
function portableRelativePath(from, to) {
|
|
1215
|
-
return
|
|
2318
|
+
return import_node_path3.default.relative(from, to).split(import_node_path3.default.sep).join("/");
|
|
1216
2319
|
}
|
|
1217
2320
|
async function discoverProjectValidationCheck(options) {
|
|
1218
|
-
const appRoot = await (0,
|
|
1219
|
-
const tsconfigPath =
|
|
2321
|
+
const appRoot = await (0, import_promises3.realpath)(options.appRoot);
|
|
2322
|
+
const tsconfigPath = import_node_path3.default.join(appRoot, "tsconfig.json");
|
|
1220
2323
|
const [manifest, projectRoot, hasTsconfig] = await Promise.all([
|
|
1221
2324
|
readManifest(appRoot),
|
|
1222
2325
|
findGitRoot(appRoot),
|
|
@@ -1304,61 +2407,86 @@ async function resolveProjectOptions(input) {
|
|
|
1304
2407
|
}
|
|
1305
2408
|
|
|
1306
2409
|
// src/registry/source-registry.ts
|
|
1307
|
-
var
|
|
2410
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
1308
2411
|
|
|
1309
2412
|
// src/registry/source-id.ts
|
|
1310
|
-
var
|
|
2413
|
+
var import_node_crypto8 = require("crypto");
|
|
1311
2414
|
var SOURCE_ID_BYTES = 8;
|
|
1312
|
-
var createRandomSourceId = () => (0,
|
|
2415
|
+
var createRandomSourceId = () => (0, import_node_crypto8.randomBytes)(SOURCE_ID_BYTES).toString("base64url");
|
|
1313
2416
|
|
|
1314
2417
|
// src/registry/source-registry.ts
|
|
1315
2418
|
function normalizeAbsolutePath(absolutePath) {
|
|
1316
|
-
return
|
|
2419
|
+
return import_node_path4.default.normalize(import_node_path4.default.resolve(absolutePath));
|
|
1317
2420
|
}
|
|
1318
2421
|
function createSourceRegistry(options = {}) {
|
|
1319
2422
|
const createId = options.createId ?? createRandomSourceId;
|
|
1320
2423
|
const pathToId = /* @__PURE__ */ new Map();
|
|
1321
2424
|
const idToPath = /* @__PURE__ */ new Map();
|
|
2425
|
+
const componentAnchors = /* @__PURE__ */ new Map();
|
|
2426
|
+
const componentIdsByPath = /* @__PURE__ */ new Map();
|
|
2427
|
+
function registerSourcePath(absolutePath) {
|
|
2428
|
+
const normalizedPath = normalizeAbsolutePath(absolutePath);
|
|
2429
|
+
const existingId = pathToId.get(normalizedPath);
|
|
2430
|
+
if (existingId !== void 0) {
|
|
2431
|
+
return existingId;
|
|
2432
|
+
}
|
|
2433
|
+
let fileId = createId();
|
|
2434
|
+
while (idToPath.has(fileId)) fileId = createId();
|
|
2435
|
+
pathToId.set(normalizedPath, fileId);
|
|
2436
|
+
idToPath.set(fileId, normalizedPath);
|
|
2437
|
+
return fileId;
|
|
2438
|
+
}
|
|
1322
2439
|
return Object.freeze({
|
|
1323
2440
|
register(absolutePath) {
|
|
2441
|
+
return registerSourcePath(absolutePath);
|
|
2442
|
+
},
|
|
2443
|
+
registerDataFlowComponents(absolutePath, sourceVersion, components) {
|
|
1324
2444
|
const normalizedPath = normalizeAbsolutePath(absolutePath);
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
|
|
2445
|
+
const previousIds = componentIdsByPath.get(normalizedPath);
|
|
2446
|
+
for (const componentSourceId of previousIds ?? []) {
|
|
2447
|
+
componentAnchors.delete(componentSourceId);
|
|
1328
2448
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
2449
|
+
const fileId = registerSourcePath(normalizedPath);
|
|
2450
|
+
const currentIds = /* @__PURE__ */ new Set();
|
|
2451
|
+
for (const component of components) {
|
|
2452
|
+
currentIds.add(component.componentSourceId);
|
|
2453
|
+
componentAnchors.set(
|
|
2454
|
+
component.componentSourceId,
|
|
2455
|
+
Object.freeze({ ...component, fileId, sourceVersion })
|
|
2456
|
+
);
|
|
1332
2457
|
}
|
|
1333
|
-
|
|
1334
|
-
idToPath.set(fileId, normalizedPath);
|
|
1335
|
-
return fileId;
|
|
2458
|
+
componentIdsByPath.set(normalizedPath, currentIds);
|
|
1336
2459
|
},
|
|
1337
2460
|
resolve(fileId) {
|
|
1338
2461
|
return idToPath.get(fileId);
|
|
1339
2462
|
},
|
|
2463
|
+
resolveDataFlowComponent(componentSourceId) {
|
|
2464
|
+
return componentAnchors.get(componentSourceId);
|
|
2465
|
+
},
|
|
1340
2466
|
clear() {
|
|
1341
2467
|
pathToId.clear();
|
|
1342
2468
|
idToPath.clear();
|
|
2469
|
+
componentAnchors.clear();
|
|
2470
|
+
componentIdsByPath.clear();
|
|
1343
2471
|
}
|
|
1344
2472
|
});
|
|
1345
2473
|
}
|
|
1346
2474
|
|
|
1347
2475
|
// src/server/middleware.ts
|
|
1348
|
-
var
|
|
2476
|
+
var import_shared18 = require("@spotpatch/shared");
|
|
1349
2477
|
|
|
1350
|
-
// src/
|
|
1351
|
-
var
|
|
2478
|
+
// src/external-handoff/browser-http.ts
|
|
2479
|
+
var import_shared12 = require("@spotpatch/shared");
|
|
1352
2480
|
|
|
1353
|
-
// src/server/
|
|
1354
|
-
var
|
|
1355
|
-
var
|
|
1356
|
-
var
|
|
2481
|
+
// src/server/annotation-authorizer.ts
|
|
2482
|
+
var import_promises6 = require("fs/promises");
|
|
2483
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2484
|
+
var import_shared11 = require("@spotpatch/shared");
|
|
1357
2485
|
|
|
1358
2486
|
// src/server/source-context.ts
|
|
1359
|
-
var
|
|
1360
|
-
var
|
|
1361
|
-
var
|
|
2487
|
+
var import_promises5 = require("fs/promises");
|
|
2488
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
2489
|
+
var import_shared10 = require("@spotpatch/shared");
|
|
1362
2490
|
|
|
1363
2491
|
// src/server/extract-code-context.ts
|
|
1364
2492
|
var import_oxc_parser = require("oxc-parser");
|
|
@@ -1587,16 +2715,9 @@ function extractCodeContext(options) {
|
|
|
1587
2715
|
}
|
|
1588
2716
|
|
|
1589
2717
|
// src/server/source-file.ts
|
|
1590
|
-
var
|
|
1591
|
-
var
|
|
1592
|
-
var
|
|
1593
|
-
|
|
1594
|
-
// src/server/constants.ts
|
|
1595
|
-
var MAX_REQUEST_BODY_BYTES = 32 * 1024;
|
|
1596
|
-
var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
|
|
1597
|
-
var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
|
1598
|
-
|
|
1599
|
-
// src/server/source-file.ts
|
|
2718
|
+
var import_promises4 = require("fs/promises");
|
|
2719
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
2720
|
+
var import_shared9 = require("@spotpatch/shared");
|
|
1600
2721
|
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
1601
2722
|
function isMissingFileError(error) {
|
|
1602
2723
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
@@ -1606,56 +2727,56 @@ async function assertInsideRoot(root, candidate) {
|
|
|
1606
2727
|
let realCandidate;
|
|
1607
2728
|
try {
|
|
1608
2729
|
[realRoot, realCandidate] = await Promise.all([
|
|
1609
|
-
(0,
|
|
1610
|
-
(0,
|
|
2730
|
+
(0, import_promises4.realpath)(root),
|
|
2731
|
+
(0, import_promises4.realpath)(candidate)
|
|
1611
2732
|
]);
|
|
1612
2733
|
} catch (error) {
|
|
1613
2734
|
if (isMissingFileError(error)) {
|
|
1614
|
-
throw new
|
|
2735
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
1615
2736
|
cause: error
|
|
1616
2737
|
});
|
|
1617
2738
|
}
|
|
1618
2739
|
throw error;
|
|
1619
2740
|
}
|
|
1620
|
-
const relative =
|
|
1621
|
-
const outside = relative.startsWith(`..${
|
|
2741
|
+
const relative = import_node_path5.default.relative(realRoot, realCandidate);
|
|
2742
|
+
const outside = relative.startsWith(`..${import_node_path5.default.sep}`) || relative === ".." || import_node_path5.default.isAbsolute(relative);
|
|
1622
2743
|
if (outside) {
|
|
1623
|
-
throw new
|
|
2744
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_OUTSIDE_ROOT);
|
|
1624
2745
|
}
|
|
1625
2746
|
return realCandidate;
|
|
1626
2747
|
}
|
|
1627
2748
|
async function resolveSourceFile(options) {
|
|
1628
2749
|
const registeredPath = options.registry.resolve(options.fileId);
|
|
1629
2750
|
if (registeredPath === void 0) {
|
|
1630
|
-
throw new
|
|
2751
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
1631
2752
|
}
|
|
1632
2753
|
const sourcePath = await assertInsideRoot(options.root, registeredPath);
|
|
1633
|
-
if (!ALLOWED_EXTENSIONS.has(
|
|
1634
|
-
throw new
|
|
2754
|
+
if (!ALLOWED_EXTENSIONS.has(import_node_path5.default.extname(sourcePath).toLowerCase())) {
|
|
2755
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
1635
2756
|
}
|
|
1636
2757
|
let sourceStat;
|
|
1637
2758
|
try {
|
|
1638
|
-
sourceStat = await (0,
|
|
2759
|
+
sourceStat = await (0, import_promises4.stat)(sourcePath);
|
|
1639
2760
|
} catch (error) {
|
|
1640
2761
|
if (isMissingFileError(error)) {
|
|
1641
|
-
throw new
|
|
2762
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
1642
2763
|
cause: error
|
|
1643
2764
|
});
|
|
1644
2765
|
}
|
|
1645
2766
|
throw error;
|
|
1646
2767
|
}
|
|
1647
2768
|
if (!sourceStat.isFile()) {
|
|
1648
|
-
throw new
|
|
2769
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
1649
2770
|
}
|
|
1650
2771
|
if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
|
|
1651
|
-
throw new
|
|
2772
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_TOO_LARGE);
|
|
1652
2773
|
}
|
|
1653
2774
|
return sourcePath;
|
|
1654
2775
|
}
|
|
1655
2776
|
|
|
1656
2777
|
// src/server/source-context.ts
|
|
1657
2778
|
function toDisplayPath(root, sourcePath) {
|
|
1658
|
-
return
|
|
2779
|
+
return import_node_path6.default.relative(root, sourcePath).split(import_node_path6.default.sep).join("/");
|
|
1659
2780
|
}
|
|
1660
2781
|
async function readSourceContext(options) {
|
|
1661
2782
|
const sourcePath = await resolveSourceFile({
|
|
@@ -1665,10 +2786,10 @@ async function readSourceContext(options) {
|
|
|
1665
2786
|
});
|
|
1666
2787
|
let source;
|
|
1667
2788
|
try {
|
|
1668
|
-
source = await (0,
|
|
2789
|
+
source = await (0, import_promises5.readFile)(sourcePath, "utf8");
|
|
1669
2790
|
} catch (error) {
|
|
1670
2791
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1671
|
-
throw new
|
|
2792
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
1672
2793
|
cause: error
|
|
1673
2794
|
});
|
|
1674
2795
|
}
|
|
@@ -1676,13 +2797,13 @@ async function readSourceContext(options) {
|
|
|
1676
2797
|
}
|
|
1677
2798
|
const lines = source.split(/\r?\n/);
|
|
1678
2799
|
if (options.request.line > lines.length) {
|
|
1679
|
-
throw new
|
|
2800
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
|
|
1680
2801
|
}
|
|
1681
|
-
const extension =
|
|
2802
|
+
const extension = import_node_path6.default.extname(sourcePath).toLowerCase();
|
|
1682
2803
|
return extractCodeContext({
|
|
1683
2804
|
source,
|
|
1684
2805
|
sourcePath,
|
|
1685
|
-
relativePath: toDisplayPath(await (0,
|
|
2806
|
+
relativePath: toDisplayPath(await (0, import_promises5.realpath)(options.root), sourcePath),
|
|
1686
2807
|
language: extension === ".tsx" ? "tsx" : "jsx",
|
|
1687
2808
|
line: options.request.line,
|
|
1688
2809
|
column: options.request.column,
|
|
@@ -1691,7 +2812,17 @@ async function readSourceContext(options) {
|
|
|
1691
2812
|
});
|
|
1692
2813
|
}
|
|
1693
2814
|
|
|
1694
|
-
// src/server/
|
|
2815
|
+
// src/server/annotation-authorizer.ts
|
|
2816
|
+
function sanitizePageContext(page) {
|
|
2817
|
+
return Object.freeze({
|
|
2818
|
+
url: (0, import_shared11.sanitizeUrl)(page.url, page.url),
|
|
2819
|
+
pathname: (0, import_shared11.redactSensitiveText)(page.pathname),
|
|
2820
|
+
title: (0, import_shared11.redactSensitiveText)(page.title),
|
|
2821
|
+
viewportWidth: page.viewportWidth,
|
|
2822
|
+
viewportHeight: page.viewportHeight,
|
|
2823
|
+
devicePixelRatio: page.devicePixelRatio
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
1695
2826
|
function compactSourceRef(source) {
|
|
1696
2827
|
return Object.freeze({
|
|
1697
2828
|
origin: source.origin,
|
|
@@ -1705,27 +2836,20 @@ function compactSourceRef(source) {
|
|
|
1705
2836
|
async function authorizeSourceRef(source, registry, root) {
|
|
1706
2837
|
const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
|
|
1707
2838
|
if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
|
|
1708
|
-
throw new
|
|
2839
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1709
2840
|
}
|
|
1710
2841
|
if (source.fileId === void 0) {
|
|
1711
2842
|
if (source.origin === "none" && source.relativePath !== void 0) {
|
|
1712
|
-
throw new
|
|
2843
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1713
2844
|
}
|
|
1714
2845
|
return compactSourceRef(source);
|
|
1715
2846
|
}
|
|
1716
|
-
const sourcePath = await resolveSourceFile({
|
|
1717
|
-
|
|
1718
|
-
registry,
|
|
1719
|
-
root
|
|
1720
|
-
});
|
|
1721
|
-
const relativePath = import_node_path6.default.relative(await (0, import_promises5.realpath)(root), sourcePath).split(import_node_path6.default.sep).join("/");
|
|
2847
|
+
const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
|
|
2848
|
+
const relativePath = import_node_path7.default.relative(await (0, import_promises6.realpath)(root), sourcePath).split(import_node_path7.default.sep).join("/");
|
|
1722
2849
|
if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
|
|
1723
|
-
throw new
|
|
2850
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1724
2851
|
}
|
|
1725
|
-
return Object.freeze({
|
|
1726
|
-
...compactSourceRef(source),
|
|
1727
|
-
relativePath
|
|
1728
|
-
});
|
|
2852
|
+
return Object.freeze({ ...compactSourceRef(source), relativePath });
|
|
1729
2853
|
}
|
|
1730
2854
|
function freezeMatchedRule(rule) {
|
|
1731
2855
|
return Object.freeze({
|
|
@@ -1759,7 +2883,7 @@ async function authorizeTarget(target, input) {
|
|
|
1759
2883
|
maxLines: input.options.budget.maxCodeLines
|
|
1760
2884
|
});
|
|
1761
2885
|
if (marker === void 0 && target.code !== void 0) {
|
|
1762
|
-
throw new
|
|
2886
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1763
2887
|
}
|
|
1764
2888
|
const code = marker === void 0 ? void 0 : await readSourceContext({
|
|
1765
2889
|
request: marker,
|
|
@@ -1769,11 +2893,11 @@ async function authorizeTarget(target, input) {
|
|
|
1769
2893
|
maxLines: input.options.budget.maxCodeLines
|
|
1770
2894
|
});
|
|
1771
2895
|
if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
|
|
1772
|
-
throw new
|
|
2896
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1773
2897
|
}
|
|
1774
2898
|
return Object.freeze({
|
|
1775
2899
|
instruction: target.instruction,
|
|
1776
|
-
...target.page === void 0 ? {} : { page:
|
|
2900
|
+
...target.page === void 0 ? {} : { page: sanitizePageContext(target.page) },
|
|
1777
2901
|
source,
|
|
1778
2902
|
react: Object.freeze({
|
|
1779
2903
|
supported: target.react.supported,
|
|
@@ -1801,76 +2925,130 @@ async function authorizeTarget(target, input) {
|
|
|
1801
2925
|
warnings: Object.freeze([...target.warnings])
|
|
1802
2926
|
});
|
|
1803
2927
|
}
|
|
1804
|
-
async function
|
|
1805
|
-
const requestedTargets = input.
|
|
2928
|
+
async function authorizeAnnotation(input) {
|
|
2929
|
+
const requestedTargets = input.annotation.targets;
|
|
1806
2930
|
if (requestedTargets.length > input.options.maxTargets) {
|
|
1807
|
-
throw new
|
|
2931
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1808
2932
|
}
|
|
1809
2933
|
const identities = requestedTargets.map(targetIdentity);
|
|
1810
2934
|
if (new Set(identities).size !== identities.length) {
|
|
1811
|
-
throw new
|
|
2935
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1812
2936
|
}
|
|
1813
2937
|
const targets = Object.freeze(
|
|
1814
2938
|
await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
|
|
1815
2939
|
);
|
|
1816
|
-
|
|
2940
|
+
return Object.freeze({
|
|
1817
2941
|
schemaVersion: 3,
|
|
1818
|
-
id: input.
|
|
1819
|
-
locale: input.
|
|
1820
|
-
page:
|
|
2942
|
+
id: input.annotation.id,
|
|
2943
|
+
locale: input.annotation.locale,
|
|
2944
|
+
page: sanitizePageContext(input.annotation.page),
|
|
1821
2945
|
targets,
|
|
1822
|
-
createdAt: input.
|
|
1823
|
-
});
|
|
1824
|
-
return Object.freeze({
|
|
1825
|
-
annotation,
|
|
1826
|
-
...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
|
|
1827
|
-
providerProfileId: input.request.providerProfileId,
|
|
1828
|
-
modelProfileId: input.request.modelProfileId,
|
|
1829
|
-
providerDataConsent: true,
|
|
1830
|
-
...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
|
|
1831
|
-
workingTreeMode: input.request.workingTreeMode
|
|
2946
|
+
createdAt: input.annotation.createdAt
|
|
1832
2947
|
});
|
|
1833
2948
|
}
|
|
1834
2949
|
|
|
1835
|
-
// src/
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.INVALID_REQUEST);
|
|
2950
|
+
// src/external-handoff/browser-http.ts
|
|
2951
|
+
function matchExternalHandoffBrowserPath(path9) {
|
|
2952
|
+
if (path9 === import_shared12.SPOTPATCH_ENDPOINTS.externalHandoffCapability) return "capability";
|
|
2953
|
+
if (path9 === import_shared12.SPOTPATCH_ENDPOINTS.externalHandoffPublish) return "publish";
|
|
2954
|
+
if (path9 === import_shared12.SPOTPATCH_ENDPOINTS.externalHandoffStatus) return "status";
|
|
2955
|
+
if (path9 === import_shared12.SPOTPATCH_ENDPOINTS.externalHandoffResolveDelivery) {
|
|
2956
|
+
return "resolve-delivery";
|
|
1843
2957
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2958
|
+
return void 0;
|
|
2959
|
+
}
|
|
2960
|
+
function requireService(options) {
|
|
2961
|
+
if (!options.options.externalAgent.enabled || options.service === void 0) {
|
|
2962
|
+
throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED);
|
|
1847
2963
|
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
2964
|
+
return options.service;
|
|
2965
|
+
}
|
|
2966
|
+
function remapAuthorizationError(error) {
|
|
2967
|
+
if (error instanceof import_shared12.SpotPatchError) {
|
|
2968
|
+
if (error.code === import_shared12.ERROR_CODES.SOURCE_NOT_FOUND || error.code === import_shared12.ERROR_CODES.SOURCE_OUTSIDE_ROOT || error.code === import_shared12.ERROR_CODES.SOURCE_TOO_LARGE) {
|
|
2969
|
+
throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.HANDOFF_SOURCE_STALE, void 0, {
|
|
2970
|
+
cause: error
|
|
2971
|
+
});
|
|
1855
2972
|
}
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
continue;
|
|
2973
|
+
if (error.code === import_shared12.ERROR_CODES.INVALID_REQUEST) {
|
|
2974
|
+
throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.HANDOFF_VALIDATION_FAILED, void 0, {
|
|
2975
|
+
cause: error
|
|
2976
|
+
});
|
|
1861
2977
|
}
|
|
1862
|
-
chunks.push(buffer);
|
|
1863
2978
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
2979
|
+
throw error;
|
|
2980
|
+
}
|
|
2981
|
+
async function handleExternalHandoffBrowserRequest(request, response, route, options, writeSuccess) {
|
|
2982
|
+
if (request.method !== "POST") {
|
|
2983
|
+
throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
|
|
1866
2984
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2985
|
+
const service = requireService(options);
|
|
2986
|
+
if (route === "capability") {
|
|
2987
|
+
const parsed2 = import_shared12.externalHandoffCapabilityRequestSchema.safeParse(
|
|
2988
|
+
await readJsonRequestBody(request)
|
|
2989
|
+
);
|
|
2990
|
+
if (!parsed2.success) throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
|
|
2991
|
+
writeSuccess(response, 200, service.capability());
|
|
2992
|
+
return;
|
|
1873
2993
|
}
|
|
2994
|
+
if (route === "status") {
|
|
2995
|
+
const parsed2 = import_shared12.externalHandoffStatusRequestSchema.safeParse(
|
|
2996
|
+
await readJsonRequestBody(request)
|
|
2997
|
+
);
|
|
2998
|
+
if (!parsed2.success) throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
|
|
2999
|
+
writeSuccess(response, 200, service.status(parsed2.data.cursor));
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
if (route === "resolve-delivery") {
|
|
3003
|
+
const parsed2 = import_shared12.externalHandoffResolveDeliveryRequestSchema.safeParse(
|
|
3004
|
+
await readJsonRequestBody(request)
|
|
3005
|
+
);
|
|
3006
|
+
if (!parsed2.success) throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
|
|
3007
|
+
writeSuccess(response, 200, service.resolveDelivery(parsed2.data.cursor));
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
const parsed = import_shared12.externalHandoffPublishRequestSchema.safeParse(
|
|
3011
|
+
await readJsonRequestBody(request, import_shared12.EXTERNAL_HANDOFF_LIMITS.maximumPublishBodyBytes)
|
|
3012
|
+
);
|
|
3013
|
+
if (!parsed.success) {
|
|
3014
|
+
throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
|
|
3015
|
+
}
|
|
3016
|
+
const result = await service.publish(parsed.data, async (annotation) => {
|
|
3017
|
+
try {
|
|
3018
|
+
return await authorizeAnnotation({
|
|
3019
|
+
annotation,
|
|
3020
|
+
options: options.options,
|
|
3021
|
+
registry: options.registry,
|
|
3022
|
+
root: options.root
|
|
3023
|
+
});
|
|
3024
|
+
} catch (error) {
|
|
3025
|
+
remapAuthorizationError(error);
|
|
3026
|
+
}
|
|
3027
|
+
});
|
|
3028
|
+
writeSuccess(response, result.replayed ? 200 : 201, result);
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
// src/server/agent-http.ts
|
|
3032
|
+
var import_shared14 = require("@spotpatch/shared");
|
|
3033
|
+
|
|
3034
|
+
// src/server/agent-request.ts
|
|
3035
|
+
var import_shared13 = require("@spotpatch/shared");
|
|
3036
|
+
async function authorizeAgentJobRequest(input) {
|
|
3037
|
+
const annotation = await authorizeAnnotation({
|
|
3038
|
+
annotation: input.request.annotation,
|
|
3039
|
+
options: input.options,
|
|
3040
|
+
registry: input.registry,
|
|
3041
|
+
root: input.root
|
|
3042
|
+
});
|
|
3043
|
+
return Object.freeze({
|
|
3044
|
+
annotation,
|
|
3045
|
+
...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
|
|
3046
|
+
providerProfileId: input.request.providerProfileId,
|
|
3047
|
+
modelProfileId: input.request.modelProfileId,
|
|
3048
|
+
providerDataConsent: true,
|
|
3049
|
+
...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
|
|
3050
|
+
workingTreeMode: input.request.workingTreeMode
|
|
3051
|
+
});
|
|
1874
3052
|
}
|
|
1875
3053
|
|
|
1876
3054
|
// src/server/agent-http.ts
|
|
@@ -1890,21 +3068,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
|
|
|
1890
3068
|
"reverted",
|
|
1891
3069
|
"failed"
|
|
1892
3070
|
]);
|
|
1893
|
-
function matchAgentRequestPath(
|
|
1894
|
-
if (
|
|
3071
|
+
function matchAgentRequestPath(path9) {
|
|
3072
|
+
if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.agentCapability) {
|
|
1895
3073
|
return Object.freeze({ kind: "capability" });
|
|
1896
3074
|
}
|
|
1897
|
-
if (
|
|
3075
|
+
if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
|
|
1898
3076
|
return Object.freeze({ kind: "workspace-health" });
|
|
1899
3077
|
}
|
|
1900
|
-
if (
|
|
3078
|
+
if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.agentJobs) {
|
|
1901
3079
|
return Object.freeze({ kind: "create-job" });
|
|
1902
3080
|
}
|
|
1903
|
-
const prefix = `${
|
|
1904
|
-
if (!
|
|
3081
|
+
const prefix = `${import_shared14.SPOTPATCH_ENDPOINTS.agentJobs}/`;
|
|
3082
|
+
if (!path9.startsWith(prefix)) {
|
|
1905
3083
|
return void 0;
|
|
1906
3084
|
}
|
|
1907
|
-
const segments =
|
|
3085
|
+
const segments = path9.slice(prefix.length).split("/");
|
|
1908
3086
|
const jobId = segments[0];
|
|
1909
3087
|
const action = segments[1];
|
|
1910
3088
|
if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
|
|
@@ -1918,7 +3096,7 @@ function matchAgentRequestPath(path8) {
|
|
|
1918
3096
|
}
|
|
1919
3097
|
function requireAgentManager(options) {
|
|
1920
3098
|
if (options.agentManager === void 0 || options.options.ai === false) {
|
|
1921
|
-
throw new
|
|
3099
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.AI_DISABLED);
|
|
1922
3100
|
}
|
|
1923
3101
|
return options.agentManager;
|
|
1924
3102
|
}
|
|
@@ -1971,13 +3149,13 @@ function streamAgentJobEvents(response, manager, jobId) {
|
|
|
1971
3149
|
}
|
|
1972
3150
|
async function handleCapability(request, response, options, writeSuccess) {
|
|
1973
3151
|
if (request.method !== "POST") {
|
|
1974
|
-
throw new
|
|
3152
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
1975
3153
|
}
|
|
1976
|
-
const parsed =
|
|
3154
|
+
const parsed = import_shared14.agentCapabilityRequestSchema.safeParse(
|
|
1977
3155
|
await readJsonRequestBody(request)
|
|
1978
3156
|
);
|
|
1979
3157
|
if (!parsed.success) {
|
|
1980
|
-
throw new
|
|
3158
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
1981
3159
|
}
|
|
1982
3160
|
const controller = new AbortController();
|
|
1983
3161
|
const abort = () => {
|
|
@@ -1996,13 +3174,13 @@ async function handleCapability(request, response, options, writeSuccess) {
|
|
|
1996
3174
|
}
|
|
1997
3175
|
async function handleCreateJob(request, response, options, writeSuccess) {
|
|
1998
3176
|
if (request.method !== "POST") {
|
|
1999
|
-
throw new
|
|
3177
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2000
3178
|
}
|
|
2001
|
-
const parsed =
|
|
3179
|
+
const parsed = import_shared14.agentJobCreateRequestSchema.safeParse(
|
|
2002
3180
|
await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
|
|
2003
3181
|
);
|
|
2004
3182
|
if (!parsed.success) {
|
|
2005
|
-
throw new
|
|
3183
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2006
3184
|
}
|
|
2007
3185
|
const authorizedRequest = await authorizeAgentJobRequest({
|
|
2008
3186
|
request: parsed.data,
|
|
@@ -2015,13 +3193,13 @@ async function handleCreateJob(request, response, options, writeSuccess) {
|
|
|
2015
3193
|
}
|
|
2016
3194
|
async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
2017
3195
|
if (request.method !== "POST") {
|
|
2018
|
-
throw new
|
|
3196
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2019
3197
|
}
|
|
2020
|
-
const parsed =
|
|
3198
|
+
const parsed = import_shared14.agentWorkspaceHealthRequestSchema.safeParse(
|
|
2021
3199
|
await readJsonRequestBody(request)
|
|
2022
3200
|
);
|
|
2023
3201
|
if (!parsed.success) {
|
|
2024
|
-
throw new
|
|
3202
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2025
3203
|
}
|
|
2026
3204
|
const controller = new AbortController();
|
|
2027
3205
|
const abort = () => {
|
|
@@ -2038,13 +3216,13 @@ async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
|
2038
3216
|
async function handleJobAction(request, response, options, route, writeSuccess) {
|
|
2039
3217
|
const manager = requireAgentManager(options);
|
|
2040
3218
|
if (request.method !== "POST") {
|
|
2041
|
-
throw new
|
|
3219
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2042
3220
|
}
|
|
2043
|
-
const parsed =
|
|
3221
|
+
const parsed = import_shared14.agentJobActionRequestSchema.safeParse(
|
|
2044
3222
|
await readJsonRequestBody(request)
|
|
2045
3223
|
);
|
|
2046
3224
|
if (!parsed.success) {
|
|
2047
|
-
throw new
|
|
3225
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2048
3226
|
}
|
|
2049
3227
|
if (route.action === "events") {
|
|
2050
3228
|
streamAgentJobEvents(response, manager, route.jobId);
|
|
@@ -2158,21 +3336,188 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
2158
3336
|
}
|
|
2159
3337
|
var launchConfiguredEditor = createEditorLauncher();
|
|
2160
3338
|
|
|
3339
|
+
// src/server/data-flow-http.ts
|
|
3340
|
+
var import_node_crypto9 = require("crypto");
|
|
3341
|
+
var import_analyzer = require("@spotpatch/analyzer");
|
|
3342
|
+
var import_shared15 = require("@spotpatch/shared");
|
|
3343
|
+
function envelopeBytes(report) {
|
|
3344
|
+
return Buffer.byteLength(JSON.stringify({ ok: true, data: report }), "utf8");
|
|
3345
|
+
}
|
|
3346
|
+
function limitDataFlowReportToBytes(report, maximumBytes) {
|
|
3347
|
+
const structurallyLimited = (0, import_shared15.limitDataFlowReportCollections)(report);
|
|
3348
|
+
if (envelopeBytes(structurallyLimited) <= maximumBytes) {
|
|
3349
|
+
return structurallyLimited;
|
|
3350
|
+
}
|
|
3351
|
+
let limited = (0, import_shared15.limitDataFlowReportCollections)(structurallyLimited, {
|
|
3352
|
+
forceTruncation: true,
|
|
3353
|
+
maximumDependencies: 0,
|
|
3354
|
+
truncatedBy: "bytes"
|
|
3355
|
+
});
|
|
3356
|
+
if (envelopeBytes(limited) > maximumBytes) {
|
|
3357
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INTERNAL_ERROR);
|
|
3358
|
+
}
|
|
3359
|
+
for (let maximumDependencies = 1; maximumDependencies <= structurallyLimited.dependencies.length; maximumDependencies += 1) {
|
|
3360
|
+
const candidate = (0, import_shared15.limitDataFlowReportCollections)(structurallyLimited, {
|
|
3361
|
+
forceTruncation: true,
|
|
3362
|
+
maximumDependencies,
|
|
3363
|
+
truncatedBy: "bytes"
|
|
3364
|
+
});
|
|
3365
|
+
if (envelopeBytes(candidate) > maximumBytes) break;
|
|
3366
|
+
limited = candidate;
|
|
3367
|
+
}
|
|
3368
|
+
return limited;
|
|
3369
|
+
}
|
|
3370
|
+
function createDataFlowAnalyzer(options) {
|
|
3371
|
+
if (!options.options.dataFlow.enabled) return void 0;
|
|
3372
|
+
return (0, import_analyzer.createStaticDataFlowAnalyzer)({
|
|
3373
|
+
root: options.root,
|
|
3374
|
+
registryEpoch: options.session.id,
|
|
3375
|
+
registerSource: (absolutePath) => options.registry.register(absolutePath),
|
|
3376
|
+
limits: options.options.dataFlow.limits
|
|
3377
|
+
});
|
|
3378
|
+
}
|
|
3379
|
+
async function analyzeTarget(request, analyzer, options) {
|
|
3380
|
+
const resolvedRequest = (() => {
|
|
3381
|
+
if ("componentSourceId" in request) {
|
|
3382
|
+
const anchor = options.registry.resolveDataFlowComponent(
|
|
3383
|
+
request.componentSourceId
|
|
3384
|
+
);
|
|
3385
|
+
if (anchor?.sourceVersion !== request.sourceVersion) {
|
|
3386
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.DATA_FLOW_SOURCE_STALE);
|
|
3387
|
+
}
|
|
3388
|
+
return anchor;
|
|
3389
|
+
}
|
|
3390
|
+
return request;
|
|
3391
|
+
})();
|
|
3392
|
+
const absolutePath = await resolveSourceFile({
|
|
3393
|
+
fileId: resolvedRequest.fileId,
|
|
3394
|
+
registry: options.registry,
|
|
3395
|
+
root: options.root
|
|
3396
|
+
});
|
|
3397
|
+
const report = analyzer.analyzeComponent({
|
|
3398
|
+
absolutePath,
|
|
3399
|
+
line: resolvedRequest.line,
|
|
3400
|
+
column: resolvedRequest.column
|
|
3401
|
+
});
|
|
3402
|
+
if (resolvedRequest.sourceVersion !== void 0 && resolvedRequest.sourceVersion !== report.component.source.sourceVersion) {
|
|
3403
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.DATA_FLOW_SOURCE_STALE);
|
|
3404
|
+
}
|
|
3405
|
+
return limitDataFlowReportToBytes(
|
|
3406
|
+
report,
|
|
3407
|
+
options.options.dataFlow.limits.reportMaxBytes
|
|
3408
|
+
);
|
|
3409
|
+
}
|
|
3410
|
+
function requireAnalyzer(analyzer) {
|
|
3411
|
+
if (analyzer === void 0) {
|
|
3412
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.DATA_FLOW_DISABLED);
|
|
3413
|
+
}
|
|
3414
|
+
return analyzer;
|
|
3415
|
+
}
|
|
3416
|
+
async function handleComponentDataFlowReport(request, analyzer, options) {
|
|
3417
|
+
const parsed = import_shared15.dataFlowComponentReportRequestSchema.safeParse(
|
|
3418
|
+
await readJsonRequestBody(
|
|
3419
|
+
request,
|
|
3420
|
+
options.options.dataFlow.limits.protocolRequestMaxBytes
|
|
3421
|
+
)
|
|
3422
|
+
);
|
|
3423
|
+
if (!parsed.success) {
|
|
3424
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
|
|
3425
|
+
}
|
|
3426
|
+
return analyzeTarget(parsed.data, requireAnalyzer(analyzer), options);
|
|
3427
|
+
}
|
|
3428
|
+
async function handlePageDataFlowReport(request, analyzer, options) {
|
|
3429
|
+
const parsed = import_shared15.dataFlowPageReportRequestSchema.safeParse(
|
|
3430
|
+
await readJsonRequestBody(
|
|
3431
|
+
request,
|
|
3432
|
+
options.options.dataFlow.limits.protocolRequestMaxBytes
|
|
3433
|
+
)
|
|
3434
|
+
);
|
|
3435
|
+
if (!parsed.success) {
|
|
3436
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
|
|
3437
|
+
}
|
|
3438
|
+
const activeAnalyzer = requireAnalyzer(analyzer);
|
|
3439
|
+
const componentReports = await Promise.all(
|
|
3440
|
+
parsed.data.targets.map((target) => analyzeTarget(target, activeAnalyzer, options))
|
|
3441
|
+
);
|
|
3442
|
+
const dependencies = new Map(
|
|
3443
|
+
componentReports.flatMap(
|
|
3444
|
+
(report2) => report2.dependencies.map((dependency) => [dependency.id, dependency])
|
|
3445
|
+
)
|
|
3446
|
+
);
|
|
3447
|
+
const evidence = new Map(
|
|
3448
|
+
componentReports.flatMap(
|
|
3449
|
+
(report2) => report2.evidence.map((entry) => [entry.id, entry])
|
|
3450
|
+
)
|
|
3451
|
+
);
|
|
3452
|
+
const diagnostics = new Map(
|
|
3453
|
+
componentReports.flatMap(
|
|
3454
|
+
(report2) => report2.diagnostics.map((entry) => [entry.id, entry])
|
|
3455
|
+
)
|
|
3456
|
+
);
|
|
3457
|
+
const analyzedVersions = new Set(
|
|
3458
|
+
componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
|
|
3459
|
+
);
|
|
3460
|
+
const reportId = `page_${(0, import_node_crypto9.createHash)("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
|
|
3461
|
+
const complete = componentReports.every((report2) => report2.completeness.complete);
|
|
3462
|
+
const report = Object.freeze({
|
|
3463
|
+
schemaVersion: import_shared15.DATA_FLOW_SCHEMA_VERSION,
|
|
3464
|
+
reportId,
|
|
3465
|
+
baseline: Object.freeze({
|
|
3466
|
+
registryEpoch: options.session.id,
|
|
3467
|
+
analyzerVersion: componentReports[0]?.baseline.analyzerVersion ?? "unavailable",
|
|
3468
|
+
adapterSetHash: componentReports[0]?.baseline.adapterSetHash ?? "unavailable",
|
|
3469
|
+
analyzedSourceVersions: Object.freeze([...analyzedVersions].sort())
|
|
3470
|
+
}),
|
|
3471
|
+
capability: Object.freeze({
|
|
3472
|
+
enabled: true,
|
|
3473
|
+
staticAnalysis: complete ? "available" : "partial",
|
|
3474
|
+
runtimeObservation: "dispatch-only",
|
|
3475
|
+
responseShape: "consumed-fields-only",
|
|
3476
|
+
aiAssistance: "disabled",
|
|
3477
|
+
reasons: Object.freeze(
|
|
3478
|
+
componentReports.flatMap((report2) => report2.capability.reasons)
|
|
3479
|
+
)
|
|
3480
|
+
}),
|
|
3481
|
+
dependencies: Object.freeze([...dependencies.values()]),
|
|
3482
|
+
evidence: Object.freeze([...evidence.values()]),
|
|
3483
|
+
diagnostics: Object.freeze([...diagnostics.values()]),
|
|
3484
|
+
completeness: Object.freeze({
|
|
3485
|
+
complete,
|
|
3486
|
+
visitedModules: componentReports.reduce(
|
|
3487
|
+
(total, report2) => total + report2.completeness.visitedModules,
|
|
3488
|
+
0
|
|
3489
|
+
),
|
|
3490
|
+
visitedCallsites: componentReports.reduce(
|
|
3491
|
+
(total, report2) => total + report2.completeness.visitedCallsites,
|
|
3492
|
+
0
|
|
3493
|
+
),
|
|
3494
|
+
frontierCount: componentReports.reduce(
|
|
3495
|
+
(total, report2) => total + report2.completeness.frontierCount,
|
|
3496
|
+
0
|
|
3497
|
+
)
|
|
3498
|
+
})
|
|
3499
|
+
});
|
|
3500
|
+
return limitDataFlowReportToBytes(
|
|
3501
|
+
report,
|
|
3502
|
+
options.options.dataFlow.limits.reportMaxBytes
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
|
|
2161
3506
|
// src/server/request-security.ts
|
|
2162
|
-
var
|
|
3507
|
+
var import_node_crypto10 = require("crypto");
|
|
2163
3508
|
var import_node_net = require("net");
|
|
2164
|
-
var
|
|
3509
|
+
var import_shared16 = require("@spotpatch/shared");
|
|
2165
3510
|
function getSingleHeader(request, name) {
|
|
2166
3511
|
const value = request.headers[name.toLowerCase()];
|
|
2167
3512
|
return Array.isArray(value) ? value[0] : value;
|
|
2168
3513
|
}
|
|
2169
|
-
function
|
|
3514
|
+
function tokensMatch2(actual, expected) {
|
|
2170
3515
|
if (actual === void 0) {
|
|
2171
3516
|
return false;
|
|
2172
3517
|
}
|
|
2173
3518
|
const actualBytes = Buffer.from(actual);
|
|
2174
3519
|
const expectedBytes = Buffer.from(expected);
|
|
2175
|
-
return actualBytes.byteLength === expectedBytes.byteLength && (0,
|
|
3520
|
+
return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto10.timingSafeEqual)(actualBytes, expectedBytes);
|
|
2176
3521
|
}
|
|
2177
3522
|
function isLoopbackHostname(hostname) {
|
|
2178
3523
|
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
@@ -2206,32 +3551,32 @@ function parseOrigin(value) {
|
|
|
2206
3551
|
}
|
|
2207
3552
|
}
|
|
2208
3553
|
function assertRequestAuthorized(request, options) {
|
|
2209
|
-
const actualToken = getSingleHeader(request,
|
|
2210
|
-
if (!
|
|
2211
|
-
throw new
|
|
3554
|
+
const actualToken = getSingleHeader(request, import_shared16.SPOTPATCH_TOKEN_HEADER);
|
|
3555
|
+
if (!tokensMatch2(actualToken, options.sessionToken)) {
|
|
3556
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_TOKEN);
|
|
2212
3557
|
}
|
|
2213
3558
|
const hostHeader = getSingleHeader(request, "host");
|
|
2214
3559
|
const originHeader = getSingleHeader(request, "origin");
|
|
2215
3560
|
const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
|
|
2216
3561
|
const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
|
|
2217
3562
|
if (host === void 0 || origin === void 0) {
|
|
2218
|
-
throw new
|
|
3563
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
2219
3564
|
}
|
|
2220
3565
|
const hostIsLoopback = isLoopbackHostname(host.hostname);
|
|
2221
3566
|
const originIsLoopback = isLoopbackHostname(origin.hostname);
|
|
2222
3567
|
if (!options.allowLan) {
|
|
2223
3568
|
if (!hostIsLoopback || !originIsLoopback) {
|
|
2224
|
-
throw new
|
|
3569
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
2225
3570
|
}
|
|
2226
3571
|
return;
|
|
2227
3572
|
}
|
|
2228
3573
|
if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
|
|
2229
|
-
throw new
|
|
3574
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
2230
3575
|
}
|
|
2231
3576
|
}
|
|
2232
3577
|
|
|
2233
3578
|
// src/server/runtime-bootstrap.ts
|
|
2234
|
-
var
|
|
3579
|
+
var import_shared17 = require("@spotpatch/shared");
|
|
2235
3580
|
function getSingleHeader2(request, name) {
|
|
2236
3581
|
const value = request.headers[name.toLowerCase()];
|
|
2237
3582
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -2246,7 +3591,7 @@ function resolveRuntimeBootstrapOptions(options) {
|
|
|
2246
3591
|
if (expectedOrigin.origin !== options.expectedOrigin || expectedOrigin.protocol !== "http:" || !isLoopbackHostname(expectedOrigin.hostname)) {
|
|
2247
3592
|
throw new TypeError("The SpotPatch bootstrap origin must be a loopback origin.");
|
|
2248
3593
|
}
|
|
2249
|
-
const parsedConfig =
|
|
3594
|
+
const parsedConfig = import_shared17.runtimeConfigSchema.safeParse(options.runtimeConfig);
|
|
2250
3595
|
if (!parsedConfig.success) {
|
|
2251
3596
|
throw new TypeError("The SpotPatch Runtime configuration is invalid.");
|
|
2252
3597
|
}
|
|
@@ -2258,7 +3603,7 @@ function resolveRuntimeBootstrapOptions(options) {
|
|
|
2258
3603
|
function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
2259
3604
|
const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
2260
3605
|
if (request.method !== "POST" || contentType !== "application/json") {
|
|
2261
|
-
throw new
|
|
3606
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
2262
3607
|
}
|
|
2263
3608
|
const host = getSingleHeader2(request, "host");
|
|
2264
3609
|
let hostIsLoopback = false;
|
|
@@ -2270,106 +3615,148 @@ function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
|
2270
3615
|
}
|
|
2271
3616
|
}
|
|
2272
3617
|
if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
|
|
2273
|
-
throw new
|
|
3618
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
2274
3619
|
}
|
|
2275
3620
|
}
|
|
2276
3621
|
async function readRuntimeBootstrap(request, options) {
|
|
2277
3622
|
assertRuntimeBootstrapRequest(request, options.expectedOrigin);
|
|
2278
|
-
const parsedBody =
|
|
3623
|
+
const parsedBody = import_shared17.runtimeBootstrapRequestSchema.safeParse(
|
|
2279
3624
|
await readJsonRequestBody(request)
|
|
2280
3625
|
);
|
|
2281
3626
|
if (!parsedBody.success) {
|
|
2282
|
-
throw new
|
|
3627
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
2283
3628
|
}
|
|
2284
3629
|
return options.runtimeConfig;
|
|
2285
3630
|
}
|
|
2286
3631
|
|
|
2287
3632
|
// src/server/middleware.ts
|
|
2288
3633
|
var STATUS_BY_ERROR = Object.freeze({
|
|
2289
|
-
[
|
|
2290
|
-
[
|
|
2291
|
-
[
|
|
2292
|
-
[
|
|
2293
|
-
[
|
|
2294
|
-
[
|
|
2295
|
-
[
|
|
2296
|
-
[
|
|
2297
|
-
[
|
|
2298
|
-
[
|
|
2299
|
-
[
|
|
2300
|
-
[
|
|
2301
|
-
[
|
|
2302
|
-
[
|
|
2303
|
-
[
|
|
2304
|
-
[
|
|
2305
|
-
[
|
|
2306
|
-
[
|
|
2307
|
-
[
|
|
2308
|
-
[
|
|
2309
|
-
[
|
|
2310
|
-
[
|
|
2311
|
-
[
|
|
2312
|
-
[
|
|
2313
|
-
[
|
|
2314
|
-
[
|
|
2315
|
-
[
|
|
2316
|
-
[
|
|
2317
|
-
[
|
|
2318
|
-
[
|
|
2319
|
-
[
|
|
2320
|
-
[
|
|
2321
|
-
[
|
|
3634
|
+
[import_shared18.ERROR_CODES.INVALID_REQUEST]: 400,
|
|
3635
|
+
[import_shared18.ERROR_CODES.INVALID_TOKEN]: 401,
|
|
3636
|
+
[import_shared18.ERROR_CODES.ORIGIN_NOT_ALLOWED]: 403,
|
|
3637
|
+
[import_shared18.ERROR_CODES.SOURCE_NOT_FOUND]: 404,
|
|
3638
|
+
[import_shared18.ERROR_CODES.SOURCE_OUTSIDE_ROOT]: 403,
|
|
3639
|
+
[import_shared18.ERROR_CODES.SOURCE_TOO_LARGE]: 413,
|
|
3640
|
+
[import_shared18.ERROR_CODES.EDITOR_OPEN_FAILED]: 500,
|
|
3641
|
+
[import_shared18.ERROR_CODES.DATA_FLOW_DISABLED]: 404,
|
|
3642
|
+
[import_shared18.ERROR_CODES.DATA_FLOW_SOURCE_STALE]: 409,
|
|
3643
|
+
[import_shared18.ERROR_CODES.DATA_FLOW_ANALYSIS_CANCELLED]: 409,
|
|
3644
|
+
[import_shared18.ERROR_CODES.AI_DISABLED]: 404,
|
|
3645
|
+
[import_shared18.ERROR_CODES.PROVIDER_NOT_CONFIGURED]: 503,
|
|
3646
|
+
[import_shared18.ERROR_CODES.PROVIDER_AUTH_FAILED]: 502,
|
|
3647
|
+
[import_shared18.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
|
|
3648
|
+
[import_shared18.ERROR_CODES.MODEL_NOT_ALLOWED]: 400,
|
|
3649
|
+
[import_shared18.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
|
|
3650
|
+
[import_shared18.ERROR_CODES.PROVIDER_RATE_LIMITED]: 429,
|
|
3651
|
+
[import_shared18.ERROR_CODES.AGENT_BUSY]: 409,
|
|
3652
|
+
[import_shared18.ERROR_CODES.AGENT_LIMIT_EXCEEDED]: 413,
|
|
3653
|
+
[import_shared18.ERROR_CODES.AGENT_CANCELLED]: 409,
|
|
3654
|
+
[import_shared18.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED]: 404,
|
|
3655
|
+
[import_shared18.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE]: 503,
|
|
3656
|
+
[import_shared18.ERROR_CODES.HANDOFF_VALIDATION_FAILED]: 422,
|
|
3657
|
+
[import_shared18.ERROR_CODES.HANDOFF_SOURCE_STALE]: 409,
|
|
3658
|
+
[import_shared18.ERROR_CODES.HANDOFF_NOT_FOUND]: 404,
|
|
3659
|
+
[import_shared18.ERROR_CODES.HANDOFF_EXPIRED]: 410,
|
|
3660
|
+
[import_shared18.ERROR_CODES.HANDOFF_CURSOR_INVALID]: 409,
|
|
3661
|
+
[import_shared18.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE]: 413,
|
|
3662
|
+
[import_shared18.ERROR_CODES.BRIDGE_UNAUTHORIZED]: 401,
|
|
3663
|
+
[import_shared18.ERROR_CODES.BRIDGE_PROTOCOL_MISMATCH]: 409,
|
|
3664
|
+
[import_shared18.ERROR_CODES.BRIDGE_BUSY]: 429,
|
|
3665
|
+
[import_shared18.ERROR_CODES.EXTERNAL_AGENT_BUSY]: 409,
|
|
3666
|
+
[import_shared18.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT]: 409,
|
|
3667
|
+
[import_shared18.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID]: 409,
|
|
3668
|
+
[import_shared18.ERROR_CODES.ACTIVE_DISPATCH_INVALID]: 409,
|
|
3669
|
+
[import_shared18.ERROR_CODES.SESSION_NOT_FOUND]: 404,
|
|
3670
|
+
[import_shared18.ERROR_CODES.SESSION_AMBIGUOUS]: 409,
|
|
3671
|
+
[import_shared18.ERROR_CODES.SESSION_CLOSED]: 410,
|
|
3672
|
+
[import_shared18.ERROR_CODES.WORKTREE_DIRTY]: 409,
|
|
3673
|
+
[import_shared18.ERROR_CODES.WORKTREE_NOT_REPOSITORY]: 409,
|
|
3674
|
+
[import_shared18.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS]: 409,
|
|
3675
|
+
[import_shared18.ERROR_CODES.WORKTREE_CONFLICTED]: 409,
|
|
3676
|
+
[import_shared18.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
|
|
3677
|
+
[import_shared18.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
|
|
3678
|
+
[import_shared18.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
|
|
3679
|
+
[import_shared18.ERROR_CODES.TOOL_DENIED]: 403,
|
|
3680
|
+
[import_shared18.ERROR_CODES.TOOL_INPUT_INVALID]: 422,
|
|
3681
|
+
[import_shared18.ERROR_CODES.TOOL_ARGUMENTS_INVALID]: 422,
|
|
3682
|
+
[import_shared18.ERROR_CODES.TOOL_CALL_ID_CONFLICT]: 422,
|
|
3683
|
+
[import_shared18.ERROR_CODES.TOOL_PATH_DENIED]: 403,
|
|
3684
|
+
[import_shared18.ERROR_CODES.PATCH_REJECTED]: 422,
|
|
3685
|
+
[import_shared18.ERROR_CODES.VALIDATION_FAILED]: 422,
|
|
3686
|
+
[import_shared18.ERROR_CODES.APPLY_CONFLICT]: 409,
|
|
3687
|
+
[import_shared18.ERROR_CODES.INTERNAL_ERROR]: 500
|
|
2322
3688
|
});
|
|
2323
3689
|
var PUBLIC_MESSAGES = Object.freeze({
|
|
2324
|
-
[
|
|
2325
|
-
[
|
|
2326
|
-
[
|
|
2327
|
-
[
|
|
2328
|
-
[
|
|
2329
|
-
[
|
|
2330
|
-
[
|
|
2331
|
-
[
|
|
2332
|
-
[
|
|
2333
|
-
[
|
|
2334
|
-
[
|
|
2335
|
-
[
|
|
2336
|
-
[
|
|
2337
|
-
[
|
|
2338
|
-
[
|
|
2339
|
-
[
|
|
2340
|
-
[
|
|
2341
|
-
[
|
|
2342
|
-
[
|
|
2343
|
-
[
|
|
2344
|
-
[
|
|
2345
|
-
[
|
|
2346
|
-
[
|
|
2347
|
-
[
|
|
2348
|
-
[
|
|
2349
|
-
[
|
|
2350
|
-
[
|
|
2351
|
-
[
|
|
2352
|
-
[
|
|
2353
|
-
[
|
|
2354
|
-
[
|
|
2355
|
-
[
|
|
2356
|
-
[
|
|
3690
|
+
[import_shared18.ERROR_CODES.INVALID_REQUEST]: "The request is invalid.",
|
|
3691
|
+
[import_shared18.ERROR_CODES.INVALID_TOKEN]: "The session token is invalid.",
|
|
3692
|
+
[import_shared18.ERROR_CODES.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
|
|
3693
|
+
[import_shared18.ERROR_CODES.SOURCE_NOT_FOUND]: "The source file is unavailable.",
|
|
3694
|
+
[import_shared18.ERROR_CODES.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
|
|
3695
|
+
[import_shared18.ERROR_CODES.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
|
|
3696
|
+
[import_shared18.ERROR_CODES.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
|
|
3697
|
+
[import_shared18.ERROR_CODES.DATA_FLOW_DISABLED]: "Component data-flow analysis is not enabled.",
|
|
3698
|
+
[import_shared18.ERROR_CODES.DATA_FLOW_SOURCE_STALE]: "The selected source version is stale.",
|
|
3699
|
+
[import_shared18.ERROR_CODES.DATA_FLOW_ANALYSIS_CANCELLED]: "The data-flow analysis was cancelled.",
|
|
3700
|
+
[import_shared18.ERROR_CODES.AI_DISABLED]: "AI execution is not enabled.",
|
|
3701
|
+
[import_shared18.ERROR_CODES.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
|
|
3702
|
+
[import_shared18.ERROR_CODES.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
|
|
3703
|
+
[import_shared18.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
|
|
3704
|
+
[import_shared18.ERROR_CODES.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
|
|
3705
|
+
[import_shared18.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
|
|
3706
|
+
[import_shared18.ERROR_CODES.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
|
|
3707
|
+
[import_shared18.ERROR_CODES.AGENT_BUSY]: "Another Agent job is already running.",
|
|
3708
|
+
[import_shared18.ERROR_CODES.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
|
|
3709
|
+
[import_shared18.ERROR_CODES.AGENT_CANCELLED]: "The Agent job was cancelled.",
|
|
3710
|
+
[import_shared18.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED]: "External Agent handoff is not enabled.",
|
|
3711
|
+
[import_shared18.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE]: "External Agent handoff is temporarily unavailable.",
|
|
3712
|
+
[import_shared18.ERROR_CODES.HANDOFF_VALIDATION_FAILED]: "The handoff content is invalid.",
|
|
3713
|
+
[import_shared18.ERROR_CODES.HANDOFF_SOURCE_STALE]: "The selected source is stale.",
|
|
3714
|
+
[import_shared18.ERROR_CODES.HANDOFF_NOT_FOUND]: "No current handoff is available.",
|
|
3715
|
+
[import_shared18.ERROR_CODES.HANDOFF_EXPIRED]: "The handoff has expired.",
|
|
3716
|
+
[import_shared18.ERROR_CODES.HANDOFF_CURSOR_INVALID]: "The handoff cursor is invalid.",
|
|
3717
|
+
[import_shared18.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE]: "The handoff exceeds the size limit.",
|
|
3718
|
+
[import_shared18.ERROR_CODES.BRIDGE_UNAUTHORIZED]: "The local bridge request is unauthorized.",
|
|
3719
|
+
[import_shared18.ERROR_CODES.BRIDGE_PROTOCOL_MISMATCH]: "The local bridge protocol is incompatible.",
|
|
3720
|
+
[import_shared18.ERROR_CODES.BRIDGE_BUSY]: "The local bridge is busy.",
|
|
3721
|
+
[import_shared18.ERROR_CODES.EXTERNAL_AGENT_BUSY]: "The connected external Agent is busy.",
|
|
3722
|
+
[import_shared18.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT]: "Another active Agent adapter is connected.",
|
|
3723
|
+
[import_shared18.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID]: "The active Agent adapter lease is invalid.",
|
|
3724
|
+
[import_shared18.ERROR_CODES.ACTIVE_DISPATCH_INVALID]: "The active Agent dispatch transition is invalid.",
|
|
3725
|
+
[import_shared18.ERROR_CODES.SESSION_NOT_FOUND]: "No active SpotPatch session was found.",
|
|
3726
|
+
[import_shared18.ERROR_CODES.SESSION_AMBIGUOUS]: "More than one SpotPatch session matches.",
|
|
3727
|
+
[import_shared18.ERROR_CODES.SESSION_CLOSED]: "The SpotPatch session has closed.",
|
|
3728
|
+
[import_shared18.ERROR_CODES.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
|
|
3729
|
+
[import_shared18.ERROR_CODES.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
|
|
3730
|
+
[import_shared18.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
|
|
3731
|
+
[import_shared18.ERROR_CODES.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
|
|
3732
|
+
[import_shared18.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
|
|
3733
|
+
[import_shared18.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
|
|
3734
|
+
[import_shared18.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
|
|
3735
|
+
[import_shared18.ERROR_CODES.TOOL_DENIED]: "The Agent tool request was denied.",
|
|
3736
|
+
[import_shared18.ERROR_CODES.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
|
|
3737
|
+
[import_shared18.ERROR_CODES.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
|
|
3738
|
+
[import_shared18.ERROR_CODES.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
|
|
3739
|
+
[import_shared18.ERROR_CODES.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
|
|
3740
|
+
[import_shared18.ERROR_CODES.PATCH_REJECTED]: "The proposed patch was rejected.",
|
|
3741
|
+
[import_shared18.ERROR_CODES.VALIDATION_FAILED]: "The proposed change failed validation.",
|
|
3742
|
+
[import_shared18.ERROR_CODES.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
|
|
3743
|
+
[import_shared18.ERROR_CODES.INTERNAL_ERROR]: "The request could not be completed."
|
|
2357
3744
|
});
|
|
2358
|
-
function
|
|
3745
|
+
function writeJson2(response, status, payload) {
|
|
2359
3746
|
response.statusCode = status;
|
|
2360
3747
|
response.setHeader("Cache-Control", "no-store");
|
|
2361
3748
|
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2362
3749
|
response.end(JSON.stringify(payload));
|
|
2363
3750
|
}
|
|
2364
3751
|
function asSpotPatchError(error) {
|
|
2365
|
-
return error instanceof
|
|
3752
|
+
return error instanceof import_shared18.SpotPatchError ? error : new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INTERNAL_ERROR, void 0, { cause: error });
|
|
2366
3753
|
}
|
|
2367
3754
|
function writeError(response, error, logger) {
|
|
2368
3755
|
const normalized = asSpotPatchError(error);
|
|
2369
|
-
if (normalized.code ===
|
|
3756
|
+
if (normalized.code === import_shared18.ERROR_CODES.INTERNAL_ERROR) {
|
|
2370
3757
|
logger?.warn("[spotpatch:server] Internal request failure.");
|
|
2371
3758
|
}
|
|
2372
|
-
|
|
3759
|
+
writeJson2(response, STATUS_BY_ERROR[normalized.code], {
|
|
2373
3760
|
ok: false,
|
|
2374
3761
|
error: {
|
|
2375
3762
|
code: normalized.code,
|
|
@@ -2385,11 +3772,11 @@ function requestPath(request) {
|
|
|
2385
3772
|
}
|
|
2386
3773
|
}
|
|
2387
3774
|
async function handleSourceContext(request, options) {
|
|
2388
|
-
const parsed =
|
|
3775
|
+
const parsed = import_shared18.sourceContextRequestSchema.safeParse(
|
|
2389
3776
|
await readJsonRequestBody(request)
|
|
2390
3777
|
);
|
|
2391
3778
|
if (!parsed.success) {
|
|
2392
|
-
throw new
|
|
3779
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
2393
3780
|
}
|
|
2394
3781
|
return readSourceContext({
|
|
2395
3782
|
request: parsed.data,
|
|
@@ -2400,9 +3787,9 @@ async function handleSourceContext(request, options) {
|
|
|
2400
3787
|
});
|
|
2401
3788
|
}
|
|
2402
3789
|
async function handleOpenEditor(request, options) {
|
|
2403
|
-
const parsed =
|
|
3790
|
+
const parsed = import_shared18.openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
|
|
2404
3791
|
if (!parsed.success) {
|
|
2405
|
-
throw new
|
|
3792
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
2406
3793
|
}
|
|
2407
3794
|
const body = parsed.data;
|
|
2408
3795
|
const sourcePath = await resolveSourceFile({
|
|
@@ -2419,51 +3806,90 @@ async function handleOpenEditor(request, options) {
|
|
|
2419
3806
|
options.logger?.warn(
|
|
2420
3807
|
`[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
|
|
2421
3808
|
);
|
|
2422
|
-
throw new
|
|
3809
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.EDITOR_OPEN_FAILED, void 0, {
|
|
2423
3810
|
cause: error
|
|
2424
3811
|
});
|
|
2425
3812
|
}
|
|
2426
3813
|
}
|
|
2427
3814
|
function createSpotPatchMiddleware(options) {
|
|
2428
3815
|
const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
|
|
3816
|
+
const dataFlowAnalyzer = createDataFlowAnalyzer(options);
|
|
2429
3817
|
return (request, response, next) => {
|
|
2430
|
-
const
|
|
2431
|
-
const agentRoute = matchAgentRequestPath(
|
|
2432
|
-
|
|
3818
|
+
const path9 = requestPath(request);
|
|
3819
|
+
const agentRoute = matchAgentRequestPath(path9);
|
|
3820
|
+
const externalHandoffRoute = matchExternalHandoffBrowserPath(path9);
|
|
3821
|
+
if (path9 !== import_shared18.SPOTPATCH_ENDPOINTS.sourceContext && path9 !== import_shared18.SPOTPATCH_ENDPOINTS.openEditor && path9 !== import_shared18.SPOTPATCH_ENDPOINTS.dataFlowComponentReport && path9 !== import_shared18.SPOTPATCH_ENDPOINTS.dataFlowPageReport && agentRoute === void 0 && externalHandoffRoute === void 0 && !path9.startsWith(`${import_shared18.SPOTPATCH_API_BASE}/`)) {
|
|
2433
3822
|
next();
|
|
2434
3823
|
return;
|
|
2435
3824
|
}
|
|
2436
3825
|
const handle = async () => {
|
|
2437
|
-
if (
|
|
3826
|
+
if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.bootstrap && bootstrap !== void 0) {
|
|
2438
3827
|
const data = await readRuntimeBootstrap(
|
|
2439
3828
|
request,
|
|
2440
3829
|
bootstrap
|
|
2441
3830
|
);
|
|
2442
|
-
|
|
3831
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2443
3832
|
return;
|
|
2444
3833
|
}
|
|
2445
3834
|
assertRequestAuthorized(request, {
|
|
2446
3835
|
allowLan: options.options.allowLan,
|
|
2447
3836
|
sessionToken: options.session.token
|
|
2448
3837
|
});
|
|
2449
|
-
if (
|
|
3838
|
+
if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.sourceContext) {
|
|
2450
3839
|
if (request.method !== "POST") {
|
|
2451
|
-
throw new
|
|
3840
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
2452
3841
|
}
|
|
2453
3842
|
const data = await handleSourceContext(request, options);
|
|
2454
|
-
|
|
3843
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2455
3844
|
return;
|
|
2456
3845
|
}
|
|
2457
|
-
if (
|
|
3846
|
+
if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.openEditor) {
|
|
2458
3847
|
if (request.method !== "POST") {
|
|
2459
|
-
throw new
|
|
3848
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
2460
3849
|
}
|
|
2461
3850
|
const data = await handleOpenEditor(request, options);
|
|
2462
|
-
|
|
3851
|
+
writeJson2(response, 200, { ok: true, data });
|
|
3852
|
+
return;
|
|
3853
|
+
}
|
|
3854
|
+
if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.dataFlowComponentReport) {
|
|
3855
|
+
if (request.method !== "POST") {
|
|
3856
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
3857
|
+
}
|
|
3858
|
+
const data = await handleComponentDataFlowReport(
|
|
3859
|
+
request,
|
|
3860
|
+
dataFlowAnalyzer,
|
|
3861
|
+
options
|
|
3862
|
+
);
|
|
3863
|
+
writeJson2(response, 200, { ok: true, data });
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.dataFlowPageReport) {
|
|
3867
|
+
if (request.method !== "POST") {
|
|
3868
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
3869
|
+
}
|
|
3870
|
+
const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
|
|
3871
|
+
writeJson2(response, 200, { ok: true, data });
|
|
3872
|
+
return;
|
|
3873
|
+
}
|
|
3874
|
+
if (externalHandoffRoute !== void 0) {
|
|
3875
|
+
await handleExternalHandoffBrowserRequest(
|
|
3876
|
+
request,
|
|
3877
|
+
response,
|
|
3878
|
+
externalHandoffRoute,
|
|
3879
|
+
{
|
|
3880
|
+
options: options.options,
|
|
3881
|
+
registry: options.registry,
|
|
3882
|
+
root: options.root,
|
|
3883
|
+
...options.externalHandoffService === void 0 ? {} : { service: options.externalHandoffService }
|
|
3884
|
+
},
|
|
3885
|
+
(target, status, data) => {
|
|
3886
|
+
writeJson2(target, status, { ok: true, data });
|
|
3887
|
+
}
|
|
3888
|
+
);
|
|
2463
3889
|
return;
|
|
2464
3890
|
}
|
|
2465
3891
|
if (agentRoute === void 0) {
|
|
2466
|
-
throw new
|
|
3892
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
2467
3893
|
}
|
|
2468
3894
|
await handleAgentRequest(
|
|
2469
3895
|
request,
|
|
@@ -2471,7 +3897,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
2471
3897
|
options,
|
|
2472
3898
|
agentRoute,
|
|
2473
3899
|
(target, status, data) => {
|
|
2474
|
-
|
|
3900
|
+
writeJson2(target, status, { ok: true, data });
|
|
2475
3901
|
}
|
|
2476
3902
|
);
|
|
2477
3903
|
};
|
|
@@ -2482,9 +3908,9 @@ function createSpotPatchMiddleware(options) {
|
|
|
2482
3908
|
}
|
|
2483
3909
|
|
|
2484
3910
|
// src/server/source-registration.ts
|
|
2485
|
-
var
|
|
2486
|
-
var
|
|
2487
|
-
var
|
|
3911
|
+
var import_node_crypto11 = require("crypto");
|
|
3912
|
+
var import_promises7 = require("fs/promises");
|
|
3913
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2488
3914
|
var import_compiler = require("@spotpatch/compiler");
|
|
2489
3915
|
var import_zod2 = require("zod");
|
|
2490
3916
|
var REGISTRATION_BODY_LIMIT_BYTES = 4096;
|
|
@@ -2505,16 +3931,16 @@ function identitiesMatch(actual, expected) {
|
|
|
2505
3931
|
}
|
|
2506
3932
|
const actualBytes = Buffer.from(actual);
|
|
2507
3933
|
const expectedBytes = Buffer.from(expected);
|
|
2508
|
-
return actualBytes.byteLength === expectedBytes.byteLength && (0,
|
|
3934
|
+
return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto11.timingSafeEqual)(actualBytes, expectedBytes);
|
|
2509
3935
|
}
|
|
2510
3936
|
function isWithinRoot(root, candidate) {
|
|
2511
|
-
const relative =
|
|
2512
|
-
return relative === "" || !relative.startsWith(`..${
|
|
3937
|
+
const relative = import_node_path8.default.relative(root, candidate);
|
|
3938
|
+
return relative === "" || !relative.startsWith(`..${import_node_path8.default.sep}`) && relative !== ".." && !import_node_path8.default.isAbsolute(relative);
|
|
2513
3939
|
}
|
|
2514
3940
|
function hasForbiddenSegment(root, candidate) {
|
|
2515
|
-
return
|
|
3941
|
+
return import_node_path8.default.relative(root, candidate).split(import_node_path8.default.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
|
|
2516
3942
|
}
|
|
2517
|
-
function
|
|
3943
|
+
function writeJson3(response, statusCode, payload) {
|
|
2518
3944
|
const body = JSON.stringify(payload);
|
|
2519
3945
|
response.statusCode = statusCode;
|
|
2520
3946
|
response.setHeader("Cache-Control", "no-store");
|
|
@@ -2534,15 +3960,15 @@ function requestComesFromLoopbackWorker(request) {
|
|
|
2534
3960
|
}
|
|
2535
3961
|
}
|
|
2536
3962
|
async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
|
|
2537
|
-
if (!
|
|
3963
|
+
if (!import_node_path8.default.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
2538
3964
|
return void 0;
|
|
2539
3965
|
}
|
|
2540
3966
|
try {
|
|
2541
|
-
const sourceStat = await (0,
|
|
3967
|
+
const sourceStat = await (0, import_promises7.lstat)(requestedPath);
|
|
2542
3968
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
2543
3969
|
return void 0;
|
|
2544
3970
|
}
|
|
2545
|
-
const resolvedPath = await (0,
|
|
3971
|
+
const resolvedPath = await (0, import_promises7.realpath)(requestedPath);
|
|
2546
3972
|
if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
|
|
2547
3973
|
return void 0;
|
|
2548
3974
|
}
|
|
@@ -2555,7 +3981,7 @@ async function createSourceRegistrationService(input) {
|
|
|
2555
3981
|
if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
|
|
2556
3982
|
throw new TypeError("The source registration identity is invalid.");
|
|
2557
3983
|
}
|
|
2558
|
-
const root = await (0,
|
|
3984
|
+
const root = await (0, import_promises7.realpath)(input.root);
|
|
2559
3985
|
const sourceFilter = (0, import_compiler.createSourceFilter)(root, input.options);
|
|
2560
3986
|
const handler = (request, response) => {
|
|
2561
3987
|
const handle = async () => {
|
|
@@ -2564,14 +3990,14 @@ async function createSourceRegistrationService(input) {
|
|
|
2564
3990
|
getSingleHeader3(request, INTERNAL_SECRET_HEADER),
|
|
2565
3991
|
input.internalSecret
|
|
2566
3992
|
)) {
|
|
2567
|
-
|
|
3993
|
+
writeJson3(response, 403, { ok: false });
|
|
2568
3994
|
return;
|
|
2569
3995
|
}
|
|
2570
3996
|
const parsed = registrationRequestSchema.safeParse(
|
|
2571
3997
|
await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
|
|
2572
3998
|
);
|
|
2573
3999
|
if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
|
|
2574
|
-
|
|
4000
|
+
writeJson3(response, 400, { ok: false });
|
|
2575
4001
|
return;
|
|
2576
4002
|
}
|
|
2577
4003
|
const sourcePath = await resolveAuthorizedSource(
|
|
@@ -2580,17 +4006,17 @@ async function createSourceRegistrationService(input) {
|
|
|
2580
4006
|
(absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
|
|
2581
4007
|
);
|
|
2582
4008
|
if (sourcePath === void 0) {
|
|
2583
|
-
|
|
4009
|
+
writeJson3(response, 403, { ok: false });
|
|
2584
4010
|
return;
|
|
2585
4011
|
}
|
|
2586
|
-
|
|
4012
|
+
writeJson3(response, 200, {
|
|
2587
4013
|
epoch: input.registryEpoch,
|
|
2588
4014
|
fileId: input.registry.register(sourcePath)
|
|
2589
4015
|
});
|
|
2590
4016
|
};
|
|
2591
4017
|
void handle().catch(() => {
|
|
2592
4018
|
if (!response.headersSent) {
|
|
2593
|
-
|
|
4019
|
+
writeJson3(response, 400, { ok: false });
|
|
2594
4020
|
} else {
|
|
2595
4021
|
response.destroy();
|
|
2596
4022
|
}
|
|
@@ -2600,11 +4026,11 @@ async function createSourceRegistrationService(input) {
|
|
|
2600
4026
|
}
|
|
2601
4027
|
|
|
2602
4028
|
// src/session/session.ts
|
|
2603
|
-
var
|
|
4029
|
+
var import_node_crypto12 = require("crypto");
|
|
2604
4030
|
function createSession() {
|
|
2605
4031
|
return Object.freeze({
|
|
2606
|
-
id: (0,
|
|
2607
|
-
token: (0,
|
|
4032
|
+
id: (0, import_node_crypto12.randomBytes)(16).toString("base64url"),
|
|
4033
|
+
token: (0, import_node_crypto12.randomBytes)(16).toString("base64url")
|
|
2608
4034
|
});
|
|
2609
4035
|
}
|
|
2610
4036
|
|
|
@@ -2614,8 +4040,10 @@ var OPTION_KEYS = Object.freeze([
|
|
|
2614
4040
|
"allowLan",
|
|
2615
4041
|
"budget",
|
|
2616
4042
|
"debug",
|
|
4043
|
+
"dataFlow",
|
|
2617
4044
|
"editor",
|
|
2618
4045
|
"enabled",
|
|
4046
|
+
"externalAgent",
|
|
2619
4047
|
"exclude",
|
|
2620
4048
|
"include",
|
|
2621
4049
|
"locale",
|
|
@@ -2701,8 +4129,12 @@ function serializeResolvedSpotPatchOptions(options) {
|
|
|
2701
4129
|
allowLan: options.allowLan,
|
|
2702
4130
|
budget: options.budget,
|
|
2703
4131
|
debug: options.debug,
|
|
4132
|
+
dataFlow: options.dataFlow.enabled ? Object.freeze({
|
|
4133
|
+
runtime: options.dataFlow.runtime
|
|
4134
|
+
}) : false,
|
|
2704
4135
|
editor: options.editor,
|
|
2705
4136
|
enabled: options.enabled,
|
|
4137
|
+
externalAgent: options.externalAgent.enabled,
|
|
2706
4138
|
exclude: Object.freeze(options.exclude.map(serializeFilter)),
|
|
2707
4139
|
include: Object.freeze(options.include.map(serializeFilter)),
|
|
2708
4140
|
locale: options.locale,
|
|
@@ -2743,11 +4175,20 @@ function parseBudget(value) {
|
|
|
2743
4175
|
);
|
|
2744
4176
|
return Object.freeze(budget);
|
|
2745
4177
|
}
|
|
4178
|
+
function parseDataFlow(value) {
|
|
4179
|
+
if (value === false) return false;
|
|
4180
|
+
if (!isRecord2(value) || !hasExactKeys(value, ["runtime"]) || value.runtime !== "dispatch") {
|
|
4181
|
+
throw new TypeError("The SpotPatch data-flow transport is invalid.");
|
|
4182
|
+
}
|
|
4183
|
+
return Object.freeze({
|
|
4184
|
+
runtime: value.runtime
|
|
4185
|
+
});
|
|
4186
|
+
}
|
|
2746
4187
|
function parseSerializedSpotPatchOptions(value) {
|
|
2747
4188
|
if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
|
|
2748
4189
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
2749
4190
|
}
|
|
2750
|
-
if (typeof value.enabled !== "boolean" || typeof value.redact !== "boolean" || typeof value.allowLan !== "boolean" || typeof value.debug !== "boolean" || typeof value.shortcut !== "string" || typeof value.maxTargets !== "number" || typeof value.editor !== "string" || typeof value.locale !== "string" || value.ai !== false && !isRecord2(value.ai)) {
|
|
4191
|
+
if (typeof value.enabled !== "boolean" || typeof value.externalAgent !== "boolean" || typeof value.redact !== "boolean" || typeof value.allowLan !== "boolean" || typeof value.debug !== "boolean" || typeof value.shortcut !== "string" || typeof value.maxTargets !== "number" || typeof value.editor !== "string" || typeof value.locale !== "string" || value.ai !== false && !isRecord2(value.ai)) {
|
|
2751
4192
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
2752
4193
|
}
|
|
2753
4194
|
try {
|
|
@@ -2756,8 +4197,10 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
2756
4197
|
allowLan: value.allowLan,
|
|
2757
4198
|
budget: parseBudget(value.budget),
|
|
2758
4199
|
debug: value.debug,
|
|
4200
|
+
dataFlow: parseDataFlow(value.dataFlow),
|
|
2759
4201
|
editor: value.editor,
|
|
2760
4202
|
enabled: value.enabled,
|
|
4203
|
+
externalAgent: value.externalAgent,
|
|
2761
4204
|
exclude: parseFilterList(value.exclude),
|
|
2762
4205
|
include: parseFilterList(value.include),
|
|
2763
4206
|
locale: value.locale,
|
|
@@ -2777,8 +4220,10 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
2777
4220
|
DEFAULT_OPTIONS,
|
|
2778
4221
|
applyIntegrationPlan,
|
|
2779
4222
|
createAgentJobManager,
|
|
4223
|
+
createExternalHandoffService,
|
|
2780
4224
|
createIntegrationFileChange,
|
|
2781
4225
|
createRuntimeAiConfig,
|
|
4226
|
+
createRuntimeDataFlowConfig,
|
|
2782
4227
|
createSession,
|
|
2783
4228
|
createSourceRegistrationService,
|
|
2784
4229
|
createSourceRegistry,
|