@spotpatch/dev-server 0.5.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 +1524 -350
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +1529 -306
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,7 @@ __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,
|
|
39
40
|
createRuntimeDataFlowConfig: () => createRuntimeDataFlowConfig,
|
|
@@ -521,6 +522,1064 @@ function createAgentJobManager(options) {
|
|
|
521
522
|
});
|
|
522
523
|
}
|
|
523
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
|
+
|
|
524
1583
|
// src/environment-ai.ts
|
|
525
1584
|
var AI_ENVIRONMENT_NAMES = Object.freeze({
|
|
526
1585
|
authentication: "SPOTPATCH_AI_AUTHENTICATION",
|
|
@@ -611,46 +1670,46 @@ function resolveEnvironmentAiConfiguration(environment) {
|
|
|
611
1670
|
}
|
|
612
1671
|
|
|
613
1672
|
// src/integration/file-plan.ts
|
|
614
|
-
var
|
|
615
|
-
var
|
|
616
|
-
var
|
|
1673
|
+
var import_node_crypto7 = require("crypto");
|
|
1674
|
+
var import_promises2 = require("fs/promises");
|
|
1675
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
617
1676
|
function isMissingPathError(error) {
|
|
618
1677
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
619
1678
|
}
|
|
620
1679
|
function isPathWithin(root, target) {
|
|
621
|
-
const relative =
|
|
622
|
-
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);
|
|
623
1682
|
}
|
|
624
1683
|
function relativePathWithin(root, target) {
|
|
625
|
-
const relative =
|
|
1684
|
+
const relative = import_node_path2.default.relative(root, target);
|
|
626
1685
|
if (relative.length === 0 || !isPathWithin(root, target)) {
|
|
627
1686
|
throw new Error("SpotPatch init refuses to modify a path outside the app root.");
|
|
628
1687
|
}
|
|
629
|
-
return relative.split(
|
|
1688
|
+
return relative.split(import_node_path2.default.sep).join("/");
|
|
630
1689
|
}
|
|
631
1690
|
async function integrationPathExists(absolutePath) {
|
|
632
1691
|
try {
|
|
633
|
-
await (0,
|
|
1692
|
+
await (0, import_promises2.access)(absolutePath);
|
|
634
1693
|
return true;
|
|
635
1694
|
} catch {
|
|
636
1695
|
return false;
|
|
637
1696
|
}
|
|
638
1697
|
}
|
|
639
1698
|
async function readIntegrationFile(absolutePath) {
|
|
640
|
-
const metadata = await (0,
|
|
1699
|
+
const metadata = await (0, import_promises2.lstat)(absolutePath);
|
|
641
1700
|
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
642
1701
|
throw new Error(
|
|
643
|
-
`SpotPatch refuses to modify the non-regular file ${
|
|
1702
|
+
`SpotPatch refuses to modify the non-regular file ${import_node_path2.default.basename(absolutePath)}.`
|
|
644
1703
|
);
|
|
645
1704
|
}
|
|
646
|
-
return (0,
|
|
1705
|
+
return (0, import_promises2.readFile)(absolutePath, "utf8");
|
|
647
1706
|
}
|
|
648
1707
|
function createIntegrationFileChange(appRoot, absolutePath, nextContent, previousContent) {
|
|
649
1708
|
if (previousContent === nextContent) {
|
|
650
1709
|
return void 0;
|
|
651
1710
|
}
|
|
652
|
-
const root =
|
|
653
|
-
const target =
|
|
1711
|
+
const root = import_node_path2.default.resolve(appRoot);
|
|
1712
|
+
const target = import_node_path2.default.resolve(absolutePath);
|
|
654
1713
|
return Object.freeze({
|
|
655
1714
|
absolutePath: target,
|
|
656
1715
|
nextContent,
|
|
@@ -659,19 +1718,19 @@ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previou
|
|
|
659
1718
|
});
|
|
660
1719
|
}
|
|
661
1720
|
function temporaryPath(absolutePath, label) {
|
|
662
|
-
return
|
|
663
|
-
|
|
664
|
-
`.${
|
|
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")}`
|
|
665
1724
|
);
|
|
666
1725
|
}
|
|
667
1726
|
async function writeAtomic(absolutePath, content, mode) {
|
|
668
|
-
await (0,
|
|
1727
|
+
await (0, import_promises2.mkdir)(import_node_path2.default.dirname(absolutePath), { recursive: true });
|
|
669
1728
|
const stagedPath = temporaryPath(absolutePath, "stage");
|
|
670
1729
|
try {
|
|
671
|
-
await (0,
|
|
672
|
-
await (0,
|
|
1730
|
+
await (0, import_promises2.writeFile)(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
|
|
1731
|
+
await (0, import_promises2.rename)(stagedPath, absolutePath);
|
|
673
1732
|
} catch (error) {
|
|
674
|
-
await (0,
|
|
1733
|
+
await (0, import_promises2.unlink)(stagedPath).catch(() => void 0);
|
|
675
1734
|
throw error;
|
|
676
1735
|
}
|
|
677
1736
|
}
|
|
@@ -683,21 +1742,21 @@ async function rollbackChange(change) {
|
|
|
683
1742
|
);
|
|
684
1743
|
}
|
|
685
1744
|
if (change.previousContent === void 0) {
|
|
686
|
-
await (0,
|
|
1745
|
+
await (0, import_promises2.unlink)(change.absolutePath);
|
|
687
1746
|
return;
|
|
688
1747
|
}
|
|
689
|
-
const mode = (await (0,
|
|
1748
|
+
const mode = (await (0, import_promises2.stat)(change.absolutePath)).mode & 511;
|
|
690
1749
|
await writeAtomic(change.absolutePath, change.previousContent, mode);
|
|
691
1750
|
}
|
|
692
1751
|
async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
693
|
-
const target =
|
|
1752
|
+
const target = import_node_path2.default.resolve(change.absolutePath);
|
|
694
1753
|
const relativePath = relativePathWithin(appRoot, target);
|
|
695
|
-
if (target !== change.absolutePath || relativePath !== change.relativePath ||
|
|
1754
|
+
if (target !== change.absolutePath || relativePath !== change.relativePath || import_node_path2.default.dirname(target) === target) {
|
|
696
1755
|
throw new Error("SpotPatch init received an invalid integration file plan.");
|
|
697
1756
|
}
|
|
698
1757
|
let targetMetadata;
|
|
699
1758
|
try {
|
|
700
|
-
targetMetadata = await (0,
|
|
1759
|
+
targetMetadata = await (0, import_promises2.lstat)(target);
|
|
701
1760
|
} catch (error) {
|
|
702
1761
|
if (!isMissingPathError(error)) {
|
|
703
1762
|
throw error;
|
|
@@ -708,8 +1767,8 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
|
708
1767
|
`SpotPatch refuses to modify the symbolic link ${change.relativePath}.`
|
|
709
1768
|
);
|
|
710
1769
|
}
|
|
711
|
-
const containmentAnchor = await (0,
|
|
712
|
-
targetMetadata === void 0 ?
|
|
1770
|
+
const containmentAnchor = await (0, import_promises2.realpath)(
|
|
1771
|
+
targetMetadata === void 0 ? import_node_path2.default.dirname(target) : target
|
|
713
1772
|
);
|
|
714
1773
|
if (!isPathWithin(realAppRoot, containmentAnchor)) {
|
|
715
1774
|
throw new Error("SpotPatch init refuses to modify a path outside the app root.");
|
|
@@ -718,7 +1777,7 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
|
718
1777
|
async function assertCurrentBaseline(change) {
|
|
719
1778
|
if (change.previousContent === void 0) {
|
|
720
1779
|
try {
|
|
721
|
-
await (0,
|
|
1780
|
+
await (0, import_promises2.lstat)(change.absolutePath);
|
|
722
1781
|
} catch (error) {
|
|
723
1782
|
if (isMissingPathError(error)) {
|
|
724
1783
|
return;
|
|
@@ -740,8 +1799,8 @@ async function applyIntegrationPlan(plan) {
|
|
|
740
1799
|
if (plan.changes.length === 0) {
|
|
741
1800
|
return;
|
|
742
1801
|
}
|
|
743
|
-
const appRoot =
|
|
744
|
-
const realAppRoot = await (0,
|
|
1802
|
+
const appRoot = import_node_path2.default.resolve(plan.appRoot);
|
|
1803
|
+
const realAppRoot = await (0, import_promises2.realpath)(appRoot);
|
|
745
1804
|
const targets = /* @__PURE__ */ new Set();
|
|
746
1805
|
for (const change of plan.changes) {
|
|
747
1806
|
if (targets.has(change.absolutePath)) {
|
|
@@ -755,7 +1814,7 @@ async function applyIntegrationPlan(plan) {
|
|
|
755
1814
|
try {
|
|
756
1815
|
for (const change of plan.changes) {
|
|
757
1816
|
await assertCurrentBaseline(change);
|
|
758
|
-
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;
|
|
759
1818
|
await writeAtomic(change.absolutePath, change.nextContent, mode);
|
|
760
1819
|
applied.push(change);
|
|
761
1820
|
}
|
|
@@ -776,7 +1835,7 @@ async function applyIntegrationPlan(plan) {
|
|
|
776
1835
|
}
|
|
777
1836
|
|
|
778
1837
|
// src/options.ts
|
|
779
|
-
var
|
|
1838
|
+
var import_shared8 = require("@spotpatch/shared");
|
|
780
1839
|
var import_zod = require("zod");
|
|
781
1840
|
var DEFAULT_EXCLUDE = Object.freeze([
|
|
782
1841
|
/node_modules/,
|
|
@@ -811,8 +1870,9 @@ var DEFAULT_OPTIONS = Object.freeze({
|
|
|
811
1870
|
dataFlow: Object.freeze({
|
|
812
1871
|
enabled: false,
|
|
813
1872
|
runtime: "dispatch",
|
|
814
|
-
limits:
|
|
815
|
-
})
|
|
1873
|
+
limits: import_shared8.DEFAULT_DATA_FLOW_LIMITS
|
|
1874
|
+
}),
|
|
1875
|
+
externalAgent: Object.freeze({ enabled: false })
|
|
816
1876
|
});
|
|
817
1877
|
var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
818
1878
|
var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
@@ -857,7 +1917,7 @@ var aiOptionsSchema = import_zod.z.strictObject({
|
|
|
857
1917
|
defaultProvider: import_zod.z.string(),
|
|
858
1918
|
execution: import_zod.z.strictObject({
|
|
859
1919
|
isolation: import_zod.z.literal("git-worktree").optional(),
|
|
860
|
-
applyMode: import_zod.z.enum(
|
|
1920
|
+
applyMode: import_zod.z.enum(import_shared8.AGENT_APPLY_MODES).optional(),
|
|
861
1921
|
checks: import_zod.z.record(import_zod.z.string(), agentCheckSchema).optional(),
|
|
862
1922
|
limits: agentLimitsSchema
|
|
863
1923
|
}).optional()
|
|
@@ -903,18 +1963,18 @@ function normalizeProviderBaseURL(value) {
|
|
|
903
1963
|
}
|
|
904
1964
|
function resolveLimits(limits) {
|
|
905
1965
|
const resolved = Object.freeze({
|
|
906
|
-
maxTurns: limits?.maxTurns ??
|
|
907
|
-
maxToolCalls: limits?.maxToolCalls ??
|
|
908
|
-
maxChangedFiles: limits?.maxChangedFiles ??
|
|
909
|
-
maxDiffBytes: limits?.maxDiffBytes ??
|
|
910
|
-
maxReadBytesPerFile: limits?.maxReadBytesPerFile ??
|
|
911
|
-
maxToolOutputCharacters: limits?.maxToolOutputCharacters ??
|
|
912
|
-
maxProviderResponseBytes: limits?.maxProviderResponseBytes ??
|
|
913
|
-
providerConnectTimeoutMs: limits?.providerConnectTimeoutMs ??
|
|
914
|
-
providerFirstByteTimeoutMs: limits?.providerFirstByteTimeoutMs ??
|
|
915
|
-
providerIdleTimeoutMs: limits?.providerIdleTimeoutMs ??
|
|
916
|
-
checkTimeoutMs: limits?.checkTimeoutMs ??
|
|
917
|
-
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
|
|
918
1978
|
});
|
|
919
1979
|
for (const [name, value] of Object.entries(resolved)) {
|
|
920
1980
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
@@ -1119,7 +2179,7 @@ function resolveDataFlowOptions(options) {
|
|
|
1119
2179
|
return Object.freeze({
|
|
1120
2180
|
enabled: true,
|
|
1121
2181
|
runtime,
|
|
1122
|
-
limits:
|
|
2182
|
+
limits: import_shared8.DEFAULT_DATA_FLOW_LIMITS
|
|
1123
2183
|
});
|
|
1124
2184
|
}
|
|
1125
2185
|
function createRuntimeDataFlowConfig(options) {
|
|
@@ -1138,6 +2198,9 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1138
2198
|
if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
|
|
1139
2199
|
throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
|
|
1140
2200
|
}
|
|
2201
|
+
if (options.externalAgent !== void 0 && typeof options.externalAgent !== "boolean") {
|
|
2202
|
+
throw new RangeError("SpotPatch externalAgent must be a boolean.");
|
|
2203
|
+
}
|
|
1141
2204
|
const budget = Object.freeze({
|
|
1142
2205
|
...DEFAULT_OPTIONS.budget,
|
|
1143
2206
|
...options.budget
|
|
@@ -1146,15 +2209,15 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1146
2209
|
const maxTargets = options.maxTargets ?? DEFAULT_OPTIONS.maxTargets;
|
|
1147
2210
|
const locale = options.locale ?? DEFAULT_OPTIONS.locale;
|
|
1148
2211
|
const editor = options.editor ?? DEFAULT_OPTIONS.editor;
|
|
1149
|
-
if (!
|
|
2212
|
+
if (!import_shared8.SPOTPATCH_LOCALE_PREFERENCES.includes(locale)) {
|
|
1150
2213
|
throw new RangeError("SpotPatch locale must be auto, en-US, or zh-CN.");
|
|
1151
2214
|
}
|
|
1152
|
-
if (!
|
|
2215
|
+
if (!import_shared8.SPOTPATCH_EDITOR_PREFERENCES.includes(editor)) {
|
|
1153
2216
|
throw new RangeError("SpotPatch editor must be auto, vscode, or cursor.");
|
|
1154
2217
|
}
|
|
1155
|
-
if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets >
|
|
2218
|
+
if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets > import_shared8.MAX_ANNOTATION_TARGETS) {
|
|
1156
2219
|
throw new RangeError(
|
|
1157
|
-
`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)}.`
|
|
1158
2221
|
);
|
|
1159
2222
|
}
|
|
1160
2223
|
const resolved = {
|
|
@@ -1170,7 +2233,10 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1170
2233
|
locale,
|
|
1171
2234
|
maxTargets,
|
|
1172
2235
|
ai: resolveAiOptions(options.ai ?? environmentAi),
|
|
1173
|
-
dataFlow: resolveDataFlowOptions(options.dataFlow)
|
|
2236
|
+
dataFlow: resolveDataFlowOptions(options.dataFlow),
|
|
2237
|
+
externalAgent: Object.freeze({
|
|
2238
|
+
enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
|
|
2239
|
+
})
|
|
1174
2240
|
};
|
|
1175
2241
|
if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
|
|
1176
2242
|
throw new RangeError("SpotPatch shortcut is invalid.");
|
|
@@ -1180,9 +2246,9 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1180
2246
|
|
|
1181
2247
|
// src/project-validation.ts
|
|
1182
2248
|
var import_node_child_process = require("child_process");
|
|
1183
|
-
var
|
|
2249
|
+
var import_promises3 = require("fs/promises");
|
|
1184
2250
|
var import_node_module = require("module");
|
|
1185
|
-
var
|
|
2251
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
1186
2252
|
var import_node_util = require("util");
|
|
1187
2253
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
1188
2254
|
var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
|
|
@@ -1192,19 +2258,19 @@ function isRecord(value) {
|
|
|
1192
2258
|
}
|
|
1193
2259
|
async function isRegularFile(absolutePath) {
|
|
1194
2260
|
try {
|
|
1195
|
-
const metadata = await (0,
|
|
2261
|
+
const metadata = await (0, import_promises3.lstat)(absolutePath);
|
|
1196
2262
|
return metadata.isFile() && !metadata.isSymbolicLink();
|
|
1197
2263
|
} catch {
|
|
1198
2264
|
return false;
|
|
1199
2265
|
}
|
|
1200
2266
|
}
|
|
1201
2267
|
async function readManifest(appRoot) {
|
|
1202
|
-
const manifestPath =
|
|
2268
|
+
const manifestPath = import_node_path3.default.join(appRoot, "package.json");
|
|
1203
2269
|
if (!await isRegularFile(manifestPath)) {
|
|
1204
2270
|
return void 0;
|
|
1205
2271
|
}
|
|
1206
2272
|
try {
|
|
1207
|
-
const value = JSON.parse(await (0,
|
|
2273
|
+
const value = JSON.parse(await (0, import_promises3.readFile)(manifestPath, "utf8"));
|
|
1208
2274
|
return isRecord(value) ? value : void 0;
|
|
1209
2275
|
} catch {
|
|
1210
2276
|
return void 0;
|
|
@@ -1227,9 +2293,9 @@ async function findGitRoot(appRoot) {
|
|
|
1227
2293
|
timeout: 5e3,
|
|
1228
2294
|
windowsHide: true
|
|
1229
2295
|
});
|
|
1230
|
-
const root = await (0,
|
|
1231
|
-
const relative =
|
|
1232
|
-
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)) {
|
|
1233
2299
|
return root;
|
|
1234
2300
|
}
|
|
1235
2301
|
} catch {
|
|
@@ -1238,22 +2304,22 @@ async function findGitRoot(appRoot) {
|
|
|
1238
2304
|
return void 0;
|
|
1239
2305
|
}
|
|
1240
2306
|
async function resolveTypeScriptCli(appRoot) {
|
|
1241
|
-
const resolveFromApplication = (0, import_node_module.createRequire)(
|
|
2307
|
+
const resolveFromApplication = (0, import_node_module.createRequire)(import_node_path3.default.join(appRoot, "package.json"));
|
|
1242
2308
|
try {
|
|
1243
2309
|
const packagePath = resolveFromApplication.resolve("typescript/package.json");
|
|
1244
|
-
const cliPath =
|
|
1245
|
-
await (0,
|
|
1246
|
-
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);
|
|
1247
2313
|
} catch {
|
|
1248
2314
|
return void 0;
|
|
1249
2315
|
}
|
|
1250
2316
|
}
|
|
1251
2317
|
function portableRelativePath(from, to) {
|
|
1252
|
-
return
|
|
2318
|
+
return import_node_path3.default.relative(from, to).split(import_node_path3.default.sep).join("/");
|
|
1253
2319
|
}
|
|
1254
2320
|
async function discoverProjectValidationCheck(options) {
|
|
1255
|
-
const appRoot = await (0,
|
|
1256
|
-
const tsconfigPath =
|
|
2321
|
+
const appRoot = await (0, import_promises3.realpath)(options.appRoot);
|
|
2322
|
+
const tsconfigPath = import_node_path3.default.join(appRoot, "tsconfig.json");
|
|
1257
2323
|
const [manifest, projectRoot, hasTsconfig] = await Promise.all([
|
|
1258
2324
|
readManifest(appRoot),
|
|
1259
2325
|
findGitRoot(appRoot),
|
|
@@ -1341,16 +2407,16 @@ async function resolveProjectOptions(input) {
|
|
|
1341
2407
|
}
|
|
1342
2408
|
|
|
1343
2409
|
// src/registry/source-registry.ts
|
|
1344
|
-
var
|
|
2410
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
1345
2411
|
|
|
1346
2412
|
// src/registry/source-id.ts
|
|
1347
|
-
var
|
|
2413
|
+
var import_node_crypto8 = require("crypto");
|
|
1348
2414
|
var SOURCE_ID_BYTES = 8;
|
|
1349
|
-
var createRandomSourceId = () => (0,
|
|
2415
|
+
var createRandomSourceId = () => (0, import_node_crypto8.randomBytes)(SOURCE_ID_BYTES).toString("base64url");
|
|
1350
2416
|
|
|
1351
2417
|
// src/registry/source-registry.ts
|
|
1352
2418
|
function normalizeAbsolutePath(absolutePath) {
|
|
1353
|
-
return
|
|
2419
|
+
return import_node_path4.default.normalize(import_node_path4.default.resolve(absolutePath));
|
|
1354
2420
|
}
|
|
1355
2421
|
function createSourceRegistry(options = {}) {
|
|
1356
2422
|
const createId = options.createId ?? createRandomSourceId;
|
|
@@ -1407,20 +2473,20 @@ function createSourceRegistry(options = {}) {
|
|
|
1407
2473
|
}
|
|
1408
2474
|
|
|
1409
2475
|
// src/server/middleware.ts
|
|
1410
|
-
var
|
|
2476
|
+
var import_shared18 = require("@spotpatch/shared");
|
|
1411
2477
|
|
|
1412
|
-
// src/
|
|
1413
|
-
var
|
|
2478
|
+
// src/external-handoff/browser-http.ts
|
|
2479
|
+
var import_shared12 = require("@spotpatch/shared");
|
|
1414
2480
|
|
|
1415
|
-
// src/server/
|
|
1416
|
-
var
|
|
1417
|
-
var
|
|
1418
|
-
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");
|
|
1419
2485
|
|
|
1420
2486
|
// src/server/source-context.ts
|
|
1421
|
-
var
|
|
1422
|
-
var
|
|
1423
|
-
var
|
|
2487
|
+
var import_promises5 = require("fs/promises");
|
|
2488
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
2489
|
+
var import_shared10 = require("@spotpatch/shared");
|
|
1424
2490
|
|
|
1425
2491
|
// src/server/extract-code-context.ts
|
|
1426
2492
|
var import_oxc_parser = require("oxc-parser");
|
|
@@ -1649,16 +2715,9 @@ function extractCodeContext(options) {
|
|
|
1649
2715
|
}
|
|
1650
2716
|
|
|
1651
2717
|
// src/server/source-file.ts
|
|
1652
|
-
var
|
|
1653
|
-
var
|
|
1654
|
-
var
|
|
1655
|
-
|
|
1656
|
-
// src/server/constants.ts
|
|
1657
|
-
var MAX_REQUEST_BODY_BYTES = 32 * 1024;
|
|
1658
|
-
var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
|
|
1659
|
-
var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
|
1660
|
-
|
|
1661
|
-
// 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");
|
|
1662
2721
|
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
1663
2722
|
function isMissingFileError(error) {
|
|
1664
2723
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
@@ -1668,56 +2727,56 @@ async function assertInsideRoot(root, candidate) {
|
|
|
1668
2727
|
let realCandidate;
|
|
1669
2728
|
try {
|
|
1670
2729
|
[realRoot, realCandidate] = await Promise.all([
|
|
1671
|
-
(0,
|
|
1672
|
-
(0,
|
|
2730
|
+
(0, import_promises4.realpath)(root),
|
|
2731
|
+
(0, import_promises4.realpath)(candidate)
|
|
1673
2732
|
]);
|
|
1674
2733
|
} catch (error) {
|
|
1675
2734
|
if (isMissingFileError(error)) {
|
|
1676
|
-
throw new
|
|
2735
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
1677
2736
|
cause: error
|
|
1678
2737
|
});
|
|
1679
2738
|
}
|
|
1680
2739
|
throw error;
|
|
1681
2740
|
}
|
|
1682
|
-
const relative =
|
|
1683
|
-
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);
|
|
1684
2743
|
if (outside) {
|
|
1685
|
-
throw new
|
|
2744
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_OUTSIDE_ROOT);
|
|
1686
2745
|
}
|
|
1687
2746
|
return realCandidate;
|
|
1688
2747
|
}
|
|
1689
2748
|
async function resolveSourceFile(options) {
|
|
1690
2749
|
const registeredPath = options.registry.resolve(options.fileId);
|
|
1691
2750
|
if (registeredPath === void 0) {
|
|
1692
|
-
throw new
|
|
2751
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
1693
2752
|
}
|
|
1694
2753
|
const sourcePath = await assertInsideRoot(options.root, registeredPath);
|
|
1695
|
-
if (!ALLOWED_EXTENSIONS.has(
|
|
1696
|
-
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);
|
|
1697
2756
|
}
|
|
1698
2757
|
let sourceStat;
|
|
1699
2758
|
try {
|
|
1700
|
-
sourceStat = await (0,
|
|
2759
|
+
sourceStat = await (0, import_promises4.stat)(sourcePath);
|
|
1701
2760
|
} catch (error) {
|
|
1702
2761
|
if (isMissingFileError(error)) {
|
|
1703
|
-
throw new
|
|
2762
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
1704
2763
|
cause: error
|
|
1705
2764
|
});
|
|
1706
2765
|
}
|
|
1707
2766
|
throw error;
|
|
1708
2767
|
}
|
|
1709
2768
|
if (!sourceStat.isFile()) {
|
|
1710
|
-
throw new
|
|
2769
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
1711
2770
|
}
|
|
1712
2771
|
if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
|
|
1713
|
-
throw new
|
|
2772
|
+
throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.SOURCE_TOO_LARGE);
|
|
1714
2773
|
}
|
|
1715
2774
|
return sourcePath;
|
|
1716
2775
|
}
|
|
1717
2776
|
|
|
1718
2777
|
// src/server/source-context.ts
|
|
1719
2778
|
function toDisplayPath(root, sourcePath) {
|
|
1720
|
-
return
|
|
2779
|
+
return import_node_path6.default.relative(root, sourcePath).split(import_node_path6.default.sep).join("/");
|
|
1721
2780
|
}
|
|
1722
2781
|
async function readSourceContext(options) {
|
|
1723
2782
|
const sourcePath = await resolveSourceFile({
|
|
@@ -1727,10 +2786,10 @@ async function readSourceContext(options) {
|
|
|
1727
2786
|
});
|
|
1728
2787
|
let source;
|
|
1729
2788
|
try {
|
|
1730
|
-
source = await (0,
|
|
2789
|
+
source = await (0, import_promises5.readFile)(sourcePath, "utf8");
|
|
1731
2790
|
} catch (error) {
|
|
1732
2791
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1733
|
-
throw new
|
|
2792
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
1734
2793
|
cause: error
|
|
1735
2794
|
});
|
|
1736
2795
|
}
|
|
@@ -1738,13 +2797,13 @@ async function readSourceContext(options) {
|
|
|
1738
2797
|
}
|
|
1739
2798
|
const lines = source.split(/\r?\n/);
|
|
1740
2799
|
if (options.request.line > lines.length) {
|
|
1741
|
-
throw new
|
|
2800
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.INVALID_REQUEST);
|
|
1742
2801
|
}
|
|
1743
|
-
const extension =
|
|
2802
|
+
const extension = import_node_path6.default.extname(sourcePath).toLowerCase();
|
|
1744
2803
|
return extractCodeContext({
|
|
1745
2804
|
source,
|
|
1746
2805
|
sourcePath,
|
|
1747
|
-
relativePath: toDisplayPath(await (0,
|
|
2806
|
+
relativePath: toDisplayPath(await (0, import_promises5.realpath)(options.root), sourcePath),
|
|
1748
2807
|
language: extension === ".tsx" ? "tsx" : "jsx",
|
|
1749
2808
|
line: options.request.line,
|
|
1750
2809
|
column: options.request.column,
|
|
@@ -1753,7 +2812,17 @@ async function readSourceContext(options) {
|
|
|
1753
2812
|
});
|
|
1754
2813
|
}
|
|
1755
2814
|
|
|
1756
|
-
// 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
|
+
}
|
|
1757
2826
|
function compactSourceRef(source) {
|
|
1758
2827
|
return Object.freeze({
|
|
1759
2828
|
origin: source.origin,
|
|
@@ -1767,27 +2836,20 @@ function compactSourceRef(source) {
|
|
|
1767
2836
|
async function authorizeSourceRef(source, registry, root) {
|
|
1768
2837
|
const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
|
|
1769
2838
|
if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
|
|
1770
|
-
throw new
|
|
2839
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1771
2840
|
}
|
|
1772
2841
|
if (source.fileId === void 0) {
|
|
1773
2842
|
if (source.origin === "none" && source.relativePath !== void 0) {
|
|
1774
|
-
throw new
|
|
2843
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1775
2844
|
}
|
|
1776
2845
|
return compactSourceRef(source);
|
|
1777
2846
|
}
|
|
1778
|
-
const sourcePath = await resolveSourceFile({
|
|
1779
|
-
|
|
1780
|
-
registry,
|
|
1781
|
-
root
|
|
1782
|
-
});
|
|
1783
|
-
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("/");
|
|
1784
2849
|
if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
|
|
1785
|
-
throw new
|
|
2850
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1786
2851
|
}
|
|
1787
|
-
return Object.freeze({
|
|
1788
|
-
...compactSourceRef(source),
|
|
1789
|
-
relativePath
|
|
1790
|
-
});
|
|
2852
|
+
return Object.freeze({ ...compactSourceRef(source), relativePath });
|
|
1791
2853
|
}
|
|
1792
2854
|
function freezeMatchedRule(rule) {
|
|
1793
2855
|
return Object.freeze({
|
|
@@ -1821,7 +2883,7 @@ async function authorizeTarget(target, input) {
|
|
|
1821
2883
|
maxLines: input.options.budget.maxCodeLines
|
|
1822
2884
|
});
|
|
1823
2885
|
if (marker === void 0 && target.code !== void 0) {
|
|
1824
|
-
throw new
|
|
2886
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1825
2887
|
}
|
|
1826
2888
|
const code = marker === void 0 ? void 0 : await readSourceContext({
|
|
1827
2889
|
request: marker,
|
|
@@ -1831,11 +2893,11 @@ async function authorizeTarget(target, input) {
|
|
|
1831
2893
|
maxLines: input.options.budget.maxCodeLines
|
|
1832
2894
|
});
|
|
1833
2895
|
if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
|
|
1834
|
-
throw new
|
|
2896
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1835
2897
|
}
|
|
1836
2898
|
return Object.freeze({
|
|
1837
2899
|
instruction: target.instruction,
|
|
1838
|
-
...target.page === void 0 ? {} : { page:
|
|
2900
|
+
...target.page === void 0 ? {} : { page: sanitizePageContext(target.page) },
|
|
1839
2901
|
source,
|
|
1840
2902
|
react: Object.freeze({
|
|
1841
2903
|
supported: target.react.supported,
|
|
@@ -1863,76 +2925,130 @@ async function authorizeTarget(target, input) {
|
|
|
1863
2925
|
warnings: Object.freeze([...target.warnings])
|
|
1864
2926
|
});
|
|
1865
2927
|
}
|
|
1866
|
-
async function
|
|
1867
|
-
const requestedTargets = input.
|
|
2928
|
+
async function authorizeAnnotation(input) {
|
|
2929
|
+
const requestedTargets = input.annotation.targets;
|
|
1868
2930
|
if (requestedTargets.length > input.options.maxTargets) {
|
|
1869
|
-
throw new
|
|
2931
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1870
2932
|
}
|
|
1871
2933
|
const identities = requestedTargets.map(targetIdentity);
|
|
1872
2934
|
if (new Set(identities).size !== identities.length) {
|
|
1873
|
-
throw new
|
|
2935
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1874
2936
|
}
|
|
1875
2937
|
const targets = Object.freeze(
|
|
1876
2938
|
await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
|
|
1877
2939
|
);
|
|
1878
|
-
|
|
2940
|
+
return Object.freeze({
|
|
1879
2941
|
schemaVersion: 3,
|
|
1880
|
-
id: input.
|
|
1881
|
-
locale: input.
|
|
1882
|
-
page:
|
|
2942
|
+
id: input.annotation.id,
|
|
2943
|
+
locale: input.annotation.locale,
|
|
2944
|
+
page: sanitizePageContext(input.annotation.page),
|
|
1883
2945
|
targets,
|
|
1884
|
-
createdAt: input.
|
|
1885
|
-
});
|
|
1886
|
-
return Object.freeze({
|
|
1887
|
-
annotation,
|
|
1888
|
-
...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
|
|
1889
|
-
providerProfileId: input.request.providerProfileId,
|
|
1890
|
-
modelProfileId: input.request.modelProfileId,
|
|
1891
|
-
providerDataConsent: true,
|
|
1892
|
-
...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
|
|
1893
|
-
workingTreeMode: input.request.workingTreeMode
|
|
2946
|
+
createdAt: input.annotation.createdAt
|
|
1894
2947
|
});
|
|
1895
2948
|
}
|
|
1896
2949
|
|
|
1897
|
-
// src/
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
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";
|
|
1905
2957
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
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);
|
|
1909
2963
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
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
|
+
});
|
|
1917
2972
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
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
|
+
});
|
|
1923
2977
|
}
|
|
1924
|
-
chunks.push(buffer);
|
|
1925
2978
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
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);
|
|
1928
2984
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
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;
|
|
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);
|
|
1935
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
|
+
});
|
|
1936
3052
|
}
|
|
1937
3053
|
|
|
1938
3054
|
// src/server/agent-http.ts
|
|
@@ -1952,21 +3068,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
|
|
|
1952
3068
|
"reverted",
|
|
1953
3069
|
"failed"
|
|
1954
3070
|
]);
|
|
1955
|
-
function matchAgentRequestPath(
|
|
1956
|
-
if (
|
|
3071
|
+
function matchAgentRequestPath(path9) {
|
|
3072
|
+
if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.agentCapability) {
|
|
1957
3073
|
return Object.freeze({ kind: "capability" });
|
|
1958
3074
|
}
|
|
1959
|
-
if (
|
|
3075
|
+
if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
|
|
1960
3076
|
return Object.freeze({ kind: "workspace-health" });
|
|
1961
3077
|
}
|
|
1962
|
-
if (
|
|
3078
|
+
if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.agentJobs) {
|
|
1963
3079
|
return Object.freeze({ kind: "create-job" });
|
|
1964
3080
|
}
|
|
1965
|
-
const prefix = `${
|
|
1966
|
-
if (!
|
|
3081
|
+
const prefix = `${import_shared14.SPOTPATCH_ENDPOINTS.agentJobs}/`;
|
|
3082
|
+
if (!path9.startsWith(prefix)) {
|
|
1967
3083
|
return void 0;
|
|
1968
3084
|
}
|
|
1969
|
-
const segments =
|
|
3085
|
+
const segments = path9.slice(prefix.length).split("/");
|
|
1970
3086
|
const jobId = segments[0];
|
|
1971
3087
|
const action = segments[1];
|
|
1972
3088
|
if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
|
|
@@ -1980,7 +3096,7 @@ function matchAgentRequestPath(path8) {
|
|
|
1980
3096
|
}
|
|
1981
3097
|
function requireAgentManager(options) {
|
|
1982
3098
|
if (options.agentManager === void 0 || options.options.ai === false) {
|
|
1983
|
-
throw new
|
|
3099
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.AI_DISABLED);
|
|
1984
3100
|
}
|
|
1985
3101
|
return options.agentManager;
|
|
1986
3102
|
}
|
|
@@ -2033,13 +3149,13 @@ function streamAgentJobEvents(response, manager, jobId) {
|
|
|
2033
3149
|
}
|
|
2034
3150
|
async function handleCapability(request, response, options, writeSuccess) {
|
|
2035
3151
|
if (request.method !== "POST") {
|
|
2036
|
-
throw new
|
|
3152
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2037
3153
|
}
|
|
2038
|
-
const parsed =
|
|
3154
|
+
const parsed = import_shared14.agentCapabilityRequestSchema.safeParse(
|
|
2039
3155
|
await readJsonRequestBody(request)
|
|
2040
3156
|
);
|
|
2041
3157
|
if (!parsed.success) {
|
|
2042
|
-
throw new
|
|
3158
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2043
3159
|
}
|
|
2044
3160
|
const controller = new AbortController();
|
|
2045
3161
|
const abort = () => {
|
|
@@ -2058,13 +3174,13 @@ async function handleCapability(request, response, options, writeSuccess) {
|
|
|
2058
3174
|
}
|
|
2059
3175
|
async function handleCreateJob(request, response, options, writeSuccess) {
|
|
2060
3176
|
if (request.method !== "POST") {
|
|
2061
|
-
throw new
|
|
3177
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2062
3178
|
}
|
|
2063
|
-
const parsed =
|
|
3179
|
+
const parsed = import_shared14.agentJobCreateRequestSchema.safeParse(
|
|
2064
3180
|
await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
|
|
2065
3181
|
);
|
|
2066
3182
|
if (!parsed.success) {
|
|
2067
|
-
throw new
|
|
3183
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2068
3184
|
}
|
|
2069
3185
|
const authorizedRequest = await authorizeAgentJobRequest({
|
|
2070
3186
|
request: parsed.data,
|
|
@@ -2077,13 +3193,13 @@ async function handleCreateJob(request, response, options, writeSuccess) {
|
|
|
2077
3193
|
}
|
|
2078
3194
|
async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
2079
3195
|
if (request.method !== "POST") {
|
|
2080
|
-
throw new
|
|
3196
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2081
3197
|
}
|
|
2082
|
-
const parsed =
|
|
3198
|
+
const parsed = import_shared14.agentWorkspaceHealthRequestSchema.safeParse(
|
|
2083
3199
|
await readJsonRequestBody(request)
|
|
2084
3200
|
);
|
|
2085
3201
|
if (!parsed.success) {
|
|
2086
|
-
throw new
|
|
3202
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2087
3203
|
}
|
|
2088
3204
|
const controller = new AbortController();
|
|
2089
3205
|
const abort = () => {
|
|
@@ -2100,13 +3216,13 @@ async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
|
2100
3216
|
async function handleJobAction(request, response, options, route, writeSuccess) {
|
|
2101
3217
|
const manager = requireAgentManager(options);
|
|
2102
3218
|
if (request.method !== "POST") {
|
|
2103
|
-
throw new
|
|
3219
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2104
3220
|
}
|
|
2105
|
-
const parsed =
|
|
3221
|
+
const parsed = import_shared14.agentJobActionRequestSchema.safeParse(
|
|
2106
3222
|
await readJsonRequestBody(request)
|
|
2107
3223
|
);
|
|
2108
3224
|
if (!parsed.success) {
|
|
2109
|
-
throw new
|
|
3225
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2110
3226
|
}
|
|
2111
3227
|
if (route.action === "events") {
|
|
2112
3228
|
streamAgentJobEvents(response, manager, route.jobId);
|
|
@@ -2221,27 +3337,27 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
2221
3337
|
var launchConfiguredEditor = createEditorLauncher();
|
|
2222
3338
|
|
|
2223
3339
|
// src/server/data-flow-http.ts
|
|
2224
|
-
var
|
|
3340
|
+
var import_node_crypto9 = require("crypto");
|
|
2225
3341
|
var import_analyzer = require("@spotpatch/analyzer");
|
|
2226
|
-
var
|
|
3342
|
+
var import_shared15 = require("@spotpatch/shared");
|
|
2227
3343
|
function envelopeBytes(report) {
|
|
2228
3344
|
return Buffer.byteLength(JSON.stringify({ ok: true, data: report }), "utf8");
|
|
2229
3345
|
}
|
|
2230
3346
|
function limitDataFlowReportToBytes(report, maximumBytes) {
|
|
2231
|
-
const structurallyLimited = (0,
|
|
3347
|
+
const structurallyLimited = (0, import_shared15.limitDataFlowReportCollections)(report);
|
|
2232
3348
|
if (envelopeBytes(structurallyLimited) <= maximumBytes) {
|
|
2233
3349
|
return structurallyLimited;
|
|
2234
3350
|
}
|
|
2235
|
-
let limited = (0,
|
|
3351
|
+
let limited = (0, import_shared15.limitDataFlowReportCollections)(structurallyLimited, {
|
|
2236
3352
|
forceTruncation: true,
|
|
2237
3353
|
maximumDependencies: 0,
|
|
2238
3354
|
truncatedBy: "bytes"
|
|
2239
3355
|
});
|
|
2240
3356
|
if (envelopeBytes(limited) > maximumBytes) {
|
|
2241
|
-
throw new
|
|
3357
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INTERNAL_ERROR);
|
|
2242
3358
|
}
|
|
2243
3359
|
for (let maximumDependencies = 1; maximumDependencies <= structurallyLimited.dependencies.length; maximumDependencies += 1) {
|
|
2244
|
-
const candidate = (0,
|
|
3360
|
+
const candidate = (0, import_shared15.limitDataFlowReportCollections)(structurallyLimited, {
|
|
2245
3361
|
forceTruncation: true,
|
|
2246
3362
|
maximumDependencies,
|
|
2247
3363
|
truncatedBy: "bytes"
|
|
@@ -2267,7 +3383,7 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
2267
3383
|
request.componentSourceId
|
|
2268
3384
|
);
|
|
2269
3385
|
if (anchor?.sourceVersion !== request.sourceVersion) {
|
|
2270
|
-
throw new
|
|
3386
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.DATA_FLOW_SOURCE_STALE);
|
|
2271
3387
|
}
|
|
2272
3388
|
return anchor;
|
|
2273
3389
|
}
|
|
@@ -2284,7 +3400,7 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
2284
3400
|
column: resolvedRequest.column
|
|
2285
3401
|
});
|
|
2286
3402
|
if (resolvedRequest.sourceVersion !== void 0 && resolvedRequest.sourceVersion !== report.component.source.sourceVersion) {
|
|
2287
|
-
throw new
|
|
3403
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.DATA_FLOW_SOURCE_STALE);
|
|
2288
3404
|
}
|
|
2289
3405
|
return limitDataFlowReportToBytes(
|
|
2290
3406
|
report,
|
|
@@ -2293,31 +3409,31 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
2293
3409
|
}
|
|
2294
3410
|
function requireAnalyzer(analyzer) {
|
|
2295
3411
|
if (analyzer === void 0) {
|
|
2296
|
-
throw new
|
|
3412
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.DATA_FLOW_DISABLED);
|
|
2297
3413
|
}
|
|
2298
3414
|
return analyzer;
|
|
2299
3415
|
}
|
|
2300
3416
|
async function handleComponentDataFlowReport(request, analyzer, options) {
|
|
2301
|
-
const parsed =
|
|
3417
|
+
const parsed = import_shared15.dataFlowComponentReportRequestSchema.safeParse(
|
|
2302
3418
|
await readJsonRequestBody(
|
|
2303
3419
|
request,
|
|
2304
3420
|
options.options.dataFlow.limits.protocolRequestMaxBytes
|
|
2305
3421
|
)
|
|
2306
3422
|
);
|
|
2307
3423
|
if (!parsed.success) {
|
|
2308
|
-
throw new
|
|
3424
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
|
|
2309
3425
|
}
|
|
2310
3426
|
return analyzeTarget(parsed.data, requireAnalyzer(analyzer), options);
|
|
2311
3427
|
}
|
|
2312
3428
|
async function handlePageDataFlowReport(request, analyzer, options) {
|
|
2313
|
-
const parsed =
|
|
3429
|
+
const parsed = import_shared15.dataFlowPageReportRequestSchema.safeParse(
|
|
2314
3430
|
await readJsonRequestBody(
|
|
2315
3431
|
request,
|
|
2316
3432
|
options.options.dataFlow.limits.protocolRequestMaxBytes
|
|
2317
3433
|
)
|
|
2318
3434
|
);
|
|
2319
3435
|
if (!parsed.success) {
|
|
2320
|
-
throw new
|
|
3436
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
|
|
2321
3437
|
}
|
|
2322
3438
|
const activeAnalyzer = requireAnalyzer(analyzer);
|
|
2323
3439
|
const componentReports = await Promise.all(
|
|
@@ -2341,10 +3457,10 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
2341
3457
|
const analyzedVersions = new Set(
|
|
2342
3458
|
componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
|
|
2343
3459
|
);
|
|
2344
|
-
const reportId = `page_${(0,
|
|
3460
|
+
const reportId = `page_${(0, import_node_crypto9.createHash)("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
|
|
2345
3461
|
const complete = componentReports.every((report2) => report2.completeness.complete);
|
|
2346
3462
|
const report = Object.freeze({
|
|
2347
|
-
schemaVersion:
|
|
3463
|
+
schemaVersion: import_shared15.DATA_FLOW_SCHEMA_VERSION,
|
|
2348
3464
|
reportId,
|
|
2349
3465
|
baseline: Object.freeze({
|
|
2350
3466
|
registryEpoch: options.session.id,
|
|
@@ -2388,20 +3504,20 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
2388
3504
|
}
|
|
2389
3505
|
|
|
2390
3506
|
// src/server/request-security.ts
|
|
2391
|
-
var
|
|
3507
|
+
var import_node_crypto10 = require("crypto");
|
|
2392
3508
|
var import_node_net = require("net");
|
|
2393
|
-
var
|
|
3509
|
+
var import_shared16 = require("@spotpatch/shared");
|
|
2394
3510
|
function getSingleHeader(request, name) {
|
|
2395
3511
|
const value = request.headers[name.toLowerCase()];
|
|
2396
3512
|
return Array.isArray(value) ? value[0] : value;
|
|
2397
3513
|
}
|
|
2398
|
-
function
|
|
3514
|
+
function tokensMatch2(actual, expected) {
|
|
2399
3515
|
if (actual === void 0) {
|
|
2400
3516
|
return false;
|
|
2401
3517
|
}
|
|
2402
3518
|
const actualBytes = Buffer.from(actual);
|
|
2403
3519
|
const expectedBytes = Buffer.from(expected);
|
|
2404
|
-
return actualBytes.byteLength === expectedBytes.byteLength && (0,
|
|
3520
|
+
return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto10.timingSafeEqual)(actualBytes, expectedBytes);
|
|
2405
3521
|
}
|
|
2406
3522
|
function isLoopbackHostname(hostname) {
|
|
2407
3523
|
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
@@ -2435,32 +3551,32 @@ function parseOrigin(value) {
|
|
|
2435
3551
|
}
|
|
2436
3552
|
}
|
|
2437
3553
|
function assertRequestAuthorized(request, options) {
|
|
2438
|
-
const actualToken = getSingleHeader(request,
|
|
2439
|
-
if (!
|
|
2440
|
-
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);
|
|
2441
3557
|
}
|
|
2442
3558
|
const hostHeader = getSingleHeader(request, "host");
|
|
2443
3559
|
const originHeader = getSingleHeader(request, "origin");
|
|
2444
3560
|
const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
|
|
2445
3561
|
const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
|
|
2446
3562
|
if (host === void 0 || origin === void 0) {
|
|
2447
|
-
throw new
|
|
3563
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
2448
3564
|
}
|
|
2449
3565
|
const hostIsLoopback = isLoopbackHostname(host.hostname);
|
|
2450
3566
|
const originIsLoopback = isLoopbackHostname(origin.hostname);
|
|
2451
3567
|
if (!options.allowLan) {
|
|
2452
3568
|
if (!hostIsLoopback || !originIsLoopback) {
|
|
2453
|
-
throw new
|
|
3569
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
2454
3570
|
}
|
|
2455
3571
|
return;
|
|
2456
3572
|
}
|
|
2457
3573
|
if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
|
|
2458
|
-
throw new
|
|
3574
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
2459
3575
|
}
|
|
2460
3576
|
}
|
|
2461
3577
|
|
|
2462
3578
|
// src/server/runtime-bootstrap.ts
|
|
2463
|
-
var
|
|
3579
|
+
var import_shared17 = require("@spotpatch/shared");
|
|
2464
3580
|
function getSingleHeader2(request, name) {
|
|
2465
3581
|
const value = request.headers[name.toLowerCase()];
|
|
2466
3582
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -2475,7 +3591,7 @@ function resolveRuntimeBootstrapOptions(options) {
|
|
|
2475
3591
|
if (expectedOrigin.origin !== options.expectedOrigin || expectedOrigin.protocol !== "http:" || !isLoopbackHostname(expectedOrigin.hostname)) {
|
|
2476
3592
|
throw new TypeError("The SpotPatch bootstrap origin must be a loopback origin.");
|
|
2477
3593
|
}
|
|
2478
|
-
const parsedConfig =
|
|
3594
|
+
const parsedConfig = import_shared17.runtimeConfigSchema.safeParse(options.runtimeConfig);
|
|
2479
3595
|
if (!parsedConfig.success) {
|
|
2480
3596
|
throw new TypeError("The SpotPatch Runtime configuration is invalid.");
|
|
2481
3597
|
}
|
|
@@ -2487,7 +3603,7 @@ function resolveRuntimeBootstrapOptions(options) {
|
|
|
2487
3603
|
function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
2488
3604
|
const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
2489
3605
|
if (request.method !== "POST" || contentType !== "application/json") {
|
|
2490
|
-
throw new
|
|
3606
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
2491
3607
|
}
|
|
2492
3608
|
const host = getSingleHeader2(request, "host");
|
|
2493
3609
|
let hostIsLoopback = false;
|
|
@@ -2499,112 +3615,148 @@ function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
|
2499
3615
|
}
|
|
2500
3616
|
}
|
|
2501
3617
|
if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
|
|
2502
|
-
throw new
|
|
3618
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
2503
3619
|
}
|
|
2504
3620
|
}
|
|
2505
3621
|
async function readRuntimeBootstrap(request, options) {
|
|
2506
3622
|
assertRuntimeBootstrapRequest(request, options.expectedOrigin);
|
|
2507
|
-
const parsedBody =
|
|
3623
|
+
const parsedBody = import_shared17.runtimeBootstrapRequestSchema.safeParse(
|
|
2508
3624
|
await readJsonRequestBody(request)
|
|
2509
3625
|
);
|
|
2510
3626
|
if (!parsedBody.success) {
|
|
2511
|
-
throw new
|
|
3627
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
2512
3628
|
}
|
|
2513
3629
|
return options.runtimeConfig;
|
|
2514
3630
|
}
|
|
2515
3631
|
|
|
2516
3632
|
// src/server/middleware.ts
|
|
2517
3633
|
var STATUS_BY_ERROR = Object.freeze({
|
|
2518
|
-
[
|
|
2519
|
-
[
|
|
2520
|
-
[
|
|
2521
|
-
[
|
|
2522
|
-
[
|
|
2523
|
-
[
|
|
2524
|
-
[
|
|
2525
|
-
[
|
|
2526
|
-
[
|
|
2527
|
-
[
|
|
2528
|
-
[
|
|
2529
|
-
[
|
|
2530
|
-
[
|
|
2531
|
-
[
|
|
2532
|
-
[
|
|
2533
|
-
[
|
|
2534
|
-
[
|
|
2535
|
-
[
|
|
2536
|
-
[
|
|
2537
|
-
[
|
|
2538
|
-
[
|
|
2539
|
-
[
|
|
2540
|
-
[
|
|
2541
|
-
[
|
|
2542
|
-
[
|
|
2543
|
-
[
|
|
2544
|
-
[
|
|
2545
|
-
[
|
|
2546
|
-
[
|
|
2547
|
-
[
|
|
2548
|
-
[
|
|
2549
|
-
[
|
|
2550
|
-
[
|
|
2551
|
-
[
|
|
2552
|
-
[
|
|
2553
|
-
[
|
|
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
|
|
2554
3688
|
});
|
|
2555
3689
|
var PUBLIC_MESSAGES = Object.freeze({
|
|
2556
|
-
[
|
|
2557
|
-
[
|
|
2558
|
-
[
|
|
2559
|
-
[
|
|
2560
|
-
[
|
|
2561
|
-
[
|
|
2562
|
-
[
|
|
2563
|
-
[
|
|
2564
|
-
[
|
|
2565
|
-
[
|
|
2566
|
-
[
|
|
2567
|
-
[
|
|
2568
|
-
[
|
|
2569
|
-
[
|
|
2570
|
-
[
|
|
2571
|
-
[
|
|
2572
|
-
[
|
|
2573
|
-
[
|
|
2574
|
-
[
|
|
2575
|
-
[
|
|
2576
|
-
[
|
|
2577
|
-
[
|
|
2578
|
-
[
|
|
2579
|
-
[
|
|
2580
|
-
[
|
|
2581
|
-
[
|
|
2582
|
-
[
|
|
2583
|
-
[
|
|
2584
|
-
[
|
|
2585
|
-
[
|
|
2586
|
-
[
|
|
2587
|
-
[
|
|
2588
|
-
[
|
|
2589
|
-
[
|
|
2590
|
-
[
|
|
2591
|
-
[
|
|
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."
|
|
2592
3744
|
});
|
|
2593
|
-
function
|
|
3745
|
+
function writeJson2(response, status, payload) {
|
|
2594
3746
|
response.statusCode = status;
|
|
2595
3747
|
response.setHeader("Cache-Control", "no-store");
|
|
2596
3748
|
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2597
3749
|
response.end(JSON.stringify(payload));
|
|
2598
3750
|
}
|
|
2599
3751
|
function asSpotPatchError(error) {
|
|
2600
|
-
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 });
|
|
2601
3753
|
}
|
|
2602
3754
|
function writeError(response, error, logger) {
|
|
2603
3755
|
const normalized = asSpotPatchError(error);
|
|
2604
|
-
if (normalized.code ===
|
|
3756
|
+
if (normalized.code === import_shared18.ERROR_CODES.INTERNAL_ERROR) {
|
|
2605
3757
|
logger?.warn("[spotpatch:server] Internal request failure.");
|
|
2606
3758
|
}
|
|
2607
|
-
|
|
3759
|
+
writeJson2(response, STATUS_BY_ERROR[normalized.code], {
|
|
2608
3760
|
ok: false,
|
|
2609
3761
|
error: {
|
|
2610
3762
|
code: normalized.code,
|
|
@@ -2620,11 +3772,11 @@ function requestPath(request) {
|
|
|
2620
3772
|
}
|
|
2621
3773
|
}
|
|
2622
3774
|
async function handleSourceContext(request, options) {
|
|
2623
|
-
const parsed =
|
|
3775
|
+
const parsed = import_shared18.sourceContextRequestSchema.safeParse(
|
|
2624
3776
|
await readJsonRequestBody(request)
|
|
2625
3777
|
);
|
|
2626
3778
|
if (!parsed.success) {
|
|
2627
|
-
throw new
|
|
3779
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
2628
3780
|
}
|
|
2629
3781
|
return readSourceContext({
|
|
2630
3782
|
request: parsed.data,
|
|
@@ -2635,9 +3787,9 @@ async function handleSourceContext(request, options) {
|
|
|
2635
3787
|
});
|
|
2636
3788
|
}
|
|
2637
3789
|
async function handleOpenEditor(request, options) {
|
|
2638
|
-
const parsed =
|
|
3790
|
+
const parsed = import_shared18.openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
|
|
2639
3791
|
if (!parsed.success) {
|
|
2640
|
-
throw new
|
|
3792
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
2641
3793
|
}
|
|
2642
3794
|
const body = parsed.data;
|
|
2643
3795
|
const sourcePath = await resolveSourceFile({
|
|
@@ -2654,7 +3806,7 @@ async function handleOpenEditor(request, options) {
|
|
|
2654
3806
|
options.logger?.warn(
|
|
2655
3807
|
`[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
|
|
2656
3808
|
);
|
|
2657
|
-
throw new
|
|
3809
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.EDITOR_OPEN_FAILED, void 0, {
|
|
2658
3810
|
cause: error
|
|
2659
3811
|
});
|
|
2660
3812
|
}
|
|
@@ -2663,63 +3815,81 @@ function createSpotPatchMiddleware(options) {
|
|
|
2663
3815
|
const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
|
|
2664
3816
|
const dataFlowAnalyzer = createDataFlowAnalyzer(options);
|
|
2665
3817
|
return (request, response, next) => {
|
|
2666
|
-
const
|
|
2667
|
-
const agentRoute = matchAgentRequestPath(
|
|
2668
|
-
|
|
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}/`)) {
|
|
2669
3822
|
next();
|
|
2670
3823
|
return;
|
|
2671
3824
|
}
|
|
2672
3825
|
const handle = async () => {
|
|
2673
|
-
if (
|
|
3826
|
+
if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.bootstrap && bootstrap !== void 0) {
|
|
2674
3827
|
const data = await readRuntimeBootstrap(
|
|
2675
3828
|
request,
|
|
2676
3829
|
bootstrap
|
|
2677
3830
|
);
|
|
2678
|
-
|
|
3831
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2679
3832
|
return;
|
|
2680
3833
|
}
|
|
2681
3834
|
assertRequestAuthorized(request, {
|
|
2682
3835
|
allowLan: options.options.allowLan,
|
|
2683
3836
|
sessionToken: options.session.token
|
|
2684
3837
|
});
|
|
2685
|
-
if (
|
|
3838
|
+
if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.sourceContext) {
|
|
2686
3839
|
if (request.method !== "POST") {
|
|
2687
|
-
throw new
|
|
3840
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
2688
3841
|
}
|
|
2689
3842
|
const data = await handleSourceContext(request, options);
|
|
2690
|
-
|
|
3843
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2691
3844
|
return;
|
|
2692
3845
|
}
|
|
2693
|
-
if (
|
|
3846
|
+
if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.openEditor) {
|
|
2694
3847
|
if (request.method !== "POST") {
|
|
2695
|
-
throw new
|
|
3848
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
2696
3849
|
}
|
|
2697
3850
|
const data = await handleOpenEditor(request, options);
|
|
2698
|
-
|
|
3851
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2699
3852
|
return;
|
|
2700
3853
|
}
|
|
2701
|
-
if (
|
|
3854
|
+
if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.dataFlowComponentReport) {
|
|
2702
3855
|
if (request.method !== "POST") {
|
|
2703
|
-
throw new
|
|
3856
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
2704
3857
|
}
|
|
2705
3858
|
const data = await handleComponentDataFlowReport(
|
|
2706
3859
|
request,
|
|
2707
3860
|
dataFlowAnalyzer,
|
|
2708
3861
|
options
|
|
2709
3862
|
);
|
|
2710
|
-
|
|
3863
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2711
3864
|
return;
|
|
2712
3865
|
}
|
|
2713
|
-
if (
|
|
3866
|
+
if (path9 === import_shared18.SPOTPATCH_ENDPOINTS.dataFlowPageReport) {
|
|
2714
3867
|
if (request.method !== "POST") {
|
|
2715
|
-
throw new
|
|
3868
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
2716
3869
|
}
|
|
2717
3870
|
const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
|
|
2718
|
-
|
|
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
|
+
);
|
|
2719
3889
|
return;
|
|
2720
3890
|
}
|
|
2721
3891
|
if (agentRoute === void 0) {
|
|
2722
|
-
throw new
|
|
3892
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_REQUEST);
|
|
2723
3893
|
}
|
|
2724
3894
|
await handleAgentRequest(
|
|
2725
3895
|
request,
|
|
@@ -2727,7 +3897,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
2727
3897
|
options,
|
|
2728
3898
|
agentRoute,
|
|
2729
3899
|
(target, status, data) => {
|
|
2730
|
-
|
|
3900
|
+
writeJson2(target, status, { ok: true, data });
|
|
2731
3901
|
}
|
|
2732
3902
|
);
|
|
2733
3903
|
};
|
|
@@ -2738,9 +3908,9 @@ function createSpotPatchMiddleware(options) {
|
|
|
2738
3908
|
}
|
|
2739
3909
|
|
|
2740
3910
|
// src/server/source-registration.ts
|
|
2741
|
-
var
|
|
2742
|
-
var
|
|
2743
|
-
var
|
|
3911
|
+
var import_node_crypto11 = require("crypto");
|
|
3912
|
+
var import_promises7 = require("fs/promises");
|
|
3913
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2744
3914
|
var import_compiler = require("@spotpatch/compiler");
|
|
2745
3915
|
var import_zod2 = require("zod");
|
|
2746
3916
|
var REGISTRATION_BODY_LIMIT_BYTES = 4096;
|
|
@@ -2761,16 +3931,16 @@ function identitiesMatch(actual, expected) {
|
|
|
2761
3931
|
}
|
|
2762
3932
|
const actualBytes = Buffer.from(actual);
|
|
2763
3933
|
const expectedBytes = Buffer.from(expected);
|
|
2764
|
-
return actualBytes.byteLength === expectedBytes.byteLength && (0,
|
|
3934
|
+
return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto11.timingSafeEqual)(actualBytes, expectedBytes);
|
|
2765
3935
|
}
|
|
2766
3936
|
function isWithinRoot(root, candidate) {
|
|
2767
|
-
const relative =
|
|
2768
|
-
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);
|
|
2769
3939
|
}
|
|
2770
3940
|
function hasForbiddenSegment(root, candidate) {
|
|
2771
|
-
return
|
|
3941
|
+
return import_node_path8.default.relative(root, candidate).split(import_node_path8.default.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
|
|
2772
3942
|
}
|
|
2773
|
-
function
|
|
3943
|
+
function writeJson3(response, statusCode, payload) {
|
|
2774
3944
|
const body = JSON.stringify(payload);
|
|
2775
3945
|
response.statusCode = statusCode;
|
|
2776
3946
|
response.setHeader("Cache-Control", "no-store");
|
|
@@ -2790,15 +3960,15 @@ function requestComesFromLoopbackWorker(request) {
|
|
|
2790
3960
|
}
|
|
2791
3961
|
}
|
|
2792
3962
|
async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
|
|
2793
|
-
if (!
|
|
3963
|
+
if (!import_node_path8.default.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
2794
3964
|
return void 0;
|
|
2795
3965
|
}
|
|
2796
3966
|
try {
|
|
2797
|
-
const sourceStat = await (0,
|
|
3967
|
+
const sourceStat = await (0, import_promises7.lstat)(requestedPath);
|
|
2798
3968
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
2799
3969
|
return void 0;
|
|
2800
3970
|
}
|
|
2801
|
-
const resolvedPath = await (0,
|
|
3971
|
+
const resolvedPath = await (0, import_promises7.realpath)(requestedPath);
|
|
2802
3972
|
if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
|
|
2803
3973
|
return void 0;
|
|
2804
3974
|
}
|
|
@@ -2811,7 +3981,7 @@ async function createSourceRegistrationService(input) {
|
|
|
2811
3981
|
if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
|
|
2812
3982
|
throw new TypeError("The source registration identity is invalid.");
|
|
2813
3983
|
}
|
|
2814
|
-
const root = await (0,
|
|
3984
|
+
const root = await (0, import_promises7.realpath)(input.root);
|
|
2815
3985
|
const sourceFilter = (0, import_compiler.createSourceFilter)(root, input.options);
|
|
2816
3986
|
const handler = (request, response) => {
|
|
2817
3987
|
const handle = async () => {
|
|
@@ -2820,14 +3990,14 @@ async function createSourceRegistrationService(input) {
|
|
|
2820
3990
|
getSingleHeader3(request, INTERNAL_SECRET_HEADER),
|
|
2821
3991
|
input.internalSecret
|
|
2822
3992
|
)) {
|
|
2823
|
-
|
|
3993
|
+
writeJson3(response, 403, { ok: false });
|
|
2824
3994
|
return;
|
|
2825
3995
|
}
|
|
2826
3996
|
const parsed = registrationRequestSchema.safeParse(
|
|
2827
3997
|
await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
|
|
2828
3998
|
);
|
|
2829
3999
|
if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
|
|
2830
|
-
|
|
4000
|
+
writeJson3(response, 400, { ok: false });
|
|
2831
4001
|
return;
|
|
2832
4002
|
}
|
|
2833
4003
|
const sourcePath = await resolveAuthorizedSource(
|
|
@@ -2836,17 +4006,17 @@ async function createSourceRegistrationService(input) {
|
|
|
2836
4006
|
(absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
|
|
2837
4007
|
);
|
|
2838
4008
|
if (sourcePath === void 0) {
|
|
2839
|
-
|
|
4009
|
+
writeJson3(response, 403, { ok: false });
|
|
2840
4010
|
return;
|
|
2841
4011
|
}
|
|
2842
|
-
|
|
4012
|
+
writeJson3(response, 200, {
|
|
2843
4013
|
epoch: input.registryEpoch,
|
|
2844
4014
|
fileId: input.registry.register(sourcePath)
|
|
2845
4015
|
});
|
|
2846
4016
|
};
|
|
2847
4017
|
void handle().catch(() => {
|
|
2848
4018
|
if (!response.headersSent) {
|
|
2849
|
-
|
|
4019
|
+
writeJson3(response, 400, { ok: false });
|
|
2850
4020
|
} else {
|
|
2851
4021
|
response.destroy();
|
|
2852
4022
|
}
|
|
@@ -2856,11 +4026,11 @@ async function createSourceRegistrationService(input) {
|
|
|
2856
4026
|
}
|
|
2857
4027
|
|
|
2858
4028
|
// src/session/session.ts
|
|
2859
|
-
var
|
|
4029
|
+
var import_node_crypto12 = require("crypto");
|
|
2860
4030
|
function createSession() {
|
|
2861
4031
|
return Object.freeze({
|
|
2862
|
-
id: (0,
|
|
2863
|
-
token: (0,
|
|
4032
|
+
id: (0, import_node_crypto12.randomBytes)(16).toString("base64url"),
|
|
4033
|
+
token: (0, import_node_crypto12.randomBytes)(16).toString("base64url")
|
|
2864
4034
|
});
|
|
2865
4035
|
}
|
|
2866
4036
|
|
|
@@ -2873,6 +4043,7 @@ var OPTION_KEYS = Object.freeze([
|
|
|
2873
4043
|
"dataFlow",
|
|
2874
4044
|
"editor",
|
|
2875
4045
|
"enabled",
|
|
4046
|
+
"externalAgent",
|
|
2876
4047
|
"exclude",
|
|
2877
4048
|
"include",
|
|
2878
4049
|
"locale",
|
|
@@ -2963,6 +4134,7 @@ function serializeResolvedSpotPatchOptions(options) {
|
|
|
2963
4134
|
}) : false,
|
|
2964
4135
|
editor: options.editor,
|
|
2965
4136
|
enabled: options.enabled,
|
|
4137
|
+
externalAgent: options.externalAgent.enabled,
|
|
2966
4138
|
exclude: Object.freeze(options.exclude.map(serializeFilter)),
|
|
2967
4139
|
include: Object.freeze(options.include.map(serializeFilter)),
|
|
2968
4140
|
locale: options.locale,
|
|
@@ -3016,7 +4188,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
3016
4188
|
if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
|
|
3017
4189
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
3018
4190
|
}
|
|
3019
|
-
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)) {
|
|
3020
4192
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
3021
4193
|
}
|
|
3022
4194
|
try {
|
|
@@ -3028,6 +4200,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
3028
4200
|
dataFlow: parseDataFlow(value.dataFlow),
|
|
3029
4201
|
editor: value.editor,
|
|
3030
4202
|
enabled: value.enabled,
|
|
4203
|
+
externalAgent: value.externalAgent,
|
|
3031
4204
|
exclude: parseFilterList(value.exclude),
|
|
3032
4205
|
include: parseFilterList(value.include),
|
|
3033
4206
|
locale: value.locale,
|
|
@@ -3047,6 +4220,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
3047
4220
|
DEFAULT_OPTIONS,
|
|
3048
4221
|
applyIntegrationPlan,
|
|
3049
4222
|
createAgentJobManager,
|
|
4223
|
+
createExternalHandoffService,
|
|
3050
4224
|
createIntegrationFileChange,
|
|
3051
4225
|
createRuntimeAiConfig,
|
|
3052
4226
|
createRuntimeDataFlowConfig,
|