@spotpatch/dev-server 0.5.0 → 0.7.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 +1739 -380
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -3
- package/dist/index.d.ts +55 -3
- package/dist/index.js +1758 -336
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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,
|
|
@@ -50,8 +51,10 @@ __export(index_exports, {
|
|
|
50
51
|
readRuntimeBootstrap: () => readRuntimeBootstrap,
|
|
51
52
|
resolveCredentialEnvironment: () => resolveCredentialEnvironment,
|
|
52
53
|
resolveEnvironmentAiConfiguration: () => resolveEnvironmentAiConfiguration,
|
|
54
|
+
resolveManagedExecutionValidation: () => resolveManagedExecutionValidation,
|
|
53
55
|
resolveOptions: () => resolveOptions,
|
|
54
56
|
resolveProjectOptions: () => resolveProjectOptions,
|
|
57
|
+
resolveProjectValidationChecks: () => resolveProjectValidationChecks,
|
|
55
58
|
resolveRuntimeBootstrapOptions: () => resolveRuntimeBootstrapOptions,
|
|
56
59
|
serializeResolvedSpotPatchOptions: () => serializeResolvedSpotPatchOptions
|
|
57
60
|
});
|
|
@@ -521,6 +524,1064 @@ function createAgentJobManager(options) {
|
|
|
521
524
|
});
|
|
522
525
|
}
|
|
523
526
|
|
|
527
|
+
// src/external-handoff/service.ts
|
|
528
|
+
var import_shared7 = require("@spotpatch/shared");
|
|
529
|
+
var import_external_agent_node3 = require("@spotpatch/shared/external-agent-node");
|
|
530
|
+
|
|
531
|
+
// src/external-handoff/active-registry.ts
|
|
532
|
+
var import_node_crypto2 = require("crypto");
|
|
533
|
+
var import_shared2 = require("@spotpatch/shared");
|
|
534
|
+
|
|
535
|
+
// src/external-handoff/clock.ts
|
|
536
|
+
var import_node_perf_hooks = require("perf_hooks");
|
|
537
|
+
var SYSTEM_EXTERNAL_HANDOFF_CLOCK = Object.freeze({
|
|
538
|
+
monotonicNow: () => import_node_perf_hooks.performance.now(),
|
|
539
|
+
wallNow: () => Date.now()
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// src/external-handoff/active-registry.ts
|
|
543
|
+
var ALLOWED_TRANSITIONS = Object.freeze({
|
|
544
|
+
queued: ["dispatching", "failed"],
|
|
545
|
+
dispatching: ["dispatched", "working", "failed", "delivery-unknown"],
|
|
546
|
+
dispatched: ["working", "failed", "delivery-unknown"],
|
|
547
|
+
working: ["completed", "failed", "delivery-unknown"],
|
|
548
|
+
completed: [],
|
|
549
|
+
failed: [],
|
|
550
|
+
"delivery-unknown": []
|
|
551
|
+
});
|
|
552
|
+
var TERMINAL_PHASES = /* @__PURE__ */ new Set([
|
|
553
|
+
"completed",
|
|
554
|
+
"failed",
|
|
555
|
+
"delivery-unknown"
|
|
556
|
+
]);
|
|
557
|
+
function defaultRandomId() {
|
|
558
|
+
return (0, import_node_crypto2.randomBytes)(32).toString("base64url");
|
|
559
|
+
}
|
|
560
|
+
function requirePresent(value) {
|
|
561
|
+
if (value === null) throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.INTERNAL_ERROR);
|
|
562
|
+
return value;
|
|
563
|
+
}
|
|
564
|
+
function createActiveAdapterRegistry(options = {}) {
|
|
565
|
+
const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
|
|
566
|
+
const randomId = options.randomId ?? defaultRandomId;
|
|
567
|
+
let blocked;
|
|
568
|
+
let closed = false;
|
|
569
|
+
let dispatch;
|
|
570
|
+
let lastReleasedToken;
|
|
571
|
+
let lease;
|
|
572
|
+
const nowIso = () => new Date(clock.wallNow()).toISOString();
|
|
573
|
+
const requireOpen = () => {
|
|
574
|
+
if (closed) throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.SESSION_CLOSED);
|
|
575
|
+
};
|
|
576
|
+
const dispatchSummary = () => dispatch === void 0 ? null : Object.freeze({
|
|
577
|
+
adapterKind: dispatch.adapterKind,
|
|
578
|
+
revision: dispatch.revision,
|
|
579
|
+
phase: dispatch.phase,
|
|
580
|
+
updatedAt: dispatch.updatedAt
|
|
581
|
+
});
|
|
582
|
+
const activeSummary = () => {
|
|
583
|
+
if (blocked !== void 0) {
|
|
584
|
+
return Object.freeze({
|
|
585
|
+
kind: blocked.adapterKind,
|
|
586
|
+
state: "blocked",
|
|
587
|
+
canDispatch: false,
|
|
588
|
+
connectedAt: blocked.connectedAt,
|
|
589
|
+
updatedAt: blocked.updatedAt
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
if (lease === void 0) return null;
|
|
593
|
+
const busy = dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase);
|
|
594
|
+
return Object.freeze({
|
|
595
|
+
kind: lease.adapterKind,
|
|
596
|
+
state: busy ? "busy" : "ready",
|
|
597
|
+
canDispatch: !busy,
|
|
598
|
+
connectedAt: lease.connectedAt,
|
|
599
|
+
updatedAt: lease.updatedAt
|
|
600
|
+
});
|
|
601
|
+
};
|
|
602
|
+
const state = (cursor) => Object.freeze({
|
|
603
|
+
activeAdapter: activeSummary(),
|
|
604
|
+
dispatch: cursor === void 0 || dispatch?.cursor === cursor ? dispatchSummary() : null
|
|
605
|
+
});
|
|
606
|
+
const enterUnknown = (activeLease) => {
|
|
607
|
+
if (dispatch === void 0) return;
|
|
608
|
+
const updatedAt = nowIso();
|
|
609
|
+
dispatch.phase = "delivery-unknown";
|
|
610
|
+
dispatch.updatedAt = updatedAt;
|
|
611
|
+
blocked = Object.freeze({
|
|
612
|
+
adapterKind: activeLease.adapterKind,
|
|
613
|
+
connectedAt: activeLease.connectedAt,
|
|
614
|
+
updatedAt
|
|
615
|
+
});
|
|
616
|
+
};
|
|
617
|
+
const endLease = (activeLease) => {
|
|
618
|
+
if (dispatch?.phase === "queued") {
|
|
619
|
+
dispatch.phase = "failed";
|
|
620
|
+
dispatch.updatedAt = nowIso();
|
|
621
|
+
} else if (dispatch?.phase === "dispatching" || dispatch?.phase === "dispatched" || dispatch?.phase === "working") {
|
|
622
|
+
enterUnknown(activeLease);
|
|
623
|
+
}
|
|
624
|
+
lastReleasedToken = activeLease.token;
|
|
625
|
+
lease = void 0;
|
|
626
|
+
};
|
|
627
|
+
const sweep = () => {
|
|
628
|
+
if (lease === void 0) return;
|
|
629
|
+
const monotonicNow = clock.monotonicNow();
|
|
630
|
+
if (monotonicNow >= lease.expiresAtMonotonic || dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase) && monotonicNow >= dispatch.deadlineMonotonic) {
|
|
631
|
+
endLease(lease);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
const assertPublishable = () => {
|
|
635
|
+
requireOpen();
|
|
636
|
+
sweep();
|
|
637
|
+
if (blocked !== void 0 || lease !== void 0 && dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase)) {
|
|
638
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.EXTERNAL_AGENT_BUSY);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
const requireLease = (leaseToken) => {
|
|
642
|
+
requireOpen();
|
|
643
|
+
sweep();
|
|
644
|
+
if (lease?.token !== leaseToken) {
|
|
645
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID);
|
|
646
|
+
}
|
|
647
|
+
return lease;
|
|
648
|
+
};
|
|
649
|
+
return Object.freeze({
|
|
650
|
+
assertPublishable,
|
|
651
|
+
claim(adapterKind, connectorInstanceId, baselineCursor) {
|
|
652
|
+
requireOpen();
|
|
653
|
+
sweep();
|
|
654
|
+
if (blocked !== void 0) {
|
|
655
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.EXTERNAL_AGENT_BUSY);
|
|
656
|
+
}
|
|
657
|
+
if (lease !== void 0) {
|
|
658
|
+
if (lease.adapterKind === adapterKind && lease.connectorInstanceId === connectorInstanceId) {
|
|
659
|
+
lease.expiresAtMonotonic = clock.monotonicNow() + import_shared2.EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
|
|
660
|
+
lease.updatedAt = nowIso();
|
|
661
|
+
return Object.freeze({
|
|
662
|
+
leaseToken: lease.token,
|
|
663
|
+
heartbeatIntervalMs: import_shared2.EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
|
|
664
|
+
baselineCursor: lease.baselineCursor,
|
|
665
|
+
activeAdapter: requirePresent(activeSummary())
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT);
|
|
669
|
+
}
|
|
670
|
+
const timestamp = nowIso();
|
|
671
|
+
lease = {
|
|
672
|
+
adapterKind,
|
|
673
|
+
baselineCursor,
|
|
674
|
+
connectedAt: timestamp,
|
|
675
|
+
connectorInstanceId,
|
|
676
|
+
token: randomId(),
|
|
677
|
+
expiresAtMonotonic: clock.monotonicNow() + import_shared2.EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs,
|
|
678
|
+
updatedAt: timestamp
|
|
679
|
+
};
|
|
680
|
+
lastReleasedToken = void 0;
|
|
681
|
+
return Object.freeze({
|
|
682
|
+
leaseToken: lease.token,
|
|
683
|
+
heartbeatIntervalMs: import_shared2.EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
|
|
684
|
+
baselineCursor,
|
|
685
|
+
activeAdapter: requirePresent(activeSummary())
|
|
686
|
+
});
|
|
687
|
+
},
|
|
688
|
+
heartbeat(leaseToken) {
|
|
689
|
+
const activeLease = requireLease(leaseToken);
|
|
690
|
+
activeLease.expiresAtMonotonic = clock.monotonicNow() + import_shared2.EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
|
|
691
|
+
activeLease.updatedAt = nowIso();
|
|
692
|
+
return state();
|
|
693
|
+
},
|
|
694
|
+
report(leaseToken, cursor, phase) {
|
|
695
|
+
const activeLease = requireLease(leaseToken);
|
|
696
|
+
if (dispatch?.cursor !== cursor || dispatch.adapterKind !== activeLease.adapterKind) {
|
|
697
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_DISPATCH_INVALID);
|
|
698
|
+
}
|
|
699
|
+
if (dispatch.phase === phase) return state(cursor);
|
|
700
|
+
const allowed = ALLOWED_TRANSITIONS[dispatch.phase];
|
|
701
|
+
if (!allowed.includes(phase)) {
|
|
702
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_DISPATCH_INVALID);
|
|
703
|
+
}
|
|
704
|
+
const updatedAt = nowIso();
|
|
705
|
+
dispatch.phase = phase;
|
|
706
|
+
dispatch.updatedAt = updatedAt;
|
|
707
|
+
activeLease.updatedAt = updatedAt;
|
|
708
|
+
if (phase === "delivery-unknown") {
|
|
709
|
+
blocked = Object.freeze({
|
|
710
|
+
adapterKind: activeLease.adapterKind,
|
|
711
|
+
connectedAt: activeLease.connectedAt,
|
|
712
|
+
updatedAt
|
|
713
|
+
});
|
|
714
|
+
lastReleasedToken = activeLease.token;
|
|
715
|
+
lease = void 0;
|
|
716
|
+
}
|
|
717
|
+
return state(cursor);
|
|
718
|
+
},
|
|
719
|
+
release(leaseToken) {
|
|
720
|
+
requireOpen();
|
|
721
|
+
sweep();
|
|
722
|
+
if (lease === void 0 && lastReleasedToken === leaseToken) return state();
|
|
723
|
+
const activeLease = lease;
|
|
724
|
+
if (activeLease?.token !== leaseToken) {
|
|
725
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID);
|
|
726
|
+
}
|
|
727
|
+
endLease(activeLease);
|
|
728
|
+
return state();
|
|
729
|
+
},
|
|
730
|
+
reserve(cursor, revision) {
|
|
731
|
+
assertPublishable();
|
|
732
|
+
if (lease === void 0) return Object.freeze({ mode: "inbox" });
|
|
733
|
+
const updatedAt = nowIso();
|
|
734
|
+
dispatch = {
|
|
735
|
+
adapterKind: lease.adapterKind,
|
|
736
|
+
cursor,
|
|
737
|
+
deadlineMonotonic: clock.monotonicNow() + import_shared2.EXTERNAL_HANDOFF_LIMITS.activeDispatchTimeoutMs,
|
|
738
|
+
revision,
|
|
739
|
+
phase: "queued",
|
|
740
|
+
updatedAt
|
|
741
|
+
};
|
|
742
|
+
lease.updatedAt = updatedAt;
|
|
743
|
+
return Object.freeze({
|
|
744
|
+
mode: "active",
|
|
745
|
+
adapter: requirePresent(activeSummary()),
|
|
746
|
+
dispatch: requirePresent(dispatchSummary())
|
|
747
|
+
});
|
|
748
|
+
},
|
|
749
|
+
resolveDelivery(cursor) {
|
|
750
|
+
requireOpen();
|
|
751
|
+
sweep();
|
|
752
|
+
const activeDispatch = dispatch;
|
|
753
|
+
if (blocked === void 0 || activeDispatch?.cursor !== cursor || activeDispatch.phase !== "delivery-unknown") {
|
|
754
|
+
throw new import_shared2.SpotPatchError(import_shared2.ERROR_CODES.ACTIVE_DISPATCH_INVALID);
|
|
755
|
+
}
|
|
756
|
+
blocked = void 0;
|
|
757
|
+
return state(cursor);
|
|
758
|
+
},
|
|
759
|
+
snapshot(cursor) {
|
|
760
|
+
requireOpen();
|
|
761
|
+
sweep();
|
|
762
|
+
return state(cursor);
|
|
763
|
+
},
|
|
764
|
+
close() {
|
|
765
|
+
if (closed) return;
|
|
766
|
+
closed = true;
|
|
767
|
+
blocked = void 0;
|
|
768
|
+
dispatch = void 0;
|
|
769
|
+
lease = void 0;
|
|
770
|
+
lastReleasedToken = void 0;
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/external-handoff/broker.ts
|
|
776
|
+
var import_node_crypto3 = require("crypto");
|
|
777
|
+
var import_node_http = require("http");
|
|
778
|
+
var import_shared4 = require("@spotpatch/shared");
|
|
779
|
+
var import_external_agent_node = require("@spotpatch/shared/external-agent-node");
|
|
780
|
+
|
|
781
|
+
// src/server/request-body.ts
|
|
782
|
+
var import_shared3 = require("@spotpatch/shared");
|
|
783
|
+
|
|
784
|
+
// src/server/constants.ts
|
|
785
|
+
var MAX_REQUEST_BODY_BYTES = 32 * 1024;
|
|
786
|
+
var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
|
|
787
|
+
var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
|
788
|
+
|
|
789
|
+
// src/server/request-body.ts
|
|
790
|
+
function isJsonContentType(value) {
|
|
791
|
+
return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
792
|
+
}
|
|
793
|
+
async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
|
|
794
|
+
if (!isJsonContentType(request.headers["content-type"])) {
|
|
795
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST);
|
|
796
|
+
}
|
|
797
|
+
const declaredLength = Number(request.headers["content-length"]);
|
|
798
|
+
if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
|
|
799
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST);
|
|
800
|
+
}
|
|
801
|
+
const chunks = [];
|
|
802
|
+
let byteLength = 0;
|
|
803
|
+
let exceededLimit = false;
|
|
804
|
+
for await (const rawChunk of request) {
|
|
805
|
+
const chunk = rawChunk;
|
|
806
|
+
if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
|
|
807
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST);
|
|
808
|
+
}
|
|
809
|
+
const buffer = Buffer.from(chunk);
|
|
810
|
+
byteLength += buffer.byteLength;
|
|
811
|
+
if (byteLength > maximumBytes) {
|
|
812
|
+
exceededLimit = true;
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
chunks.push(buffer);
|
|
816
|
+
}
|
|
817
|
+
if (exceededLimit || byteLength === 0) {
|
|
818
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST);
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
822
|
+
} catch (error) {
|
|
823
|
+
throw new import_shared3.SpotPatchError(import_shared3.ERROR_CODES.INVALID_REQUEST, void 0, {
|
|
824
|
+
cause: error
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/external-handoff/broker.ts
|
|
830
|
+
function singleHeader(request, name) {
|
|
831
|
+
const value = request.headers[name.toLowerCase()];
|
|
832
|
+
return Array.isArray(value) ? void 0 : value;
|
|
833
|
+
}
|
|
834
|
+
function tokensMatch(actual, expected) {
|
|
835
|
+
if (actual === void 0) return false;
|
|
836
|
+
const actualBytes = Buffer.from(actual);
|
|
837
|
+
const expectedBytes = Buffer.from(expected);
|
|
838
|
+
return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto3.timingSafeEqual)(actualBytes, expectedBytes);
|
|
839
|
+
}
|
|
840
|
+
function writeJson(response, status, payload) {
|
|
841
|
+
response.statusCode = status;
|
|
842
|
+
response.setHeader("Cache-Control", "no-store");
|
|
843
|
+
response.setHeader("Connection", "close");
|
|
844
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
845
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
846
|
+
response.end(JSON.stringify(payload));
|
|
847
|
+
}
|
|
848
|
+
function statusForError(code) {
|
|
849
|
+
if (code === import_shared4.ERROR_CODES.BRIDGE_UNAUTHORIZED) return 401;
|
|
850
|
+
if (code === import_shared4.ERROR_CODES.HANDOFF_NOT_FOUND) return 404;
|
|
851
|
+
if (code === import_shared4.ERROR_CODES.HANDOFF_EXPIRED || code === import_shared4.ERROR_CODES.SESSION_CLOSED) {
|
|
852
|
+
return 410;
|
|
853
|
+
}
|
|
854
|
+
if (code === import_shared4.ERROR_CODES.BRIDGE_BUSY) return 429;
|
|
855
|
+
if (code === import_shared4.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID) return 401;
|
|
856
|
+
if (code === import_shared4.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE) return 413;
|
|
857
|
+
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) {
|
|
858
|
+
return 409;
|
|
859
|
+
}
|
|
860
|
+
if (code === import_shared4.ERROR_CODES.INVALID_REQUEST) return 400;
|
|
861
|
+
return 500;
|
|
862
|
+
}
|
|
863
|
+
function normalizeError2(error) {
|
|
864
|
+
return error instanceof import_shared4.SpotPatchError ? error : new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INTERNAL_ERROR, void 0, { cause: error });
|
|
865
|
+
}
|
|
866
|
+
function assertAuthorized(request, expectedHost, bridgeToken) {
|
|
867
|
+
if (request.socket.remoteAddress !== "127.0.0.1" || singleHeader(request, "host") !== expectedHost || !tokensMatch(singleHeader(request, import_external_agent_node.SPOTPATCH_BRIDGE_TOKEN_HEADER), bridgeToken)) {
|
|
868
|
+
throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.BRIDGE_UNAUTHORIZED);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
async function closeServer(server, sockets) {
|
|
872
|
+
await new Promise((resolve) => {
|
|
873
|
+
server.close(() => {
|
|
874
|
+
resolve();
|
|
875
|
+
});
|
|
876
|
+
for (const socket of sockets) {
|
|
877
|
+
socket.destroy();
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
async function createExternalHandoffBroker(options) {
|
|
882
|
+
const bridgeToken = (0, import_node_crypto3.randomBytes)(32).toString("base64url");
|
|
883
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
884
|
+
let expectedHost = "";
|
|
885
|
+
const server = (0, import_node_http.createServer)(
|
|
886
|
+
{ maxHeaderSize: import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumBrokerHeaderBytes },
|
|
887
|
+
(request, response) => {
|
|
888
|
+
const handle = async () => {
|
|
889
|
+
assertAuthorized(request, expectedHost, bridgeToken);
|
|
890
|
+
if (request.method !== "POST") {
|
|
891
|
+
throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
892
|
+
}
|
|
893
|
+
const body = await readJsonRequestBody(
|
|
894
|
+
request,
|
|
895
|
+
import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumBrokerRequestBytes
|
|
896
|
+
);
|
|
897
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.status) {
|
|
898
|
+
const parsed = import_external_agent_node.bridgeStatusRequestSchema.safeParse(body);
|
|
899
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
900
|
+
let current = null;
|
|
901
|
+
try {
|
|
902
|
+
current = options.store.status();
|
|
903
|
+
} catch (error) {
|
|
904
|
+
if (!(error instanceof import_shared4.SpotPatchError) || error.code !== import_shared4.ERROR_CODES.HANDOFF_NOT_FOUND) {
|
|
905
|
+
throw error;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
writeJson(response, 200, {
|
|
909
|
+
ok: true,
|
|
910
|
+
data: Object.freeze({
|
|
911
|
+
brokerProtocolVersion: import_shared4.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
|
|
912
|
+
projectKey: options.projectKey,
|
|
913
|
+
sessionId: options.sessionId,
|
|
914
|
+
framework: options.framework,
|
|
915
|
+
current
|
|
916
|
+
})
|
|
917
|
+
});
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.current) {
|
|
921
|
+
const parsed = import_external_agent_node.bridgeCurrentRequestSchema.safeParse(body);
|
|
922
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
923
|
+
const snapshot2 = options.store.current(parsed.data.cursor);
|
|
924
|
+
writeJson(response, 200, {
|
|
925
|
+
ok: true,
|
|
926
|
+
data: Object.freeze({ outcome: "handoff", snapshot: snapshot2 })
|
|
927
|
+
});
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.ack) {
|
|
931
|
+
const parsed = import_external_agent_node.bridgeAckRequestSchema.safeParse(body);
|
|
932
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
933
|
+
const summary = options.store.ack(
|
|
934
|
+
parsed.data.cursor,
|
|
935
|
+
parsed.data.connectorInstanceId
|
|
936
|
+
);
|
|
937
|
+
writeJson(response, 200, {
|
|
938
|
+
ok: true,
|
|
939
|
+
data: Object.freeze({ summary })
|
|
940
|
+
});
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.wait) {
|
|
944
|
+
const parsed = import_external_agent_node.bridgeWaitRequestSchema.safeParse(body);
|
|
945
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
946
|
+
const controller = new AbortController();
|
|
947
|
+
const abort = () => {
|
|
948
|
+
if (!response.writableEnded) controller.abort("bridge-client-closed");
|
|
949
|
+
};
|
|
950
|
+
response.once("close", abort);
|
|
951
|
+
try {
|
|
952
|
+
const data = await options.store.wait(
|
|
953
|
+
parsed.data.afterCursor,
|
|
954
|
+
parsed.data.timeoutMs,
|
|
955
|
+
controller.signal
|
|
956
|
+
);
|
|
957
|
+
writeJson(response, 200, { ok: true, data });
|
|
958
|
+
} finally {
|
|
959
|
+
response.removeListener("close", abort);
|
|
960
|
+
}
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.activeClaim) {
|
|
964
|
+
const parsed = import_external_agent_node.bridgeActiveClaimRequestSchema.safeParse(body);
|
|
965
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
966
|
+
const data = options.activeRegistry.claim(
|
|
967
|
+
parsed.data.adapterKind,
|
|
968
|
+
parsed.data.connectorInstanceId,
|
|
969
|
+
options.store.currentCursor()
|
|
970
|
+
);
|
|
971
|
+
writeJson(response, 200, { ok: true, data });
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.activeHeartbeat) {
|
|
975
|
+
const parsed = import_external_agent_node.bridgeActiveHeartbeatRequestSchema.safeParse(body);
|
|
976
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
977
|
+
const data = options.activeRegistry.heartbeat(parsed.data.leaseToken);
|
|
978
|
+
writeJson(response, 200, { ok: true, data });
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.activeReport) {
|
|
982
|
+
const parsed = import_external_agent_node.bridgeActiveReportRequestSchema.safeParse(body);
|
|
983
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
984
|
+
const data = options.activeRegistry.report(
|
|
985
|
+
parsed.data.leaseToken,
|
|
986
|
+
parsed.data.cursor,
|
|
987
|
+
parsed.data.phase
|
|
988
|
+
);
|
|
989
|
+
writeJson(response, 200, { ok: true, data });
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (request.url === import_external_agent_node.SPOTPATCH_BRIDGE_PATHS.activeRelease) {
|
|
993
|
+
const parsed = import_external_agent_node.bridgeActiveReleaseRequestSchema.safeParse(body);
|
|
994
|
+
if (!parsed.success) throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
995
|
+
const data = options.activeRegistry.release(parsed.data.leaseToken);
|
|
996
|
+
writeJson(response, 200, { ok: true, data });
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
throw new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.INVALID_REQUEST);
|
|
1000
|
+
};
|
|
1001
|
+
void handle().catch((error) => {
|
|
1002
|
+
if (response.writableEnded || response.destroyed) return;
|
|
1003
|
+
const normalized = normalizeError2(error);
|
|
1004
|
+
if (normalized.code === import_shared4.ERROR_CODES.BRIDGE_BUSY) {
|
|
1005
|
+
response.setHeader("Retry-After", "1");
|
|
1006
|
+
}
|
|
1007
|
+
writeJson(response, statusForError(normalized.code), {
|
|
1008
|
+
ok: false,
|
|
1009
|
+
error: {
|
|
1010
|
+
code: normalized.code,
|
|
1011
|
+
message: "The local SpotPatch bridge request failed."
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
);
|
|
1017
|
+
server.maxConnections = import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumBrokerSockets;
|
|
1018
|
+
server.headersTimeout = 5e3;
|
|
1019
|
+
server.requestTimeout = import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumWaitMs + 5e3;
|
|
1020
|
+
server.keepAliveTimeout = 1;
|
|
1021
|
+
server.on("connection", (socket) => {
|
|
1022
|
+
if (sockets.size >= import_shared4.EXTERNAL_HANDOFF_LIMITS.maximumBrokerSockets) {
|
|
1023
|
+
socket.destroy();
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
sockets.add(socket);
|
|
1027
|
+
socket.once("close", () => sockets.delete(socket));
|
|
1028
|
+
});
|
|
1029
|
+
await new Promise((resolve, reject) => {
|
|
1030
|
+
server.once("error", reject);
|
|
1031
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
1032
|
+
});
|
|
1033
|
+
const address = server.address();
|
|
1034
|
+
if (address === null || typeof address === "string" || address.address !== "127.0.0.1") {
|
|
1035
|
+
await closeServer(server, sockets);
|
|
1036
|
+
throw new Error("SpotPatch external Agent broker did not bind IPv4 loopback.");
|
|
1037
|
+
}
|
|
1038
|
+
expectedHost = `127.0.0.1:${String(address.port)}`;
|
|
1039
|
+
let closed = false;
|
|
1040
|
+
let ready = true;
|
|
1041
|
+
server.removeAllListeners("error");
|
|
1042
|
+
server.on("error", () => {
|
|
1043
|
+
ready = false;
|
|
1044
|
+
for (const socket of sockets) socket.destroy();
|
|
1045
|
+
});
|
|
1046
|
+
return Object.freeze({
|
|
1047
|
+
bridgeToken,
|
|
1048
|
+
endpoint: `http://${expectedHost}`,
|
|
1049
|
+
isReady: () => ready && !closed,
|
|
1050
|
+
async close() {
|
|
1051
|
+
if (closed) return;
|
|
1052
|
+
closed = true;
|
|
1053
|
+
ready = false;
|
|
1054
|
+
await closeServer(server, sockets);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/external-handoff/discovery.ts
|
|
1060
|
+
var import_node_crypto4 = require("crypto");
|
|
1061
|
+
var import_promises = require("fs/promises");
|
|
1062
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
1063
|
+
var import_shared5 = require("@spotpatch/shared");
|
|
1064
|
+
var import_external_agent_node2 = require("@spotpatch/shared/external-agent-node");
|
|
1065
|
+
async function syncDirectory(directory) {
|
|
1066
|
+
const handle = await (0, import_promises.open)(directory, "r");
|
|
1067
|
+
try {
|
|
1068
|
+
await handle.sync();
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
const code = error.code;
|
|
1071
|
+
if (code !== "EINVAL" && code !== "ENOTSUP") {
|
|
1072
|
+
throw error;
|
|
1073
|
+
}
|
|
1074
|
+
} finally {
|
|
1075
|
+
await handle.close();
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
async function publishExternalHandoffDescriptor(options) {
|
|
1079
|
+
const directory = await (0, import_external_agent_node2.resolveExternalHandoffRuntimeDirectory)(true);
|
|
1080
|
+
const descriptor = import_external_agent_node2.externalHandoffDescriptorSchema.parse({
|
|
1081
|
+
schemaVersion: 1,
|
|
1082
|
+
brokerProtocolVersion: import_shared5.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
|
|
1083
|
+
projectKey: await (0, import_external_agent_node2.computeExternalHandoffProjectKey)(options.root),
|
|
1084
|
+
sessionId: options.sessionId,
|
|
1085
|
+
framework: options.framework,
|
|
1086
|
+
endpoint: options.endpoint,
|
|
1087
|
+
bridgeToken: options.bridgeToken,
|
|
1088
|
+
pid: process.pid,
|
|
1089
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1090
|
+
});
|
|
1091
|
+
const serialized = JSON.stringify(descriptor);
|
|
1092
|
+
if (Buffer.byteLength(serialized, "utf8") > import_shared5.EXTERNAL_HANDOFF_LIMITS.maximumDescriptorBytes) {
|
|
1093
|
+
throw new RangeError("SpotPatch external Agent descriptor exceeds its limit.");
|
|
1094
|
+
}
|
|
1095
|
+
const destination = import_node_path.default.join(directory, `${descriptor.sessionId}.json`);
|
|
1096
|
+
const temporary = import_node_path.default.join(
|
|
1097
|
+
directory,
|
|
1098
|
+
`.${descriptor.sessionId}.${(0, import_node_crypto4.randomBytes)(8).toString("hex")}.tmp`
|
|
1099
|
+
);
|
|
1100
|
+
let temporaryExists = false;
|
|
1101
|
+
let published = false;
|
|
1102
|
+
let descriptorIdentity = Object.freeze({
|
|
1103
|
+
device: -1,
|
|
1104
|
+
inode: -1
|
|
1105
|
+
});
|
|
1106
|
+
try {
|
|
1107
|
+
const handle = await (0, import_promises.open)(temporary, "wx", 384);
|
|
1108
|
+
temporaryExists = true;
|
|
1109
|
+
try {
|
|
1110
|
+
await handle.writeFile(serialized, "utf8");
|
|
1111
|
+
await handle.sync();
|
|
1112
|
+
} finally {
|
|
1113
|
+
await handle.close();
|
|
1114
|
+
}
|
|
1115
|
+
await (0, import_promises.rename)(temporary, destination);
|
|
1116
|
+
temporaryExists = false;
|
|
1117
|
+
published = true;
|
|
1118
|
+
const status = await (0, import_promises.lstat)(destination);
|
|
1119
|
+
const uid = process.getuid?.();
|
|
1120
|
+
if (!status.isFile() || status.isSymbolicLink() || uid === void 0 || status.uid !== uid || (status.mode & 63) !== 0) {
|
|
1121
|
+
throw new Error("SpotPatch external Agent descriptor is not private.");
|
|
1122
|
+
}
|
|
1123
|
+
descriptorIdentity = Object.freeze({ device: status.dev, inode: status.ino });
|
|
1124
|
+
await syncDirectory(directory);
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
if (temporaryExists) {
|
|
1127
|
+
await (0, import_promises.unlink)(temporary).catch(() => void 0);
|
|
1128
|
+
}
|
|
1129
|
+
if (published) {
|
|
1130
|
+
await (0, import_promises.unlink)(destination).catch(() => void 0);
|
|
1131
|
+
}
|
|
1132
|
+
throw error;
|
|
1133
|
+
}
|
|
1134
|
+
let closed = false;
|
|
1135
|
+
return Object.freeze({
|
|
1136
|
+
descriptor,
|
|
1137
|
+
async close() {
|
|
1138
|
+
if (closed) return;
|
|
1139
|
+
closed = true;
|
|
1140
|
+
await (0, import_promises.lstat)(destination).then(async (status) => {
|
|
1141
|
+
if (status.dev === descriptorIdentity.device && status.ino === descriptorIdentity.inode) {
|
|
1142
|
+
await (0, import_promises.unlink)(destination);
|
|
1143
|
+
}
|
|
1144
|
+
}).catch((error) => {
|
|
1145
|
+
if (error.code !== "ENOENT") {
|
|
1146
|
+
throw error;
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
await syncDirectory(directory);
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// src/external-handoff/fingerprint.ts
|
|
1155
|
+
var import_node_crypto5 = require("crypto");
|
|
1156
|
+
function canonicalJson(value) {
|
|
1157
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
1158
|
+
return JSON.stringify(value);
|
|
1159
|
+
}
|
|
1160
|
+
if (typeof value === "number") {
|
|
1161
|
+
if (!Number.isFinite(value)) throw new TypeError("Non-finite JSON number.");
|
|
1162
|
+
return JSON.stringify(value);
|
|
1163
|
+
}
|
|
1164
|
+
if (Array.isArray(value)) {
|
|
1165
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
1166
|
+
}
|
|
1167
|
+
if (typeof value === "object") {
|
|
1168
|
+
const record = value;
|
|
1169
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
1170
|
+
}
|
|
1171
|
+
throw new TypeError("Unsupported JSON value.");
|
|
1172
|
+
}
|
|
1173
|
+
function fingerprintExternalHandoffAnnotation(annotation) {
|
|
1174
|
+
return (0, import_node_crypto5.createHash)("sha256").update(canonicalJson(annotation)).digest("hex");
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// src/external-handoff/store.ts
|
|
1178
|
+
var import_node_crypto6 = require("crypto");
|
|
1179
|
+
var import_shared6 = require("@spotpatch/shared");
|
|
1180
|
+
function defaultRandomId2() {
|
|
1181
|
+
return (0, import_node_crypto6.randomBytes)(24).toString("base64url");
|
|
1182
|
+
}
|
|
1183
|
+
function pageSummary(annotation) {
|
|
1184
|
+
let origin = "[unavailable]";
|
|
1185
|
+
try {
|
|
1186
|
+
const url = new URL(annotation.page.url);
|
|
1187
|
+
origin = url.origin === "null" ? "[unavailable]" : url.origin;
|
|
1188
|
+
} catch {
|
|
1189
|
+
}
|
|
1190
|
+
return Object.freeze({ origin, pathname: annotation.page.pathname });
|
|
1191
|
+
}
|
|
1192
|
+
function summaryOf(current, state) {
|
|
1193
|
+
const snapshot2 = current.snapshot;
|
|
1194
|
+
return Object.freeze({
|
|
1195
|
+
sessionId: snapshot2.session.id,
|
|
1196
|
+
framework: snapshot2.session.framework,
|
|
1197
|
+
revision: snapshot2.revision,
|
|
1198
|
+
cursor: snapshot2.cursor,
|
|
1199
|
+
targetCount: snapshot2.annotation.targets.length,
|
|
1200
|
+
page: pageSummary(snapshot2.annotation),
|
|
1201
|
+
publishedAt: snapshot2.publishedAt,
|
|
1202
|
+
expiresAt: snapshot2.expiresAt,
|
|
1203
|
+
state,
|
|
1204
|
+
pickupCount: current.receipts.size,
|
|
1205
|
+
...current.pickedUpAt === void 0 ? {} : { pickedUpAt: current.pickedUpAt }
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
function replayResult(record) {
|
|
1209
|
+
return Object.freeze({ ...record.result, replayed: true });
|
|
1210
|
+
}
|
|
1211
|
+
function createExternalHandoffStore(options) {
|
|
1212
|
+
const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
|
|
1213
|
+
const randomId = options.randomId ?? defaultRandomId2;
|
|
1214
|
+
const history = [];
|
|
1215
|
+
const idempotency = /* @__PURE__ */ new Map();
|
|
1216
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
1217
|
+
let closed = false;
|
|
1218
|
+
let current;
|
|
1219
|
+
let revision = 0;
|
|
1220
|
+
const requireOpen = () => {
|
|
1221
|
+
if (closed) throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.SESSION_CLOSED);
|
|
1222
|
+
};
|
|
1223
|
+
const archive = (state) => {
|
|
1224
|
+
if (current === void 0) return;
|
|
1225
|
+
history.unshift(summaryOf(current, state));
|
|
1226
|
+
history.length = Math.min(
|
|
1227
|
+
history.length,
|
|
1228
|
+
import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumHistorySummaries
|
|
1229
|
+
);
|
|
1230
|
+
current = void 0;
|
|
1231
|
+
};
|
|
1232
|
+
const sweep = () => {
|
|
1233
|
+
const monotonicNow = clock.monotonicNow();
|
|
1234
|
+
if (current !== void 0 && monotonicNow >= current.expiresAtMonotonic) {
|
|
1235
|
+
archive("expired");
|
|
1236
|
+
}
|
|
1237
|
+
for (const [requestId, record] of idempotency) {
|
|
1238
|
+
if (monotonicNow >= record.expiresAtMonotonic) idempotency.delete(requestId);
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
const knownSummary = (cursor) => {
|
|
1242
|
+
sweep();
|
|
1243
|
+
if (current?.snapshot.cursor === cursor) {
|
|
1244
|
+
return summaryOf(current, "available");
|
|
1245
|
+
}
|
|
1246
|
+
return history.find((summary) => summary.cursor === cursor);
|
|
1247
|
+
};
|
|
1248
|
+
const readCurrent = (cursor) => {
|
|
1249
|
+
requireOpen();
|
|
1250
|
+
sweep();
|
|
1251
|
+
if (current === void 0) {
|
|
1252
|
+
const prior = cursor === void 0 ? void 0 : knownSummary(cursor);
|
|
1253
|
+
throw new import_shared6.SpotPatchError(
|
|
1254
|
+
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
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
if (cursor !== void 0 && cursor !== current.snapshot.cursor) {
|
|
1258
|
+
const prior = knownSummary(cursor);
|
|
1259
|
+
throw new import_shared6.SpotPatchError(
|
|
1260
|
+
prior?.state === "expired" ? import_shared6.ERROR_CODES.HANDOFF_EXPIRED : import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
return current.snapshot;
|
|
1264
|
+
};
|
|
1265
|
+
const findReplay = (requestId, fingerprint) => {
|
|
1266
|
+
requireOpen();
|
|
1267
|
+
sweep();
|
|
1268
|
+
const record = idempotency.get(requestId);
|
|
1269
|
+
if (record === void 0) return void 0;
|
|
1270
|
+
if (record.fingerprint !== fingerprint) {
|
|
1271
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
|
|
1272
|
+
}
|
|
1273
|
+
return replayResult(record);
|
|
1274
|
+
};
|
|
1275
|
+
const settleWaiters = (result) => {
|
|
1276
|
+
const pending = [...waiters];
|
|
1277
|
+
waiters.clear();
|
|
1278
|
+
for (const waiter of pending) waiter.resolve(result);
|
|
1279
|
+
};
|
|
1280
|
+
return Object.freeze({
|
|
1281
|
+
activeWaitCount: () => waiters.size,
|
|
1282
|
+
replay: findReplay,
|
|
1283
|
+
publish(input) {
|
|
1284
|
+
requireOpen();
|
|
1285
|
+
const replayed = findReplay(input.requestId, input.fingerprint);
|
|
1286
|
+
if (replayed !== void 0) return replayed;
|
|
1287
|
+
if (idempotency.size >= import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumRequestIdRecords) {
|
|
1288
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE);
|
|
1289
|
+
}
|
|
1290
|
+
const nextRevision = revision + 1;
|
|
1291
|
+
const publishedAtMs = clock.wallNow();
|
|
1292
|
+
const publishedAtMonotonic = clock.monotonicNow();
|
|
1293
|
+
const snapshot2 = Object.freeze({
|
|
1294
|
+
schemaVersion: import_shared6.EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
|
|
1295
|
+
cursor: randomId(),
|
|
1296
|
+
session: Object.freeze({ id: options.sessionId, framework: options.framework }),
|
|
1297
|
+
revision: nextRevision,
|
|
1298
|
+
publishedAt: new Date(publishedAtMs).toISOString(),
|
|
1299
|
+
expiresAt: new Date(
|
|
1300
|
+
publishedAtMs + import_shared6.EXTERNAL_HANDOFF_LIMITS.handoffTtlMs
|
|
1301
|
+
).toISOString(),
|
|
1302
|
+
annotation: input.annotation
|
|
1303
|
+
});
|
|
1304
|
+
if (Buffer.byteLength(JSON.stringify(snapshot2), "utf8") > import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumSnapshotBytes) {
|
|
1305
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE);
|
|
1306
|
+
}
|
|
1307
|
+
const delivery = input.reserve(snapshot2.cursor, nextRevision);
|
|
1308
|
+
if (current !== void 0) archive("superseded");
|
|
1309
|
+
revision = nextRevision;
|
|
1310
|
+
current = {
|
|
1311
|
+
expiresAtMonotonic: publishedAtMonotonic + import_shared6.EXTERNAL_HANDOFF_LIMITS.handoffTtlMs,
|
|
1312
|
+
receipts: /* @__PURE__ */ new Set(),
|
|
1313
|
+
snapshot: snapshot2
|
|
1314
|
+
};
|
|
1315
|
+
const result = Object.freeze({
|
|
1316
|
+
handoff: summaryOf(current, "available"),
|
|
1317
|
+
delivery,
|
|
1318
|
+
replayed: false
|
|
1319
|
+
});
|
|
1320
|
+
idempotency.set(
|
|
1321
|
+
input.requestId,
|
|
1322
|
+
Object.freeze({
|
|
1323
|
+
expiresAtMonotonic: publishedAtMonotonic + import_shared6.EXTERNAL_HANDOFF_LIMITS.requestIdTtlMs,
|
|
1324
|
+
fingerprint: input.fingerprint,
|
|
1325
|
+
result
|
|
1326
|
+
})
|
|
1327
|
+
);
|
|
1328
|
+
settleWaiters(Object.freeze({ outcome: "handoff", snapshot: snapshot2 }));
|
|
1329
|
+
return result;
|
|
1330
|
+
},
|
|
1331
|
+
current: readCurrent,
|
|
1332
|
+
currentCursor() {
|
|
1333
|
+
requireOpen();
|
|
1334
|
+
sweep();
|
|
1335
|
+
return current?.snapshot.cursor ?? null;
|
|
1336
|
+
},
|
|
1337
|
+
status(cursor) {
|
|
1338
|
+
requireOpen();
|
|
1339
|
+
sweep();
|
|
1340
|
+
if (cursor === void 0) {
|
|
1341
|
+
if (current === void 0) {
|
|
1342
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_NOT_FOUND);
|
|
1343
|
+
}
|
|
1344
|
+
return summaryOf(current, "available");
|
|
1345
|
+
}
|
|
1346
|
+
const summary = knownSummary(cursor);
|
|
1347
|
+
if (summary === void 0) {
|
|
1348
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID);
|
|
1349
|
+
}
|
|
1350
|
+
return summary;
|
|
1351
|
+
},
|
|
1352
|
+
ack(cursor, connectorInstanceId) {
|
|
1353
|
+
const snapshot2 = readCurrent(cursor);
|
|
1354
|
+
if (snapshot2 !== current?.snapshot) {
|
|
1355
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID);
|
|
1356
|
+
}
|
|
1357
|
+
if (!current.receipts.has(connectorInstanceId)) {
|
|
1358
|
+
if (current.receipts.size >= import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumConnectorReceipts) {
|
|
1359
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.BRIDGE_BUSY);
|
|
1360
|
+
}
|
|
1361
|
+
current.receipts.add(connectorInstanceId);
|
|
1362
|
+
current.pickedUpAt = new Date(clock.wallNow()).toISOString();
|
|
1363
|
+
}
|
|
1364
|
+
return summaryOf(current, "available");
|
|
1365
|
+
},
|
|
1366
|
+
async wait(afterCursor, timeoutMs, signal) {
|
|
1367
|
+
requireOpen();
|
|
1368
|
+
sweep();
|
|
1369
|
+
if (afterCursor === void 0 && current !== void 0) {
|
|
1370
|
+
return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
|
|
1371
|
+
}
|
|
1372
|
+
if (afterCursor !== void 0) {
|
|
1373
|
+
const known = knownSummary(afterCursor);
|
|
1374
|
+
if (known === void 0) {
|
|
1375
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_CURSOR_INVALID);
|
|
1376
|
+
}
|
|
1377
|
+
if (current !== void 0 && current.snapshot.cursor !== afterCursor) {
|
|
1378
|
+
return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (waiters.size >= import_shared6.EXTERNAL_HANDOFF_LIMITS.maximumWaiters) {
|
|
1382
|
+
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.BRIDGE_BUSY);
|
|
1383
|
+
}
|
|
1384
|
+
if (signal.aborted) throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.SESSION_CLOSED);
|
|
1385
|
+
return new Promise((resolve, reject) => {
|
|
1386
|
+
let settled = false;
|
|
1387
|
+
const finish = () => {
|
|
1388
|
+
if (settled) return false;
|
|
1389
|
+
settled = true;
|
|
1390
|
+
waiters.delete(waiter);
|
|
1391
|
+
clearTimeout(timer);
|
|
1392
|
+
signal.removeEventListener("abort", abort);
|
|
1393
|
+
return true;
|
|
1394
|
+
};
|
|
1395
|
+
const waiter = {
|
|
1396
|
+
reject(error) {
|
|
1397
|
+
if (finish()) reject(error);
|
|
1398
|
+
},
|
|
1399
|
+
resolve(result) {
|
|
1400
|
+
if (finish()) resolve(result);
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
const abort = () => {
|
|
1404
|
+
waiter.reject(new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.SESSION_CLOSED));
|
|
1405
|
+
};
|
|
1406
|
+
const timer = setTimeout(() => {
|
|
1407
|
+
waiter.resolve(Object.freeze({ outcome: "timeout" }));
|
|
1408
|
+
}, timeoutMs);
|
|
1409
|
+
timer.unref();
|
|
1410
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1411
|
+
waiters.add(waiter);
|
|
1412
|
+
});
|
|
1413
|
+
},
|
|
1414
|
+
close() {
|
|
1415
|
+
if (closed) return;
|
|
1416
|
+
closed = true;
|
|
1417
|
+
current = void 0;
|
|
1418
|
+
history.length = 0;
|
|
1419
|
+
idempotency.clear();
|
|
1420
|
+
const pending = [...waiters];
|
|
1421
|
+
waiters.clear();
|
|
1422
|
+
for (const waiter of pending) {
|
|
1423
|
+
waiter.reject(new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.SESSION_CLOSED));
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/external-handoff/service.ts
|
|
1430
|
+
function asReplay(result) {
|
|
1431
|
+
return Object.freeze({ ...result, replayed: true });
|
|
1432
|
+
}
|
|
1433
|
+
function createExternalHandoffService(options) {
|
|
1434
|
+
const activeRegistry = createActiveAdapterRegistry();
|
|
1435
|
+
const store = createExternalHandoffStore({
|
|
1436
|
+
framework: options.framework,
|
|
1437
|
+
sessionId: options.sessionId
|
|
1438
|
+
});
|
|
1439
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1440
|
+
let broker;
|
|
1441
|
+
let descriptor;
|
|
1442
|
+
let startPromise;
|
|
1443
|
+
let closePromise;
|
|
1444
|
+
let state = "idle";
|
|
1445
|
+
const isClosed = () => state === "closed";
|
|
1446
|
+
const requireReady = () => {
|
|
1447
|
+
if (state !== "ready" || broker?.isReady() !== true) {
|
|
1448
|
+
throw new import_shared7.SpotPatchError(
|
|
1449
|
+
state === "closed" ? import_shared7.ERROR_CODES.SESSION_CLOSED : import_shared7.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
const start = async () => {
|
|
1454
|
+
if (state === "ready") return;
|
|
1455
|
+
if (state === "closed") throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.SESSION_CLOSED);
|
|
1456
|
+
if (startPromise !== void 0) return startPromise;
|
|
1457
|
+
state = "starting";
|
|
1458
|
+
startPromise = (async () => {
|
|
1459
|
+
let createdBroker;
|
|
1460
|
+
let createdDescriptor;
|
|
1461
|
+
try {
|
|
1462
|
+
const projectKey = await (0, import_external_agent_node3.computeExternalHandoffProjectKey)(options.root);
|
|
1463
|
+
createdBroker = await createExternalHandoffBroker({
|
|
1464
|
+
activeRegistry,
|
|
1465
|
+
framework: options.framework,
|
|
1466
|
+
projectKey,
|
|
1467
|
+
sessionId: options.sessionId,
|
|
1468
|
+
store
|
|
1469
|
+
});
|
|
1470
|
+
createdDescriptor = await publishExternalHandoffDescriptor({
|
|
1471
|
+
bridgeToken: createdBroker.bridgeToken,
|
|
1472
|
+
endpoint: createdBroker.endpoint,
|
|
1473
|
+
framework: options.framework,
|
|
1474
|
+
root: options.root,
|
|
1475
|
+
sessionId: options.sessionId
|
|
1476
|
+
});
|
|
1477
|
+
if (isClosed()) {
|
|
1478
|
+
await createdDescriptor.close();
|
|
1479
|
+
await createdBroker.close();
|
|
1480
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.SESSION_CLOSED);
|
|
1481
|
+
}
|
|
1482
|
+
broker = createdBroker;
|
|
1483
|
+
descriptor = createdDescriptor;
|
|
1484
|
+
state = "ready";
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
if (createdDescriptor !== void 0 && descriptor !== createdDescriptor) {
|
|
1487
|
+
await createdDescriptor.close().catch(() => void 0);
|
|
1488
|
+
}
|
|
1489
|
+
if (createdBroker !== void 0 && broker !== createdBroker) {
|
|
1490
|
+
await createdBroker.close().catch(() => void 0);
|
|
1491
|
+
}
|
|
1492
|
+
if (!isClosed()) state = "failed";
|
|
1493
|
+
throw error;
|
|
1494
|
+
}
|
|
1495
|
+
})();
|
|
1496
|
+
return startPromise;
|
|
1497
|
+
};
|
|
1498
|
+
return Object.freeze({
|
|
1499
|
+
start,
|
|
1500
|
+
capability() {
|
|
1501
|
+
const currentCursor = store.currentCursor();
|
|
1502
|
+
const active = activeRegistry.snapshot(currentCursor ?? void 0);
|
|
1503
|
+
return Object.freeze({
|
|
1504
|
+
enabled: true,
|
|
1505
|
+
brokerReady: state === "ready" && broker?.isReady() === true,
|
|
1506
|
+
activeWaitCount: store.activeWaitCount(),
|
|
1507
|
+
snapshotSchemaVersion: import_shared7.EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
|
|
1508
|
+
brokerProtocolVersion: import_shared7.EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
|
|
1509
|
+
activeAdapter: active.activeAdapter,
|
|
1510
|
+
dispatch: currentCursor === null ? null : active.dispatch
|
|
1511
|
+
});
|
|
1512
|
+
},
|
|
1513
|
+
async publish(request, authorize) {
|
|
1514
|
+
requireReady();
|
|
1515
|
+
const fingerprint = fingerprintExternalHandoffAnnotation(request.annotation);
|
|
1516
|
+
const replayed = store.replay(request.requestId, fingerprint);
|
|
1517
|
+
if (replayed !== void 0) return replayed;
|
|
1518
|
+
const pending = inFlight.get(request.requestId);
|
|
1519
|
+
if (pending !== void 0) {
|
|
1520
|
+
if (pending.fingerprint !== fingerprint) {
|
|
1521
|
+
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
|
|
1522
|
+
}
|
|
1523
|
+
return asReplay(await pending.promise);
|
|
1524
|
+
}
|
|
1525
|
+
activeRegistry.assertPublishable();
|
|
1526
|
+
const promise = (async () => {
|
|
1527
|
+
const annotation = await authorize(request.annotation);
|
|
1528
|
+
return store.publish({
|
|
1529
|
+
annotation,
|
|
1530
|
+
fingerprint,
|
|
1531
|
+
requestId: request.requestId,
|
|
1532
|
+
reserve: activeRegistry.reserve
|
|
1533
|
+
});
|
|
1534
|
+
})();
|
|
1535
|
+
const activePublish = Object.freeze({ fingerprint, promise });
|
|
1536
|
+
inFlight.set(request.requestId, activePublish);
|
|
1537
|
+
try {
|
|
1538
|
+
return await promise;
|
|
1539
|
+
} finally {
|
|
1540
|
+
if (inFlight.get(request.requestId) === activePublish) {
|
|
1541
|
+
inFlight.delete(request.requestId);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
status(cursor) {
|
|
1546
|
+
requireReady();
|
|
1547
|
+
const handoff = store.status(cursor);
|
|
1548
|
+
const active = activeRegistry.snapshot(cursor ?? handoff.cursor);
|
|
1549
|
+
return Object.freeze({
|
|
1550
|
+
handoff,
|
|
1551
|
+
activeAdapter: active.activeAdapter,
|
|
1552
|
+
dispatch: active.dispatch
|
|
1553
|
+
});
|
|
1554
|
+
},
|
|
1555
|
+
resolveDelivery(cursor) {
|
|
1556
|
+
requireReady();
|
|
1557
|
+
const handoff = store.status(cursor);
|
|
1558
|
+
const active = activeRegistry.resolveDelivery(cursor);
|
|
1559
|
+
return Object.freeze({
|
|
1560
|
+
handoff,
|
|
1561
|
+
activeAdapter: active.activeAdapter,
|
|
1562
|
+
dispatch: active.dispatch
|
|
1563
|
+
});
|
|
1564
|
+
},
|
|
1565
|
+
close() {
|
|
1566
|
+
closePromise ??= (async () => {
|
|
1567
|
+
if (state === "closed") return;
|
|
1568
|
+
state = "closed";
|
|
1569
|
+
activeRegistry.close();
|
|
1570
|
+
store.close();
|
|
1571
|
+
inFlight.clear();
|
|
1572
|
+
await startPromise?.catch(() => void 0);
|
|
1573
|
+
const publishedDescriptor = descriptor;
|
|
1574
|
+
descriptor = void 0;
|
|
1575
|
+
const activeBroker = broker;
|
|
1576
|
+
broker = void 0;
|
|
1577
|
+
await publishedDescriptor?.close().catch(() => void 0);
|
|
1578
|
+
await activeBroker?.close().catch(() => void 0);
|
|
1579
|
+
})();
|
|
1580
|
+
return closePromise;
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
|
|
524
1585
|
// src/environment-ai.ts
|
|
525
1586
|
var AI_ENVIRONMENT_NAMES = Object.freeze({
|
|
526
1587
|
authentication: "SPOTPATCH_AI_AUTHENTICATION",
|
|
@@ -611,46 +1672,46 @@ function resolveEnvironmentAiConfiguration(environment) {
|
|
|
611
1672
|
}
|
|
612
1673
|
|
|
613
1674
|
// src/integration/file-plan.ts
|
|
614
|
-
var
|
|
615
|
-
var
|
|
616
|
-
var
|
|
1675
|
+
var import_node_crypto7 = require("crypto");
|
|
1676
|
+
var import_promises2 = require("fs/promises");
|
|
1677
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
617
1678
|
function isMissingPathError(error) {
|
|
618
1679
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
619
1680
|
}
|
|
620
1681
|
function isPathWithin(root, target) {
|
|
621
|
-
const relative =
|
|
622
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
1682
|
+
const relative = import_node_path2.default.relative(root, target);
|
|
1683
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path2.default.sep}`) && !import_node_path2.default.isAbsolute(relative);
|
|
623
1684
|
}
|
|
624
1685
|
function relativePathWithin(root, target) {
|
|
625
|
-
const relative =
|
|
1686
|
+
const relative = import_node_path2.default.relative(root, target);
|
|
626
1687
|
if (relative.length === 0 || !isPathWithin(root, target)) {
|
|
627
1688
|
throw new Error("SpotPatch init refuses to modify a path outside the app root.");
|
|
628
1689
|
}
|
|
629
|
-
return relative.split(
|
|
1690
|
+
return relative.split(import_node_path2.default.sep).join("/");
|
|
630
1691
|
}
|
|
631
1692
|
async function integrationPathExists(absolutePath) {
|
|
632
1693
|
try {
|
|
633
|
-
await (0,
|
|
1694
|
+
await (0, import_promises2.access)(absolutePath);
|
|
634
1695
|
return true;
|
|
635
1696
|
} catch {
|
|
636
1697
|
return false;
|
|
637
1698
|
}
|
|
638
1699
|
}
|
|
639
1700
|
async function readIntegrationFile(absolutePath) {
|
|
640
|
-
const metadata = await (0,
|
|
1701
|
+
const metadata = await (0, import_promises2.lstat)(absolutePath);
|
|
641
1702
|
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
642
1703
|
throw new Error(
|
|
643
|
-
`SpotPatch refuses to modify the non-regular file ${
|
|
1704
|
+
`SpotPatch refuses to modify the non-regular file ${import_node_path2.default.basename(absolutePath)}.`
|
|
644
1705
|
);
|
|
645
1706
|
}
|
|
646
|
-
return (0,
|
|
1707
|
+
return (0, import_promises2.readFile)(absolutePath, "utf8");
|
|
647
1708
|
}
|
|
648
1709
|
function createIntegrationFileChange(appRoot, absolutePath, nextContent, previousContent) {
|
|
649
1710
|
if (previousContent === nextContent) {
|
|
650
1711
|
return void 0;
|
|
651
1712
|
}
|
|
652
|
-
const root =
|
|
653
|
-
const target =
|
|
1713
|
+
const root = import_node_path2.default.resolve(appRoot);
|
|
1714
|
+
const target = import_node_path2.default.resolve(absolutePath);
|
|
654
1715
|
return Object.freeze({
|
|
655
1716
|
absolutePath: target,
|
|
656
1717
|
nextContent,
|
|
@@ -659,19 +1720,19 @@ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previou
|
|
|
659
1720
|
});
|
|
660
1721
|
}
|
|
661
1722
|
function temporaryPath(absolutePath, label) {
|
|
662
|
-
return
|
|
663
|
-
|
|
664
|
-
`.${
|
|
1723
|
+
return import_node_path2.default.join(
|
|
1724
|
+
import_node_path2.default.dirname(absolutePath),
|
|
1725
|
+
`.${import_node_path2.default.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${(0, import_node_crypto7.randomBytes)(8).toString("hex")}`
|
|
665
1726
|
);
|
|
666
1727
|
}
|
|
667
1728
|
async function writeAtomic(absolutePath, content, mode) {
|
|
668
|
-
await (0,
|
|
1729
|
+
await (0, import_promises2.mkdir)(import_node_path2.default.dirname(absolutePath), { recursive: true });
|
|
669
1730
|
const stagedPath = temporaryPath(absolutePath, "stage");
|
|
670
1731
|
try {
|
|
671
|
-
await (0,
|
|
672
|
-
await (0,
|
|
1732
|
+
await (0, import_promises2.writeFile)(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
|
|
1733
|
+
await (0, import_promises2.rename)(stagedPath, absolutePath);
|
|
673
1734
|
} catch (error) {
|
|
674
|
-
await (0,
|
|
1735
|
+
await (0, import_promises2.unlink)(stagedPath).catch(() => void 0);
|
|
675
1736
|
throw error;
|
|
676
1737
|
}
|
|
677
1738
|
}
|
|
@@ -683,21 +1744,21 @@ async function rollbackChange(change) {
|
|
|
683
1744
|
);
|
|
684
1745
|
}
|
|
685
1746
|
if (change.previousContent === void 0) {
|
|
686
|
-
await (0,
|
|
1747
|
+
await (0, import_promises2.unlink)(change.absolutePath);
|
|
687
1748
|
return;
|
|
688
1749
|
}
|
|
689
|
-
const mode = (await (0,
|
|
1750
|
+
const mode = (await (0, import_promises2.stat)(change.absolutePath)).mode & 511;
|
|
690
1751
|
await writeAtomic(change.absolutePath, change.previousContent, mode);
|
|
691
1752
|
}
|
|
692
1753
|
async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
693
|
-
const target =
|
|
1754
|
+
const target = import_node_path2.default.resolve(change.absolutePath);
|
|
694
1755
|
const relativePath = relativePathWithin(appRoot, target);
|
|
695
|
-
if (target !== change.absolutePath || relativePath !== change.relativePath ||
|
|
1756
|
+
if (target !== change.absolutePath || relativePath !== change.relativePath || import_node_path2.default.dirname(target) === target) {
|
|
696
1757
|
throw new Error("SpotPatch init received an invalid integration file plan.");
|
|
697
1758
|
}
|
|
698
1759
|
let targetMetadata;
|
|
699
1760
|
try {
|
|
700
|
-
targetMetadata = await (0,
|
|
1761
|
+
targetMetadata = await (0, import_promises2.lstat)(target);
|
|
701
1762
|
} catch (error) {
|
|
702
1763
|
if (!isMissingPathError(error)) {
|
|
703
1764
|
throw error;
|
|
@@ -708,8 +1769,8 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
|
708
1769
|
`SpotPatch refuses to modify the symbolic link ${change.relativePath}.`
|
|
709
1770
|
);
|
|
710
1771
|
}
|
|
711
|
-
const containmentAnchor = await (0,
|
|
712
|
-
targetMetadata === void 0 ?
|
|
1772
|
+
const containmentAnchor = await (0, import_promises2.realpath)(
|
|
1773
|
+
targetMetadata === void 0 ? import_node_path2.default.dirname(target) : target
|
|
713
1774
|
);
|
|
714
1775
|
if (!isPathWithin(realAppRoot, containmentAnchor)) {
|
|
715
1776
|
throw new Error("SpotPatch init refuses to modify a path outside the app root.");
|
|
@@ -718,7 +1779,7 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
|
718
1779
|
async function assertCurrentBaseline(change) {
|
|
719
1780
|
if (change.previousContent === void 0) {
|
|
720
1781
|
try {
|
|
721
|
-
await (0,
|
|
1782
|
+
await (0, import_promises2.lstat)(change.absolutePath);
|
|
722
1783
|
} catch (error) {
|
|
723
1784
|
if (isMissingPathError(error)) {
|
|
724
1785
|
return;
|
|
@@ -740,8 +1801,8 @@ async function applyIntegrationPlan(plan) {
|
|
|
740
1801
|
if (plan.changes.length === 0) {
|
|
741
1802
|
return;
|
|
742
1803
|
}
|
|
743
|
-
const appRoot =
|
|
744
|
-
const realAppRoot = await (0,
|
|
1804
|
+
const appRoot = import_node_path2.default.resolve(plan.appRoot);
|
|
1805
|
+
const realAppRoot = await (0, import_promises2.realpath)(appRoot);
|
|
745
1806
|
const targets = /* @__PURE__ */ new Set();
|
|
746
1807
|
for (const change of plan.changes) {
|
|
747
1808
|
if (targets.has(change.absolutePath)) {
|
|
@@ -755,7 +1816,7 @@ async function applyIntegrationPlan(plan) {
|
|
|
755
1816
|
try {
|
|
756
1817
|
for (const change of plan.changes) {
|
|
757
1818
|
await assertCurrentBaseline(change);
|
|
758
|
-
const mode = change.previousContent === void 0 ? 384 : (await (0,
|
|
1819
|
+
const mode = change.previousContent === void 0 ? 384 : (await (0, import_promises2.stat)(change.absolutePath)).mode & 511;
|
|
759
1820
|
await writeAtomic(change.absolutePath, change.nextContent, mode);
|
|
760
1821
|
applied.push(change);
|
|
761
1822
|
}
|
|
@@ -776,7 +1837,7 @@ async function applyIntegrationPlan(plan) {
|
|
|
776
1837
|
}
|
|
777
1838
|
|
|
778
1839
|
// src/options.ts
|
|
779
|
-
var
|
|
1840
|
+
var import_shared8 = require("@spotpatch/shared");
|
|
780
1841
|
var import_zod = require("zod");
|
|
781
1842
|
var DEFAULT_EXCLUDE = Object.freeze([
|
|
782
1843
|
/node_modules/,
|
|
@@ -811,8 +1872,9 @@ var DEFAULT_OPTIONS = Object.freeze({
|
|
|
811
1872
|
dataFlow: Object.freeze({
|
|
812
1873
|
enabled: false,
|
|
813
1874
|
runtime: "dispatch",
|
|
814
|
-
limits:
|
|
815
|
-
})
|
|
1875
|
+
limits: import_shared8.DEFAULT_DATA_FLOW_LIMITS
|
|
1876
|
+
}),
|
|
1877
|
+
externalAgent: Object.freeze({ enabled: false })
|
|
816
1878
|
});
|
|
817
1879
|
var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
818
1880
|
var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
@@ -857,7 +1919,7 @@ var aiOptionsSchema = import_zod.z.strictObject({
|
|
|
857
1919
|
defaultProvider: import_zod.z.string(),
|
|
858
1920
|
execution: import_zod.z.strictObject({
|
|
859
1921
|
isolation: import_zod.z.literal("git-worktree").optional(),
|
|
860
|
-
applyMode: import_zod.z.enum(
|
|
1922
|
+
applyMode: import_zod.z.enum(import_shared8.AGENT_APPLY_MODES).optional(),
|
|
861
1923
|
checks: import_zod.z.record(import_zod.z.string(), agentCheckSchema).optional(),
|
|
862
1924
|
limits: agentLimitsSchema
|
|
863
1925
|
}).optional()
|
|
@@ -903,18 +1965,18 @@ function normalizeProviderBaseURL(value) {
|
|
|
903
1965
|
}
|
|
904
1966
|
function resolveLimits(limits) {
|
|
905
1967
|
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 ??
|
|
1968
|
+
maxTurns: limits?.maxTurns ?? import_shared8.DEFAULT_AGENT_LIMITS.maxTurns,
|
|
1969
|
+
maxToolCalls: limits?.maxToolCalls ?? import_shared8.DEFAULT_AGENT_LIMITS.maxToolCalls,
|
|
1970
|
+
maxChangedFiles: limits?.maxChangedFiles ?? import_shared8.DEFAULT_AGENT_LIMITS.maxChangedFiles,
|
|
1971
|
+
maxDiffBytes: limits?.maxDiffBytes ?? import_shared8.DEFAULT_AGENT_LIMITS.maxDiffBytes,
|
|
1972
|
+
maxReadBytesPerFile: limits?.maxReadBytesPerFile ?? import_shared8.DEFAULT_AGENT_LIMITS.maxReadBytesPerFile,
|
|
1973
|
+
maxToolOutputCharacters: limits?.maxToolOutputCharacters ?? import_shared8.DEFAULT_AGENT_LIMITS.maxToolOutputCharacters,
|
|
1974
|
+
maxProviderResponseBytes: limits?.maxProviderResponseBytes ?? import_shared8.DEFAULT_AGENT_LIMITS.maxProviderResponseBytes,
|
|
1975
|
+
providerConnectTimeoutMs: limits?.providerConnectTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.providerConnectTimeoutMs,
|
|
1976
|
+
providerFirstByteTimeoutMs: limits?.providerFirstByteTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.providerFirstByteTimeoutMs,
|
|
1977
|
+
providerIdleTimeoutMs: limits?.providerIdleTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.providerIdleTimeoutMs,
|
|
1978
|
+
checkTimeoutMs: limits?.checkTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.checkTimeoutMs,
|
|
1979
|
+
jobTimeoutMs: limits?.jobTimeoutMs ?? import_shared8.DEFAULT_AGENT_LIMITS.jobTimeoutMs
|
|
918
1980
|
});
|
|
919
1981
|
for (const [name, value] of Object.entries(resolved)) {
|
|
920
1982
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
@@ -1119,7 +2181,7 @@ function resolveDataFlowOptions(options) {
|
|
|
1119
2181
|
return Object.freeze({
|
|
1120
2182
|
enabled: true,
|
|
1121
2183
|
runtime,
|
|
1122
|
-
limits:
|
|
2184
|
+
limits: import_shared8.DEFAULT_DATA_FLOW_LIMITS
|
|
1123
2185
|
});
|
|
1124
2186
|
}
|
|
1125
2187
|
function createRuntimeDataFlowConfig(options) {
|
|
@@ -1138,6 +2200,9 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1138
2200
|
if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
|
|
1139
2201
|
throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
|
|
1140
2202
|
}
|
|
2203
|
+
if (options.externalAgent !== void 0 && typeof options.externalAgent !== "boolean") {
|
|
2204
|
+
throw new RangeError("SpotPatch externalAgent must be a boolean.");
|
|
2205
|
+
}
|
|
1141
2206
|
const budget = Object.freeze({
|
|
1142
2207
|
...DEFAULT_OPTIONS.budget,
|
|
1143
2208
|
...options.budget
|
|
@@ -1146,15 +2211,15 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1146
2211
|
const maxTargets = options.maxTargets ?? DEFAULT_OPTIONS.maxTargets;
|
|
1147
2212
|
const locale = options.locale ?? DEFAULT_OPTIONS.locale;
|
|
1148
2213
|
const editor = options.editor ?? DEFAULT_OPTIONS.editor;
|
|
1149
|
-
if (!
|
|
2214
|
+
if (!import_shared8.SPOTPATCH_LOCALE_PREFERENCES.includes(locale)) {
|
|
1150
2215
|
throw new RangeError("SpotPatch locale must be auto, en-US, or zh-CN.");
|
|
1151
2216
|
}
|
|
1152
|
-
if (!
|
|
2217
|
+
if (!import_shared8.SPOTPATCH_EDITOR_PREFERENCES.includes(editor)) {
|
|
1153
2218
|
throw new RangeError("SpotPatch editor must be auto, vscode, or cursor.");
|
|
1154
2219
|
}
|
|
1155
|
-
if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets >
|
|
2220
|
+
if (!Number.isSafeInteger(maxTargets) || maxTargets < 1 || maxTargets > import_shared8.MAX_ANNOTATION_TARGETS) {
|
|
1156
2221
|
throw new RangeError(
|
|
1157
|
-
`SpotPatch maxTargets must be an integer between 1 and ${String(
|
|
2222
|
+
`SpotPatch maxTargets must be an integer between 1 and ${String(import_shared8.MAX_ANNOTATION_TARGETS)}.`
|
|
1158
2223
|
);
|
|
1159
2224
|
}
|
|
1160
2225
|
const resolved = {
|
|
@@ -1170,7 +2235,10 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1170
2235
|
locale,
|
|
1171
2236
|
maxTargets,
|
|
1172
2237
|
ai: resolveAiOptions(options.ai ?? environmentAi),
|
|
1173
|
-
dataFlow: resolveDataFlowOptions(options.dataFlow)
|
|
2238
|
+
dataFlow: resolveDataFlowOptions(options.dataFlow),
|
|
2239
|
+
externalAgent: Object.freeze({
|
|
2240
|
+
enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
|
|
2241
|
+
})
|
|
1174
2242
|
};
|
|
1175
2243
|
if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
|
|
1176
2244
|
throw new RangeError("SpotPatch shortcut is invalid.");
|
|
@@ -1180,10 +2248,11 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1180
2248
|
|
|
1181
2249
|
// src/project-validation.ts
|
|
1182
2250
|
var import_node_child_process = require("child_process");
|
|
1183
|
-
var
|
|
2251
|
+
var import_promises3 = require("fs/promises");
|
|
1184
2252
|
var import_node_module = require("module");
|
|
1185
|
-
var
|
|
2253
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
1186
2254
|
var import_node_util = require("util");
|
|
2255
|
+
var import_shared9 = require("@spotpatch/shared");
|
|
1187
2256
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
1188
2257
|
var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
|
|
1189
2258
|
var TYPESCRIPT_CHECK_LABEL = "TypeScript";
|
|
@@ -1192,19 +2261,19 @@ function isRecord(value) {
|
|
|
1192
2261
|
}
|
|
1193
2262
|
async function isRegularFile(absolutePath) {
|
|
1194
2263
|
try {
|
|
1195
|
-
const metadata = await (0,
|
|
2264
|
+
const metadata = await (0, import_promises3.lstat)(absolutePath);
|
|
1196
2265
|
return metadata.isFile() && !metadata.isSymbolicLink();
|
|
1197
2266
|
} catch {
|
|
1198
2267
|
return false;
|
|
1199
2268
|
}
|
|
1200
2269
|
}
|
|
1201
2270
|
async function readManifest(appRoot) {
|
|
1202
|
-
const manifestPath =
|
|
2271
|
+
const manifestPath = import_node_path3.default.join(appRoot, "package.json");
|
|
1203
2272
|
if (!await isRegularFile(manifestPath)) {
|
|
1204
2273
|
return void 0;
|
|
1205
2274
|
}
|
|
1206
2275
|
try {
|
|
1207
|
-
const value = JSON.parse(await (0,
|
|
2276
|
+
const value = JSON.parse(await (0, import_promises3.readFile)(manifestPath, "utf8"));
|
|
1208
2277
|
return isRecord(value) ? value : void 0;
|
|
1209
2278
|
} catch {
|
|
1210
2279
|
return void 0;
|
|
@@ -1227,9 +2296,9 @@ async function findGitRoot(appRoot) {
|
|
|
1227
2296
|
timeout: 5e3,
|
|
1228
2297
|
windowsHide: true
|
|
1229
2298
|
});
|
|
1230
|
-
const root = await (0,
|
|
1231
|
-
const relative =
|
|
1232
|
-
if (relative === "" || !relative.startsWith(`..${
|
|
2299
|
+
const root = await (0, import_promises3.realpath)(result.stdout.trim());
|
|
2300
|
+
const relative = import_node_path3.default.relative(root, appRoot);
|
|
2301
|
+
if (relative === "" || !relative.startsWith(`..${import_node_path3.default.sep}`) && relative !== ".." && !import_node_path3.default.isAbsolute(relative)) {
|
|
1233
2302
|
return root;
|
|
1234
2303
|
}
|
|
1235
2304
|
} catch {
|
|
@@ -1238,22 +2307,31 @@ async function findGitRoot(appRoot) {
|
|
|
1238
2307
|
return void 0;
|
|
1239
2308
|
}
|
|
1240
2309
|
async function resolveTypeScriptCli(appRoot) {
|
|
1241
|
-
const resolveFromApplication = (0, import_node_module.createRequire)(
|
|
2310
|
+
const resolveFromApplication = (0, import_node_module.createRequire)(import_node_path3.default.join(appRoot, "package.json"));
|
|
1242
2311
|
try {
|
|
1243
2312
|
const packagePath = resolveFromApplication.resolve("typescript/package.json");
|
|
1244
|
-
const cliPath =
|
|
1245
|
-
await (0,
|
|
1246
|
-
return await (0,
|
|
2313
|
+
const cliPath = import_node_path3.default.join(import_node_path3.default.dirname(packagePath), "bin", "tsc");
|
|
2314
|
+
await (0, import_promises3.access)(cliPath);
|
|
2315
|
+
return await (0, import_promises3.realpath)(cliPath);
|
|
1247
2316
|
} catch {
|
|
1248
2317
|
return void 0;
|
|
1249
2318
|
}
|
|
1250
2319
|
}
|
|
1251
2320
|
function portableRelativePath(from, to) {
|
|
1252
|
-
return
|
|
2321
|
+
return import_node_path3.default.relative(from, to).split(import_node_path3.default.sep).join("/");
|
|
2322
|
+
}
|
|
2323
|
+
function hasRequiredCheck(checks) {
|
|
2324
|
+
return Object.values(checks).some((check) => check.required);
|
|
2325
|
+
}
|
|
2326
|
+
function availableCheckId(checks, preferred) {
|
|
2327
|
+
if (checks[preferred] === void 0) return preferred;
|
|
2328
|
+
let suffix = 2;
|
|
2329
|
+
while (checks[`${preferred}-${String(suffix)}`] !== void 0) suffix += 1;
|
|
2330
|
+
return `${preferred}-${String(suffix)}`;
|
|
1253
2331
|
}
|
|
1254
2332
|
async function discoverProjectValidationCheck(options) {
|
|
1255
|
-
const appRoot = await (0,
|
|
1256
|
-
const tsconfigPath =
|
|
2333
|
+
const appRoot = await (0, import_promises3.realpath)(options.appRoot);
|
|
2334
|
+
const tsconfigPath = import_node_path3.default.join(appRoot, "tsconfig.json");
|
|
1257
2335
|
const [manifest, projectRoot, hasTsconfig] = await Promise.all([
|
|
1258
2336
|
readManifest(appRoot),
|
|
1259
2337
|
findGitRoot(appRoot),
|
|
@@ -1279,6 +2357,8 @@ async function discoverProjectValidationCheck(options) {
|
|
|
1279
2357
|
"--noEmit",
|
|
1280
2358
|
"--pretty",
|
|
1281
2359
|
"false",
|
|
2360
|
+
"--incremental",
|
|
2361
|
+
"false",
|
|
1282
2362
|
"--project",
|
|
1283
2363
|
projectPath
|
|
1284
2364
|
]),
|
|
@@ -1286,21 +2366,30 @@ async function discoverProjectValidationCheck(options) {
|
|
|
1286
2366
|
timeoutMs: options.timeoutMs
|
|
1287
2367
|
});
|
|
1288
2368
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
2369
|
+
async function resolveProjectValidationChecks(options) {
|
|
2370
|
+
if (hasRequiredCheck(options.checks)) return options.checks;
|
|
2371
|
+
const discovered = await discoverProjectValidationCheck(options);
|
|
2372
|
+
if (discovered === void 0) return options.checks;
|
|
2373
|
+
const id = availableCheckId(options.checks, discovered.id);
|
|
2374
|
+
return Object.freeze({
|
|
2375
|
+
...options.checks,
|
|
2376
|
+
[id]: Object.freeze({ ...discovered, id })
|
|
2377
|
+
});
|
|
1293
2378
|
}
|
|
1294
|
-
function
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
2379
|
+
async function resolveManagedExecutionValidation(options) {
|
|
2380
|
+
const checks = options.ai === false ? Object.freeze({}) : options.ai.execution.checks;
|
|
2381
|
+
const limits = options.ai === false ? import_shared9.DEFAULT_AGENT_LIMITS : options.ai.execution.limits;
|
|
2382
|
+
return Object.freeze({
|
|
2383
|
+
checks: await resolveProjectValidationChecks({
|
|
2384
|
+
appRoot: options.appRoot,
|
|
2385
|
+
checks,
|
|
2386
|
+
timeoutMs: limits.checkTimeoutMs
|
|
2387
|
+
}),
|
|
2388
|
+
limits
|
|
2389
|
+
});
|
|
1303
2390
|
}
|
|
2391
|
+
|
|
2392
|
+
// src/project-options.ts
|
|
1304
2393
|
async function resolveProjectOptions(input) {
|
|
1305
2394
|
const userOptions = input.options ?? {};
|
|
1306
2395
|
const resolved = resolveOptions(userOptions, input.environmentAi);
|
|
@@ -1312,22 +2401,15 @@ async function resolveProjectOptions(input) {
|
|
|
1312
2401
|
"SpotPatch trustedFastMode cannot be combined with applyMode auto."
|
|
1313
2402
|
);
|
|
1314
2403
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
);
|
|
1325
|
-
}
|
|
1326
|
-
const id = availableCheckId(checks, discovered.id);
|
|
1327
|
-
checks = Object.freeze({
|
|
1328
|
-
...checks,
|
|
1329
|
-
[id]: Object.freeze({ ...discovered, id })
|
|
1330
|
-
});
|
|
2404
|
+
const checks = await resolveProjectValidationChecks({
|
|
2405
|
+
appRoot: input.appRoot,
|
|
2406
|
+
checks: resolved.ai.execution.checks,
|
|
2407
|
+
timeoutMs: resolved.ai.execution.limits.checkTimeoutMs
|
|
2408
|
+
});
|
|
2409
|
+
if (!Object.values(checks).some((check) => check.required)) {
|
|
2410
|
+
throw new RangeError(
|
|
2411
|
+
"SpotPatch trustedFastMode requires a configured required check or a local TypeScript project with tsconfig.json."
|
|
2412
|
+
);
|
|
1331
2413
|
}
|
|
1332
2414
|
const ai = Object.freeze({
|
|
1333
2415
|
...resolved.ai,
|
|
@@ -1341,16 +2423,16 @@ async function resolveProjectOptions(input) {
|
|
|
1341
2423
|
}
|
|
1342
2424
|
|
|
1343
2425
|
// src/registry/source-registry.ts
|
|
1344
|
-
var
|
|
2426
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
1345
2427
|
|
|
1346
2428
|
// src/registry/source-id.ts
|
|
1347
|
-
var
|
|
2429
|
+
var import_node_crypto8 = require("crypto");
|
|
1348
2430
|
var SOURCE_ID_BYTES = 8;
|
|
1349
|
-
var createRandomSourceId = () => (0,
|
|
2431
|
+
var createRandomSourceId = () => (0, import_node_crypto8.randomBytes)(SOURCE_ID_BYTES).toString("base64url");
|
|
1350
2432
|
|
|
1351
2433
|
// src/registry/source-registry.ts
|
|
1352
2434
|
function normalizeAbsolutePath(absolutePath) {
|
|
1353
|
-
return
|
|
2435
|
+
return import_node_path4.default.normalize(import_node_path4.default.resolve(absolutePath));
|
|
1354
2436
|
}
|
|
1355
2437
|
function createSourceRegistry(options = {}) {
|
|
1356
2438
|
const createId = options.createId ?? createRandomSourceId;
|
|
@@ -1407,20 +2489,20 @@ function createSourceRegistry(options = {}) {
|
|
|
1407
2489
|
}
|
|
1408
2490
|
|
|
1409
2491
|
// src/server/middleware.ts
|
|
1410
|
-
var
|
|
2492
|
+
var import_shared20 = require("@spotpatch/shared");
|
|
1411
2493
|
|
|
1412
|
-
// src/
|
|
1413
|
-
var
|
|
2494
|
+
// src/external-handoff/browser-http.ts
|
|
2495
|
+
var import_shared13 = require("@spotpatch/shared");
|
|
1414
2496
|
|
|
1415
|
-
// src/server/
|
|
1416
|
-
var
|
|
1417
|
-
var
|
|
1418
|
-
var
|
|
2497
|
+
// src/server/annotation-authorizer.ts
|
|
2498
|
+
var import_promises6 = require("fs/promises");
|
|
2499
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2500
|
+
var import_shared12 = require("@spotpatch/shared");
|
|
1419
2501
|
|
|
1420
2502
|
// src/server/source-context.ts
|
|
1421
|
-
var
|
|
1422
|
-
var
|
|
1423
|
-
var
|
|
2503
|
+
var import_promises5 = require("fs/promises");
|
|
2504
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
2505
|
+
var import_shared11 = require("@spotpatch/shared");
|
|
1424
2506
|
|
|
1425
2507
|
// src/server/extract-code-context.ts
|
|
1426
2508
|
var import_oxc_parser = require("oxc-parser");
|
|
@@ -1649,16 +2731,9 @@ function extractCodeContext(options) {
|
|
|
1649
2731
|
}
|
|
1650
2732
|
|
|
1651
2733
|
// 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
|
|
2734
|
+
var import_promises4 = require("fs/promises");
|
|
2735
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
2736
|
+
var import_shared10 = require("@spotpatch/shared");
|
|
1662
2737
|
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
1663
2738
|
function isMissingFileError(error) {
|
|
1664
2739
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
@@ -1668,56 +2743,56 @@ async function assertInsideRoot(root, candidate) {
|
|
|
1668
2743
|
let realCandidate;
|
|
1669
2744
|
try {
|
|
1670
2745
|
[realRoot, realCandidate] = await Promise.all([
|
|
1671
|
-
(0,
|
|
1672
|
-
(0,
|
|
2746
|
+
(0, import_promises4.realpath)(root),
|
|
2747
|
+
(0, import_promises4.realpath)(candidate)
|
|
1673
2748
|
]);
|
|
1674
2749
|
} catch (error) {
|
|
1675
2750
|
if (isMissingFileError(error)) {
|
|
1676
|
-
throw new
|
|
2751
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
1677
2752
|
cause: error
|
|
1678
2753
|
});
|
|
1679
2754
|
}
|
|
1680
2755
|
throw error;
|
|
1681
2756
|
}
|
|
1682
|
-
const relative =
|
|
1683
|
-
const outside = relative.startsWith(`..${
|
|
2757
|
+
const relative = import_node_path5.default.relative(realRoot, realCandidate);
|
|
2758
|
+
const outside = relative.startsWith(`..${import_node_path5.default.sep}`) || relative === ".." || import_node_path5.default.isAbsolute(relative);
|
|
1684
2759
|
if (outside) {
|
|
1685
|
-
throw new
|
|
2760
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.SOURCE_OUTSIDE_ROOT);
|
|
1686
2761
|
}
|
|
1687
2762
|
return realCandidate;
|
|
1688
2763
|
}
|
|
1689
2764
|
async function resolveSourceFile(options) {
|
|
1690
2765
|
const registeredPath = options.registry.resolve(options.fileId);
|
|
1691
2766
|
if (registeredPath === void 0) {
|
|
1692
|
-
throw new
|
|
2767
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
1693
2768
|
}
|
|
1694
2769
|
const sourcePath = await assertInsideRoot(options.root, registeredPath);
|
|
1695
|
-
if (!ALLOWED_EXTENSIONS.has(
|
|
1696
|
-
throw new
|
|
2770
|
+
if (!ALLOWED_EXTENSIONS.has(import_node_path5.default.extname(sourcePath).toLowerCase())) {
|
|
2771
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
1697
2772
|
}
|
|
1698
2773
|
let sourceStat;
|
|
1699
2774
|
try {
|
|
1700
|
-
sourceStat = await (0,
|
|
2775
|
+
sourceStat = await (0, import_promises4.stat)(sourcePath);
|
|
1701
2776
|
} catch (error) {
|
|
1702
2777
|
if (isMissingFileError(error)) {
|
|
1703
|
-
throw new
|
|
2778
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
1704
2779
|
cause: error
|
|
1705
2780
|
});
|
|
1706
2781
|
}
|
|
1707
2782
|
throw error;
|
|
1708
2783
|
}
|
|
1709
2784
|
if (!sourceStat.isFile()) {
|
|
1710
|
-
throw new
|
|
2785
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
1711
2786
|
}
|
|
1712
2787
|
if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
|
|
1713
|
-
throw new
|
|
2788
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.SOURCE_TOO_LARGE);
|
|
1714
2789
|
}
|
|
1715
2790
|
return sourcePath;
|
|
1716
2791
|
}
|
|
1717
2792
|
|
|
1718
2793
|
// src/server/source-context.ts
|
|
1719
2794
|
function toDisplayPath(root, sourcePath) {
|
|
1720
|
-
return
|
|
2795
|
+
return import_node_path6.default.relative(root, sourcePath).split(import_node_path6.default.sep).join("/");
|
|
1721
2796
|
}
|
|
1722
2797
|
async function readSourceContext(options) {
|
|
1723
2798
|
const sourcePath = await resolveSourceFile({
|
|
@@ -1727,10 +2802,10 @@ async function readSourceContext(options) {
|
|
|
1727
2802
|
});
|
|
1728
2803
|
let source;
|
|
1729
2804
|
try {
|
|
1730
|
-
source = await (0,
|
|
2805
|
+
source = await (0, import_promises5.readFile)(sourcePath, "utf8");
|
|
1731
2806
|
} catch (error) {
|
|
1732
2807
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1733
|
-
throw new
|
|
2808
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
1734
2809
|
cause: error
|
|
1735
2810
|
});
|
|
1736
2811
|
}
|
|
@@ -1738,13 +2813,13 @@ async function readSourceContext(options) {
|
|
|
1738
2813
|
}
|
|
1739
2814
|
const lines = source.split(/\r?\n/);
|
|
1740
2815
|
if (options.request.line > lines.length) {
|
|
1741
|
-
throw new
|
|
2816
|
+
throw new import_shared11.SpotPatchError(import_shared11.ERROR_CODES.INVALID_REQUEST);
|
|
1742
2817
|
}
|
|
1743
|
-
const extension =
|
|
2818
|
+
const extension = import_node_path6.default.extname(sourcePath).toLowerCase();
|
|
1744
2819
|
return extractCodeContext({
|
|
1745
2820
|
source,
|
|
1746
2821
|
sourcePath,
|
|
1747
|
-
relativePath: toDisplayPath(await (0,
|
|
2822
|
+
relativePath: toDisplayPath(await (0, import_promises5.realpath)(options.root), sourcePath),
|
|
1748
2823
|
language: extension === ".tsx" ? "tsx" : "jsx",
|
|
1749
2824
|
line: options.request.line,
|
|
1750
2825
|
column: options.request.column,
|
|
@@ -1753,7 +2828,17 @@ async function readSourceContext(options) {
|
|
|
1753
2828
|
});
|
|
1754
2829
|
}
|
|
1755
2830
|
|
|
1756
|
-
// src/server/
|
|
2831
|
+
// src/server/annotation-authorizer.ts
|
|
2832
|
+
function sanitizePageContext(page) {
|
|
2833
|
+
return Object.freeze({
|
|
2834
|
+
url: (0, import_shared12.sanitizeUrl)(page.url, page.url),
|
|
2835
|
+
pathname: (0, import_shared12.redactSensitiveText)(page.pathname),
|
|
2836
|
+
title: (0, import_shared12.redactSensitiveText)(page.title),
|
|
2837
|
+
viewportWidth: page.viewportWidth,
|
|
2838
|
+
viewportHeight: page.viewportHeight,
|
|
2839
|
+
devicePixelRatio: page.devicePixelRatio
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
1757
2842
|
function compactSourceRef(source) {
|
|
1758
2843
|
return Object.freeze({
|
|
1759
2844
|
origin: source.origin,
|
|
@@ -1767,27 +2852,20 @@ function compactSourceRef(source) {
|
|
|
1767
2852
|
async function authorizeSourceRef(source, registry, root) {
|
|
1768
2853
|
const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
|
|
1769
2854
|
if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
|
|
1770
|
-
throw new
|
|
2855
|
+
throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
|
|
1771
2856
|
}
|
|
1772
2857
|
if (source.fileId === void 0) {
|
|
1773
2858
|
if (source.origin === "none" && source.relativePath !== void 0) {
|
|
1774
|
-
throw new
|
|
2859
|
+
throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
|
|
1775
2860
|
}
|
|
1776
2861
|
return compactSourceRef(source);
|
|
1777
2862
|
}
|
|
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("/");
|
|
2863
|
+
const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
|
|
2864
|
+
const relativePath = import_node_path7.default.relative(await (0, import_promises6.realpath)(root), sourcePath).split(import_node_path7.default.sep).join("/");
|
|
1784
2865
|
if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
|
|
1785
|
-
throw new
|
|
2866
|
+
throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
|
|
1786
2867
|
}
|
|
1787
|
-
return Object.freeze({
|
|
1788
|
-
...compactSourceRef(source),
|
|
1789
|
-
relativePath
|
|
1790
|
-
});
|
|
2868
|
+
return Object.freeze({ ...compactSourceRef(source), relativePath });
|
|
1791
2869
|
}
|
|
1792
2870
|
function freezeMatchedRule(rule) {
|
|
1793
2871
|
return Object.freeze({
|
|
@@ -1821,7 +2899,7 @@ async function authorizeTarget(target, input) {
|
|
|
1821
2899
|
maxLines: input.options.budget.maxCodeLines
|
|
1822
2900
|
});
|
|
1823
2901
|
if (marker === void 0 && target.code !== void 0) {
|
|
1824
|
-
throw new
|
|
2902
|
+
throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
|
|
1825
2903
|
}
|
|
1826
2904
|
const code = marker === void 0 ? void 0 : await readSourceContext({
|
|
1827
2905
|
request: marker,
|
|
@@ -1831,11 +2909,11 @@ async function authorizeTarget(target, input) {
|
|
|
1831
2909
|
maxLines: input.options.budget.maxCodeLines
|
|
1832
2910
|
});
|
|
1833
2911
|
if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
|
|
1834
|
-
throw new
|
|
2912
|
+
throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
|
|
1835
2913
|
}
|
|
1836
2914
|
return Object.freeze({
|
|
1837
2915
|
instruction: target.instruction,
|
|
1838
|
-
...target.page === void 0 ? {} : { page:
|
|
2916
|
+
...target.page === void 0 ? {} : { page: sanitizePageContext(target.page) },
|
|
1839
2917
|
source,
|
|
1840
2918
|
react: Object.freeze({
|
|
1841
2919
|
supported: target.react.supported,
|
|
@@ -1863,76 +2941,276 @@ async function authorizeTarget(target, input) {
|
|
|
1863
2941
|
warnings: Object.freeze([...target.warnings])
|
|
1864
2942
|
});
|
|
1865
2943
|
}
|
|
1866
|
-
async function
|
|
1867
|
-
const requestedTargets = input.
|
|
2944
|
+
async function authorizeAnnotation(input) {
|
|
2945
|
+
const requestedTargets = input.annotation.targets;
|
|
1868
2946
|
if (requestedTargets.length > input.options.maxTargets) {
|
|
1869
|
-
throw new
|
|
2947
|
+
throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
|
|
1870
2948
|
}
|
|
1871
2949
|
const identities = requestedTargets.map(targetIdentity);
|
|
1872
2950
|
if (new Set(identities).size !== identities.length) {
|
|
1873
|
-
throw new
|
|
2951
|
+
throw new import_shared12.SpotPatchError(import_shared12.ERROR_CODES.INVALID_REQUEST);
|
|
1874
2952
|
}
|
|
1875
2953
|
const targets = Object.freeze(
|
|
1876
2954
|
await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
|
|
1877
2955
|
);
|
|
1878
|
-
|
|
2956
|
+
return Object.freeze({
|
|
1879
2957
|
schemaVersion: 3,
|
|
1880
|
-
id: input.
|
|
1881
|
-
locale: input.
|
|
1882
|
-
page:
|
|
2958
|
+
id: input.annotation.id,
|
|
2959
|
+
locale: input.annotation.locale,
|
|
2960
|
+
page: sanitizePageContext(input.annotation.page),
|
|
1883
2961
|
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
|
|
2962
|
+
createdAt: input.annotation.createdAt
|
|
1894
2963
|
});
|
|
1895
2964
|
}
|
|
1896
2965
|
|
|
1897
|
-
// src/
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.INVALID_REQUEST);
|
|
2966
|
+
// src/external-handoff/browser-http.ts
|
|
2967
|
+
function matchExternalHandoffBrowserPath(path9) {
|
|
2968
|
+
if (path9 === import_shared13.SPOTPATCH_ENDPOINTS.externalHandoffCapability) return "capability";
|
|
2969
|
+
if (path9 === import_shared13.SPOTPATCH_ENDPOINTS.externalHandoffPublish) return "publish";
|
|
2970
|
+
if (path9 === import_shared13.SPOTPATCH_ENDPOINTS.externalHandoffStatus) return "status";
|
|
2971
|
+
if (path9 === import_shared13.SPOTPATCH_ENDPOINTS.externalHandoffResolveDelivery) {
|
|
2972
|
+
return "resolve-delivery";
|
|
1905
2973
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
2974
|
+
return void 0;
|
|
2975
|
+
}
|
|
2976
|
+
function requireService(options) {
|
|
2977
|
+
if (!options.options.externalAgent.enabled || options.service === void 0) {
|
|
2978
|
+
throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED);
|
|
1909
2979
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2980
|
+
return options.service;
|
|
2981
|
+
}
|
|
2982
|
+
function remapAuthorizationError(error) {
|
|
2983
|
+
if (error instanceof import_shared13.SpotPatchError) {
|
|
2984
|
+
if (error.code === import_shared13.ERROR_CODES.SOURCE_NOT_FOUND || error.code === import_shared13.ERROR_CODES.SOURCE_OUTSIDE_ROOT || error.code === import_shared13.ERROR_CODES.SOURCE_TOO_LARGE) {
|
|
2985
|
+
throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.HANDOFF_SOURCE_STALE, void 0, {
|
|
2986
|
+
cause: error
|
|
2987
|
+
});
|
|
1917
2988
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
continue;
|
|
2989
|
+
if (error.code === import_shared13.ERROR_CODES.INVALID_REQUEST) {
|
|
2990
|
+
throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.HANDOFF_VALIDATION_FAILED, void 0, {
|
|
2991
|
+
cause: error
|
|
2992
|
+
});
|
|
1923
2993
|
}
|
|
1924
|
-
chunks.push(buffer);
|
|
1925
2994
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
2995
|
+
throw error;
|
|
2996
|
+
}
|
|
2997
|
+
async function handleExternalHandoffBrowserRequest(request, response, route, options, writeSuccess) {
|
|
2998
|
+
if (request.method !== "POST") {
|
|
2999
|
+
throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.INVALID_REQUEST);
|
|
1928
3000
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
3001
|
+
const service = requireService(options);
|
|
3002
|
+
if (route === "capability") {
|
|
3003
|
+
const parsed2 = import_shared13.externalHandoffCapabilityRequestSchema.safeParse(
|
|
3004
|
+
await readJsonRequestBody(request)
|
|
3005
|
+
);
|
|
3006
|
+
if (!parsed2.success) throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.INVALID_REQUEST);
|
|
3007
|
+
writeSuccess(response, 200, service.capability());
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
if (route === "status") {
|
|
3011
|
+
const parsed2 = import_shared13.externalHandoffStatusRequestSchema.safeParse(
|
|
3012
|
+
await readJsonRequestBody(request)
|
|
3013
|
+
);
|
|
3014
|
+
if (!parsed2.success) throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.INVALID_REQUEST);
|
|
3015
|
+
writeSuccess(response, 200, service.status(parsed2.data.cursor));
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
if (route === "resolve-delivery") {
|
|
3019
|
+
const parsed2 = import_shared13.externalHandoffResolveDeliveryRequestSchema.safeParse(
|
|
3020
|
+
await readJsonRequestBody(request)
|
|
3021
|
+
);
|
|
3022
|
+
if (!parsed2.success) throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.INVALID_REQUEST);
|
|
3023
|
+
writeSuccess(response, 200, service.resolveDelivery(parsed2.data.cursor));
|
|
3024
|
+
return;
|
|
1935
3025
|
}
|
|
3026
|
+
const parsed = import_shared13.externalHandoffPublishRequestSchema.safeParse(
|
|
3027
|
+
await readJsonRequestBody(request, import_shared13.EXTERNAL_HANDOFF_LIMITS.maximumPublishBodyBytes)
|
|
3028
|
+
);
|
|
3029
|
+
if (!parsed.success) {
|
|
3030
|
+
throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
|
|
3031
|
+
}
|
|
3032
|
+
const result = await service.publish(parsed.data, async (annotation) => {
|
|
3033
|
+
try {
|
|
3034
|
+
return await authorizeAnnotation({
|
|
3035
|
+
annotation,
|
|
3036
|
+
options: options.options,
|
|
3037
|
+
registry: options.registry,
|
|
3038
|
+
root: options.root
|
|
3039
|
+
});
|
|
3040
|
+
} catch (error) {
|
|
3041
|
+
remapAuthorizationError(error);
|
|
3042
|
+
}
|
|
3043
|
+
});
|
|
3044
|
+
writeSuccess(response, result.replayed ? 200 : 201, result);
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
// src/external-agent/browser-http.ts
|
|
3048
|
+
var import_shared14 = require("@spotpatch/shared");
|
|
3049
|
+
function matchExternalAgentBrowserPath(path9) {
|
|
3050
|
+
if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.externalAgentControlStatus) return "status";
|
|
3051
|
+
if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.externalAgentControlConnect) return "connect";
|
|
3052
|
+
if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.externalAgentControlDisconnect) {
|
|
3053
|
+
return "disconnect";
|
|
3054
|
+
}
|
|
3055
|
+
if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.externalAgentControlCancel) return "cancel";
|
|
3056
|
+
if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.externalAgentEvents) return "events";
|
|
3057
|
+
if (path9 === import_shared14.SPOTPATCH_ENDPOINTS.externalAgentResult) return "result";
|
|
3058
|
+
return void 0;
|
|
3059
|
+
}
|
|
3060
|
+
function writeEvent(response, event) {
|
|
3061
|
+
response.write(`${JSON.stringify(import_shared14.externalAgentEventSchema.parse(event))}
|
|
3062
|
+
`);
|
|
3063
|
+
}
|
|
3064
|
+
function createExternalAgentBrowserController(port) {
|
|
3065
|
+
const streams = /* @__PURE__ */ new Set();
|
|
3066
|
+
let disposed = false;
|
|
3067
|
+
const handleEvents = async (request, response) => {
|
|
3068
|
+
const parsed = import_shared14.externalAgentEventsRequestSchema.safeParse(
|
|
3069
|
+
await readJsonRequestBody(request)
|
|
3070
|
+
);
|
|
3071
|
+
if (!parsed.success) {
|
|
3072
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
3073
|
+
}
|
|
3074
|
+
if (disposed || streams.size >= import_shared14.EXTERNAL_AGENT_CONTROL_LIMITS.maximumEventSubscribers) {
|
|
3075
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.BRIDGE_BUSY);
|
|
3076
|
+
}
|
|
3077
|
+
response.statusCode = 200;
|
|
3078
|
+
response.setHeader("Cache-Control", "no-store");
|
|
3079
|
+
response.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
3080
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
3081
|
+
streams.add(response);
|
|
3082
|
+
const initial = import_shared14.externalAgentControlStatusSchema.parse(port.getStatus());
|
|
3083
|
+
writeEvent(response, { type: "status", data: initial });
|
|
3084
|
+
const unsubscribe = port.subscribe((status) => {
|
|
3085
|
+
if (!response.destroyed && status.sequence > (parsed.data.afterSequence ?? -1)) {
|
|
3086
|
+
writeEvent(response, { type: "status", data: status });
|
|
3087
|
+
}
|
|
3088
|
+
});
|
|
3089
|
+
const heartbeat = setInterval(() => {
|
|
3090
|
+
if (!response.destroyed) {
|
|
3091
|
+
const status = port.getStatus();
|
|
3092
|
+
writeEvent(response, {
|
|
3093
|
+
type: "heartbeat",
|
|
3094
|
+
sequence: status.sequence,
|
|
3095
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
}, import_shared14.EXTERNAL_AGENT_CONTROL_LIMITS.eventHeartbeatMs);
|
|
3099
|
+
heartbeat.unref();
|
|
3100
|
+
await new Promise((resolve) => {
|
|
3101
|
+
const close = () => {
|
|
3102
|
+
response.off("close", close);
|
|
3103
|
+
clearInterval(heartbeat);
|
|
3104
|
+
unsubscribe();
|
|
3105
|
+
streams.delete(response);
|
|
3106
|
+
resolve();
|
|
3107
|
+
};
|
|
3108
|
+
response.once("close", close);
|
|
3109
|
+
});
|
|
3110
|
+
};
|
|
3111
|
+
return Object.freeze({
|
|
3112
|
+
async handle(request, response, route, writeSuccess) {
|
|
3113
|
+
if (request.method !== "POST" || disposed) {
|
|
3114
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
3115
|
+
}
|
|
3116
|
+
if (route === "events") {
|
|
3117
|
+
await handleEvents(request, response);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
if (route === "status") {
|
|
3121
|
+
const parsed2 = import_shared14.externalAgentControlStatusRequestSchema.safeParse(
|
|
3122
|
+
await readJsonRequestBody(request)
|
|
3123
|
+
);
|
|
3124
|
+
if (!parsed2.success) throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
3125
|
+
writeSuccess(
|
|
3126
|
+
response,
|
|
3127
|
+
200,
|
|
3128
|
+
import_shared14.externalAgentControlStatusSchema.parse(port.getStatus())
|
|
3129
|
+
);
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
if (route === "connect") {
|
|
3133
|
+
const parsed2 = import_shared14.externalAgentControlConnectRequestSchema.safeParse(
|
|
3134
|
+
await readJsonRequestBody(request)
|
|
3135
|
+
);
|
|
3136
|
+
if (!parsed2.success) throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
3137
|
+
const controller = new AbortController();
|
|
3138
|
+
response.once("close", () => {
|
|
3139
|
+
if (!response.writableEnded) controller.abort("browser-disconnected");
|
|
3140
|
+
});
|
|
3141
|
+
writeSuccess(
|
|
3142
|
+
response,
|
|
3143
|
+
200,
|
|
3144
|
+
import_shared14.externalAgentControlStatusSchema.parse(
|
|
3145
|
+
await port.connect(parsed2.data, controller.signal)
|
|
3146
|
+
)
|
|
3147
|
+
);
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
if (route === "disconnect") {
|
|
3151
|
+
const parsed2 = import_shared14.externalAgentControlDisconnectRequestSchema.safeParse(
|
|
3152
|
+
await readJsonRequestBody(request)
|
|
3153
|
+
);
|
|
3154
|
+
if (!parsed2.success) throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
3155
|
+
writeSuccess(
|
|
3156
|
+
response,
|
|
3157
|
+
200,
|
|
3158
|
+
import_shared14.externalAgentControlStatusSchema.parse(await port.disconnect(parsed2.data))
|
|
3159
|
+
);
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
if (route === "cancel") {
|
|
3163
|
+
const parsed2 = import_shared14.externalAgentControlCancelRequestSchema.safeParse(
|
|
3164
|
+
await readJsonRequestBody(request)
|
|
3165
|
+
);
|
|
3166
|
+
if (!parsed2.success) throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
3167
|
+
writeSuccess(
|
|
3168
|
+
response,
|
|
3169
|
+
200,
|
|
3170
|
+
import_shared14.externalAgentControlStatusSchema.parse(await port.cancel(parsed2.data))
|
|
3171
|
+
);
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
const parsed = import_shared14.externalAgentResultRequestSchema.safeParse(
|
|
3175
|
+
await readJsonRequestBody(request)
|
|
3176
|
+
);
|
|
3177
|
+
if (!parsed.success) throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
3178
|
+
const result = port.getResult(parsed.data.revision);
|
|
3179
|
+
if (result === void 0) {
|
|
3180
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.HANDOFF_NOT_FOUND);
|
|
3181
|
+
}
|
|
3182
|
+
writeSuccess(response, 200, import_shared14.externalAgentManagedResultSchema.parse(result));
|
|
3183
|
+
},
|
|
3184
|
+
dispose() {
|
|
3185
|
+
if (disposed) return;
|
|
3186
|
+
disposed = true;
|
|
3187
|
+
for (const response of streams) response.end();
|
|
3188
|
+
streams.clear();
|
|
3189
|
+
}
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
// src/server/agent-http.ts
|
|
3194
|
+
var import_shared16 = require("@spotpatch/shared");
|
|
3195
|
+
|
|
3196
|
+
// src/server/agent-request.ts
|
|
3197
|
+
var import_shared15 = require("@spotpatch/shared");
|
|
3198
|
+
async function authorizeAgentJobRequest(input) {
|
|
3199
|
+
const annotation = await authorizeAnnotation({
|
|
3200
|
+
annotation: input.request.annotation,
|
|
3201
|
+
options: input.options,
|
|
3202
|
+
registry: input.registry,
|
|
3203
|
+
root: input.root
|
|
3204
|
+
});
|
|
3205
|
+
return Object.freeze({
|
|
3206
|
+
annotation,
|
|
3207
|
+
...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
|
|
3208
|
+
providerProfileId: input.request.providerProfileId,
|
|
3209
|
+
modelProfileId: input.request.modelProfileId,
|
|
3210
|
+
providerDataConsent: true,
|
|
3211
|
+
...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
|
|
3212
|
+
workingTreeMode: input.request.workingTreeMode
|
|
3213
|
+
});
|
|
1936
3214
|
}
|
|
1937
3215
|
|
|
1938
3216
|
// src/server/agent-http.ts
|
|
@@ -1952,21 +3230,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
|
|
|
1952
3230
|
"reverted",
|
|
1953
3231
|
"failed"
|
|
1954
3232
|
]);
|
|
1955
|
-
function matchAgentRequestPath(
|
|
1956
|
-
if (
|
|
3233
|
+
function matchAgentRequestPath(path9) {
|
|
3234
|
+
if (path9 === import_shared16.SPOTPATCH_ENDPOINTS.agentCapability) {
|
|
1957
3235
|
return Object.freeze({ kind: "capability" });
|
|
1958
3236
|
}
|
|
1959
|
-
if (
|
|
3237
|
+
if (path9 === import_shared16.SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
|
|
1960
3238
|
return Object.freeze({ kind: "workspace-health" });
|
|
1961
3239
|
}
|
|
1962
|
-
if (
|
|
3240
|
+
if (path9 === import_shared16.SPOTPATCH_ENDPOINTS.agentJobs) {
|
|
1963
3241
|
return Object.freeze({ kind: "create-job" });
|
|
1964
3242
|
}
|
|
1965
|
-
const prefix = `${
|
|
1966
|
-
if (!
|
|
3243
|
+
const prefix = `${import_shared16.SPOTPATCH_ENDPOINTS.agentJobs}/`;
|
|
3244
|
+
if (!path9.startsWith(prefix)) {
|
|
1967
3245
|
return void 0;
|
|
1968
3246
|
}
|
|
1969
|
-
const segments =
|
|
3247
|
+
const segments = path9.slice(prefix.length).split("/");
|
|
1970
3248
|
const jobId = segments[0];
|
|
1971
3249
|
const action = segments[1];
|
|
1972
3250
|
if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
|
|
@@ -1980,7 +3258,7 @@ function matchAgentRequestPath(path8) {
|
|
|
1980
3258
|
}
|
|
1981
3259
|
function requireAgentManager(options) {
|
|
1982
3260
|
if (options.agentManager === void 0 || options.options.ai === false) {
|
|
1983
|
-
throw new
|
|
3261
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.AI_DISABLED);
|
|
1984
3262
|
}
|
|
1985
3263
|
return options.agentManager;
|
|
1986
3264
|
}
|
|
@@ -2033,13 +3311,13 @@ function streamAgentJobEvents(response, manager, jobId) {
|
|
|
2033
3311
|
}
|
|
2034
3312
|
async function handleCapability(request, response, options, writeSuccess) {
|
|
2035
3313
|
if (request.method !== "POST") {
|
|
2036
|
-
throw new
|
|
3314
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_REQUEST);
|
|
2037
3315
|
}
|
|
2038
|
-
const parsed =
|
|
3316
|
+
const parsed = import_shared16.agentCapabilityRequestSchema.safeParse(
|
|
2039
3317
|
await readJsonRequestBody(request)
|
|
2040
3318
|
);
|
|
2041
3319
|
if (!parsed.success) {
|
|
2042
|
-
throw new
|
|
3320
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_REQUEST);
|
|
2043
3321
|
}
|
|
2044
3322
|
const controller = new AbortController();
|
|
2045
3323
|
const abort = () => {
|
|
@@ -2058,13 +3336,13 @@ async function handleCapability(request, response, options, writeSuccess) {
|
|
|
2058
3336
|
}
|
|
2059
3337
|
async function handleCreateJob(request, response, options, writeSuccess) {
|
|
2060
3338
|
if (request.method !== "POST") {
|
|
2061
|
-
throw new
|
|
3339
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_REQUEST);
|
|
2062
3340
|
}
|
|
2063
|
-
const parsed =
|
|
3341
|
+
const parsed = import_shared16.agentJobCreateRequestSchema.safeParse(
|
|
2064
3342
|
await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
|
|
2065
3343
|
);
|
|
2066
3344
|
if (!parsed.success) {
|
|
2067
|
-
throw new
|
|
3345
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_REQUEST);
|
|
2068
3346
|
}
|
|
2069
3347
|
const authorizedRequest = await authorizeAgentJobRequest({
|
|
2070
3348
|
request: parsed.data,
|
|
@@ -2077,13 +3355,13 @@ async function handleCreateJob(request, response, options, writeSuccess) {
|
|
|
2077
3355
|
}
|
|
2078
3356
|
async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
2079
3357
|
if (request.method !== "POST") {
|
|
2080
|
-
throw new
|
|
3358
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_REQUEST);
|
|
2081
3359
|
}
|
|
2082
|
-
const parsed =
|
|
3360
|
+
const parsed = import_shared16.agentWorkspaceHealthRequestSchema.safeParse(
|
|
2083
3361
|
await readJsonRequestBody(request)
|
|
2084
3362
|
);
|
|
2085
3363
|
if (!parsed.success) {
|
|
2086
|
-
throw new
|
|
3364
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_REQUEST);
|
|
2087
3365
|
}
|
|
2088
3366
|
const controller = new AbortController();
|
|
2089
3367
|
const abort = () => {
|
|
@@ -2100,13 +3378,13 @@ async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
|
2100
3378
|
async function handleJobAction(request, response, options, route, writeSuccess) {
|
|
2101
3379
|
const manager = requireAgentManager(options);
|
|
2102
3380
|
if (request.method !== "POST") {
|
|
2103
|
-
throw new
|
|
3381
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_REQUEST);
|
|
2104
3382
|
}
|
|
2105
|
-
const parsed =
|
|
3383
|
+
const parsed = import_shared16.agentJobActionRequestSchema.safeParse(
|
|
2106
3384
|
await readJsonRequestBody(request)
|
|
2107
3385
|
);
|
|
2108
3386
|
if (!parsed.success) {
|
|
2109
|
-
throw new
|
|
3387
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_REQUEST);
|
|
2110
3388
|
}
|
|
2111
3389
|
if (route.action === "events") {
|
|
2112
3390
|
streamAgentJobEvents(response, manager, route.jobId);
|
|
@@ -2221,27 +3499,27 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
2221
3499
|
var launchConfiguredEditor = createEditorLauncher();
|
|
2222
3500
|
|
|
2223
3501
|
// src/server/data-flow-http.ts
|
|
2224
|
-
var
|
|
3502
|
+
var import_node_crypto9 = require("crypto");
|
|
2225
3503
|
var import_analyzer = require("@spotpatch/analyzer");
|
|
2226
|
-
var
|
|
3504
|
+
var import_shared17 = require("@spotpatch/shared");
|
|
2227
3505
|
function envelopeBytes(report) {
|
|
2228
3506
|
return Buffer.byteLength(JSON.stringify({ ok: true, data: report }), "utf8");
|
|
2229
3507
|
}
|
|
2230
3508
|
function limitDataFlowReportToBytes(report, maximumBytes) {
|
|
2231
|
-
const structurallyLimited = (0,
|
|
3509
|
+
const structurallyLimited = (0, import_shared17.limitDataFlowReportCollections)(report);
|
|
2232
3510
|
if (envelopeBytes(structurallyLimited) <= maximumBytes) {
|
|
2233
3511
|
return structurallyLimited;
|
|
2234
3512
|
}
|
|
2235
|
-
let limited = (0,
|
|
3513
|
+
let limited = (0, import_shared17.limitDataFlowReportCollections)(structurallyLimited, {
|
|
2236
3514
|
forceTruncation: true,
|
|
2237
3515
|
maximumDependencies: 0,
|
|
2238
3516
|
truncatedBy: "bytes"
|
|
2239
3517
|
});
|
|
2240
3518
|
if (envelopeBytes(limited) > maximumBytes) {
|
|
2241
|
-
throw new
|
|
3519
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INTERNAL_ERROR);
|
|
2242
3520
|
}
|
|
2243
3521
|
for (let maximumDependencies = 1; maximumDependencies <= structurallyLimited.dependencies.length; maximumDependencies += 1) {
|
|
2244
|
-
const candidate = (0,
|
|
3522
|
+
const candidate = (0, import_shared17.limitDataFlowReportCollections)(structurallyLimited, {
|
|
2245
3523
|
forceTruncation: true,
|
|
2246
3524
|
maximumDependencies,
|
|
2247
3525
|
truncatedBy: "bytes"
|
|
@@ -2267,7 +3545,7 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
2267
3545
|
request.componentSourceId
|
|
2268
3546
|
);
|
|
2269
3547
|
if (anchor?.sourceVersion !== request.sourceVersion) {
|
|
2270
|
-
throw new
|
|
3548
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.DATA_FLOW_SOURCE_STALE);
|
|
2271
3549
|
}
|
|
2272
3550
|
return anchor;
|
|
2273
3551
|
}
|
|
@@ -2284,7 +3562,7 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
2284
3562
|
column: resolvedRequest.column
|
|
2285
3563
|
});
|
|
2286
3564
|
if (resolvedRequest.sourceVersion !== void 0 && resolvedRequest.sourceVersion !== report.component.source.sourceVersion) {
|
|
2287
|
-
throw new
|
|
3565
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.DATA_FLOW_SOURCE_STALE);
|
|
2288
3566
|
}
|
|
2289
3567
|
return limitDataFlowReportToBytes(
|
|
2290
3568
|
report,
|
|
@@ -2293,31 +3571,31 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
2293
3571
|
}
|
|
2294
3572
|
function requireAnalyzer(analyzer) {
|
|
2295
3573
|
if (analyzer === void 0) {
|
|
2296
|
-
throw new
|
|
3574
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.DATA_FLOW_DISABLED);
|
|
2297
3575
|
}
|
|
2298
3576
|
return analyzer;
|
|
2299
3577
|
}
|
|
2300
3578
|
async function handleComponentDataFlowReport(request, analyzer, options) {
|
|
2301
|
-
const parsed =
|
|
3579
|
+
const parsed = import_shared17.dataFlowComponentReportRequestSchema.safeParse(
|
|
2302
3580
|
await readJsonRequestBody(
|
|
2303
3581
|
request,
|
|
2304
3582
|
options.options.dataFlow.limits.protocolRequestMaxBytes
|
|
2305
3583
|
)
|
|
2306
3584
|
);
|
|
2307
3585
|
if (!parsed.success) {
|
|
2308
|
-
throw new
|
|
3586
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
2309
3587
|
}
|
|
2310
3588
|
return analyzeTarget(parsed.data, requireAnalyzer(analyzer), options);
|
|
2311
3589
|
}
|
|
2312
3590
|
async function handlePageDataFlowReport(request, analyzer, options) {
|
|
2313
|
-
const parsed =
|
|
3591
|
+
const parsed = import_shared17.dataFlowPageReportRequestSchema.safeParse(
|
|
2314
3592
|
await readJsonRequestBody(
|
|
2315
3593
|
request,
|
|
2316
3594
|
options.options.dataFlow.limits.protocolRequestMaxBytes
|
|
2317
3595
|
)
|
|
2318
3596
|
);
|
|
2319
3597
|
if (!parsed.success) {
|
|
2320
|
-
throw new
|
|
3598
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
2321
3599
|
}
|
|
2322
3600
|
const activeAnalyzer = requireAnalyzer(analyzer);
|
|
2323
3601
|
const componentReports = await Promise.all(
|
|
@@ -2341,10 +3619,10 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
2341
3619
|
const analyzedVersions = new Set(
|
|
2342
3620
|
componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
|
|
2343
3621
|
);
|
|
2344
|
-
const reportId = `page_${(0,
|
|
3622
|
+
const reportId = `page_${(0, import_node_crypto9.createHash)("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
|
|
2345
3623
|
const complete = componentReports.every((report2) => report2.completeness.complete);
|
|
2346
3624
|
const report = Object.freeze({
|
|
2347
|
-
schemaVersion:
|
|
3625
|
+
schemaVersion: import_shared17.DATA_FLOW_SCHEMA_VERSION,
|
|
2348
3626
|
reportId,
|
|
2349
3627
|
baseline: Object.freeze({
|
|
2350
3628
|
registryEpoch: options.session.id,
|
|
@@ -2388,20 +3666,20 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
2388
3666
|
}
|
|
2389
3667
|
|
|
2390
3668
|
// src/server/request-security.ts
|
|
2391
|
-
var
|
|
3669
|
+
var import_node_crypto10 = require("crypto");
|
|
2392
3670
|
var import_node_net = require("net");
|
|
2393
|
-
var
|
|
3671
|
+
var import_shared18 = require("@spotpatch/shared");
|
|
2394
3672
|
function getSingleHeader(request, name) {
|
|
2395
3673
|
const value = request.headers[name.toLowerCase()];
|
|
2396
3674
|
return Array.isArray(value) ? value[0] : value;
|
|
2397
3675
|
}
|
|
2398
|
-
function
|
|
3676
|
+
function tokensMatch2(actual, expected) {
|
|
2399
3677
|
if (actual === void 0) {
|
|
2400
3678
|
return false;
|
|
2401
3679
|
}
|
|
2402
3680
|
const actualBytes = Buffer.from(actual);
|
|
2403
3681
|
const expectedBytes = Buffer.from(expected);
|
|
2404
|
-
return actualBytes.byteLength === expectedBytes.byteLength && (0,
|
|
3682
|
+
return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto10.timingSafeEqual)(actualBytes, expectedBytes);
|
|
2405
3683
|
}
|
|
2406
3684
|
function isLoopbackHostname(hostname) {
|
|
2407
3685
|
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
@@ -2435,32 +3713,32 @@ function parseOrigin(value) {
|
|
|
2435
3713
|
}
|
|
2436
3714
|
}
|
|
2437
3715
|
function assertRequestAuthorized(request, options) {
|
|
2438
|
-
const actualToken = getSingleHeader(request,
|
|
2439
|
-
if (!
|
|
2440
|
-
throw new
|
|
3716
|
+
const actualToken = getSingleHeader(request, import_shared18.SPOTPATCH_TOKEN_HEADER);
|
|
3717
|
+
if (!tokensMatch2(actualToken, options.sessionToken)) {
|
|
3718
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INVALID_TOKEN);
|
|
2441
3719
|
}
|
|
2442
3720
|
const hostHeader = getSingleHeader(request, "host");
|
|
2443
3721
|
const originHeader = getSingleHeader(request, "origin");
|
|
2444
3722
|
const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
|
|
2445
3723
|
const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
|
|
2446
3724
|
if (host === void 0 || origin === void 0) {
|
|
2447
|
-
throw new
|
|
3725
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
2448
3726
|
}
|
|
2449
3727
|
const hostIsLoopback = isLoopbackHostname(host.hostname);
|
|
2450
3728
|
const originIsLoopback = isLoopbackHostname(origin.hostname);
|
|
2451
3729
|
if (!options.allowLan) {
|
|
2452
3730
|
if (!hostIsLoopback || !originIsLoopback) {
|
|
2453
|
-
throw new
|
|
3731
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
2454
3732
|
}
|
|
2455
3733
|
return;
|
|
2456
3734
|
}
|
|
2457
3735
|
if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
|
|
2458
|
-
throw new
|
|
3736
|
+
throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
2459
3737
|
}
|
|
2460
3738
|
}
|
|
2461
3739
|
|
|
2462
3740
|
// src/server/runtime-bootstrap.ts
|
|
2463
|
-
var
|
|
3741
|
+
var import_shared19 = require("@spotpatch/shared");
|
|
2464
3742
|
function getSingleHeader2(request, name) {
|
|
2465
3743
|
const value = request.headers[name.toLowerCase()];
|
|
2466
3744
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -2475,7 +3753,7 @@ function resolveRuntimeBootstrapOptions(options) {
|
|
|
2475
3753
|
if (expectedOrigin.origin !== options.expectedOrigin || expectedOrigin.protocol !== "http:" || !isLoopbackHostname(expectedOrigin.hostname)) {
|
|
2476
3754
|
throw new TypeError("The SpotPatch bootstrap origin must be a loopback origin.");
|
|
2477
3755
|
}
|
|
2478
|
-
const parsedConfig =
|
|
3756
|
+
const parsedConfig = import_shared19.runtimeConfigSchema.safeParse(options.runtimeConfig);
|
|
2479
3757
|
if (!parsedConfig.success) {
|
|
2480
3758
|
throw new TypeError("The SpotPatch Runtime configuration is invalid.");
|
|
2481
3759
|
}
|
|
@@ -2487,7 +3765,7 @@ function resolveRuntimeBootstrapOptions(options) {
|
|
|
2487
3765
|
function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
2488
3766
|
const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
2489
3767
|
if (request.method !== "POST" || contentType !== "application/json") {
|
|
2490
|
-
throw new
|
|
3768
|
+
throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.INVALID_REQUEST);
|
|
2491
3769
|
}
|
|
2492
3770
|
const host = getSingleHeader2(request, "host");
|
|
2493
3771
|
let hostIsLoopback = false;
|
|
@@ -2499,112 +3777,148 @@ function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
|
2499
3777
|
}
|
|
2500
3778
|
}
|
|
2501
3779
|
if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
|
|
2502
|
-
throw new
|
|
3780
|
+
throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
2503
3781
|
}
|
|
2504
3782
|
}
|
|
2505
3783
|
async function readRuntimeBootstrap(request, options) {
|
|
2506
3784
|
assertRuntimeBootstrapRequest(request, options.expectedOrigin);
|
|
2507
|
-
const parsedBody =
|
|
3785
|
+
const parsedBody = import_shared19.runtimeBootstrapRequestSchema.safeParse(
|
|
2508
3786
|
await readJsonRequestBody(request)
|
|
2509
3787
|
);
|
|
2510
3788
|
if (!parsedBody.success) {
|
|
2511
|
-
throw new
|
|
3789
|
+
throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.INVALID_REQUEST);
|
|
2512
3790
|
}
|
|
2513
3791
|
return options.runtimeConfig;
|
|
2514
3792
|
}
|
|
2515
3793
|
|
|
2516
3794
|
// src/server/middleware.ts
|
|
2517
3795
|
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
|
-
[
|
|
3796
|
+
[import_shared20.ERROR_CODES.INVALID_REQUEST]: 400,
|
|
3797
|
+
[import_shared20.ERROR_CODES.INVALID_TOKEN]: 401,
|
|
3798
|
+
[import_shared20.ERROR_CODES.ORIGIN_NOT_ALLOWED]: 403,
|
|
3799
|
+
[import_shared20.ERROR_CODES.SOURCE_NOT_FOUND]: 404,
|
|
3800
|
+
[import_shared20.ERROR_CODES.SOURCE_OUTSIDE_ROOT]: 403,
|
|
3801
|
+
[import_shared20.ERROR_CODES.SOURCE_TOO_LARGE]: 413,
|
|
3802
|
+
[import_shared20.ERROR_CODES.EDITOR_OPEN_FAILED]: 500,
|
|
3803
|
+
[import_shared20.ERROR_CODES.DATA_FLOW_DISABLED]: 404,
|
|
3804
|
+
[import_shared20.ERROR_CODES.DATA_FLOW_SOURCE_STALE]: 409,
|
|
3805
|
+
[import_shared20.ERROR_CODES.DATA_FLOW_ANALYSIS_CANCELLED]: 409,
|
|
3806
|
+
[import_shared20.ERROR_CODES.AI_DISABLED]: 404,
|
|
3807
|
+
[import_shared20.ERROR_CODES.PROVIDER_NOT_CONFIGURED]: 503,
|
|
3808
|
+
[import_shared20.ERROR_CODES.PROVIDER_AUTH_FAILED]: 502,
|
|
3809
|
+
[import_shared20.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
|
|
3810
|
+
[import_shared20.ERROR_CODES.MODEL_NOT_ALLOWED]: 400,
|
|
3811
|
+
[import_shared20.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
|
|
3812
|
+
[import_shared20.ERROR_CODES.PROVIDER_RATE_LIMITED]: 429,
|
|
3813
|
+
[import_shared20.ERROR_CODES.AGENT_BUSY]: 409,
|
|
3814
|
+
[import_shared20.ERROR_CODES.AGENT_LIMIT_EXCEEDED]: 413,
|
|
3815
|
+
[import_shared20.ERROR_CODES.AGENT_CANCELLED]: 409,
|
|
3816
|
+
[import_shared20.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED]: 404,
|
|
3817
|
+
[import_shared20.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE]: 503,
|
|
3818
|
+
[import_shared20.ERROR_CODES.HANDOFF_VALIDATION_FAILED]: 422,
|
|
3819
|
+
[import_shared20.ERROR_CODES.HANDOFF_SOURCE_STALE]: 409,
|
|
3820
|
+
[import_shared20.ERROR_CODES.HANDOFF_NOT_FOUND]: 404,
|
|
3821
|
+
[import_shared20.ERROR_CODES.HANDOFF_EXPIRED]: 410,
|
|
3822
|
+
[import_shared20.ERROR_CODES.HANDOFF_CURSOR_INVALID]: 409,
|
|
3823
|
+
[import_shared20.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE]: 413,
|
|
3824
|
+
[import_shared20.ERROR_CODES.BRIDGE_UNAUTHORIZED]: 401,
|
|
3825
|
+
[import_shared20.ERROR_CODES.BRIDGE_PROTOCOL_MISMATCH]: 409,
|
|
3826
|
+
[import_shared20.ERROR_CODES.BRIDGE_BUSY]: 429,
|
|
3827
|
+
[import_shared20.ERROR_CODES.EXTERNAL_AGENT_BUSY]: 409,
|
|
3828
|
+
[import_shared20.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT]: 409,
|
|
3829
|
+
[import_shared20.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID]: 409,
|
|
3830
|
+
[import_shared20.ERROR_CODES.ACTIVE_DISPATCH_INVALID]: 409,
|
|
3831
|
+
[import_shared20.ERROR_CODES.SESSION_NOT_FOUND]: 404,
|
|
3832
|
+
[import_shared20.ERROR_CODES.SESSION_AMBIGUOUS]: 409,
|
|
3833
|
+
[import_shared20.ERROR_CODES.SESSION_CLOSED]: 410,
|
|
3834
|
+
[import_shared20.ERROR_CODES.WORKTREE_DIRTY]: 409,
|
|
3835
|
+
[import_shared20.ERROR_CODES.WORKTREE_NOT_REPOSITORY]: 409,
|
|
3836
|
+
[import_shared20.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS]: 409,
|
|
3837
|
+
[import_shared20.ERROR_CODES.WORKTREE_CONFLICTED]: 409,
|
|
3838
|
+
[import_shared20.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
|
|
3839
|
+
[import_shared20.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
|
|
3840
|
+
[import_shared20.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
|
|
3841
|
+
[import_shared20.ERROR_CODES.TOOL_DENIED]: 403,
|
|
3842
|
+
[import_shared20.ERROR_CODES.TOOL_INPUT_INVALID]: 422,
|
|
3843
|
+
[import_shared20.ERROR_CODES.TOOL_ARGUMENTS_INVALID]: 422,
|
|
3844
|
+
[import_shared20.ERROR_CODES.TOOL_CALL_ID_CONFLICT]: 422,
|
|
3845
|
+
[import_shared20.ERROR_CODES.TOOL_PATH_DENIED]: 403,
|
|
3846
|
+
[import_shared20.ERROR_CODES.PATCH_REJECTED]: 422,
|
|
3847
|
+
[import_shared20.ERROR_CODES.VALIDATION_FAILED]: 422,
|
|
3848
|
+
[import_shared20.ERROR_CODES.APPLY_CONFLICT]: 409,
|
|
3849
|
+
[import_shared20.ERROR_CODES.INTERNAL_ERROR]: 500
|
|
2554
3850
|
});
|
|
2555
3851
|
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
|
-
[
|
|
3852
|
+
[import_shared20.ERROR_CODES.INVALID_REQUEST]: "The request is invalid.",
|
|
3853
|
+
[import_shared20.ERROR_CODES.INVALID_TOKEN]: "The session token is invalid.",
|
|
3854
|
+
[import_shared20.ERROR_CODES.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
|
|
3855
|
+
[import_shared20.ERROR_CODES.SOURCE_NOT_FOUND]: "The source file is unavailable.",
|
|
3856
|
+
[import_shared20.ERROR_CODES.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
|
|
3857
|
+
[import_shared20.ERROR_CODES.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
|
|
3858
|
+
[import_shared20.ERROR_CODES.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
|
|
3859
|
+
[import_shared20.ERROR_CODES.DATA_FLOW_DISABLED]: "Component data-flow analysis is not enabled.",
|
|
3860
|
+
[import_shared20.ERROR_CODES.DATA_FLOW_SOURCE_STALE]: "The selected source version is stale.",
|
|
3861
|
+
[import_shared20.ERROR_CODES.DATA_FLOW_ANALYSIS_CANCELLED]: "The data-flow analysis was cancelled.",
|
|
3862
|
+
[import_shared20.ERROR_CODES.AI_DISABLED]: "AI execution is not enabled.",
|
|
3863
|
+
[import_shared20.ERROR_CODES.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
|
|
3864
|
+
[import_shared20.ERROR_CODES.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
|
|
3865
|
+
[import_shared20.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
|
|
3866
|
+
[import_shared20.ERROR_CODES.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
|
|
3867
|
+
[import_shared20.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
|
|
3868
|
+
[import_shared20.ERROR_CODES.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
|
|
3869
|
+
[import_shared20.ERROR_CODES.AGENT_BUSY]: "Another Agent job is already running.",
|
|
3870
|
+
[import_shared20.ERROR_CODES.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
|
|
3871
|
+
[import_shared20.ERROR_CODES.AGENT_CANCELLED]: "The Agent job was cancelled.",
|
|
3872
|
+
[import_shared20.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED]: "External Agent handoff is not enabled.",
|
|
3873
|
+
[import_shared20.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE]: "External Agent handoff is temporarily unavailable.",
|
|
3874
|
+
[import_shared20.ERROR_CODES.HANDOFF_VALIDATION_FAILED]: "The handoff content is invalid.",
|
|
3875
|
+
[import_shared20.ERROR_CODES.HANDOFF_SOURCE_STALE]: "The selected source is stale.",
|
|
3876
|
+
[import_shared20.ERROR_CODES.HANDOFF_NOT_FOUND]: "No current handoff is available.",
|
|
3877
|
+
[import_shared20.ERROR_CODES.HANDOFF_EXPIRED]: "The handoff has expired.",
|
|
3878
|
+
[import_shared20.ERROR_CODES.HANDOFF_CURSOR_INVALID]: "The handoff cursor is invalid.",
|
|
3879
|
+
[import_shared20.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE]: "The handoff exceeds the size limit.",
|
|
3880
|
+
[import_shared20.ERROR_CODES.BRIDGE_UNAUTHORIZED]: "The local bridge request is unauthorized.",
|
|
3881
|
+
[import_shared20.ERROR_CODES.BRIDGE_PROTOCOL_MISMATCH]: "The local bridge protocol is incompatible.",
|
|
3882
|
+
[import_shared20.ERROR_CODES.BRIDGE_BUSY]: "The local bridge is busy.",
|
|
3883
|
+
[import_shared20.ERROR_CODES.EXTERNAL_AGENT_BUSY]: "The connected external Agent is busy.",
|
|
3884
|
+
[import_shared20.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT]: "Another active Agent adapter is connected.",
|
|
3885
|
+
[import_shared20.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID]: "The active Agent adapter lease is invalid.",
|
|
3886
|
+
[import_shared20.ERROR_CODES.ACTIVE_DISPATCH_INVALID]: "The active Agent dispatch transition is invalid.",
|
|
3887
|
+
[import_shared20.ERROR_CODES.SESSION_NOT_FOUND]: "No active SpotPatch session was found.",
|
|
3888
|
+
[import_shared20.ERROR_CODES.SESSION_AMBIGUOUS]: "More than one SpotPatch session matches.",
|
|
3889
|
+
[import_shared20.ERROR_CODES.SESSION_CLOSED]: "The SpotPatch session has closed.",
|
|
3890
|
+
[import_shared20.ERROR_CODES.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
|
|
3891
|
+
[import_shared20.ERROR_CODES.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
|
|
3892
|
+
[import_shared20.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
|
|
3893
|
+
[import_shared20.ERROR_CODES.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
|
|
3894
|
+
[import_shared20.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
|
|
3895
|
+
[import_shared20.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
|
|
3896
|
+
[import_shared20.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
|
|
3897
|
+
[import_shared20.ERROR_CODES.TOOL_DENIED]: "The Agent tool request was denied.",
|
|
3898
|
+
[import_shared20.ERROR_CODES.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
|
|
3899
|
+
[import_shared20.ERROR_CODES.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
|
|
3900
|
+
[import_shared20.ERROR_CODES.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
|
|
3901
|
+
[import_shared20.ERROR_CODES.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
|
|
3902
|
+
[import_shared20.ERROR_CODES.PATCH_REJECTED]: "The proposed patch was rejected.",
|
|
3903
|
+
[import_shared20.ERROR_CODES.VALIDATION_FAILED]: "The proposed change failed validation.",
|
|
3904
|
+
[import_shared20.ERROR_CODES.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
|
|
3905
|
+
[import_shared20.ERROR_CODES.INTERNAL_ERROR]: "The request could not be completed."
|
|
2592
3906
|
});
|
|
2593
|
-
function
|
|
3907
|
+
function writeJson2(response, status, payload) {
|
|
2594
3908
|
response.statusCode = status;
|
|
2595
3909
|
response.setHeader("Cache-Control", "no-store");
|
|
2596
3910
|
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2597
3911
|
response.end(JSON.stringify(payload));
|
|
2598
3912
|
}
|
|
2599
3913
|
function asSpotPatchError(error) {
|
|
2600
|
-
return error instanceof
|
|
3914
|
+
return error instanceof import_shared20.SpotPatchError ? error : new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INTERNAL_ERROR, void 0, { cause: error });
|
|
2601
3915
|
}
|
|
2602
3916
|
function writeError(response, error, logger) {
|
|
2603
3917
|
const normalized = asSpotPatchError(error);
|
|
2604
|
-
if (normalized.code ===
|
|
3918
|
+
if (normalized.code === import_shared20.ERROR_CODES.INTERNAL_ERROR) {
|
|
2605
3919
|
logger?.warn("[spotpatch:server] Internal request failure.");
|
|
2606
3920
|
}
|
|
2607
|
-
|
|
3921
|
+
writeJson2(response, STATUS_BY_ERROR[normalized.code], {
|
|
2608
3922
|
ok: false,
|
|
2609
3923
|
error: {
|
|
2610
3924
|
code: normalized.code,
|
|
@@ -2620,11 +3934,11 @@ function requestPath(request) {
|
|
|
2620
3934
|
}
|
|
2621
3935
|
}
|
|
2622
3936
|
async function handleSourceContext(request, options) {
|
|
2623
|
-
const parsed =
|
|
3937
|
+
const parsed = import_shared20.sourceContextRequestSchema.safeParse(
|
|
2624
3938
|
await readJsonRequestBody(request)
|
|
2625
3939
|
);
|
|
2626
3940
|
if (!parsed.success) {
|
|
2627
|
-
throw new
|
|
3941
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INVALID_REQUEST);
|
|
2628
3942
|
}
|
|
2629
3943
|
return readSourceContext({
|
|
2630
3944
|
request: parsed.data,
|
|
@@ -2635,9 +3949,9 @@ async function handleSourceContext(request, options) {
|
|
|
2635
3949
|
});
|
|
2636
3950
|
}
|
|
2637
3951
|
async function handleOpenEditor(request, options) {
|
|
2638
|
-
const parsed =
|
|
3952
|
+
const parsed = import_shared20.openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
|
|
2639
3953
|
if (!parsed.success) {
|
|
2640
|
-
throw new
|
|
3954
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INVALID_REQUEST);
|
|
2641
3955
|
}
|
|
2642
3956
|
const body = parsed.data;
|
|
2643
3957
|
const sourcePath = await resolveSourceFile({
|
|
@@ -2654,7 +3968,7 @@ async function handleOpenEditor(request, options) {
|
|
|
2654
3968
|
options.logger?.warn(
|
|
2655
3969
|
`[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
|
|
2656
3970
|
);
|
|
2657
|
-
throw new
|
|
3971
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.EDITOR_OPEN_FAILED, void 0, {
|
|
2658
3972
|
cause: error
|
|
2659
3973
|
});
|
|
2660
3974
|
}
|
|
@@ -2662,64 +3976,98 @@ async function handleOpenEditor(request, options) {
|
|
|
2662
3976
|
function createSpotPatchMiddleware(options) {
|
|
2663
3977
|
const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
|
|
2664
3978
|
const dataFlowAnalyzer = createDataFlowAnalyzer(options);
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
const
|
|
2668
|
-
|
|
3979
|
+
const externalAgentController = options.externalAgentControl === void 0 ? void 0 : createExternalAgentBrowserController(options.externalAgentControl);
|
|
3980
|
+
const middleware = (request, response, next) => {
|
|
3981
|
+
const path9 = requestPath(request);
|
|
3982
|
+
const agentRoute = matchAgentRequestPath(path9);
|
|
3983
|
+
const externalAgentRoute = matchExternalAgentBrowserPath(path9);
|
|
3984
|
+
const externalHandoffRoute = matchExternalHandoffBrowserPath(path9);
|
|
3985
|
+
if (path9 !== import_shared20.SPOTPATCH_ENDPOINTS.sourceContext && path9 !== import_shared20.SPOTPATCH_ENDPOINTS.openEditor && path9 !== import_shared20.SPOTPATCH_ENDPOINTS.dataFlowComponentReport && path9 !== import_shared20.SPOTPATCH_ENDPOINTS.dataFlowPageReport && agentRoute === void 0 && externalAgentRoute === void 0 && externalHandoffRoute === void 0 && !path9.startsWith(`${import_shared20.SPOTPATCH_API_BASE}/`)) {
|
|
2669
3986
|
next();
|
|
2670
3987
|
return;
|
|
2671
3988
|
}
|
|
2672
3989
|
const handle = async () => {
|
|
2673
|
-
if (
|
|
3990
|
+
if (path9 === import_shared20.SPOTPATCH_ENDPOINTS.bootstrap && bootstrap !== void 0) {
|
|
2674
3991
|
const data = await readRuntimeBootstrap(
|
|
2675
3992
|
request,
|
|
2676
3993
|
bootstrap
|
|
2677
3994
|
);
|
|
2678
|
-
|
|
3995
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2679
3996
|
return;
|
|
2680
3997
|
}
|
|
2681
3998
|
assertRequestAuthorized(request, {
|
|
2682
3999
|
allowLan: options.options.allowLan,
|
|
2683
4000
|
sessionToken: options.session.token
|
|
2684
4001
|
});
|
|
2685
|
-
if (
|
|
4002
|
+
if (path9 === import_shared20.SPOTPATCH_ENDPOINTS.sourceContext) {
|
|
2686
4003
|
if (request.method !== "POST") {
|
|
2687
|
-
throw new
|
|
4004
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INVALID_REQUEST);
|
|
2688
4005
|
}
|
|
2689
4006
|
const data = await handleSourceContext(request, options);
|
|
2690
|
-
|
|
4007
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2691
4008
|
return;
|
|
2692
4009
|
}
|
|
2693
|
-
if (
|
|
4010
|
+
if (path9 === import_shared20.SPOTPATCH_ENDPOINTS.openEditor) {
|
|
2694
4011
|
if (request.method !== "POST") {
|
|
2695
|
-
throw new
|
|
4012
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INVALID_REQUEST);
|
|
2696
4013
|
}
|
|
2697
4014
|
const data = await handleOpenEditor(request, options);
|
|
2698
|
-
|
|
4015
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2699
4016
|
return;
|
|
2700
4017
|
}
|
|
2701
|
-
if (
|
|
4018
|
+
if (path9 === import_shared20.SPOTPATCH_ENDPOINTS.dataFlowComponentReport) {
|
|
2702
4019
|
if (request.method !== "POST") {
|
|
2703
|
-
throw new
|
|
4020
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INVALID_REQUEST);
|
|
2704
4021
|
}
|
|
2705
4022
|
const data = await handleComponentDataFlowReport(
|
|
2706
4023
|
request,
|
|
2707
4024
|
dataFlowAnalyzer,
|
|
2708
4025
|
options
|
|
2709
4026
|
);
|
|
2710
|
-
|
|
4027
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2711
4028
|
return;
|
|
2712
4029
|
}
|
|
2713
|
-
if (
|
|
4030
|
+
if (path9 === import_shared20.SPOTPATCH_ENDPOINTS.dataFlowPageReport) {
|
|
2714
4031
|
if (request.method !== "POST") {
|
|
2715
|
-
throw new
|
|
4032
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INVALID_REQUEST);
|
|
2716
4033
|
}
|
|
2717
4034
|
const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
|
|
2718
|
-
|
|
4035
|
+
writeJson2(response, 200, { ok: true, data });
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
if (externalAgentRoute !== void 0) {
|
|
4039
|
+
if (!options.options.externalAgent.enabled || externalAgentController === void 0) {
|
|
4040
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED);
|
|
4041
|
+
}
|
|
4042
|
+
await externalAgentController.handle(
|
|
4043
|
+
request,
|
|
4044
|
+
response,
|
|
4045
|
+
externalAgentRoute,
|
|
4046
|
+
(target, status, data) => {
|
|
4047
|
+
writeJson2(target, status, { ok: true, data });
|
|
4048
|
+
}
|
|
4049
|
+
);
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
if (externalHandoffRoute !== void 0) {
|
|
4053
|
+
await handleExternalHandoffBrowserRequest(
|
|
4054
|
+
request,
|
|
4055
|
+
response,
|
|
4056
|
+
externalHandoffRoute,
|
|
4057
|
+
{
|
|
4058
|
+
options: options.options,
|
|
4059
|
+
registry: options.registry,
|
|
4060
|
+
root: options.root,
|
|
4061
|
+
...options.externalHandoffService === void 0 ? {} : { service: options.externalHandoffService }
|
|
4062
|
+
},
|
|
4063
|
+
(target, status, data) => {
|
|
4064
|
+
writeJson2(target, status, { ok: true, data });
|
|
4065
|
+
}
|
|
4066
|
+
);
|
|
2719
4067
|
return;
|
|
2720
4068
|
}
|
|
2721
4069
|
if (agentRoute === void 0) {
|
|
2722
|
-
throw new
|
|
4070
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INVALID_REQUEST);
|
|
2723
4071
|
}
|
|
2724
4072
|
await handleAgentRequest(
|
|
2725
4073
|
request,
|
|
@@ -2727,7 +4075,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
2727
4075
|
options,
|
|
2728
4076
|
agentRoute,
|
|
2729
4077
|
(target, status, data) => {
|
|
2730
|
-
|
|
4078
|
+
writeJson2(target, status, { ok: true, data });
|
|
2731
4079
|
}
|
|
2732
4080
|
);
|
|
2733
4081
|
};
|
|
@@ -2735,12 +4083,17 @@ function createSpotPatchMiddleware(options) {
|
|
|
2735
4083
|
writeError(response, error, options.logger);
|
|
2736
4084
|
});
|
|
2737
4085
|
};
|
|
4086
|
+
return Object.assign(middleware, {
|
|
4087
|
+
dispose() {
|
|
4088
|
+
externalAgentController?.dispose();
|
|
4089
|
+
}
|
|
4090
|
+
});
|
|
2738
4091
|
}
|
|
2739
4092
|
|
|
2740
4093
|
// src/server/source-registration.ts
|
|
2741
|
-
var
|
|
2742
|
-
var
|
|
2743
|
-
var
|
|
4094
|
+
var import_node_crypto11 = require("crypto");
|
|
4095
|
+
var import_promises7 = require("fs/promises");
|
|
4096
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2744
4097
|
var import_compiler = require("@spotpatch/compiler");
|
|
2745
4098
|
var import_zod2 = require("zod");
|
|
2746
4099
|
var REGISTRATION_BODY_LIMIT_BYTES = 4096;
|
|
@@ -2761,16 +4114,16 @@ function identitiesMatch(actual, expected) {
|
|
|
2761
4114
|
}
|
|
2762
4115
|
const actualBytes = Buffer.from(actual);
|
|
2763
4116
|
const expectedBytes = Buffer.from(expected);
|
|
2764
|
-
return actualBytes.byteLength === expectedBytes.byteLength && (0,
|
|
4117
|
+
return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto11.timingSafeEqual)(actualBytes, expectedBytes);
|
|
2765
4118
|
}
|
|
2766
4119
|
function isWithinRoot(root, candidate) {
|
|
2767
|
-
const relative =
|
|
2768
|
-
return relative === "" || !relative.startsWith(`..${
|
|
4120
|
+
const relative = import_node_path8.default.relative(root, candidate);
|
|
4121
|
+
return relative === "" || !relative.startsWith(`..${import_node_path8.default.sep}`) && relative !== ".." && !import_node_path8.default.isAbsolute(relative);
|
|
2769
4122
|
}
|
|
2770
4123
|
function hasForbiddenSegment(root, candidate) {
|
|
2771
|
-
return
|
|
4124
|
+
return import_node_path8.default.relative(root, candidate).split(import_node_path8.default.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
|
|
2772
4125
|
}
|
|
2773
|
-
function
|
|
4126
|
+
function writeJson3(response, statusCode, payload) {
|
|
2774
4127
|
const body = JSON.stringify(payload);
|
|
2775
4128
|
response.statusCode = statusCode;
|
|
2776
4129
|
response.setHeader("Cache-Control", "no-store");
|
|
@@ -2790,15 +4143,15 @@ function requestComesFromLoopbackWorker(request) {
|
|
|
2790
4143
|
}
|
|
2791
4144
|
}
|
|
2792
4145
|
async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
|
|
2793
|
-
if (!
|
|
4146
|
+
if (!import_node_path8.default.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
2794
4147
|
return void 0;
|
|
2795
4148
|
}
|
|
2796
4149
|
try {
|
|
2797
|
-
const sourceStat = await (0,
|
|
4150
|
+
const sourceStat = await (0, import_promises7.lstat)(requestedPath);
|
|
2798
4151
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
2799
4152
|
return void 0;
|
|
2800
4153
|
}
|
|
2801
|
-
const resolvedPath = await (0,
|
|
4154
|
+
const resolvedPath = await (0, import_promises7.realpath)(requestedPath);
|
|
2802
4155
|
if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
|
|
2803
4156
|
return void 0;
|
|
2804
4157
|
}
|
|
@@ -2811,7 +4164,7 @@ async function createSourceRegistrationService(input) {
|
|
|
2811
4164
|
if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
|
|
2812
4165
|
throw new TypeError("The source registration identity is invalid.");
|
|
2813
4166
|
}
|
|
2814
|
-
const root = await (0,
|
|
4167
|
+
const root = await (0, import_promises7.realpath)(input.root);
|
|
2815
4168
|
const sourceFilter = (0, import_compiler.createSourceFilter)(root, input.options);
|
|
2816
4169
|
const handler = (request, response) => {
|
|
2817
4170
|
const handle = async () => {
|
|
@@ -2820,14 +4173,14 @@ async function createSourceRegistrationService(input) {
|
|
|
2820
4173
|
getSingleHeader3(request, INTERNAL_SECRET_HEADER),
|
|
2821
4174
|
input.internalSecret
|
|
2822
4175
|
)) {
|
|
2823
|
-
|
|
4176
|
+
writeJson3(response, 403, { ok: false });
|
|
2824
4177
|
return;
|
|
2825
4178
|
}
|
|
2826
4179
|
const parsed = registrationRequestSchema.safeParse(
|
|
2827
4180
|
await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
|
|
2828
4181
|
);
|
|
2829
4182
|
if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
|
|
2830
|
-
|
|
4183
|
+
writeJson3(response, 400, { ok: false });
|
|
2831
4184
|
return;
|
|
2832
4185
|
}
|
|
2833
4186
|
const sourcePath = await resolveAuthorizedSource(
|
|
@@ -2836,17 +4189,17 @@ async function createSourceRegistrationService(input) {
|
|
|
2836
4189
|
(absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
|
|
2837
4190
|
);
|
|
2838
4191
|
if (sourcePath === void 0) {
|
|
2839
|
-
|
|
4192
|
+
writeJson3(response, 403, { ok: false });
|
|
2840
4193
|
return;
|
|
2841
4194
|
}
|
|
2842
|
-
|
|
4195
|
+
writeJson3(response, 200, {
|
|
2843
4196
|
epoch: input.registryEpoch,
|
|
2844
4197
|
fileId: input.registry.register(sourcePath)
|
|
2845
4198
|
});
|
|
2846
4199
|
};
|
|
2847
4200
|
void handle().catch(() => {
|
|
2848
4201
|
if (!response.headersSent) {
|
|
2849
|
-
|
|
4202
|
+
writeJson3(response, 400, { ok: false });
|
|
2850
4203
|
} else {
|
|
2851
4204
|
response.destroy();
|
|
2852
4205
|
}
|
|
@@ -2856,11 +4209,11 @@ async function createSourceRegistrationService(input) {
|
|
|
2856
4209
|
}
|
|
2857
4210
|
|
|
2858
4211
|
// src/session/session.ts
|
|
2859
|
-
var
|
|
4212
|
+
var import_node_crypto12 = require("crypto");
|
|
2860
4213
|
function createSession() {
|
|
2861
4214
|
return Object.freeze({
|
|
2862
|
-
id: (0,
|
|
2863
|
-
token: (0,
|
|
4215
|
+
id: (0, import_node_crypto12.randomBytes)(16).toString("base64url"),
|
|
4216
|
+
token: (0, import_node_crypto12.randomBytes)(16).toString("base64url")
|
|
2864
4217
|
});
|
|
2865
4218
|
}
|
|
2866
4219
|
|
|
@@ -2873,6 +4226,7 @@ var OPTION_KEYS = Object.freeze([
|
|
|
2873
4226
|
"dataFlow",
|
|
2874
4227
|
"editor",
|
|
2875
4228
|
"enabled",
|
|
4229
|
+
"externalAgent",
|
|
2876
4230
|
"exclude",
|
|
2877
4231
|
"include",
|
|
2878
4232
|
"locale",
|
|
@@ -2963,6 +4317,7 @@ function serializeResolvedSpotPatchOptions(options) {
|
|
|
2963
4317
|
}) : false,
|
|
2964
4318
|
editor: options.editor,
|
|
2965
4319
|
enabled: options.enabled,
|
|
4320
|
+
externalAgent: options.externalAgent.enabled,
|
|
2966
4321
|
exclude: Object.freeze(options.exclude.map(serializeFilter)),
|
|
2967
4322
|
include: Object.freeze(options.include.map(serializeFilter)),
|
|
2968
4323
|
locale: options.locale,
|
|
@@ -3016,7 +4371,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
3016
4371
|
if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
|
|
3017
4372
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
3018
4373
|
}
|
|
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)) {
|
|
4374
|
+
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
4375
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
3021
4376
|
}
|
|
3022
4377
|
try {
|
|
@@ -3028,6 +4383,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
3028
4383
|
dataFlow: parseDataFlow(value.dataFlow),
|
|
3029
4384
|
editor: value.editor,
|
|
3030
4385
|
enabled: value.enabled,
|
|
4386
|
+
externalAgent: value.externalAgent,
|
|
3031
4387
|
exclude: parseFilterList(value.exclude),
|
|
3032
4388
|
include: parseFilterList(value.include),
|
|
3033
4389
|
locale: value.locale,
|
|
@@ -3047,6 +4403,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
3047
4403
|
DEFAULT_OPTIONS,
|
|
3048
4404
|
applyIntegrationPlan,
|
|
3049
4405
|
createAgentJobManager,
|
|
4406
|
+
createExternalHandoffService,
|
|
3050
4407
|
createIntegrationFileChange,
|
|
3051
4408
|
createRuntimeAiConfig,
|
|
3052
4409
|
createRuntimeDataFlowConfig,
|
|
@@ -3063,8 +4420,10 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
3063
4420
|
readRuntimeBootstrap,
|
|
3064
4421
|
resolveCredentialEnvironment,
|
|
3065
4422
|
resolveEnvironmentAiConfiguration,
|
|
4423
|
+
resolveManagedExecutionValidation,
|
|
3066
4424
|
resolveOptions,
|
|
3067
4425
|
resolveProjectOptions,
|
|
4426
|
+
resolveProjectValidationChecks,
|
|
3068
4427
|
resolveRuntimeBootstrapOptions,
|
|
3069
4428
|
serializeResolvedSpotPatchOptions
|
|
3070
4429
|
});
|