@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.js
CHANGED
|
@@ -472,6 +472,1103 @@ function createAgentJobManager(options) {
|
|
|
472
472
|
});
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
// src/external-handoff/service.ts
|
|
476
|
+
import {
|
|
477
|
+
ERROR_CODES as ERROR_CODES6,
|
|
478
|
+
EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION as EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION3,
|
|
479
|
+
EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION as EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION2,
|
|
480
|
+
SpotPatchError as SpotPatchError6
|
|
481
|
+
} from "@spotpatch/shared";
|
|
482
|
+
import { computeExternalHandoffProjectKey as computeExternalHandoffProjectKey2 } from "@spotpatch/shared/external-agent-node";
|
|
483
|
+
|
|
484
|
+
// src/external-handoff/active-registry.ts
|
|
485
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
486
|
+
import {
|
|
487
|
+
ERROR_CODES as ERROR_CODES2,
|
|
488
|
+
EXTERNAL_HANDOFF_LIMITS,
|
|
489
|
+
SpotPatchError as SpotPatchError2
|
|
490
|
+
} from "@spotpatch/shared";
|
|
491
|
+
|
|
492
|
+
// src/external-handoff/clock.ts
|
|
493
|
+
import { performance } from "perf_hooks";
|
|
494
|
+
var SYSTEM_EXTERNAL_HANDOFF_CLOCK = Object.freeze({
|
|
495
|
+
monotonicNow: () => performance.now(),
|
|
496
|
+
wallNow: () => Date.now()
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// src/external-handoff/active-registry.ts
|
|
500
|
+
var ALLOWED_TRANSITIONS = Object.freeze({
|
|
501
|
+
queued: ["dispatching", "failed"],
|
|
502
|
+
dispatching: ["dispatched", "working", "failed", "delivery-unknown"],
|
|
503
|
+
dispatched: ["working", "failed", "delivery-unknown"],
|
|
504
|
+
working: ["completed", "failed", "delivery-unknown"],
|
|
505
|
+
completed: [],
|
|
506
|
+
failed: [],
|
|
507
|
+
"delivery-unknown": []
|
|
508
|
+
});
|
|
509
|
+
var TERMINAL_PHASES = /* @__PURE__ */ new Set([
|
|
510
|
+
"completed",
|
|
511
|
+
"failed",
|
|
512
|
+
"delivery-unknown"
|
|
513
|
+
]);
|
|
514
|
+
function defaultRandomId() {
|
|
515
|
+
return randomBytes2(32).toString("base64url");
|
|
516
|
+
}
|
|
517
|
+
function requirePresent(value) {
|
|
518
|
+
if (value === null) throw new SpotPatchError2(ERROR_CODES2.INTERNAL_ERROR);
|
|
519
|
+
return value;
|
|
520
|
+
}
|
|
521
|
+
function createActiveAdapterRegistry(options = {}) {
|
|
522
|
+
const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
|
|
523
|
+
const randomId = options.randomId ?? defaultRandomId;
|
|
524
|
+
let blocked;
|
|
525
|
+
let closed = false;
|
|
526
|
+
let dispatch;
|
|
527
|
+
let lastReleasedToken;
|
|
528
|
+
let lease;
|
|
529
|
+
const nowIso = () => new Date(clock.wallNow()).toISOString();
|
|
530
|
+
const requireOpen = () => {
|
|
531
|
+
if (closed) throw new SpotPatchError2(ERROR_CODES2.SESSION_CLOSED);
|
|
532
|
+
};
|
|
533
|
+
const dispatchSummary = () => dispatch === void 0 ? null : Object.freeze({
|
|
534
|
+
adapterKind: dispatch.adapterKind,
|
|
535
|
+
revision: dispatch.revision,
|
|
536
|
+
phase: dispatch.phase,
|
|
537
|
+
updatedAt: dispatch.updatedAt
|
|
538
|
+
});
|
|
539
|
+
const activeSummary = () => {
|
|
540
|
+
if (blocked !== void 0) {
|
|
541
|
+
return Object.freeze({
|
|
542
|
+
kind: blocked.adapterKind,
|
|
543
|
+
state: "blocked",
|
|
544
|
+
canDispatch: false,
|
|
545
|
+
connectedAt: blocked.connectedAt,
|
|
546
|
+
updatedAt: blocked.updatedAt
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
if (lease === void 0) return null;
|
|
550
|
+
const busy = dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase);
|
|
551
|
+
return Object.freeze({
|
|
552
|
+
kind: lease.adapterKind,
|
|
553
|
+
state: busy ? "busy" : "ready",
|
|
554
|
+
canDispatch: !busy,
|
|
555
|
+
connectedAt: lease.connectedAt,
|
|
556
|
+
updatedAt: lease.updatedAt
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
const state = (cursor) => Object.freeze({
|
|
560
|
+
activeAdapter: activeSummary(),
|
|
561
|
+
dispatch: cursor === void 0 || dispatch?.cursor === cursor ? dispatchSummary() : null
|
|
562
|
+
});
|
|
563
|
+
const enterUnknown = (activeLease) => {
|
|
564
|
+
if (dispatch === void 0) return;
|
|
565
|
+
const updatedAt = nowIso();
|
|
566
|
+
dispatch.phase = "delivery-unknown";
|
|
567
|
+
dispatch.updatedAt = updatedAt;
|
|
568
|
+
blocked = Object.freeze({
|
|
569
|
+
adapterKind: activeLease.adapterKind,
|
|
570
|
+
connectedAt: activeLease.connectedAt,
|
|
571
|
+
updatedAt
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
const endLease = (activeLease) => {
|
|
575
|
+
if (dispatch?.phase === "queued") {
|
|
576
|
+
dispatch.phase = "failed";
|
|
577
|
+
dispatch.updatedAt = nowIso();
|
|
578
|
+
} else if (dispatch?.phase === "dispatching" || dispatch?.phase === "dispatched" || dispatch?.phase === "working") {
|
|
579
|
+
enterUnknown(activeLease);
|
|
580
|
+
}
|
|
581
|
+
lastReleasedToken = activeLease.token;
|
|
582
|
+
lease = void 0;
|
|
583
|
+
};
|
|
584
|
+
const sweep = () => {
|
|
585
|
+
if (lease === void 0) return;
|
|
586
|
+
const monotonicNow = clock.monotonicNow();
|
|
587
|
+
if (monotonicNow >= lease.expiresAtMonotonic || dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase) && monotonicNow >= dispatch.deadlineMonotonic) {
|
|
588
|
+
endLease(lease);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
const assertPublishable = () => {
|
|
592
|
+
requireOpen();
|
|
593
|
+
sweep();
|
|
594
|
+
if (blocked !== void 0 || lease !== void 0 && dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase)) {
|
|
595
|
+
throw new SpotPatchError2(ERROR_CODES2.EXTERNAL_AGENT_BUSY);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
const requireLease = (leaseToken) => {
|
|
599
|
+
requireOpen();
|
|
600
|
+
sweep();
|
|
601
|
+
if (lease?.token !== leaseToken) {
|
|
602
|
+
throw new SpotPatchError2(ERROR_CODES2.ACTIVE_ADAPTER_LEASE_INVALID);
|
|
603
|
+
}
|
|
604
|
+
return lease;
|
|
605
|
+
};
|
|
606
|
+
return Object.freeze({
|
|
607
|
+
assertPublishable,
|
|
608
|
+
claim(adapterKind, connectorInstanceId, baselineCursor) {
|
|
609
|
+
requireOpen();
|
|
610
|
+
sweep();
|
|
611
|
+
if (blocked !== void 0) {
|
|
612
|
+
throw new SpotPatchError2(ERROR_CODES2.EXTERNAL_AGENT_BUSY);
|
|
613
|
+
}
|
|
614
|
+
if (lease !== void 0) {
|
|
615
|
+
if (lease.adapterKind === adapterKind && lease.connectorInstanceId === connectorInstanceId) {
|
|
616
|
+
lease.expiresAtMonotonic = clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
|
|
617
|
+
lease.updatedAt = nowIso();
|
|
618
|
+
return Object.freeze({
|
|
619
|
+
leaseToken: lease.token,
|
|
620
|
+
heartbeatIntervalMs: EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
|
|
621
|
+
baselineCursor: lease.baselineCursor,
|
|
622
|
+
activeAdapter: requirePresent(activeSummary())
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
throw new SpotPatchError2(ERROR_CODES2.ACTIVE_ADAPTER_CONFLICT);
|
|
626
|
+
}
|
|
627
|
+
const timestamp = nowIso();
|
|
628
|
+
lease = {
|
|
629
|
+
adapterKind,
|
|
630
|
+
baselineCursor,
|
|
631
|
+
connectedAt: timestamp,
|
|
632
|
+
connectorInstanceId,
|
|
633
|
+
token: randomId(),
|
|
634
|
+
expiresAtMonotonic: clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs,
|
|
635
|
+
updatedAt: timestamp
|
|
636
|
+
};
|
|
637
|
+
lastReleasedToken = void 0;
|
|
638
|
+
return Object.freeze({
|
|
639
|
+
leaseToken: lease.token,
|
|
640
|
+
heartbeatIntervalMs: EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
|
|
641
|
+
baselineCursor,
|
|
642
|
+
activeAdapter: requirePresent(activeSummary())
|
|
643
|
+
});
|
|
644
|
+
},
|
|
645
|
+
heartbeat(leaseToken) {
|
|
646
|
+
const activeLease = requireLease(leaseToken);
|
|
647
|
+
activeLease.expiresAtMonotonic = clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
|
|
648
|
+
activeLease.updatedAt = nowIso();
|
|
649
|
+
return state();
|
|
650
|
+
},
|
|
651
|
+
report(leaseToken, cursor, phase) {
|
|
652
|
+
const activeLease = requireLease(leaseToken);
|
|
653
|
+
if (dispatch?.cursor !== cursor || dispatch.adapterKind !== activeLease.adapterKind) {
|
|
654
|
+
throw new SpotPatchError2(ERROR_CODES2.ACTIVE_DISPATCH_INVALID);
|
|
655
|
+
}
|
|
656
|
+
if (dispatch.phase === phase) return state(cursor);
|
|
657
|
+
const allowed = ALLOWED_TRANSITIONS[dispatch.phase];
|
|
658
|
+
if (!allowed.includes(phase)) {
|
|
659
|
+
throw new SpotPatchError2(ERROR_CODES2.ACTIVE_DISPATCH_INVALID);
|
|
660
|
+
}
|
|
661
|
+
const updatedAt = nowIso();
|
|
662
|
+
dispatch.phase = phase;
|
|
663
|
+
dispatch.updatedAt = updatedAt;
|
|
664
|
+
activeLease.updatedAt = updatedAt;
|
|
665
|
+
if (phase === "delivery-unknown") {
|
|
666
|
+
blocked = Object.freeze({
|
|
667
|
+
adapterKind: activeLease.adapterKind,
|
|
668
|
+
connectedAt: activeLease.connectedAt,
|
|
669
|
+
updatedAt
|
|
670
|
+
});
|
|
671
|
+
lastReleasedToken = activeLease.token;
|
|
672
|
+
lease = void 0;
|
|
673
|
+
}
|
|
674
|
+
return state(cursor);
|
|
675
|
+
},
|
|
676
|
+
release(leaseToken) {
|
|
677
|
+
requireOpen();
|
|
678
|
+
sweep();
|
|
679
|
+
if (lease === void 0 && lastReleasedToken === leaseToken) return state();
|
|
680
|
+
const activeLease = lease;
|
|
681
|
+
if (activeLease?.token !== leaseToken) {
|
|
682
|
+
throw new SpotPatchError2(ERROR_CODES2.ACTIVE_ADAPTER_LEASE_INVALID);
|
|
683
|
+
}
|
|
684
|
+
endLease(activeLease);
|
|
685
|
+
return state();
|
|
686
|
+
},
|
|
687
|
+
reserve(cursor, revision) {
|
|
688
|
+
assertPublishable();
|
|
689
|
+
if (lease === void 0) return Object.freeze({ mode: "inbox" });
|
|
690
|
+
const updatedAt = nowIso();
|
|
691
|
+
dispatch = {
|
|
692
|
+
adapterKind: lease.adapterKind,
|
|
693
|
+
cursor,
|
|
694
|
+
deadlineMonotonic: clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeDispatchTimeoutMs,
|
|
695
|
+
revision,
|
|
696
|
+
phase: "queued",
|
|
697
|
+
updatedAt
|
|
698
|
+
};
|
|
699
|
+
lease.updatedAt = updatedAt;
|
|
700
|
+
return Object.freeze({
|
|
701
|
+
mode: "active",
|
|
702
|
+
adapter: requirePresent(activeSummary()),
|
|
703
|
+
dispatch: requirePresent(dispatchSummary())
|
|
704
|
+
});
|
|
705
|
+
},
|
|
706
|
+
resolveDelivery(cursor) {
|
|
707
|
+
requireOpen();
|
|
708
|
+
sweep();
|
|
709
|
+
const activeDispatch = dispatch;
|
|
710
|
+
if (blocked === void 0 || activeDispatch?.cursor !== cursor || activeDispatch.phase !== "delivery-unknown") {
|
|
711
|
+
throw new SpotPatchError2(ERROR_CODES2.ACTIVE_DISPATCH_INVALID);
|
|
712
|
+
}
|
|
713
|
+
blocked = void 0;
|
|
714
|
+
return state(cursor);
|
|
715
|
+
},
|
|
716
|
+
snapshot(cursor) {
|
|
717
|
+
requireOpen();
|
|
718
|
+
sweep();
|
|
719
|
+
return state(cursor);
|
|
720
|
+
},
|
|
721
|
+
close() {
|
|
722
|
+
if (closed) return;
|
|
723
|
+
closed = true;
|
|
724
|
+
blocked = void 0;
|
|
725
|
+
dispatch = void 0;
|
|
726
|
+
lease = void 0;
|
|
727
|
+
lastReleasedToken = void 0;
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/external-handoff/broker.ts
|
|
733
|
+
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
734
|
+
import {
|
|
735
|
+
createServer
|
|
736
|
+
} from "http";
|
|
737
|
+
import {
|
|
738
|
+
ERROR_CODES as ERROR_CODES4,
|
|
739
|
+
EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
|
|
740
|
+
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS2,
|
|
741
|
+
SpotPatchError as SpotPatchError4
|
|
742
|
+
} from "@spotpatch/shared";
|
|
743
|
+
import {
|
|
744
|
+
SPOTPATCH_BRIDGE_PATHS,
|
|
745
|
+
SPOTPATCH_BRIDGE_TOKEN_HEADER,
|
|
746
|
+
bridgeAckRequestSchema,
|
|
747
|
+
bridgeActiveClaimRequestSchema,
|
|
748
|
+
bridgeActiveHeartbeatRequestSchema,
|
|
749
|
+
bridgeActiveReleaseRequestSchema,
|
|
750
|
+
bridgeActiveReportRequestSchema,
|
|
751
|
+
bridgeCurrentRequestSchema,
|
|
752
|
+
bridgeStatusRequestSchema,
|
|
753
|
+
bridgeWaitRequestSchema
|
|
754
|
+
} from "@spotpatch/shared/external-agent-node";
|
|
755
|
+
|
|
756
|
+
// src/server/request-body.ts
|
|
757
|
+
import { ERROR_CODES as ERROR_CODES3, SpotPatchError as SpotPatchError3 } from "@spotpatch/shared";
|
|
758
|
+
|
|
759
|
+
// src/server/constants.ts
|
|
760
|
+
var MAX_REQUEST_BODY_BYTES = 32 * 1024;
|
|
761
|
+
var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
|
|
762
|
+
var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
|
763
|
+
|
|
764
|
+
// src/server/request-body.ts
|
|
765
|
+
function isJsonContentType(value) {
|
|
766
|
+
return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
767
|
+
}
|
|
768
|
+
async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
|
|
769
|
+
if (!isJsonContentType(request.headers["content-type"])) {
|
|
770
|
+
throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
|
|
771
|
+
}
|
|
772
|
+
const declaredLength = Number(request.headers["content-length"]);
|
|
773
|
+
if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
|
|
774
|
+
throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
|
|
775
|
+
}
|
|
776
|
+
const chunks = [];
|
|
777
|
+
let byteLength = 0;
|
|
778
|
+
let exceededLimit = false;
|
|
779
|
+
for await (const rawChunk of request) {
|
|
780
|
+
const chunk = rawChunk;
|
|
781
|
+
if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
|
|
782
|
+
throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
|
|
783
|
+
}
|
|
784
|
+
const buffer = Buffer.from(chunk);
|
|
785
|
+
byteLength += buffer.byteLength;
|
|
786
|
+
if (byteLength > maximumBytes) {
|
|
787
|
+
exceededLimit = true;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
chunks.push(buffer);
|
|
791
|
+
}
|
|
792
|
+
if (exceededLimit || byteLength === 0) {
|
|
793
|
+
throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
797
|
+
} catch (error) {
|
|
798
|
+
throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST, void 0, {
|
|
799
|
+
cause: error
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/external-handoff/broker.ts
|
|
805
|
+
function singleHeader(request, name) {
|
|
806
|
+
const value = request.headers[name.toLowerCase()];
|
|
807
|
+
return Array.isArray(value) ? void 0 : value;
|
|
808
|
+
}
|
|
809
|
+
function tokensMatch(actual, expected) {
|
|
810
|
+
if (actual === void 0) return false;
|
|
811
|
+
const actualBytes = Buffer.from(actual);
|
|
812
|
+
const expectedBytes = Buffer.from(expected);
|
|
813
|
+
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual(actualBytes, expectedBytes);
|
|
814
|
+
}
|
|
815
|
+
function writeJson(response, status, payload) {
|
|
816
|
+
response.statusCode = status;
|
|
817
|
+
response.setHeader("Cache-Control", "no-store");
|
|
818
|
+
response.setHeader("Connection", "close");
|
|
819
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
820
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
821
|
+
response.end(JSON.stringify(payload));
|
|
822
|
+
}
|
|
823
|
+
function statusForError(code) {
|
|
824
|
+
if (code === ERROR_CODES4.BRIDGE_UNAUTHORIZED) return 401;
|
|
825
|
+
if (code === ERROR_CODES4.HANDOFF_NOT_FOUND) return 404;
|
|
826
|
+
if (code === ERROR_CODES4.HANDOFF_EXPIRED || code === ERROR_CODES4.SESSION_CLOSED) {
|
|
827
|
+
return 410;
|
|
828
|
+
}
|
|
829
|
+
if (code === ERROR_CODES4.BRIDGE_BUSY) return 429;
|
|
830
|
+
if (code === ERROR_CODES4.ACTIVE_ADAPTER_LEASE_INVALID) return 401;
|
|
831
|
+
if (code === ERROR_CODES4.HANDOFF_RESPONSE_TOO_LARGE) return 413;
|
|
832
|
+
if (code === ERROR_CODES4.HANDOFF_CURSOR_INVALID || code === ERROR_CODES4.BRIDGE_PROTOCOL_MISMATCH || code === ERROR_CODES4.EXTERNAL_AGENT_BUSY || code === ERROR_CODES4.ACTIVE_ADAPTER_CONFLICT || code === ERROR_CODES4.ACTIVE_DISPATCH_INVALID) {
|
|
833
|
+
return 409;
|
|
834
|
+
}
|
|
835
|
+
if (code === ERROR_CODES4.INVALID_REQUEST) return 400;
|
|
836
|
+
return 500;
|
|
837
|
+
}
|
|
838
|
+
function normalizeError2(error) {
|
|
839
|
+
return error instanceof SpotPatchError4 ? error : new SpotPatchError4(ERROR_CODES4.INTERNAL_ERROR, void 0, { cause: error });
|
|
840
|
+
}
|
|
841
|
+
function assertAuthorized(request, expectedHost, bridgeToken) {
|
|
842
|
+
if (request.socket.remoteAddress !== "127.0.0.1" || singleHeader(request, "host") !== expectedHost || !tokensMatch(singleHeader(request, SPOTPATCH_BRIDGE_TOKEN_HEADER), bridgeToken)) {
|
|
843
|
+
throw new SpotPatchError4(ERROR_CODES4.BRIDGE_UNAUTHORIZED);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async function closeServer(server, sockets) {
|
|
847
|
+
await new Promise((resolve) => {
|
|
848
|
+
server.close(() => {
|
|
849
|
+
resolve();
|
|
850
|
+
});
|
|
851
|
+
for (const socket of sockets) {
|
|
852
|
+
socket.destroy();
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
async function createExternalHandoffBroker(options) {
|
|
857
|
+
const bridgeToken = randomBytes3(32).toString("base64url");
|
|
858
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
859
|
+
let expectedHost = "";
|
|
860
|
+
const server = createServer(
|
|
861
|
+
{ maxHeaderSize: EXTERNAL_HANDOFF_LIMITS2.maximumBrokerHeaderBytes },
|
|
862
|
+
(request, response) => {
|
|
863
|
+
const handle = async () => {
|
|
864
|
+
assertAuthorized(request, expectedHost, bridgeToken);
|
|
865
|
+
if (request.method !== "POST") {
|
|
866
|
+
throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
867
|
+
}
|
|
868
|
+
const body = await readJsonRequestBody(
|
|
869
|
+
request,
|
|
870
|
+
EXTERNAL_HANDOFF_LIMITS2.maximumBrokerRequestBytes
|
|
871
|
+
);
|
|
872
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.status) {
|
|
873
|
+
const parsed = bridgeStatusRequestSchema.safeParse(body);
|
|
874
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
875
|
+
let current = null;
|
|
876
|
+
try {
|
|
877
|
+
current = options.store.status();
|
|
878
|
+
} catch (error) {
|
|
879
|
+
if (!(error instanceof SpotPatchError4) || error.code !== ERROR_CODES4.HANDOFF_NOT_FOUND) {
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
writeJson(response, 200, {
|
|
884
|
+
ok: true,
|
|
885
|
+
data: Object.freeze({
|
|
886
|
+
brokerProtocolVersion: EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
|
|
887
|
+
projectKey: options.projectKey,
|
|
888
|
+
sessionId: options.sessionId,
|
|
889
|
+
framework: options.framework,
|
|
890
|
+
current
|
|
891
|
+
})
|
|
892
|
+
});
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.current) {
|
|
896
|
+
const parsed = bridgeCurrentRequestSchema.safeParse(body);
|
|
897
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
898
|
+
const snapshot2 = options.store.current(parsed.data.cursor);
|
|
899
|
+
writeJson(response, 200, {
|
|
900
|
+
ok: true,
|
|
901
|
+
data: Object.freeze({ outcome: "handoff", snapshot: snapshot2 })
|
|
902
|
+
});
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.ack) {
|
|
906
|
+
const parsed = bridgeAckRequestSchema.safeParse(body);
|
|
907
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
908
|
+
const summary = options.store.ack(
|
|
909
|
+
parsed.data.cursor,
|
|
910
|
+
parsed.data.connectorInstanceId
|
|
911
|
+
);
|
|
912
|
+
writeJson(response, 200, {
|
|
913
|
+
ok: true,
|
|
914
|
+
data: Object.freeze({ summary })
|
|
915
|
+
});
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.wait) {
|
|
919
|
+
const parsed = bridgeWaitRequestSchema.safeParse(body);
|
|
920
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
921
|
+
const controller = new AbortController();
|
|
922
|
+
const abort = () => {
|
|
923
|
+
if (!response.writableEnded) controller.abort("bridge-client-closed");
|
|
924
|
+
};
|
|
925
|
+
response.once("close", abort);
|
|
926
|
+
try {
|
|
927
|
+
const data = await options.store.wait(
|
|
928
|
+
parsed.data.afterCursor,
|
|
929
|
+
parsed.data.timeoutMs,
|
|
930
|
+
controller.signal
|
|
931
|
+
);
|
|
932
|
+
writeJson(response, 200, { ok: true, data });
|
|
933
|
+
} finally {
|
|
934
|
+
response.removeListener("close", abort);
|
|
935
|
+
}
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.activeClaim) {
|
|
939
|
+
const parsed = bridgeActiveClaimRequestSchema.safeParse(body);
|
|
940
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
941
|
+
const data = options.activeRegistry.claim(
|
|
942
|
+
parsed.data.adapterKind,
|
|
943
|
+
parsed.data.connectorInstanceId,
|
|
944
|
+
options.store.currentCursor()
|
|
945
|
+
);
|
|
946
|
+
writeJson(response, 200, { ok: true, data });
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.activeHeartbeat) {
|
|
950
|
+
const parsed = bridgeActiveHeartbeatRequestSchema.safeParse(body);
|
|
951
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
952
|
+
const data = options.activeRegistry.heartbeat(parsed.data.leaseToken);
|
|
953
|
+
writeJson(response, 200, { ok: true, data });
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.activeReport) {
|
|
957
|
+
const parsed = bridgeActiveReportRequestSchema.safeParse(body);
|
|
958
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
959
|
+
const data = options.activeRegistry.report(
|
|
960
|
+
parsed.data.leaseToken,
|
|
961
|
+
parsed.data.cursor,
|
|
962
|
+
parsed.data.phase
|
|
963
|
+
);
|
|
964
|
+
writeJson(response, 200, { ok: true, data });
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.activeRelease) {
|
|
968
|
+
const parsed = bridgeActiveReleaseRequestSchema.safeParse(body);
|
|
969
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
970
|
+
const data = options.activeRegistry.release(parsed.data.leaseToken);
|
|
971
|
+
writeJson(response, 200, { ok: true, data });
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
975
|
+
};
|
|
976
|
+
void handle().catch((error) => {
|
|
977
|
+
if (response.writableEnded || response.destroyed) return;
|
|
978
|
+
const normalized = normalizeError2(error);
|
|
979
|
+
if (normalized.code === ERROR_CODES4.BRIDGE_BUSY) {
|
|
980
|
+
response.setHeader("Retry-After", "1");
|
|
981
|
+
}
|
|
982
|
+
writeJson(response, statusForError(normalized.code), {
|
|
983
|
+
ok: false,
|
|
984
|
+
error: {
|
|
985
|
+
code: normalized.code,
|
|
986
|
+
message: "The local SpotPatch bridge request failed."
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
);
|
|
992
|
+
server.maxConnections = EXTERNAL_HANDOFF_LIMITS2.maximumBrokerSockets;
|
|
993
|
+
server.headersTimeout = 5e3;
|
|
994
|
+
server.requestTimeout = EXTERNAL_HANDOFF_LIMITS2.maximumWaitMs + 5e3;
|
|
995
|
+
server.keepAliveTimeout = 1;
|
|
996
|
+
server.on("connection", (socket) => {
|
|
997
|
+
if (sockets.size >= EXTERNAL_HANDOFF_LIMITS2.maximumBrokerSockets) {
|
|
998
|
+
socket.destroy();
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
sockets.add(socket);
|
|
1002
|
+
socket.once("close", () => sockets.delete(socket));
|
|
1003
|
+
});
|
|
1004
|
+
await new Promise((resolve, reject) => {
|
|
1005
|
+
server.once("error", reject);
|
|
1006
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
1007
|
+
});
|
|
1008
|
+
const address = server.address();
|
|
1009
|
+
if (address === null || typeof address === "string" || address.address !== "127.0.0.1") {
|
|
1010
|
+
await closeServer(server, sockets);
|
|
1011
|
+
throw new Error("SpotPatch external Agent broker did not bind IPv4 loopback.");
|
|
1012
|
+
}
|
|
1013
|
+
expectedHost = `127.0.0.1:${String(address.port)}`;
|
|
1014
|
+
let closed = false;
|
|
1015
|
+
let ready = true;
|
|
1016
|
+
server.removeAllListeners("error");
|
|
1017
|
+
server.on("error", () => {
|
|
1018
|
+
ready = false;
|
|
1019
|
+
for (const socket of sockets) socket.destroy();
|
|
1020
|
+
});
|
|
1021
|
+
return Object.freeze({
|
|
1022
|
+
bridgeToken,
|
|
1023
|
+
endpoint: `http://${expectedHost}`,
|
|
1024
|
+
isReady: () => ready && !closed,
|
|
1025
|
+
async close() {
|
|
1026
|
+
if (closed) return;
|
|
1027
|
+
closed = true;
|
|
1028
|
+
ready = false;
|
|
1029
|
+
await closeServer(server, sockets);
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/external-handoff/discovery.ts
|
|
1035
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
1036
|
+
import { lstat, open, rename, unlink } from "fs/promises";
|
|
1037
|
+
import path from "path";
|
|
1038
|
+
import {
|
|
1039
|
+
EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION as EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION2,
|
|
1040
|
+
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS3
|
|
1041
|
+
} from "@spotpatch/shared";
|
|
1042
|
+
import {
|
|
1043
|
+
computeExternalHandoffProjectKey,
|
|
1044
|
+
externalHandoffDescriptorSchema,
|
|
1045
|
+
resolveExternalHandoffRuntimeDirectory
|
|
1046
|
+
} from "@spotpatch/shared/external-agent-node";
|
|
1047
|
+
async function syncDirectory(directory) {
|
|
1048
|
+
const handle = await open(directory, "r");
|
|
1049
|
+
try {
|
|
1050
|
+
await handle.sync();
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
const code = error.code;
|
|
1053
|
+
if (code !== "EINVAL" && code !== "ENOTSUP") {
|
|
1054
|
+
throw error;
|
|
1055
|
+
}
|
|
1056
|
+
} finally {
|
|
1057
|
+
await handle.close();
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
async function publishExternalHandoffDescriptor(options) {
|
|
1061
|
+
const directory = await resolveExternalHandoffRuntimeDirectory(true);
|
|
1062
|
+
const descriptor = externalHandoffDescriptorSchema.parse({
|
|
1063
|
+
schemaVersion: 1,
|
|
1064
|
+
brokerProtocolVersion: EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION2,
|
|
1065
|
+
projectKey: await computeExternalHandoffProjectKey(options.root),
|
|
1066
|
+
sessionId: options.sessionId,
|
|
1067
|
+
framework: options.framework,
|
|
1068
|
+
endpoint: options.endpoint,
|
|
1069
|
+
bridgeToken: options.bridgeToken,
|
|
1070
|
+
pid: process.pid,
|
|
1071
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1072
|
+
});
|
|
1073
|
+
const serialized = JSON.stringify(descriptor);
|
|
1074
|
+
if (Buffer.byteLength(serialized, "utf8") > EXTERNAL_HANDOFF_LIMITS3.maximumDescriptorBytes) {
|
|
1075
|
+
throw new RangeError("SpotPatch external Agent descriptor exceeds its limit.");
|
|
1076
|
+
}
|
|
1077
|
+
const destination = path.join(directory, `${descriptor.sessionId}.json`);
|
|
1078
|
+
const temporary = path.join(
|
|
1079
|
+
directory,
|
|
1080
|
+
`.${descriptor.sessionId}.${randomBytes4(8).toString("hex")}.tmp`
|
|
1081
|
+
);
|
|
1082
|
+
let temporaryExists = false;
|
|
1083
|
+
let published = false;
|
|
1084
|
+
let descriptorIdentity = Object.freeze({
|
|
1085
|
+
device: -1,
|
|
1086
|
+
inode: -1
|
|
1087
|
+
});
|
|
1088
|
+
try {
|
|
1089
|
+
const handle = await open(temporary, "wx", 384);
|
|
1090
|
+
temporaryExists = true;
|
|
1091
|
+
try {
|
|
1092
|
+
await handle.writeFile(serialized, "utf8");
|
|
1093
|
+
await handle.sync();
|
|
1094
|
+
} finally {
|
|
1095
|
+
await handle.close();
|
|
1096
|
+
}
|
|
1097
|
+
await rename(temporary, destination);
|
|
1098
|
+
temporaryExists = false;
|
|
1099
|
+
published = true;
|
|
1100
|
+
const status = await lstat(destination);
|
|
1101
|
+
const uid = process.getuid?.();
|
|
1102
|
+
if (!status.isFile() || status.isSymbolicLink() || uid === void 0 || status.uid !== uid || (status.mode & 63) !== 0) {
|
|
1103
|
+
throw new Error("SpotPatch external Agent descriptor is not private.");
|
|
1104
|
+
}
|
|
1105
|
+
descriptorIdentity = Object.freeze({ device: status.dev, inode: status.ino });
|
|
1106
|
+
await syncDirectory(directory);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
if (temporaryExists) {
|
|
1109
|
+
await unlink(temporary).catch(() => void 0);
|
|
1110
|
+
}
|
|
1111
|
+
if (published) {
|
|
1112
|
+
await unlink(destination).catch(() => void 0);
|
|
1113
|
+
}
|
|
1114
|
+
throw error;
|
|
1115
|
+
}
|
|
1116
|
+
let closed = false;
|
|
1117
|
+
return Object.freeze({
|
|
1118
|
+
descriptor,
|
|
1119
|
+
async close() {
|
|
1120
|
+
if (closed) return;
|
|
1121
|
+
closed = true;
|
|
1122
|
+
await lstat(destination).then(async (status) => {
|
|
1123
|
+
if (status.dev === descriptorIdentity.device && status.ino === descriptorIdentity.inode) {
|
|
1124
|
+
await unlink(destination);
|
|
1125
|
+
}
|
|
1126
|
+
}).catch((error) => {
|
|
1127
|
+
if (error.code !== "ENOENT") {
|
|
1128
|
+
throw error;
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
await syncDirectory(directory);
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/external-handoff/fingerprint.ts
|
|
1137
|
+
import { createHash as createHash2 } from "crypto";
|
|
1138
|
+
function canonicalJson(value) {
|
|
1139
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
1140
|
+
return JSON.stringify(value);
|
|
1141
|
+
}
|
|
1142
|
+
if (typeof value === "number") {
|
|
1143
|
+
if (!Number.isFinite(value)) throw new TypeError("Non-finite JSON number.");
|
|
1144
|
+
return JSON.stringify(value);
|
|
1145
|
+
}
|
|
1146
|
+
if (Array.isArray(value)) {
|
|
1147
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
1148
|
+
}
|
|
1149
|
+
if (typeof value === "object") {
|
|
1150
|
+
const record = value;
|
|
1151
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
1152
|
+
}
|
|
1153
|
+
throw new TypeError("Unsupported JSON value.");
|
|
1154
|
+
}
|
|
1155
|
+
function fingerprintExternalHandoffAnnotation(annotation) {
|
|
1156
|
+
return createHash2("sha256").update(canonicalJson(annotation)).digest("hex");
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// src/external-handoff/store.ts
|
|
1160
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
1161
|
+
import {
|
|
1162
|
+
ERROR_CODES as ERROR_CODES5,
|
|
1163
|
+
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS4,
|
|
1164
|
+
EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
|
|
1165
|
+
SpotPatchError as SpotPatchError5
|
|
1166
|
+
} from "@spotpatch/shared";
|
|
1167
|
+
function defaultRandomId2() {
|
|
1168
|
+
return randomBytes5(24).toString("base64url");
|
|
1169
|
+
}
|
|
1170
|
+
function pageSummary(annotation) {
|
|
1171
|
+
let origin = "[unavailable]";
|
|
1172
|
+
try {
|
|
1173
|
+
const url = new URL(annotation.page.url);
|
|
1174
|
+
origin = url.origin === "null" ? "[unavailable]" : url.origin;
|
|
1175
|
+
} catch {
|
|
1176
|
+
}
|
|
1177
|
+
return Object.freeze({ origin, pathname: annotation.page.pathname });
|
|
1178
|
+
}
|
|
1179
|
+
function summaryOf(current, state) {
|
|
1180
|
+
const snapshot2 = current.snapshot;
|
|
1181
|
+
return Object.freeze({
|
|
1182
|
+
sessionId: snapshot2.session.id,
|
|
1183
|
+
framework: snapshot2.session.framework,
|
|
1184
|
+
revision: snapshot2.revision,
|
|
1185
|
+
cursor: snapshot2.cursor,
|
|
1186
|
+
targetCount: snapshot2.annotation.targets.length,
|
|
1187
|
+
page: pageSummary(snapshot2.annotation),
|
|
1188
|
+
publishedAt: snapshot2.publishedAt,
|
|
1189
|
+
expiresAt: snapshot2.expiresAt,
|
|
1190
|
+
state,
|
|
1191
|
+
pickupCount: current.receipts.size,
|
|
1192
|
+
...current.pickedUpAt === void 0 ? {} : { pickedUpAt: current.pickedUpAt }
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
function replayResult(record) {
|
|
1196
|
+
return Object.freeze({ ...record.result, replayed: true });
|
|
1197
|
+
}
|
|
1198
|
+
function createExternalHandoffStore(options) {
|
|
1199
|
+
const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
|
|
1200
|
+
const randomId = options.randomId ?? defaultRandomId2;
|
|
1201
|
+
const history = [];
|
|
1202
|
+
const idempotency = /* @__PURE__ */ new Map();
|
|
1203
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
1204
|
+
let closed = false;
|
|
1205
|
+
let current;
|
|
1206
|
+
let revision = 0;
|
|
1207
|
+
const requireOpen = () => {
|
|
1208
|
+
if (closed) throw new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED);
|
|
1209
|
+
};
|
|
1210
|
+
const archive = (state) => {
|
|
1211
|
+
if (current === void 0) return;
|
|
1212
|
+
history.unshift(summaryOf(current, state));
|
|
1213
|
+
history.length = Math.min(
|
|
1214
|
+
history.length,
|
|
1215
|
+
EXTERNAL_HANDOFF_LIMITS4.maximumHistorySummaries
|
|
1216
|
+
);
|
|
1217
|
+
current = void 0;
|
|
1218
|
+
};
|
|
1219
|
+
const sweep = () => {
|
|
1220
|
+
const monotonicNow = clock.monotonicNow();
|
|
1221
|
+
if (current !== void 0 && monotonicNow >= current.expiresAtMonotonic) {
|
|
1222
|
+
archive("expired");
|
|
1223
|
+
}
|
|
1224
|
+
for (const [requestId, record] of idempotency) {
|
|
1225
|
+
if (monotonicNow >= record.expiresAtMonotonic) idempotency.delete(requestId);
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
const knownSummary = (cursor) => {
|
|
1229
|
+
sweep();
|
|
1230
|
+
if (current?.snapshot.cursor === cursor) {
|
|
1231
|
+
return summaryOf(current, "available");
|
|
1232
|
+
}
|
|
1233
|
+
return history.find((summary) => summary.cursor === cursor);
|
|
1234
|
+
};
|
|
1235
|
+
const readCurrent = (cursor) => {
|
|
1236
|
+
requireOpen();
|
|
1237
|
+
sweep();
|
|
1238
|
+
if (current === void 0) {
|
|
1239
|
+
const prior = cursor === void 0 ? void 0 : knownSummary(cursor);
|
|
1240
|
+
throw new SpotPatchError5(
|
|
1241
|
+
prior?.state === "expired" ? ERROR_CODES5.HANDOFF_EXPIRED : cursor === void 0 ? ERROR_CODES5.HANDOFF_NOT_FOUND : ERROR_CODES5.HANDOFF_CURSOR_INVALID
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
if (cursor !== void 0 && cursor !== current.snapshot.cursor) {
|
|
1245
|
+
const prior = knownSummary(cursor);
|
|
1246
|
+
throw new SpotPatchError5(
|
|
1247
|
+
prior?.state === "expired" ? ERROR_CODES5.HANDOFF_EXPIRED : ERROR_CODES5.HANDOFF_CURSOR_INVALID
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
return current.snapshot;
|
|
1251
|
+
};
|
|
1252
|
+
const findReplay = (requestId, fingerprint) => {
|
|
1253
|
+
requireOpen();
|
|
1254
|
+
sweep();
|
|
1255
|
+
const record = idempotency.get(requestId);
|
|
1256
|
+
if (record === void 0) return void 0;
|
|
1257
|
+
if (record.fingerprint !== fingerprint) {
|
|
1258
|
+
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_VALIDATION_FAILED);
|
|
1259
|
+
}
|
|
1260
|
+
return replayResult(record);
|
|
1261
|
+
};
|
|
1262
|
+
const settleWaiters = (result) => {
|
|
1263
|
+
const pending = [...waiters];
|
|
1264
|
+
waiters.clear();
|
|
1265
|
+
for (const waiter of pending) waiter.resolve(result);
|
|
1266
|
+
};
|
|
1267
|
+
return Object.freeze({
|
|
1268
|
+
activeWaitCount: () => waiters.size,
|
|
1269
|
+
replay: findReplay,
|
|
1270
|
+
publish(input) {
|
|
1271
|
+
requireOpen();
|
|
1272
|
+
const replayed = findReplay(input.requestId, input.fingerprint);
|
|
1273
|
+
if (replayed !== void 0) return replayed;
|
|
1274
|
+
if (idempotency.size >= EXTERNAL_HANDOFF_LIMITS4.maximumRequestIdRecords) {
|
|
1275
|
+
throw new SpotPatchError5(ERROR_CODES5.EXTERNAL_HANDOFF_UNAVAILABLE);
|
|
1276
|
+
}
|
|
1277
|
+
const nextRevision = revision + 1;
|
|
1278
|
+
const publishedAtMs = clock.wallNow();
|
|
1279
|
+
const publishedAtMonotonic = clock.monotonicNow();
|
|
1280
|
+
const snapshot2 = Object.freeze({
|
|
1281
|
+
schemaVersion: EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
|
|
1282
|
+
cursor: randomId(),
|
|
1283
|
+
session: Object.freeze({ id: options.sessionId, framework: options.framework }),
|
|
1284
|
+
revision: nextRevision,
|
|
1285
|
+
publishedAt: new Date(publishedAtMs).toISOString(),
|
|
1286
|
+
expiresAt: new Date(
|
|
1287
|
+
publishedAtMs + EXTERNAL_HANDOFF_LIMITS4.handoffTtlMs
|
|
1288
|
+
).toISOString(),
|
|
1289
|
+
annotation: input.annotation
|
|
1290
|
+
});
|
|
1291
|
+
if (Buffer.byteLength(JSON.stringify(snapshot2), "utf8") > EXTERNAL_HANDOFF_LIMITS4.maximumSnapshotBytes) {
|
|
1292
|
+
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_RESPONSE_TOO_LARGE);
|
|
1293
|
+
}
|
|
1294
|
+
const delivery = input.reserve(snapshot2.cursor, nextRevision);
|
|
1295
|
+
if (current !== void 0) archive("superseded");
|
|
1296
|
+
revision = nextRevision;
|
|
1297
|
+
current = {
|
|
1298
|
+
expiresAtMonotonic: publishedAtMonotonic + EXTERNAL_HANDOFF_LIMITS4.handoffTtlMs,
|
|
1299
|
+
receipts: /* @__PURE__ */ new Set(),
|
|
1300
|
+
snapshot: snapshot2
|
|
1301
|
+
};
|
|
1302
|
+
const result = Object.freeze({
|
|
1303
|
+
handoff: summaryOf(current, "available"),
|
|
1304
|
+
delivery,
|
|
1305
|
+
replayed: false
|
|
1306
|
+
});
|
|
1307
|
+
idempotency.set(
|
|
1308
|
+
input.requestId,
|
|
1309
|
+
Object.freeze({
|
|
1310
|
+
expiresAtMonotonic: publishedAtMonotonic + EXTERNAL_HANDOFF_LIMITS4.requestIdTtlMs,
|
|
1311
|
+
fingerprint: input.fingerprint,
|
|
1312
|
+
result
|
|
1313
|
+
})
|
|
1314
|
+
);
|
|
1315
|
+
settleWaiters(Object.freeze({ outcome: "handoff", snapshot: snapshot2 }));
|
|
1316
|
+
return result;
|
|
1317
|
+
},
|
|
1318
|
+
current: readCurrent,
|
|
1319
|
+
currentCursor() {
|
|
1320
|
+
requireOpen();
|
|
1321
|
+
sweep();
|
|
1322
|
+
return current?.snapshot.cursor ?? null;
|
|
1323
|
+
},
|
|
1324
|
+
status(cursor) {
|
|
1325
|
+
requireOpen();
|
|
1326
|
+
sweep();
|
|
1327
|
+
if (cursor === void 0) {
|
|
1328
|
+
if (current === void 0) {
|
|
1329
|
+
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_NOT_FOUND);
|
|
1330
|
+
}
|
|
1331
|
+
return summaryOf(current, "available");
|
|
1332
|
+
}
|
|
1333
|
+
const summary = knownSummary(cursor);
|
|
1334
|
+
if (summary === void 0) {
|
|
1335
|
+
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_CURSOR_INVALID);
|
|
1336
|
+
}
|
|
1337
|
+
return summary;
|
|
1338
|
+
},
|
|
1339
|
+
ack(cursor, connectorInstanceId) {
|
|
1340
|
+
const snapshot2 = readCurrent(cursor);
|
|
1341
|
+
if (snapshot2 !== current?.snapshot) {
|
|
1342
|
+
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_CURSOR_INVALID);
|
|
1343
|
+
}
|
|
1344
|
+
if (!current.receipts.has(connectorInstanceId)) {
|
|
1345
|
+
if (current.receipts.size >= EXTERNAL_HANDOFF_LIMITS4.maximumConnectorReceipts) {
|
|
1346
|
+
throw new SpotPatchError5(ERROR_CODES5.BRIDGE_BUSY);
|
|
1347
|
+
}
|
|
1348
|
+
current.receipts.add(connectorInstanceId);
|
|
1349
|
+
current.pickedUpAt = new Date(clock.wallNow()).toISOString();
|
|
1350
|
+
}
|
|
1351
|
+
return summaryOf(current, "available");
|
|
1352
|
+
},
|
|
1353
|
+
async wait(afterCursor, timeoutMs, signal) {
|
|
1354
|
+
requireOpen();
|
|
1355
|
+
sweep();
|
|
1356
|
+
if (afterCursor === void 0 && current !== void 0) {
|
|
1357
|
+
return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
|
|
1358
|
+
}
|
|
1359
|
+
if (afterCursor !== void 0) {
|
|
1360
|
+
const known = knownSummary(afterCursor);
|
|
1361
|
+
if (known === void 0) {
|
|
1362
|
+
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_CURSOR_INVALID);
|
|
1363
|
+
}
|
|
1364
|
+
if (current !== void 0 && current.snapshot.cursor !== afterCursor) {
|
|
1365
|
+
return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (waiters.size >= EXTERNAL_HANDOFF_LIMITS4.maximumWaiters) {
|
|
1369
|
+
throw new SpotPatchError5(ERROR_CODES5.BRIDGE_BUSY);
|
|
1370
|
+
}
|
|
1371
|
+
if (signal.aborted) throw new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED);
|
|
1372
|
+
return new Promise((resolve, reject) => {
|
|
1373
|
+
let settled = false;
|
|
1374
|
+
const finish = () => {
|
|
1375
|
+
if (settled) return false;
|
|
1376
|
+
settled = true;
|
|
1377
|
+
waiters.delete(waiter);
|
|
1378
|
+
clearTimeout(timer);
|
|
1379
|
+
signal.removeEventListener("abort", abort);
|
|
1380
|
+
return true;
|
|
1381
|
+
};
|
|
1382
|
+
const waiter = {
|
|
1383
|
+
reject(error) {
|
|
1384
|
+
if (finish()) reject(error);
|
|
1385
|
+
},
|
|
1386
|
+
resolve(result) {
|
|
1387
|
+
if (finish()) resolve(result);
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
const abort = () => {
|
|
1391
|
+
waiter.reject(new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED));
|
|
1392
|
+
};
|
|
1393
|
+
const timer = setTimeout(() => {
|
|
1394
|
+
waiter.resolve(Object.freeze({ outcome: "timeout" }));
|
|
1395
|
+
}, timeoutMs);
|
|
1396
|
+
timer.unref();
|
|
1397
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1398
|
+
waiters.add(waiter);
|
|
1399
|
+
});
|
|
1400
|
+
},
|
|
1401
|
+
close() {
|
|
1402
|
+
if (closed) return;
|
|
1403
|
+
closed = true;
|
|
1404
|
+
current = void 0;
|
|
1405
|
+
history.length = 0;
|
|
1406
|
+
idempotency.clear();
|
|
1407
|
+
const pending = [...waiters];
|
|
1408
|
+
waiters.clear();
|
|
1409
|
+
for (const waiter of pending) {
|
|
1410
|
+
waiter.reject(new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED));
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/external-handoff/service.ts
|
|
1417
|
+
function asReplay(result) {
|
|
1418
|
+
return Object.freeze({ ...result, replayed: true });
|
|
1419
|
+
}
|
|
1420
|
+
function createExternalHandoffService(options) {
|
|
1421
|
+
const activeRegistry = createActiveAdapterRegistry();
|
|
1422
|
+
const store = createExternalHandoffStore({
|
|
1423
|
+
framework: options.framework,
|
|
1424
|
+
sessionId: options.sessionId
|
|
1425
|
+
});
|
|
1426
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1427
|
+
let broker;
|
|
1428
|
+
let descriptor;
|
|
1429
|
+
let startPromise;
|
|
1430
|
+
let closePromise;
|
|
1431
|
+
let state = "idle";
|
|
1432
|
+
const isClosed = () => state === "closed";
|
|
1433
|
+
const requireReady = () => {
|
|
1434
|
+
if (state !== "ready" || broker?.isReady() !== true) {
|
|
1435
|
+
throw new SpotPatchError6(
|
|
1436
|
+
state === "closed" ? ERROR_CODES6.SESSION_CLOSED : ERROR_CODES6.EXTERNAL_HANDOFF_UNAVAILABLE
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
const start = async () => {
|
|
1441
|
+
if (state === "ready") return;
|
|
1442
|
+
if (state === "closed") throw new SpotPatchError6(ERROR_CODES6.SESSION_CLOSED);
|
|
1443
|
+
if (startPromise !== void 0) return startPromise;
|
|
1444
|
+
state = "starting";
|
|
1445
|
+
startPromise = (async () => {
|
|
1446
|
+
let createdBroker;
|
|
1447
|
+
let createdDescriptor;
|
|
1448
|
+
try {
|
|
1449
|
+
const projectKey = await computeExternalHandoffProjectKey2(options.root);
|
|
1450
|
+
createdBroker = await createExternalHandoffBroker({
|
|
1451
|
+
activeRegistry,
|
|
1452
|
+
framework: options.framework,
|
|
1453
|
+
projectKey,
|
|
1454
|
+
sessionId: options.sessionId,
|
|
1455
|
+
store
|
|
1456
|
+
});
|
|
1457
|
+
createdDescriptor = await publishExternalHandoffDescriptor({
|
|
1458
|
+
bridgeToken: createdBroker.bridgeToken,
|
|
1459
|
+
endpoint: createdBroker.endpoint,
|
|
1460
|
+
framework: options.framework,
|
|
1461
|
+
root: options.root,
|
|
1462
|
+
sessionId: options.sessionId
|
|
1463
|
+
});
|
|
1464
|
+
if (isClosed()) {
|
|
1465
|
+
await createdDescriptor.close();
|
|
1466
|
+
await createdBroker.close();
|
|
1467
|
+
throw new SpotPatchError6(ERROR_CODES6.SESSION_CLOSED);
|
|
1468
|
+
}
|
|
1469
|
+
broker = createdBroker;
|
|
1470
|
+
descriptor = createdDescriptor;
|
|
1471
|
+
state = "ready";
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
if (createdDescriptor !== void 0 && descriptor !== createdDescriptor) {
|
|
1474
|
+
await createdDescriptor.close().catch(() => void 0);
|
|
1475
|
+
}
|
|
1476
|
+
if (createdBroker !== void 0 && broker !== createdBroker) {
|
|
1477
|
+
await createdBroker.close().catch(() => void 0);
|
|
1478
|
+
}
|
|
1479
|
+
if (!isClosed()) state = "failed";
|
|
1480
|
+
throw error;
|
|
1481
|
+
}
|
|
1482
|
+
})();
|
|
1483
|
+
return startPromise;
|
|
1484
|
+
};
|
|
1485
|
+
return Object.freeze({
|
|
1486
|
+
start,
|
|
1487
|
+
capability() {
|
|
1488
|
+
const currentCursor = store.currentCursor();
|
|
1489
|
+
const active = activeRegistry.snapshot(currentCursor ?? void 0);
|
|
1490
|
+
return Object.freeze({
|
|
1491
|
+
enabled: true,
|
|
1492
|
+
brokerReady: state === "ready" && broker?.isReady() === true,
|
|
1493
|
+
activeWaitCount: store.activeWaitCount(),
|
|
1494
|
+
snapshotSchemaVersion: EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION2,
|
|
1495
|
+
brokerProtocolVersion: EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION3,
|
|
1496
|
+
activeAdapter: active.activeAdapter,
|
|
1497
|
+
dispatch: currentCursor === null ? null : active.dispatch
|
|
1498
|
+
});
|
|
1499
|
+
},
|
|
1500
|
+
async publish(request, authorize) {
|
|
1501
|
+
requireReady();
|
|
1502
|
+
const fingerprint = fingerprintExternalHandoffAnnotation(request.annotation);
|
|
1503
|
+
const replayed = store.replay(request.requestId, fingerprint);
|
|
1504
|
+
if (replayed !== void 0) return replayed;
|
|
1505
|
+
const pending = inFlight.get(request.requestId);
|
|
1506
|
+
if (pending !== void 0) {
|
|
1507
|
+
if (pending.fingerprint !== fingerprint) {
|
|
1508
|
+
throw new SpotPatchError6(ERROR_CODES6.HANDOFF_VALIDATION_FAILED);
|
|
1509
|
+
}
|
|
1510
|
+
return asReplay(await pending.promise);
|
|
1511
|
+
}
|
|
1512
|
+
activeRegistry.assertPublishable();
|
|
1513
|
+
const promise = (async () => {
|
|
1514
|
+
const annotation = await authorize(request.annotation);
|
|
1515
|
+
return store.publish({
|
|
1516
|
+
annotation,
|
|
1517
|
+
fingerprint,
|
|
1518
|
+
requestId: request.requestId,
|
|
1519
|
+
reserve: activeRegistry.reserve
|
|
1520
|
+
});
|
|
1521
|
+
})();
|
|
1522
|
+
const activePublish = Object.freeze({ fingerprint, promise });
|
|
1523
|
+
inFlight.set(request.requestId, activePublish);
|
|
1524
|
+
try {
|
|
1525
|
+
return await promise;
|
|
1526
|
+
} finally {
|
|
1527
|
+
if (inFlight.get(request.requestId) === activePublish) {
|
|
1528
|
+
inFlight.delete(request.requestId);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
},
|
|
1532
|
+
status(cursor) {
|
|
1533
|
+
requireReady();
|
|
1534
|
+
const handoff = store.status(cursor);
|
|
1535
|
+
const active = activeRegistry.snapshot(cursor ?? handoff.cursor);
|
|
1536
|
+
return Object.freeze({
|
|
1537
|
+
handoff,
|
|
1538
|
+
activeAdapter: active.activeAdapter,
|
|
1539
|
+
dispatch: active.dispatch
|
|
1540
|
+
});
|
|
1541
|
+
},
|
|
1542
|
+
resolveDelivery(cursor) {
|
|
1543
|
+
requireReady();
|
|
1544
|
+
const handoff = store.status(cursor);
|
|
1545
|
+
const active = activeRegistry.resolveDelivery(cursor);
|
|
1546
|
+
return Object.freeze({
|
|
1547
|
+
handoff,
|
|
1548
|
+
activeAdapter: active.activeAdapter,
|
|
1549
|
+
dispatch: active.dispatch
|
|
1550
|
+
});
|
|
1551
|
+
},
|
|
1552
|
+
close() {
|
|
1553
|
+
closePromise ??= (async () => {
|
|
1554
|
+
if (state === "closed") return;
|
|
1555
|
+
state = "closed";
|
|
1556
|
+
activeRegistry.close();
|
|
1557
|
+
store.close();
|
|
1558
|
+
inFlight.clear();
|
|
1559
|
+
await startPromise?.catch(() => void 0);
|
|
1560
|
+
const publishedDescriptor = descriptor;
|
|
1561
|
+
descriptor = void 0;
|
|
1562
|
+
const activeBroker = broker;
|
|
1563
|
+
broker = void 0;
|
|
1564
|
+
await publishedDescriptor?.close().catch(() => void 0);
|
|
1565
|
+
await activeBroker?.close().catch(() => void 0);
|
|
1566
|
+
})();
|
|
1567
|
+
return closePromise;
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
|
|
475
1572
|
// src/environment-ai.ts
|
|
476
1573
|
var AI_ENVIRONMENT_NAMES = Object.freeze({
|
|
477
1574
|
authentication: "SPOTPATCH_AI_AUTHENTICATION",
|
|
@@ -562,32 +1659,32 @@ function resolveEnvironmentAiConfiguration(environment) {
|
|
|
562
1659
|
}
|
|
563
1660
|
|
|
564
1661
|
// src/integration/file-plan.ts
|
|
565
|
-
import { randomBytes as
|
|
1662
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
566
1663
|
import {
|
|
567
1664
|
access,
|
|
568
|
-
lstat,
|
|
1665
|
+
lstat as lstat2,
|
|
569
1666
|
mkdir,
|
|
570
1667
|
readFile,
|
|
571
1668
|
realpath,
|
|
572
|
-
rename,
|
|
1669
|
+
rename as rename2,
|
|
573
1670
|
stat,
|
|
574
|
-
unlink,
|
|
1671
|
+
unlink as unlink2,
|
|
575
1672
|
writeFile
|
|
576
1673
|
} from "fs/promises";
|
|
577
|
-
import
|
|
1674
|
+
import path2 from "path";
|
|
578
1675
|
function isMissingPathError(error) {
|
|
579
1676
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
580
1677
|
}
|
|
581
1678
|
function isPathWithin(root, target) {
|
|
582
|
-
const relative =
|
|
583
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
1679
|
+
const relative = path2.relative(root, target);
|
|
1680
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(relative);
|
|
584
1681
|
}
|
|
585
1682
|
function relativePathWithin(root, target) {
|
|
586
|
-
const relative =
|
|
1683
|
+
const relative = path2.relative(root, target);
|
|
587
1684
|
if (relative.length === 0 || !isPathWithin(root, target)) {
|
|
588
1685
|
throw new Error("SpotPatch init refuses to modify a path outside the app root.");
|
|
589
1686
|
}
|
|
590
|
-
return relative.split(
|
|
1687
|
+
return relative.split(path2.sep).join("/");
|
|
591
1688
|
}
|
|
592
1689
|
async function integrationPathExists(absolutePath) {
|
|
593
1690
|
try {
|
|
@@ -598,10 +1695,10 @@ async function integrationPathExists(absolutePath) {
|
|
|
598
1695
|
}
|
|
599
1696
|
}
|
|
600
1697
|
async function readIntegrationFile(absolutePath) {
|
|
601
|
-
const metadata = await
|
|
1698
|
+
const metadata = await lstat2(absolutePath);
|
|
602
1699
|
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
603
1700
|
throw new Error(
|
|
604
|
-
`SpotPatch refuses to modify the non-regular file ${
|
|
1701
|
+
`SpotPatch refuses to modify the non-regular file ${path2.basename(absolutePath)}.`
|
|
605
1702
|
);
|
|
606
1703
|
}
|
|
607
1704
|
return readFile(absolutePath, "utf8");
|
|
@@ -610,8 +1707,8 @@ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previou
|
|
|
610
1707
|
if (previousContent === nextContent) {
|
|
611
1708
|
return void 0;
|
|
612
1709
|
}
|
|
613
|
-
const root =
|
|
614
|
-
const target =
|
|
1710
|
+
const root = path2.resolve(appRoot);
|
|
1711
|
+
const target = path2.resolve(absolutePath);
|
|
615
1712
|
return Object.freeze({
|
|
616
1713
|
absolutePath: target,
|
|
617
1714
|
nextContent,
|
|
@@ -620,19 +1717,19 @@ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previou
|
|
|
620
1717
|
});
|
|
621
1718
|
}
|
|
622
1719
|
function temporaryPath(absolutePath, label) {
|
|
623
|
-
return
|
|
624
|
-
|
|
625
|
-
`.${
|
|
1720
|
+
return path2.join(
|
|
1721
|
+
path2.dirname(absolutePath),
|
|
1722
|
+
`.${path2.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${randomBytes6(8).toString("hex")}`
|
|
626
1723
|
);
|
|
627
1724
|
}
|
|
628
1725
|
async function writeAtomic(absolutePath, content, mode) {
|
|
629
|
-
await mkdir(
|
|
1726
|
+
await mkdir(path2.dirname(absolutePath), { recursive: true });
|
|
630
1727
|
const stagedPath = temporaryPath(absolutePath, "stage");
|
|
631
1728
|
try {
|
|
632
1729
|
await writeFile(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
|
|
633
|
-
await
|
|
1730
|
+
await rename2(stagedPath, absolutePath);
|
|
634
1731
|
} catch (error) {
|
|
635
|
-
await
|
|
1732
|
+
await unlink2(stagedPath).catch(() => void 0);
|
|
636
1733
|
throw error;
|
|
637
1734
|
}
|
|
638
1735
|
}
|
|
@@ -644,21 +1741,21 @@ async function rollbackChange(change) {
|
|
|
644
1741
|
);
|
|
645
1742
|
}
|
|
646
1743
|
if (change.previousContent === void 0) {
|
|
647
|
-
await
|
|
1744
|
+
await unlink2(change.absolutePath);
|
|
648
1745
|
return;
|
|
649
1746
|
}
|
|
650
1747
|
const mode = (await stat(change.absolutePath)).mode & 511;
|
|
651
1748
|
await writeAtomic(change.absolutePath, change.previousContent, mode);
|
|
652
1749
|
}
|
|
653
1750
|
async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
654
|
-
const target =
|
|
1751
|
+
const target = path2.resolve(change.absolutePath);
|
|
655
1752
|
const relativePath = relativePathWithin(appRoot, target);
|
|
656
|
-
if (target !== change.absolutePath || relativePath !== change.relativePath ||
|
|
1753
|
+
if (target !== change.absolutePath || relativePath !== change.relativePath || path2.dirname(target) === target) {
|
|
657
1754
|
throw new Error("SpotPatch init received an invalid integration file plan.");
|
|
658
1755
|
}
|
|
659
1756
|
let targetMetadata;
|
|
660
1757
|
try {
|
|
661
|
-
targetMetadata = await
|
|
1758
|
+
targetMetadata = await lstat2(target);
|
|
662
1759
|
} catch (error) {
|
|
663
1760
|
if (!isMissingPathError(error)) {
|
|
664
1761
|
throw error;
|
|
@@ -670,7 +1767,7 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
|
670
1767
|
);
|
|
671
1768
|
}
|
|
672
1769
|
const containmentAnchor = await realpath(
|
|
673
|
-
targetMetadata === void 0 ?
|
|
1770
|
+
targetMetadata === void 0 ? path2.dirname(target) : target
|
|
674
1771
|
);
|
|
675
1772
|
if (!isPathWithin(realAppRoot, containmentAnchor)) {
|
|
676
1773
|
throw new Error("SpotPatch init refuses to modify a path outside the app root.");
|
|
@@ -679,7 +1776,7 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
|
679
1776
|
async function assertCurrentBaseline(change) {
|
|
680
1777
|
if (change.previousContent === void 0) {
|
|
681
1778
|
try {
|
|
682
|
-
await
|
|
1779
|
+
await lstat2(change.absolutePath);
|
|
683
1780
|
} catch (error) {
|
|
684
1781
|
if (isMissingPathError(error)) {
|
|
685
1782
|
return;
|
|
@@ -701,7 +1798,7 @@ async function applyIntegrationPlan(plan) {
|
|
|
701
1798
|
if (plan.changes.length === 0) {
|
|
702
1799
|
return;
|
|
703
1800
|
}
|
|
704
|
-
const appRoot =
|
|
1801
|
+
const appRoot = path2.resolve(plan.appRoot);
|
|
705
1802
|
const realAppRoot = await realpath(appRoot);
|
|
706
1803
|
const targets = /* @__PURE__ */ new Set();
|
|
707
1804
|
for (const change of plan.changes) {
|
|
@@ -780,7 +1877,8 @@ var DEFAULT_OPTIONS = Object.freeze({
|
|
|
780
1877
|
enabled: false,
|
|
781
1878
|
runtime: "dispatch",
|
|
782
1879
|
limits: DEFAULT_DATA_FLOW_LIMITS
|
|
783
|
-
})
|
|
1880
|
+
}),
|
|
1881
|
+
externalAgent: Object.freeze({ enabled: false })
|
|
784
1882
|
});
|
|
785
1883
|
var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
786
1884
|
var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
@@ -1106,6 +2204,9 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1106
2204
|
if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
|
|
1107
2205
|
throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
|
|
1108
2206
|
}
|
|
2207
|
+
if (options.externalAgent !== void 0 && typeof options.externalAgent !== "boolean") {
|
|
2208
|
+
throw new RangeError("SpotPatch externalAgent must be a boolean.");
|
|
2209
|
+
}
|
|
1109
2210
|
const budget = Object.freeze({
|
|
1110
2211
|
...DEFAULT_OPTIONS.budget,
|
|
1111
2212
|
...options.budget
|
|
@@ -1138,7 +2239,10 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1138
2239
|
locale,
|
|
1139
2240
|
maxTargets,
|
|
1140
2241
|
ai: resolveAiOptions(options.ai ?? environmentAi),
|
|
1141
|
-
dataFlow: resolveDataFlowOptions(options.dataFlow)
|
|
2242
|
+
dataFlow: resolveDataFlowOptions(options.dataFlow),
|
|
2243
|
+
externalAgent: Object.freeze({
|
|
2244
|
+
enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
|
|
2245
|
+
})
|
|
1142
2246
|
};
|
|
1143
2247
|
if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
|
|
1144
2248
|
throw new RangeError("SpotPatch shortcut is invalid.");
|
|
@@ -1148,10 +2252,13 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1148
2252
|
|
|
1149
2253
|
// src/project-validation.ts
|
|
1150
2254
|
import { execFile } from "child_process";
|
|
1151
|
-
import { access as access2, lstat as
|
|
2255
|
+
import { access as access2, lstat as lstat3, readFile as readFile2, realpath as realpath2 } from "fs/promises";
|
|
1152
2256
|
import { createRequire } from "module";
|
|
1153
|
-
import
|
|
2257
|
+
import path3 from "path";
|
|
1154
2258
|
import { promisify } from "util";
|
|
2259
|
+
import {
|
|
2260
|
+
DEFAULT_AGENT_LIMITS as DEFAULT_AGENT_LIMITS2
|
|
2261
|
+
} from "@spotpatch/shared";
|
|
1155
2262
|
var execFileAsync = promisify(execFile);
|
|
1156
2263
|
var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
|
|
1157
2264
|
var TYPESCRIPT_CHECK_LABEL = "TypeScript";
|
|
@@ -1160,14 +2267,14 @@ function isRecord(value) {
|
|
|
1160
2267
|
}
|
|
1161
2268
|
async function isRegularFile(absolutePath) {
|
|
1162
2269
|
try {
|
|
1163
|
-
const metadata = await
|
|
2270
|
+
const metadata = await lstat3(absolutePath);
|
|
1164
2271
|
return metadata.isFile() && !metadata.isSymbolicLink();
|
|
1165
2272
|
} catch {
|
|
1166
2273
|
return false;
|
|
1167
2274
|
}
|
|
1168
2275
|
}
|
|
1169
2276
|
async function readManifest(appRoot) {
|
|
1170
|
-
const manifestPath =
|
|
2277
|
+
const manifestPath = path3.join(appRoot, "package.json");
|
|
1171
2278
|
if (!await isRegularFile(manifestPath)) {
|
|
1172
2279
|
return void 0;
|
|
1173
2280
|
}
|
|
@@ -1196,8 +2303,8 @@ async function findGitRoot(appRoot) {
|
|
|
1196
2303
|
windowsHide: true
|
|
1197
2304
|
});
|
|
1198
2305
|
const root = await realpath2(result.stdout.trim());
|
|
1199
|
-
const relative =
|
|
1200
|
-
if (relative === "" || !relative.startsWith(`..${
|
|
2306
|
+
const relative = path3.relative(root, appRoot);
|
|
2307
|
+
if (relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative)) {
|
|
1201
2308
|
return root;
|
|
1202
2309
|
}
|
|
1203
2310
|
} catch {
|
|
@@ -1206,10 +2313,10 @@ async function findGitRoot(appRoot) {
|
|
|
1206
2313
|
return void 0;
|
|
1207
2314
|
}
|
|
1208
2315
|
async function resolveTypeScriptCli(appRoot) {
|
|
1209
|
-
const resolveFromApplication = createRequire(
|
|
2316
|
+
const resolveFromApplication = createRequire(path3.join(appRoot, "package.json"));
|
|
1210
2317
|
try {
|
|
1211
2318
|
const packagePath = resolveFromApplication.resolve("typescript/package.json");
|
|
1212
|
-
const cliPath =
|
|
2319
|
+
const cliPath = path3.join(path3.dirname(packagePath), "bin", "tsc");
|
|
1213
2320
|
await access2(cliPath);
|
|
1214
2321
|
return await realpath2(cliPath);
|
|
1215
2322
|
} catch {
|
|
@@ -1217,11 +2324,20 @@ async function resolveTypeScriptCli(appRoot) {
|
|
|
1217
2324
|
}
|
|
1218
2325
|
}
|
|
1219
2326
|
function portableRelativePath(from, to) {
|
|
1220
|
-
return
|
|
2327
|
+
return path3.relative(from, to).split(path3.sep).join("/");
|
|
2328
|
+
}
|
|
2329
|
+
function hasRequiredCheck(checks) {
|
|
2330
|
+
return Object.values(checks).some((check) => check.required);
|
|
2331
|
+
}
|
|
2332
|
+
function availableCheckId(checks, preferred) {
|
|
2333
|
+
if (checks[preferred] === void 0) return preferred;
|
|
2334
|
+
let suffix = 2;
|
|
2335
|
+
while (checks[`${preferred}-${String(suffix)}`] !== void 0) suffix += 1;
|
|
2336
|
+
return `${preferred}-${String(suffix)}`;
|
|
1221
2337
|
}
|
|
1222
2338
|
async function discoverProjectValidationCheck(options) {
|
|
1223
2339
|
const appRoot = await realpath2(options.appRoot);
|
|
1224
|
-
const tsconfigPath =
|
|
2340
|
+
const tsconfigPath = path3.join(appRoot, "tsconfig.json");
|
|
1225
2341
|
const [manifest, projectRoot, hasTsconfig] = await Promise.all([
|
|
1226
2342
|
readManifest(appRoot),
|
|
1227
2343
|
findGitRoot(appRoot),
|
|
@@ -1247,6 +2363,8 @@ async function discoverProjectValidationCheck(options) {
|
|
|
1247
2363
|
"--noEmit",
|
|
1248
2364
|
"--pretty",
|
|
1249
2365
|
"false",
|
|
2366
|
+
"--incremental",
|
|
2367
|
+
"false",
|
|
1250
2368
|
"--project",
|
|
1251
2369
|
projectPath
|
|
1252
2370
|
]),
|
|
@@ -1254,21 +2372,30 @@ async function discoverProjectValidationCheck(options) {
|
|
|
1254
2372
|
timeoutMs: options.timeoutMs
|
|
1255
2373
|
});
|
|
1256
2374
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
2375
|
+
async function resolveProjectValidationChecks(options) {
|
|
2376
|
+
if (hasRequiredCheck(options.checks)) return options.checks;
|
|
2377
|
+
const discovered = await discoverProjectValidationCheck(options);
|
|
2378
|
+
if (discovered === void 0) return options.checks;
|
|
2379
|
+
const id = availableCheckId(options.checks, discovered.id);
|
|
2380
|
+
return Object.freeze({
|
|
2381
|
+
...options.checks,
|
|
2382
|
+
[id]: Object.freeze({ ...discovered, id })
|
|
2383
|
+
});
|
|
1261
2384
|
}
|
|
1262
|
-
function
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
2385
|
+
async function resolveManagedExecutionValidation(options) {
|
|
2386
|
+
const checks = options.ai === false ? Object.freeze({}) : options.ai.execution.checks;
|
|
2387
|
+
const limits = options.ai === false ? DEFAULT_AGENT_LIMITS2 : options.ai.execution.limits;
|
|
2388
|
+
return Object.freeze({
|
|
2389
|
+
checks: await resolveProjectValidationChecks({
|
|
2390
|
+
appRoot: options.appRoot,
|
|
2391
|
+
checks,
|
|
2392
|
+
timeoutMs: limits.checkTimeoutMs
|
|
2393
|
+
}),
|
|
2394
|
+
limits
|
|
2395
|
+
});
|
|
1271
2396
|
}
|
|
2397
|
+
|
|
2398
|
+
// src/project-options.ts
|
|
1272
2399
|
async function resolveProjectOptions(input) {
|
|
1273
2400
|
const userOptions = input.options ?? {};
|
|
1274
2401
|
const resolved = resolveOptions(userOptions, input.environmentAi);
|
|
@@ -1280,22 +2407,15 @@ async function resolveProjectOptions(input) {
|
|
|
1280
2407
|
"SpotPatch trustedFastMode cannot be combined with applyMode auto."
|
|
1281
2408
|
);
|
|
1282
2409
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
);
|
|
1293
|
-
}
|
|
1294
|
-
const id = availableCheckId(checks, discovered.id);
|
|
1295
|
-
checks = Object.freeze({
|
|
1296
|
-
...checks,
|
|
1297
|
-
[id]: Object.freeze({ ...discovered, id })
|
|
1298
|
-
});
|
|
2410
|
+
const checks = await resolveProjectValidationChecks({
|
|
2411
|
+
appRoot: input.appRoot,
|
|
2412
|
+
checks: resolved.ai.execution.checks,
|
|
2413
|
+
timeoutMs: resolved.ai.execution.limits.checkTimeoutMs
|
|
2414
|
+
});
|
|
2415
|
+
if (!Object.values(checks).some((check) => check.required)) {
|
|
2416
|
+
throw new RangeError(
|
|
2417
|
+
"SpotPatch trustedFastMode requires a configured required check or a local TypeScript project with tsconfig.json."
|
|
2418
|
+
);
|
|
1299
2419
|
}
|
|
1300
2420
|
const ai = Object.freeze({
|
|
1301
2421
|
...resolved.ai,
|
|
@@ -1309,16 +2429,16 @@ async function resolveProjectOptions(input) {
|
|
|
1309
2429
|
}
|
|
1310
2430
|
|
|
1311
2431
|
// src/registry/source-registry.ts
|
|
1312
|
-
import
|
|
2432
|
+
import path4 from "path";
|
|
1313
2433
|
|
|
1314
2434
|
// src/registry/source-id.ts
|
|
1315
|
-
import { randomBytes as
|
|
2435
|
+
import { randomBytes as randomBytes7 } from "crypto";
|
|
1316
2436
|
var SOURCE_ID_BYTES = 8;
|
|
1317
|
-
var createRandomSourceId = () =>
|
|
2437
|
+
var createRandomSourceId = () => randomBytes7(SOURCE_ID_BYTES).toString("base64url");
|
|
1318
2438
|
|
|
1319
2439
|
// src/registry/source-registry.ts
|
|
1320
2440
|
function normalizeAbsolutePath(absolutePath) {
|
|
1321
|
-
return
|
|
2441
|
+
return path4.normalize(path4.resolve(absolutePath));
|
|
1322
2442
|
}
|
|
1323
2443
|
function createSourceRegistry(options = {}) {
|
|
1324
2444
|
const createId = options.createId ?? createRandomSourceId;
|
|
@@ -1376,39 +2496,42 @@ function createSourceRegistry(options = {}) {
|
|
|
1376
2496
|
|
|
1377
2497
|
// src/server/middleware.ts
|
|
1378
2498
|
import {
|
|
1379
|
-
ERROR_CODES as
|
|
2499
|
+
ERROR_CODES as ERROR_CODES16,
|
|
1380
2500
|
SPOTPATCH_API_BASE,
|
|
1381
|
-
SPOTPATCH_ENDPOINTS as
|
|
1382
|
-
SpotPatchError as
|
|
2501
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS4,
|
|
2502
|
+
SpotPatchError as SpotPatchError16,
|
|
1383
2503
|
openEditorRequestSchema,
|
|
1384
2504
|
sourceContextRequestSchema
|
|
1385
2505
|
} from "@spotpatch/shared";
|
|
1386
2506
|
|
|
1387
|
-
// src/
|
|
2507
|
+
// src/external-handoff/browser-http.ts
|
|
1388
2508
|
import {
|
|
1389
|
-
ERROR_CODES as
|
|
2509
|
+
ERROR_CODES as ERROR_CODES10,
|
|
2510
|
+
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS5,
|
|
1390
2511
|
SPOTPATCH_ENDPOINTS,
|
|
1391
|
-
SpotPatchError as
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
2512
|
+
SpotPatchError as SpotPatchError10,
|
|
2513
|
+
externalHandoffCapabilityRequestSchema,
|
|
2514
|
+
externalHandoffPublishRequestSchema,
|
|
2515
|
+
externalHandoffResolveDeliveryRequestSchema,
|
|
2516
|
+
externalHandoffStatusRequestSchema
|
|
1396
2517
|
} from "@spotpatch/shared";
|
|
1397
2518
|
|
|
1398
|
-
// src/server/
|
|
2519
|
+
// src/server/annotation-authorizer.ts
|
|
1399
2520
|
import { realpath as realpath5 } from "fs/promises";
|
|
1400
|
-
import
|
|
2521
|
+
import path7 from "path";
|
|
1401
2522
|
import {
|
|
1402
|
-
ERROR_CODES as
|
|
1403
|
-
SpotPatchError as
|
|
2523
|
+
ERROR_CODES as ERROR_CODES9,
|
|
2524
|
+
SpotPatchError as SpotPatchError9,
|
|
2525
|
+
redactSensitiveText,
|
|
2526
|
+
sanitizeUrl
|
|
1404
2527
|
} from "@spotpatch/shared";
|
|
1405
2528
|
|
|
1406
2529
|
// src/server/source-context.ts
|
|
1407
2530
|
import { readFile as readFile3, realpath as realpath4 } from "fs/promises";
|
|
1408
|
-
import
|
|
2531
|
+
import path6 from "path";
|
|
1409
2532
|
import {
|
|
1410
|
-
ERROR_CODES as
|
|
1411
|
-
SpotPatchError as
|
|
2533
|
+
ERROR_CODES as ERROR_CODES8,
|
|
2534
|
+
SpotPatchError as SpotPatchError8
|
|
1412
2535
|
} from "@spotpatch/shared";
|
|
1413
2536
|
|
|
1414
2537
|
// src/server/extract-code-context.ts
|
|
@@ -1642,15 +2765,8 @@ function extractCodeContext(options) {
|
|
|
1642
2765
|
|
|
1643
2766
|
// src/server/source-file.ts
|
|
1644
2767
|
import { realpath as realpath3, stat as stat2 } from "fs/promises";
|
|
1645
|
-
import
|
|
1646
|
-
import { ERROR_CODES as
|
|
1647
|
-
|
|
1648
|
-
// src/server/constants.ts
|
|
1649
|
-
var MAX_REQUEST_BODY_BYTES = 32 * 1024;
|
|
1650
|
-
var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
|
|
1651
|
-
var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
|
1652
|
-
|
|
1653
|
-
// src/server/source-file.ts
|
|
2768
|
+
import path5 from "path";
|
|
2769
|
+
import { ERROR_CODES as ERROR_CODES7, SpotPatchError as SpotPatchError7 } from "@spotpatch/shared";
|
|
1654
2770
|
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
1655
2771
|
function isMissingFileError(error) {
|
|
1656
2772
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
@@ -1665,51 +2781,51 @@ async function assertInsideRoot(root, candidate) {
|
|
|
1665
2781
|
]);
|
|
1666
2782
|
} catch (error) {
|
|
1667
2783
|
if (isMissingFileError(error)) {
|
|
1668
|
-
throw new
|
|
2784
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND, void 0, {
|
|
1669
2785
|
cause: error
|
|
1670
2786
|
});
|
|
1671
2787
|
}
|
|
1672
2788
|
throw error;
|
|
1673
2789
|
}
|
|
1674
|
-
const relative =
|
|
1675
|
-
const outside = relative.startsWith(`..${
|
|
2790
|
+
const relative = path5.relative(realRoot, realCandidate);
|
|
2791
|
+
const outside = relative.startsWith(`..${path5.sep}`) || relative === ".." || path5.isAbsolute(relative);
|
|
1676
2792
|
if (outside) {
|
|
1677
|
-
throw new
|
|
2793
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_OUTSIDE_ROOT);
|
|
1678
2794
|
}
|
|
1679
2795
|
return realCandidate;
|
|
1680
2796
|
}
|
|
1681
2797
|
async function resolveSourceFile(options) {
|
|
1682
2798
|
const registeredPath = options.registry.resolve(options.fileId);
|
|
1683
2799
|
if (registeredPath === void 0) {
|
|
1684
|
-
throw new
|
|
2800
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
1685
2801
|
}
|
|
1686
2802
|
const sourcePath = await assertInsideRoot(options.root, registeredPath);
|
|
1687
|
-
if (!ALLOWED_EXTENSIONS.has(
|
|
1688
|
-
throw new
|
|
2803
|
+
if (!ALLOWED_EXTENSIONS.has(path5.extname(sourcePath).toLowerCase())) {
|
|
2804
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
1689
2805
|
}
|
|
1690
2806
|
let sourceStat;
|
|
1691
2807
|
try {
|
|
1692
2808
|
sourceStat = await stat2(sourcePath);
|
|
1693
2809
|
} catch (error) {
|
|
1694
2810
|
if (isMissingFileError(error)) {
|
|
1695
|
-
throw new
|
|
2811
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND, void 0, {
|
|
1696
2812
|
cause: error
|
|
1697
2813
|
});
|
|
1698
2814
|
}
|
|
1699
2815
|
throw error;
|
|
1700
2816
|
}
|
|
1701
2817
|
if (!sourceStat.isFile()) {
|
|
1702
|
-
throw new
|
|
2818
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
1703
2819
|
}
|
|
1704
2820
|
if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
|
|
1705
|
-
throw new
|
|
2821
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_TOO_LARGE);
|
|
1706
2822
|
}
|
|
1707
2823
|
return sourcePath;
|
|
1708
2824
|
}
|
|
1709
2825
|
|
|
1710
2826
|
// src/server/source-context.ts
|
|
1711
2827
|
function toDisplayPath(root, sourcePath) {
|
|
1712
|
-
return
|
|
2828
|
+
return path6.relative(root, sourcePath).split(path6.sep).join("/");
|
|
1713
2829
|
}
|
|
1714
2830
|
async function readSourceContext(options) {
|
|
1715
2831
|
const sourcePath = await resolveSourceFile({
|
|
@@ -1722,7 +2838,7 @@ async function readSourceContext(options) {
|
|
|
1722
2838
|
source = await readFile3(sourcePath, "utf8");
|
|
1723
2839
|
} catch (error) {
|
|
1724
2840
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1725
|
-
throw new
|
|
2841
|
+
throw new SpotPatchError8(ERROR_CODES8.SOURCE_NOT_FOUND, void 0, {
|
|
1726
2842
|
cause: error
|
|
1727
2843
|
});
|
|
1728
2844
|
}
|
|
@@ -1730,9 +2846,9 @@ async function readSourceContext(options) {
|
|
|
1730
2846
|
}
|
|
1731
2847
|
const lines = source.split(/\r?\n/);
|
|
1732
2848
|
if (options.request.line > lines.length) {
|
|
1733
|
-
throw new
|
|
2849
|
+
throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
|
|
1734
2850
|
}
|
|
1735
|
-
const extension =
|
|
2851
|
+
const extension = path6.extname(sourcePath).toLowerCase();
|
|
1736
2852
|
return extractCodeContext({
|
|
1737
2853
|
source,
|
|
1738
2854
|
sourcePath,
|
|
@@ -1745,7 +2861,17 @@ async function readSourceContext(options) {
|
|
|
1745
2861
|
});
|
|
1746
2862
|
}
|
|
1747
2863
|
|
|
1748
|
-
// src/server/
|
|
2864
|
+
// src/server/annotation-authorizer.ts
|
|
2865
|
+
function sanitizePageContext(page) {
|
|
2866
|
+
return Object.freeze({
|
|
2867
|
+
url: sanitizeUrl(page.url, page.url),
|
|
2868
|
+
pathname: redactSensitiveText(page.pathname),
|
|
2869
|
+
title: redactSensitiveText(page.title),
|
|
2870
|
+
viewportWidth: page.viewportWidth,
|
|
2871
|
+
viewportHeight: page.viewportHeight,
|
|
2872
|
+
devicePixelRatio: page.devicePixelRatio
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
1749
2875
|
function compactSourceRef(source) {
|
|
1750
2876
|
return Object.freeze({
|
|
1751
2877
|
origin: source.origin,
|
|
@@ -1759,27 +2885,20 @@ function compactSourceRef(source) {
|
|
|
1759
2885
|
async function authorizeSourceRef(source, registry, root) {
|
|
1760
2886
|
const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
|
|
1761
2887
|
if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
|
|
1762
|
-
throw new
|
|
2888
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1763
2889
|
}
|
|
1764
2890
|
if (source.fileId === void 0) {
|
|
1765
2891
|
if (source.origin === "none" && source.relativePath !== void 0) {
|
|
1766
|
-
throw new
|
|
2892
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1767
2893
|
}
|
|
1768
2894
|
return compactSourceRef(source);
|
|
1769
2895
|
}
|
|
1770
|
-
const sourcePath = await resolveSourceFile({
|
|
1771
|
-
|
|
1772
|
-
registry,
|
|
1773
|
-
root
|
|
1774
|
-
});
|
|
1775
|
-
const relativePath = path6.relative(await realpath5(root), sourcePath).split(path6.sep).join("/");
|
|
2896
|
+
const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
|
|
2897
|
+
const relativePath = path7.relative(await realpath5(root), sourcePath).split(path7.sep).join("/");
|
|
1776
2898
|
if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
|
|
1777
|
-
throw new
|
|
2899
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1778
2900
|
}
|
|
1779
|
-
return Object.freeze({
|
|
1780
|
-
...compactSourceRef(source),
|
|
1781
|
-
relativePath
|
|
1782
|
-
});
|
|
2901
|
+
return Object.freeze({ ...compactSourceRef(source), relativePath });
|
|
1783
2902
|
}
|
|
1784
2903
|
function freezeMatchedRule(rule) {
|
|
1785
2904
|
return Object.freeze({
|
|
@@ -1813,7 +2932,7 @@ async function authorizeTarget(target, input) {
|
|
|
1813
2932
|
maxLines: input.options.budget.maxCodeLines
|
|
1814
2933
|
});
|
|
1815
2934
|
if (marker === void 0 && target.code !== void 0) {
|
|
1816
|
-
throw new
|
|
2935
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1817
2936
|
}
|
|
1818
2937
|
const code = marker === void 0 ? void 0 : await readSourceContext({
|
|
1819
2938
|
request: marker,
|
|
@@ -1823,11 +2942,11 @@ async function authorizeTarget(target, input) {
|
|
|
1823
2942
|
maxLines: input.options.budget.maxCodeLines
|
|
1824
2943
|
});
|
|
1825
2944
|
if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
|
|
1826
|
-
throw new
|
|
2945
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1827
2946
|
}
|
|
1828
2947
|
return Object.freeze({
|
|
1829
2948
|
instruction: target.instruction,
|
|
1830
|
-
...target.page === void 0 ? {} : { page:
|
|
2949
|
+
...target.page === void 0 ? {} : { page: sanitizePageContext(target.page) },
|
|
1831
2950
|
source,
|
|
1832
2951
|
react: Object.freeze({
|
|
1833
2952
|
supported: target.react.supported,
|
|
@@ -1855,76 +2974,298 @@ async function authorizeTarget(target, input) {
|
|
|
1855
2974
|
warnings: Object.freeze([...target.warnings])
|
|
1856
2975
|
});
|
|
1857
2976
|
}
|
|
1858
|
-
async function
|
|
1859
|
-
const requestedTargets = input.
|
|
2977
|
+
async function authorizeAnnotation(input) {
|
|
2978
|
+
const requestedTargets = input.annotation.targets;
|
|
1860
2979
|
if (requestedTargets.length > input.options.maxTargets) {
|
|
1861
|
-
throw new
|
|
2980
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1862
2981
|
}
|
|
1863
2982
|
const identities = requestedTargets.map(targetIdentity);
|
|
1864
2983
|
if (new Set(identities).size !== identities.length) {
|
|
1865
|
-
throw new
|
|
2984
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1866
2985
|
}
|
|
1867
2986
|
const targets = Object.freeze(
|
|
1868
2987
|
await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
|
|
1869
2988
|
);
|
|
1870
|
-
|
|
2989
|
+
return Object.freeze({
|
|
1871
2990
|
schemaVersion: 3,
|
|
1872
|
-
id: input.
|
|
1873
|
-
locale: input.
|
|
1874
|
-
page:
|
|
2991
|
+
id: input.annotation.id,
|
|
2992
|
+
locale: input.annotation.locale,
|
|
2993
|
+
page: sanitizePageContext(input.annotation.page),
|
|
1875
2994
|
targets,
|
|
1876
|
-
createdAt: input.
|
|
1877
|
-
});
|
|
1878
|
-
return Object.freeze({
|
|
1879
|
-
annotation,
|
|
1880
|
-
...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
|
|
1881
|
-
providerProfileId: input.request.providerProfileId,
|
|
1882
|
-
modelProfileId: input.request.modelProfileId,
|
|
1883
|
-
providerDataConsent: true,
|
|
1884
|
-
...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
|
|
1885
|
-
workingTreeMode: input.request.workingTreeMode
|
|
2995
|
+
createdAt: input.annotation.createdAt
|
|
1886
2996
|
});
|
|
1887
2997
|
}
|
|
1888
2998
|
|
|
1889
|
-
// src/
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
|
|
2999
|
+
// src/external-handoff/browser-http.ts
|
|
3000
|
+
function matchExternalHandoffBrowserPath(path9) {
|
|
3001
|
+
if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffCapability) return "capability";
|
|
3002
|
+
if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffPublish) return "publish";
|
|
3003
|
+
if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffStatus) return "status";
|
|
3004
|
+
if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffResolveDelivery) {
|
|
3005
|
+
return "resolve-delivery";
|
|
1897
3006
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
3007
|
+
return void 0;
|
|
3008
|
+
}
|
|
3009
|
+
function requireService(options) {
|
|
3010
|
+
if (!options.options.externalAgent.enabled || options.service === void 0) {
|
|
3011
|
+
throw new SpotPatchError10(ERROR_CODES10.EXTERNAL_HANDOFF_DISABLED);
|
|
1901
3012
|
}
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
3013
|
+
return options.service;
|
|
3014
|
+
}
|
|
3015
|
+
function remapAuthorizationError(error) {
|
|
3016
|
+
if (error instanceof SpotPatchError10) {
|
|
3017
|
+
if (error.code === ERROR_CODES10.SOURCE_NOT_FOUND || error.code === ERROR_CODES10.SOURCE_OUTSIDE_ROOT || error.code === ERROR_CODES10.SOURCE_TOO_LARGE) {
|
|
3018
|
+
throw new SpotPatchError10(ERROR_CODES10.HANDOFF_SOURCE_STALE, void 0, {
|
|
3019
|
+
cause: error
|
|
3020
|
+
});
|
|
1909
3021
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
continue;
|
|
3022
|
+
if (error.code === ERROR_CODES10.INVALID_REQUEST) {
|
|
3023
|
+
throw new SpotPatchError10(ERROR_CODES10.HANDOFF_VALIDATION_FAILED, void 0, {
|
|
3024
|
+
cause: error
|
|
3025
|
+
});
|
|
1915
3026
|
}
|
|
1916
|
-
chunks.push(buffer);
|
|
1917
3027
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
3028
|
+
throw error;
|
|
3029
|
+
}
|
|
3030
|
+
async function handleExternalHandoffBrowserRequest(request, response, route, options, writeSuccess) {
|
|
3031
|
+
if (request.method !== "POST") {
|
|
3032
|
+
throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
1920
3033
|
}
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
3034
|
+
const service = requireService(options);
|
|
3035
|
+
if (route === "capability") {
|
|
3036
|
+
const parsed2 = externalHandoffCapabilityRequestSchema.safeParse(
|
|
3037
|
+
await readJsonRequestBody(request)
|
|
3038
|
+
);
|
|
3039
|
+
if (!parsed2.success) throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
3040
|
+
writeSuccess(response, 200, service.capability());
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
if (route === "status") {
|
|
3044
|
+
const parsed2 = externalHandoffStatusRequestSchema.safeParse(
|
|
3045
|
+
await readJsonRequestBody(request)
|
|
3046
|
+
);
|
|
3047
|
+
if (!parsed2.success) throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
3048
|
+
writeSuccess(response, 200, service.status(parsed2.data.cursor));
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
if (route === "resolve-delivery") {
|
|
3052
|
+
const parsed2 = externalHandoffResolveDeliveryRequestSchema.safeParse(
|
|
3053
|
+
await readJsonRequestBody(request)
|
|
3054
|
+
);
|
|
3055
|
+
if (!parsed2.success) throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
3056
|
+
writeSuccess(response, 200, service.resolveDelivery(parsed2.data.cursor));
|
|
3057
|
+
return;
|
|
1927
3058
|
}
|
|
3059
|
+
const parsed = externalHandoffPublishRequestSchema.safeParse(
|
|
3060
|
+
await readJsonRequestBody(request, EXTERNAL_HANDOFF_LIMITS5.maximumPublishBodyBytes)
|
|
3061
|
+
);
|
|
3062
|
+
if (!parsed.success) {
|
|
3063
|
+
throw new SpotPatchError10(ERROR_CODES10.HANDOFF_VALIDATION_FAILED);
|
|
3064
|
+
}
|
|
3065
|
+
const result = await service.publish(parsed.data, async (annotation) => {
|
|
3066
|
+
try {
|
|
3067
|
+
return await authorizeAnnotation({
|
|
3068
|
+
annotation,
|
|
3069
|
+
options: options.options,
|
|
3070
|
+
registry: options.registry,
|
|
3071
|
+
root: options.root
|
|
3072
|
+
});
|
|
3073
|
+
} catch (error) {
|
|
3074
|
+
remapAuthorizationError(error);
|
|
3075
|
+
}
|
|
3076
|
+
});
|
|
3077
|
+
writeSuccess(response, result.replayed ? 200 : 201, result);
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
// src/external-agent/browser-http.ts
|
|
3081
|
+
import {
|
|
3082
|
+
ERROR_CODES as ERROR_CODES11,
|
|
3083
|
+
EXTERNAL_AGENT_CONTROL_LIMITS,
|
|
3084
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
|
|
3085
|
+
SpotPatchError as SpotPatchError11,
|
|
3086
|
+
externalAgentControlCancelRequestSchema,
|
|
3087
|
+
externalAgentControlConnectRequestSchema,
|
|
3088
|
+
externalAgentControlDisconnectRequestSchema,
|
|
3089
|
+
externalAgentControlStatusRequestSchema,
|
|
3090
|
+
externalAgentControlStatusSchema,
|
|
3091
|
+
externalAgentEventSchema,
|
|
3092
|
+
externalAgentEventsRequestSchema,
|
|
3093
|
+
externalAgentManagedResultSchema,
|
|
3094
|
+
externalAgentResultRequestSchema
|
|
3095
|
+
} from "@spotpatch/shared";
|
|
3096
|
+
function matchExternalAgentBrowserPath(path9) {
|
|
3097
|
+
if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlStatus) return "status";
|
|
3098
|
+
if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlConnect) return "connect";
|
|
3099
|
+
if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlDisconnect) {
|
|
3100
|
+
return "disconnect";
|
|
3101
|
+
}
|
|
3102
|
+
if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentControlCancel) return "cancel";
|
|
3103
|
+
if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentEvents) return "events";
|
|
3104
|
+
if (path9 === SPOTPATCH_ENDPOINTS2.externalAgentResult) return "result";
|
|
3105
|
+
return void 0;
|
|
3106
|
+
}
|
|
3107
|
+
function writeEvent(response, event) {
|
|
3108
|
+
response.write(`${JSON.stringify(externalAgentEventSchema.parse(event))}
|
|
3109
|
+
`);
|
|
3110
|
+
}
|
|
3111
|
+
function createExternalAgentBrowserController(port) {
|
|
3112
|
+
const streams = /* @__PURE__ */ new Set();
|
|
3113
|
+
let disposed = false;
|
|
3114
|
+
const handleEvents = async (request, response) => {
|
|
3115
|
+
const parsed = externalAgentEventsRequestSchema.safeParse(
|
|
3116
|
+
await readJsonRequestBody(request)
|
|
3117
|
+
);
|
|
3118
|
+
if (!parsed.success) {
|
|
3119
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3120
|
+
}
|
|
3121
|
+
if (disposed || streams.size >= EXTERNAL_AGENT_CONTROL_LIMITS.maximumEventSubscribers) {
|
|
3122
|
+
throw new SpotPatchError11(ERROR_CODES11.BRIDGE_BUSY);
|
|
3123
|
+
}
|
|
3124
|
+
response.statusCode = 200;
|
|
3125
|
+
response.setHeader("Cache-Control", "no-store");
|
|
3126
|
+
response.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
3127
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
3128
|
+
streams.add(response);
|
|
3129
|
+
const initial = externalAgentControlStatusSchema.parse(port.getStatus());
|
|
3130
|
+
writeEvent(response, { type: "status", data: initial });
|
|
3131
|
+
const unsubscribe = port.subscribe((status) => {
|
|
3132
|
+
if (!response.destroyed && status.sequence > (parsed.data.afterSequence ?? -1)) {
|
|
3133
|
+
writeEvent(response, { type: "status", data: status });
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
const heartbeat = setInterval(() => {
|
|
3137
|
+
if (!response.destroyed) {
|
|
3138
|
+
const status = port.getStatus();
|
|
3139
|
+
writeEvent(response, {
|
|
3140
|
+
type: "heartbeat",
|
|
3141
|
+
sequence: status.sequence,
|
|
3142
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
}, EXTERNAL_AGENT_CONTROL_LIMITS.eventHeartbeatMs);
|
|
3146
|
+
heartbeat.unref();
|
|
3147
|
+
await new Promise((resolve) => {
|
|
3148
|
+
const close = () => {
|
|
3149
|
+
response.off("close", close);
|
|
3150
|
+
clearInterval(heartbeat);
|
|
3151
|
+
unsubscribe();
|
|
3152
|
+
streams.delete(response);
|
|
3153
|
+
resolve();
|
|
3154
|
+
};
|
|
3155
|
+
response.once("close", close);
|
|
3156
|
+
});
|
|
3157
|
+
};
|
|
3158
|
+
return Object.freeze({
|
|
3159
|
+
async handle(request, response, route, writeSuccess) {
|
|
3160
|
+
if (request.method !== "POST" || disposed) {
|
|
3161
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3162
|
+
}
|
|
3163
|
+
if (route === "events") {
|
|
3164
|
+
await handleEvents(request, response);
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
if (route === "status") {
|
|
3168
|
+
const parsed2 = externalAgentControlStatusRequestSchema.safeParse(
|
|
3169
|
+
await readJsonRequestBody(request)
|
|
3170
|
+
);
|
|
3171
|
+
if (!parsed2.success) throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3172
|
+
writeSuccess(
|
|
3173
|
+
response,
|
|
3174
|
+
200,
|
|
3175
|
+
externalAgentControlStatusSchema.parse(port.getStatus())
|
|
3176
|
+
);
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
if (route === "connect") {
|
|
3180
|
+
const parsed2 = externalAgentControlConnectRequestSchema.safeParse(
|
|
3181
|
+
await readJsonRequestBody(request)
|
|
3182
|
+
);
|
|
3183
|
+
if (!parsed2.success) throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3184
|
+
const controller = new AbortController();
|
|
3185
|
+
response.once("close", () => {
|
|
3186
|
+
if (!response.writableEnded) controller.abort("browser-disconnected");
|
|
3187
|
+
});
|
|
3188
|
+
writeSuccess(
|
|
3189
|
+
response,
|
|
3190
|
+
200,
|
|
3191
|
+
externalAgentControlStatusSchema.parse(
|
|
3192
|
+
await port.connect(parsed2.data, controller.signal)
|
|
3193
|
+
)
|
|
3194
|
+
);
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
if (route === "disconnect") {
|
|
3198
|
+
const parsed2 = externalAgentControlDisconnectRequestSchema.safeParse(
|
|
3199
|
+
await readJsonRequestBody(request)
|
|
3200
|
+
);
|
|
3201
|
+
if (!parsed2.success) throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3202
|
+
writeSuccess(
|
|
3203
|
+
response,
|
|
3204
|
+
200,
|
|
3205
|
+
externalAgentControlStatusSchema.parse(await port.disconnect(parsed2.data))
|
|
3206
|
+
);
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
if (route === "cancel") {
|
|
3210
|
+
const parsed2 = externalAgentControlCancelRequestSchema.safeParse(
|
|
3211
|
+
await readJsonRequestBody(request)
|
|
3212
|
+
);
|
|
3213
|
+
if (!parsed2.success) throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3214
|
+
writeSuccess(
|
|
3215
|
+
response,
|
|
3216
|
+
200,
|
|
3217
|
+
externalAgentControlStatusSchema.parse(await port.cancel(parsed2.data))
|
|
3218
|
+
);
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
const parsed = externalAgentResultRequestSchema.safeParse(
|
|
3222
|
+
await readJsonRequestBody(request)
|
|
3223
|
+
);
|
|
3224
|
+
if (!parsed.success) throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
3225
|
+
const result = port.getResult(parsed.data.revision);
|
|
3226
|
+
if (result === void 0) {
|
|
3227
|
+
throw new SpotPatchError11(ERROR_CODES11.HANDOFF_NOT_FOUND);
|
|
3228
|
+
}
|
|
3229
|
+
writeSuccess(response, 200, externalAgentManagedResultSchema.parse(result));
|
|
3230
|
+
},
|
|
3231
|
+
dispose() {
|
|
3232
|
+
if (disposed) return;
|
|
3233
|
+
disposed = true;
|
|
3234
|
+
for (const response of streams) response.end();
|
|
3235
|
+
streams.clear();
|
|
3236
|
+
}
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
// src/server/agent-http.ts
|
|
3241
|
+
import {
|
|
3242
|
+
ERROR_CODES as ERROR_CODES12,
|
|
3243
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS3,
|
|
3244
|
+
SpotPatchError as SpotPatchError12,
|
|
3245
|
+
agentCapabilityRequestSchema,
|
|
3246
|
+
agentJobActionRequestSchema,
|
|
3247
|
+
agentJobCreateRequestSchema,
|
|
3248
|
+
agentWorkspaceHealthRequestSchema
|
|
3249
|
+
} from "@spotpatch/shared";
|
|
3250
|
+
|
|
3251
|
+
// src/server/agent-request.ts
|
|
3252
|
+
import "@spotpatch/shared";
|
|
3253
|
+
async function authorizeAgentJobRequest(input) {
|
|
3254
|
+
const annotation = await authorizeAnnotation({
|
|
3255
|
+
annotation: input.request.annotation,
|
|
3256
|
+
options: input.options,
|
|
3257
|
+
registry: input.registry,
|
|
3258
|
+
root: input.root
|
|
3259
|
+
});
|
|
3260
|
+
return Object.freeze({
|
|
3261
|
+
annotation,
|
|
3262
|
+
...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
|
|
3263
|
+
providerProfileId: input.request.providerProfileId,
|
|
3264
|
+
modelProfileId: input.request.modelProfileId,
|
|
3265
|
+
providerDataConsent: true,
|
|
3266
|
+
...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
|
|
3267
|
+
workingTreeMode: input.request.workingTreeMode
|
|
3268
|
+
});
|
|
1928
3269
|
}
|
|
1929
3270
|
|
|
1930
3271
|
// src/server/agent-http.ts
|
|
@@ -1944,21 +3285,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
|
|
|
1944
3285
|
"reverted",
|
|
1945
3286
|
"failed"
|
|
1946
3287
|
]);
|
|
1947
|
-
function matchAgentRequestPath(
|
|
1948
|
-
if (
|
|
3288
|
+
function matchAgentRequestPath(path9) {
|
|
3289
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.agentCapability) {
|
|
1949
3290
|
return Object.freeze({ kind: "capability" });
|
|
1950
3291
|
}
|
|
1951
|
-
if (
|
|
3292
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.agentWorkspaceHealth) {
|
|
1952
3293
|
return Object.freeze({ kind: "workspace-health" });
|
|
1953
3294
|
}
|
|
1954
|
-
if (
|
|
3295
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.agentJobs) {
|
|
1955
3296
|
return Object.freeze({ kind: "create-job" });
|
|
1956
3297
|
}
|
|
1957
|
-
const prefix = `${
|
|
1958
|
-
if (!
|
|
3298
|
+
const prefix = `${SPOTPATCH_ENDPOINTS3.agentJobs}/`;
|
|
3299
|
+
if (!path9.startsWith(prefix)) {
|
|
1959
3300
|
return void 0;
|
|
1960
3301
|
}
|
|
1961
|
-
const segments =
|
|
3302
|
+
const segments = path9.slice(prefix.length).split("/");
|
|
1962
3303
|
const jobId = segments[0];
|
|
1963
3304
|
const action = segments[1];
|
|
1964
3305
|
if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
|
|
@@ -1972,7 +3313,7 @@ function matchAgentRequestPath(path8) {
|
|
|
1972
3313
|
}
|
|
1973
3314
|
function requireAgentManager(options) {
|
|
1974
3315
|
if (options.agentManager === void 0 || options.options.ai === false) {
|
|
1975
|
-
throw new
|
|
3316
|
+
throw new SpotPatchError12(ERROR_CODES12.AI_DISABLED);
|
|
1976
3317
|
}
|
|
1977
3318
|
return options.agentManager;
|
|
1978
3319
|
}
|
|
@@ -2025,13 +3366,13 @@ function streamAgentJobEvents(response, manager, jobId) {
|
|
|
2025
3366
|
}
|
|
2026
3367
|
async function handleCapability(request, response, options, writeSuccess) {
|
|
2027
3368
|
if (request.method !== "POST") {
|
|
2028
|
-
throw new
|
|
3369
|
+
throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
|
|
2029
3370
|
}
|
|
2030
3371
|
const parsed = agentCapabilityRequestSchema.safeParse(
|
|
2031
3372
|
await readJsonRequestBody(request)
|
|
2032
3373
|
);
|
|
2033
3374
|
if (!parsed.success) {
|
|
2034
|
-
throw new
|
|
3375
|
+
throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
|
|
2035
3376
|
}
|
|
2036
3377
|
const controller = new AbortController();
|
|
2037
3378
|
const abort = () => {
|
|
@@ -2050,13 +3391,13 @@ async function handleCapability(request, response, options, writeSuccess) {
|
|
|
2050
3391
|
}
|
|
2051
3392
|
async function handleCreateJob(request, response, options, writeSuccess) {
|
|
2052
3393
|
if (request.method !== "POST") {
|
|
2053
|
-
throw new
|
|
3394
|
+
throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
|
|
2054
3395
|
}
|
|
2055
3396
|
const parsed = agentJobCreateRequestSchema.safeParse(
|
|
2056
3397
|
await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
|
|
2057
3398
|
);
|
|
2058
3399
|
if (!parsed.success) {
|
|
2059
|
-
throw new
|
|
3400
|
+
throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
|
|
2060
3401
|
}
|
|
2061
3402
|
const authorizedRequest = await authorizeAgentJobRequest({
|
|
2062
3403
|
request: parsed.data,
|
|
@@ -2069,13 +3410,13 @@ async function handleCreateJob(request, response, options, writeSuccess) {
|
|
|
2069
3410
|
}
|
|
2070
3411
|
async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
2071
3412
|
if (request.method !== "POST") {
|
|
2072
|
-
throw new
|
|
3413
|
+
throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
|
|
2073
3414
|
}
|
|
2074
3415
|
const parsed = agentWorkspaceHealthRequestSchema.safeParse(
|
|
2075
3416
|
await readJsonRequestBody(request)
|
|
2076
3417
|
);
|
|
2077
3418
|
if (!parsed.success) {
|
|
2078
|
-
throw new
|
|
3419
|
+
throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
|
|
2079
3420
|
}
|
|
2080
3421
|
const controller = new AbortController();
|
|
2081
3422
|
const abort = () => {
|
|
@@ -2092,13 +3433,13 @@ async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
|
2092
3433
|
async function handleJobAction(request, response, options, route, writeSuccess) {
|
|
2093
3434
|
const manager = requireAgentManager(options);
|
|
2094
3435
|
if (request.method !== "POST") {
|
|
2095
|
-
throw new
|
|
3436
|
+
throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
|
|
2096
3437
|
}
|
|
2097
3438
|
const parsed = agentJobActionRequestSchema.safeParse(
|
|
2098
3439
|
await readJsonRequestBody(request)
|
|
2099
3440
|
);
|
|
2100
3441
|
if (!parsed.success) {
|
|
2101
|
-
throw new
|
|
3442
|
+
throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
|
|
2102
3443
|
}
|
|
2103
3444
|
if (route.action === "events") {
|
|
2104
3445
|
streamAgentJobEvents(response, manager, route.jobId);
|
|
@@ -2213,14 +3554,14 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
2213
3554
|
var launchConfiguredEditor = createEditorLauncher();
|
|
2214
3555
|
|
|
2215
3556
|
// src/server/data-flow-http.ts
|
|
2216
|
-
import { createHash as
|
|
3557
|
+
import { createHash as createHash3 } from "crypto";
|
|
2217
3558
|
import {
|
|
2218
3559
|
createStaticDataFlowAnalyzer
|
|
2219
3560
|
} from "@spotpatch/analyzer";
|
|
2220
3561
|
import {
|
|
2221
3562
|
DATA_FLOW_SCHEMA_VERSION,
|
|
2222
|
-
ERROR_CODES as
|
|
2223
|
-
SpotPatchError as
|
|
3563
|
+
ERROR_CODES as ERROR_CODES13,
|
|
3564
|
+
SpotPatchError as SpotPatchError13,
|
|
2224
3565
|
dataFlowComponentReportRequestSchema,
|
|
2225
3566
|
dataFlowPageReportRequestSchema,
|
|
2226
3567
|
limitDataFlowReportCollections
|
|
@@ -2239,7 +3580,7 @@ function limitDataFlowReportToBytes(report, maximumBytes) {
|
|
|
2239
3580
|
truncatedBy: "bytes"
|
|
2240
3581
|
});
|
|
2241
3582
|
if (envelopeBytes(limited) > maximumBytes) {
|
|
2242
|
-
throw new
|
|
3583
|
+
throw new SpotPatchError13(ERROR_CODES13.INTERNAL_ERROR);
|
|
2243
3584
|
}
|
|
2244
3585
|
for (let maximumDependencies = 1; maximumDependencies <= structurallyLimited.dependencies.length; maximumDependencies += 1) {
|
|
2245
3586
|
const candidate = limitDataFlowReportCollections(structurallyLimited, {
|
|
@@ -2268,7 +3609,7 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
2268
3609
|
request.componentSourceId
|
|
2269
3610
|
);
|
|
2270
3611
|
if (anchor?.sourceVersion !== request.sourceVersion) {
|
|
2271
|
-
throw new
|
|
3612
|
+
throw new SpotPatchError13(ERROR_CODES13.DATA_FLOW_SOURCE_STALE);
|
|
2272
3613
|
}
|
|
2273
3614
|
return anchor;
|
|
2274
3615
|
}
|
|
@@ -2285,7 +3626,7 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
2285
3626
|
column: resolvedRequest.column
|
|
2286
3627
|
});
|
|
2287
3628
|
if (resolvedRequest.sourceVersion !== void 0 && resolvedRequest.sourceVersion !== report.component.source.sourceVersion) {
|
|
2288
|
-
throw new
|
|
3629
|
+
throw new SpotPatchError13(ERROR_CODES13.DATA_FLOW_SOURCE_STALE);
|
|
2289
3630
|
}
|
|
2290
3631
|
return limitDataFlowReportToBytes(
|
|
2291
3632
|
report,
|
|
@@ -2294,7 +3635,7 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
2294
3635
|
}
|
|
2295
3636
|
function requireAnalyzer(analyzer) {
|
|
2296
3637
|
if (analyzer === void 0) {
|
|
2297
|
-
throw new
|
|
3638
|
+
throw new SpotPatchError13(ERROR_CODES13.DATA_FLOW_DISABLED);
|
|
2298
3639
|
}
|
|
2299
3640
|
return analyzer;
|
|
2300
3641
|
}
|
|
@@ -2306,7 +3647,7 @@ async function handleComponentDataFlowReport(request, analyzer, options) {
|
|
|
2306
3647
|
)
|
|
2307
3648
|
);
|
|
2308
3649
|
if (!parsed.success) {
|
|
2309
|
-
throw new
|
|
3650
|
+
throw new SpotPatchError13(ERROR_CODES13.INVALID_REQUEST);
|
|
2310
3651
|
}
|
|
2311
3652
|
return analyzeTarget(parsed.data, requireAnalyzer(analyzer), options);
|
|
2312
3653
|
}
|
|
@@ -2318,7 +3659,7 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
2318
3659
|
)
|
|
2319
3660
|
);
|
|
2320
3661
|
if (!parsed.success) {
|
|
2321
|
-
throw new
|
|
3662
|
+
throw new SpotPatchError13(ERROR_CODES13.INVALID_REQUEST);
|
|
2322
3663
|
}
|
|
2323
3664
|
const activeAnalyzer = requireAnalyzer(analyzer);
|
|
2324
3665
|
const componentReports = await Promise.all(
|
|
@@ -2342,7 +3683,7 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
2342
3683
|
const analyzedVersions = new Set(
|
|
2343
3684
|
componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
|
|
2344
3685
|
);
|
|
2345
|
-
const reportId = `page_${
|
|
3686
|
+
const reportId = `page_${createHash3("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
|
|
2346
3687
|
const complete = componentReports.every((report2) => report2.completeness.complete);
|
|
2347
3688
|
const report = Object.freeze({
|
|
2348
3689
|
schemaVersion: DATA_FLOW_SCHEMA_VERSION,
|
|
@@ -2389,20 +3730,20 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
2389
3730
|
}
|
|
2390
3731
|
|
|
2391
3732
|
// src/server/request-security.ts
|
|
2392
|
-
import { timingSafeEqual } from "crypto";
|
|
3733
|
+
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
2393
3734
|
import { isIP } from "net";
|
|
2394
|
-
import { ERROR_CODES as
|
|
3735
|
+
import { ERROR_CODES as ERROR_CODES14, SPOTPATCH_TOKEN_HEADER, SpotPatchError as SpotPatchError14 } from "@spotpatch/shared";
|
|
2395
3736
|
function getSingleHeader(request, name) {
|
|
2396
3737
|
const value = request.headers[name.toLowerCase()];
|
|
2397
3738
|
return Array.isArray(value) ? value[0] : value;
|
|
2398
3739
|
}
|
|
2399
|
-
function
|
|
3740
|
+
function tokensMatch2(actual, expected) {
|
|
2400
3741
|
if (actual === void 0) {
|
|
2401
3742
|
return false;
|
|
2402
3743
|
}
|
|
2403
3744
|
const actualBytes = Buffer.from(actual);
|
|
2404
3745
|
const expectedBytes = Buffer.from(expected);
|
|
2405
|
-
return actualBytes.byteLength === expectedBytes.byteLength &&
|
|
3746
|
+
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual2(actualBytes, expectedBytes);
|
|
2406
3747
|
}
|
|
2407
3748
|
function isLoopbackHostname(hostname) {
|
|
2408
3749
|
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
@@ -2437,33 +3778,33 @@ function parseOrigin(value) {
|
|
|
2437
3778
|
}
|
|
2438
3779
|
function assertRequestAuthorized(request, options) {
|
|
2439
3780
|
const actualToken = getSingleHeader(request, SPOTPATCH_TOKEN_HEADER);
|
|
2440
|
-
if (!
|
|
2441
|
-
throw new
|
|
3781
|
+
if (!tokensMatch2(actualToken, options.sessionToken)) {
|
|
3782
|
+
throw new SpotPatchError14(ERROR_CODES14.INVALID_TOKEN);
|
|
2442
3783
|
}
|
|
2443
3784
|
const hostHeader = getSingleHeader(request, "host");
|
|
2444
3785
|
const originHeader = getSingleHeader(request, "origin");
|
|
2445
3786
|
const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
|
|
2446
3787
|
const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
|
|
2447
3788
|
if (host === void 0 || origin === void 0) {
|
|
2448
|
-
throw new
|
|
3789
|
+
throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
|
|
2449
3790
|
}
|
|
2450
3791
|
const hostIsLoopback = isLoopbackHostname(host.hostname);
|
|
2451
3792
|
const originIsLoopback = isLoopbackHostname(origin.hostname);
|
|
2452
3793
|
if (!options.allowLan) {
|
|
2453
3794
|
if (!hostIsLoopback || !originIsLoopback) {
|
|
2454
|
-
throw new
|
|
3795
|
+
throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
|
|
2455
3796
|
}
|
|
2456
3797
|
return;
|
|
2457
3798
|
}
|
|
2458
3799
|
if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
|
|
2459
|
-
throw new
|
|
3800
|
+
throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
|
|
2460
3801
|
}
|
|
2461
3802
|
}
|
|
2462
3803
|
|
|
2463
3804
|
// src/server/runtime-bootstrap.ts
|
|
2464
3805
|
import {
|
|
2465
|
-
ERROR_CODES as
|
|
2466
|
-
SpotPatchError as
|
|
3806
|
+
ERROR_CODES as ERROR_CODES15,
|
|
3807
|
+
SpotPatchError as SpotPatchError15,
|
|
2467
3808
|
runtimeBootstrapRequestSchema,
|
|
2468
3809
|
runtimeConfigSchema
|
|
2469
3810
|
} from "@spotpatch/shared";
|
|
@@ -2493,7 +3834,7 @@ function resolveRuntimeBootstrapOptions(options) {
|
|
|
2493
3834
|
function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
2494
3835
|
const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
2495
3836
|
if (request.method !== "POST" || contentType !== "application/json") {
|
|
2496
|
-
throw new
|
|
3837
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2497
3838
|
}
|
|
2498
3839
|
const host = getSingleHeader2(request, "host");
|
|
2499
3840
|
let hostIsLoopback = false;
|
|
@@ -2505,7 +3846,7 @@ function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
|
2505
3846
|
}
|
|
2506
3847
|
}
|
|
2507
3848
|
if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
|
|
2508
|
-
throw new
|
|
3849
|
+
throw new SpotPatchError15(ERROR_CODES15.ORIGIN_NOT_ALLOWED);
|
|
2509
3850
|
}
|
|
2510
3851
|
}
|
|
2511
3852
|
async function readRuntimeBootstrap(request, options) {
|
|
@@ -2514,103 +3855,139 @@ async function readRuntimeBootstrap(request, options) {
|
|
|
2514
3855
|
await readJsonRequestBody(request)
|
|
2515
3856
|
);
|
|
2516
3857
|
if (!parsedBody.success) {
|
|
2517
|
-
throw new
|
|
3858
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2518
3859
|
}
|
|
2519
3860
|
return options.runtimeConfig;
|
|
2520
3861
|
}
|
|
2521
3862
|
|
|
2522
3863
|
// src/server/middleware.ts
|
|
2523
3864
|
var STATUS_BY_ERROR = Object.freeze({
|
|
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
|
-
[
|
|
2554
|
-
[
|
|
2555
|
-
[
|
|
2556
|
-
[
|
|
2557
|
-
[
|
|
2558
|
-
[
|
|
2559
|
-
[
|
|
3865
|
+
[ERROR_CODES16.INVALID_REQUEST]: 400,
|
|
3866
|
+
[ERROR_CODES16.INVALID_TOKEN]: 401,
|
|
3867
|
+
[ERROR_CODES16.ORIGIN_NOT_ALLOWED]: 403,
|
|
3868
|
+
[ERROR_CODES16.SOURCE_NOT_FOUND]: 404,
|
|
3869
|
+
[ERROR_CODES16.SOURCE_OUTSIDE_ROOT]: 403,
|
|
3870
|
+
[ERROR_CODES16.SOURCE_TOO_LARGE]: 413,
|
|
3871
|
+
[ERROR_CODES16.EDITOR_OPEN_FAILED]: 500,
|
|
3872
|
+
[ERROR_CODES16.DATA_FLOW_DISABLED]: 404,
|
|
3873
|
+
[ERROR_CODES16.DATA_FLOW_SOURCE_STALE]: 409,
|
|
3874
|
+
[ERROR_CODES16.DATA_FLOW_ANALYSIS_CANCELLED]: 409,
|
|
3875
|
+
[ERROR_CODES16.AI_DISABLED]: 404,
|
|
3876
|
+
[ERROR_CODES16.PROVIDER_NOT_CONFIGURED]: 503,
|
|
3877
|
+
[ERROR_CODES16.PROVIDER_AUTH_FAILED]: 502,
|
|
3878
|
+
[ERROR_CODES16.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
|
|
3879
|
+
[ERROR_CODES16.MODEL_NOT_ALLOWED]: 400,
|
|
3880
|
+
[ERROR_CODES16.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
|
|
3881
|
+
[ERROR_CODES16.PROVIDER_RATE_LIMITED]: 429,
|
|
3882
|
+
[ERROR_CODES16.AGENT_BUSY]: 409,
|
|
3883
|
+
[ERROR_CODES16.AGENT_LIMIT_EXCEEDED]: 413,
|
|
3884
|
+
[ERROR_CODES16.AGENT_CANCELLED]: 409,
|
|
3885
|
+
[ERROR_CODES16.EXTERNAL_HANDOFF_DISABLED]: 404,
|
|
3886
|
+
[ERROR_CODES16.EXTERNAL_HANDOFF_UNAVAILABLE]: 503,
|
|
3887
|
+
[ERROR_CODES16.HANDOFF_VALIDATION_FAILED]: 422,
|
|
3888
|
+
[ERROR_CODES16.HANDOFF_SOURCE_STALE]: 409,
|
|
3889
|
+
[ERROR_CODES16.HANDOFF_NOT_FOUND]: 404,
|
|
3890
|
+
[ERROR_CODES16.HANDOFF_EXPIRED]: 410,
|
|
3891
|
+
[ERROR_CODES16.HANDOFF_CURSOR_INVALID]: 409,
|
|
3892
|
+
[ERROR_CODES16.HANDOFF_RESPONSE_TOO_LARGE]: 413,
|
|
3893
|
+
[ERROR_CODES16.BRIDGE_UNAUTHORIZED]: 401,
|
|
3894
|
+
[ERROR_CODES16.BRIDGE_PROTOCOL_MISMATCH]: 409,
|
|
3895
|
+
[ERROR_CODES16.BRIDGE_BUSY]: 429,
|
|
3896
|
+
[ERROR_CODES16.EXTERNAL_AGENT_BUSY]: 409,
|
|
3897
|
+
[ERROR_CODES16.ACTIVE_ADAPTER_CONFLICT]: 409,
|
|
3898
|
+
[ERROR_CODES16.ACTIVE_ADAPTER_LEASE_INVALID]: 409,
|
|
3899
|
+
[ERROR_CODES16.ACTIVE_DISPATCH_INVALID]: 409,
|
|
3900
|
+
[ERROR_CODES16.SESSION_NOT_FOUND]: 404,
|
|
3901
|
+
[ERROR_CODES16.SESSION_AMBIGUOUS]: 409,
|
|
3902
|
+
[ERROR_CODES16.SESSION_CLOSED]: 410,
|
|
3903
|
+
[ERROR_CODES16.WORKTREE_DIRTY]: 409,
|
|
3904
|
+
[ERROR_CODES16.WORKTREE_NOT_REPOSITORY]: 409,
|
|
3905
|
+
[ERROR_CODES16.WORKTREE_OPERATION_IN_PROGRESS]: 409,
|
|
3906
|
+
[ERROR_CODES16.WORKTREE_CONFLICTED]: 409,
|
|
3907
|
+
[ERROR_CODES16.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
|
|
3908
|
+
[ERROR_CODES16.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
|
|
3909
|
+
[ERROR_CODES16.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
|
|
3910
|
+
[ERROR_CODES16.TOOL_DENIED]: 403,
|
|
3911
|
+
[ERROR_CODES16.TOOL_INPUT_INVALID]: 422,
|
|
3912
|
+
[ERROR_CODES16.TOOL_ARGUMENTS_INVALID]: 422,
|
|
3913
|
+
[ERROR_CODES16.TOOL_CALL_ID_CONFLICT]: 422,
|
|
3914
|
+
[ERROR_CODES16.TOOL_PATH_DENIED]: 403,
|
|
3915
|
+
[ERROR_CODES16.PATCH_REJECTED]: 422,
|
|
3916
|
+
[ERROR_CODES16.VALIDATION_FAILED]: 422,
|
|
3917
|
+
[ERROR_CODES16.APPLY_CONFLICT]: 409,
|
|
3918
|
+
[ERROR_CODES16.INTERNAL_ERROR]: 500
|
|
2560
3919
|
});
|
|
2561
3920
|
var PUBLIC_MESSAGES = Object.freeze({
|
|
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
|
-
[
|
|
2592
|
-
[
|
|
2593
|
-
[
|
|
2594
|
-
[
|
|
2595
|
-
[
|
|
2596
|
-
[
|
|
2597
|
-
[
|
|
3921
|
+
[ERROR_CODES16.INVALID_REQUEST]: "The request is invalid.",
|
|
3922
|
+
[ERROR_CODES16.INVALID_TOKEN]: "The session token is invalid.",
|
|
3923
|
+
[ERROR_CODES16.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
|
|
3924
|
+
[ERROR_CODES16.SOURCE_NOT_FOUND]: "The source file is unavailable.",
|
|
3925
|
+
[ERROR_CODES16.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
|
|
3926
|
+
[ERROR_CODES16.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
|
|
3927
|
+
[ERROR_CODES16.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
|
|
3928
|
+
[ERROR_CODES16.DATA_FLOW_DISABLED]: "Component data-flow analysis is not enabled.",
|
|
3929
|
+
[ERROR_CODES16.DATA_FLOW_SOURCE_STALE]: "The selected source version is stale.",
|
|
3930
|
+
[ERROR_CODES16.DATA_FLOW_ANALYSIS_CANCELLED]: "The data-flow analysis was cancelled.",
|
|
3931
|
+
[ERROR_CODES16.AI_DISABLED]: "AI execution is not enabled.",
|
|
3932
|
+
[ERROR_CODES16.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
|
|
3933
|
+
[ERROR_CODES16.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
|
|
3934
|
+
[ERROR_CODES16.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
|
|
3935
|
+
[ERROR_CODES16.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
|
|
3936
|
+
[ERROR_CODES16.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
|
|
3937
|
+
[ERROR_CODES16.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
|
|
3938
|
+
[ERROR_CODES16.AGENT_BUSY]: "Another Agent job is already running.",
|
|
3939
|
+
[ERROR_CODES16.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
|
|
3940
|
+
[ERROR_CODES16.AGENT_CANCELLED]: "The Agent job was cancelled.",
|
|
3941
|
+
[ERROR_CODES16.EXTERNAL_HANDOFF_DISABLED]: "External Agent handoff is not enabled.",
|
|
3942
|
+
[ERROR_CODES16.EXTERNAL_HANDOFF_UNAVAILABLE]: "External Agent handoff is temporarily unavailable.",
|
|
3943
|
+
[ERROR_CODES16.HANDOFF_VALIDATION_FAILED]: "The handoff content is invalid.",
|
|
3944
|
+
[ERROR_CODES16.HANDOFF_SOURCE_STALE]: "The selected source is stale.",
|
|
3945
|
+
[ERROR_CODES16.HANDOFF_NOT_FOUND]: "No current handoff is available.",
|
|
3946
|
+
[ERROR_CODES16.HANDOFF_EXPIRED]: "The handoff has expired.",
|
|
3947
|
+
[ERROR_CODES16.HANDOFF_CURSOR_INVALID]: "The handoff cursor is invalid.",
|
|
3948
|
+
[ERROR_CODES16.HANDOFF_RESPONSE_TOO_LARGE]: "The handoff exceeds the size limit.",
|
|
3949
|
+
[ERROR_CODES16.BRIDGE_UNAUTHORIZED]: "The local bridge request is unauthorized.",
|
|
3950
|
+
[ERROR_CODES16.BRIDGE_PROTOCOL_MISMATCH]: "The local bridge protocol is incompatible.",
|
|
3951
|
+
[ERROR_CODES16.BRIDGE_BUSY]: "The local bridge is busy.",
|
|
3952
|
+
[ERROR_CODES16.EXTERNAL_AGENT_BUSY]: "The connected external Agent is busy.",
|
|
3953
|
+
[ERROR_CODES16.ACTIVE_ADAPTER_CONFLICT]: "Another active Agent adapter is connected.",
|
|
3954
|
+
[ERROR_CODES16.ACTIVE_ADAPTER_LEASE_INVALID]: "The active Agent adapter lease is invalid.",
|
|
3955
|
+
[ERROR_CODES16.ACTIVE_DISPATCH_INVALID]: "The active Agent dispatch transition is invalid.",
|
|
3956
|
+
[ERROR_CODES16.SESSION_NOT_FOUND]: "No active SpotPatch session was found.",
|
|
3957
|
+
[ERROR_CODES16.SESSION_AMBIGUOUS]: "More than one SpotPatch session matches.",
|
|
3958
|
+
[ERROR_CODES16.SESSION_CLOSED]: "The SpotPatch session has closed.",
|
|
3959
|
+
[ERROR_CODES16.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
|
|
3960
|
+
[ERROR_CODES16.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
|
|
3961
|
+
[ERROR_CODES16.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
|
|
3962
|
+
[ERROR_CODES16.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
|
|
3963
|
+
[ERROR_CODES16.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
|
|
3964
|
+
[ERROR_CODES16.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
|
|
3965
|
+
[ERROR_CODES16.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
|
|
3966
|
+
[ERROR_CODES16.TOOL_DENIED]: "The Agent tool request was denied.",
|
|
3967
|
+
[ERROR_CODES16.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
|
|
3968
|
+
[ERROR_CODES16.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
|
|
3969
|
+
[ERROR_CODES16.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
|
|
3970
|
+
[ERROR_CODES16.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
|
|
3971
|
+
[ERROR_CODES16.PATCH_REJECTED]: "The proposed patch was rejected.",
|
|
3972
|
+
[ERROR_CODES16.VALIDATION_FAILED]: "The proposed change failed validation.",
|
|
3973
|
+
[ERROR_CODES16.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
|
|
3974
|
+
[ERROR_CODES16.INTERNAL_ERROR]: "The request could not be completed."
|
|
2598
3975
|
});
|
|
2599
|
-
function
|
|
3976
|
+
function writeJson2(response, status, payload) {
|
|
2600
3977
|
response.statusCode = status;
|
|
2601
3978
|
response.setHeader("Cache-Control", "no-store");
|
|
2602
3979
|
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2603
3980
|
response.end(JSON.stringify(payload));
|
|
2604
3981
|
}
|
|
2605
3982
|
function asSpotPatchError(error) {
|
|
2606
|
-
return error instanceof
|
|
3983
|
+
return error instanceof SpotPatchError16 ? error : new SpotPatchError16(ERROR_CODES16.INTERNAL_ERROR, void 0, { cause: error });
|
|
2607
3984
|
}
|
|
2608
3985
|
function writeError(response, error, logger) {
|
|
2609
3986
|
const normalized = asSpotPatchError(error);
|
|
2610
|
-
if (normalized.code ===
|
|
3987
|
+
if (normalized.code === ERROR_CODES16.INTERNAL_ERROR) {
|
|
2611
3988
|
logger?.warn("[spotpatch:server] Internal request failure.");
|
|
2612
3989
|
}
|
|
2613
|
-
|
|
3990
|
+
writeJson2(response, STATUS_BY_ERROR[normalized.code], {
|
|
2614
3991
|
ok: false,
|
|
2615
3992
|
error: {
|
|
2616
3993
|
code: normalized.code,
|
|
@@ -2630,7 +4007,7 @@ async function handleSourceContext(request, options) {
|
|
|
2630
4007
|
await readJsonRequestBody(request)
|
|
2631
4008
|
);
|
|
2632
4009
|
if (!parsed.success) {
|
|
2633
|
-
throw new
|
|
4010
|
+
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
2634
4011
|
}
|
|
2635
4012
|
return readSourceContext({
|
|
2636
4013
|
request: parsed.data,
|
|
@@ -2643,7 +4020,7 @@ async function handleSourceContext(request, options) {
|
|
|
2643
4020
|
async function handleOpenEditor(request, options) {
|
|
2644
4021
|
const parsed = openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
|
|
2645
4022
|
if (!parsed.success) {
|
|
2646
|
-
throw new
|
|
4023
|
+
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
2647
4024
|
}
|
|
2648
4025
|
const body = parsed.data;
|
|
2649
4026
|
const sourcePath = await resolveSourceFile({
|
|
@@ -2660,7 +4037,7 @@ async function handleOpenEditor(request, options) {
|
|
|
2660
4037
|
options.logger?.warn(
|
|
2661
4038
|
`[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
|
|
2662
4039
|
);
|
|
2663
|
-
throw new
|
|
4040
|
+
throw new SpotPatchError16(ERROR_CODES16.EDITOR_OPEN_FAILED, void 0, {
|
|
2664
4041
|
cause: error
|
|
2665
4042
|
});
|
|
2666
4043
|
}
|
|
@@ -2668,64 +4045,98 @@ async function handleOpenEditor(request, options) {
|
|
|
2668
4045
|
function createSpotPatchMiddleware(options) {
|
|
2669
4046
|
const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
|
|
2670
4047
|
const dataFlowAnalyzer = createDataFlowAnalyzer(options);
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
const
|
|
2674
|
-
|
|
4048
|
+
const externalAgentController = options.externalAgentControl === void 0 ? void 0 : createExternalAgentBrowserController(options.externalAgentControl);
|
|
4049
|
+
const middleware = (request, response, next) => {
|
|
4050
|
+
const path9 = requestPath(request);
|
|
4051
|
+
const agentRoute = matchAgentRequestPath(path9);
|
|
4052
|
+
const externalAgentRoute = matchExternalAgentBrowserPath(path9);
|
|
4053
|
+
const externalHandoffRoute = matchExternalHandoffBrowserPath(path9);
|
|
4054
|
+
if (path9 !== SPOTPATCH_ENDPOINTS4.sourceContext && path9 !== SPOTPATCH_ENDPOINTS4.openEditor && path9 !== SPOTPATCH_ENDPOINTS4.dataFlowComponentReport && path9 !== SPOTPATCH_ENDPOINTS4.dataFlowPageReport && agentRoute === void 0 && externalAgentRoute === void 0 && externalHandoffRoute === void 0 && !path9.startsWith(`${SPOTPATCH_API_BASE}/`)) {
|
|
2675
4055
|
next();
|
|
2676
4056
|
return;
|
|
2677
4057
|
}
|
|
2678
4058
|
const handle = async () => {
|
|
2679
|
-
if (
|
|
4059
|
+
if (path9 === SPOTPATCH_ENDPOINTS4.bootstrap && bootstrap !== void 0) {
|
|
2680
4060
|
const data = await readRuntimeBootstrap(
|
|
2681
4061
|
request,
|
|
2682
4062
|
bootstrap
|
|
2683
4063
|
);
|
|
2684
|
-
|
|
4064
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2685
4065
|
return;
|
|
2686
4066
|
}
|
|
2687
4067
|
assertRequestAuthorized(request, {
|
|
2688
4068
|
allowLan: options.options.allowLan,
|
|
2689
4069
|
sessionToken: options.session.token
|
|
2690
4070
|
});
|
|
2691
|
-
if (
|
|
4071
|
+
if (path9 === SPOTPATCH_ENDPOINTS4.sourceContext) {
|
|
2692
4072
|
if (request.method !== "POST") {
|
|
2693
|
-
throw new
|
|
4073
|
+
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
2694
4074
|
}
|
|
2695
4075
|
const data = await handleSourceContext(request, options);
|
|
2696
|
-
|
|
4076
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2697
4077
|
return;
|
|
2698
4078
|
}
|
|
2699
|
-
if (
|
|
4079
|
+
if (path9 === SPOTPATCH_ENDPOINTS4.openEditor) {
|
|
2700
4080
|
if (request.method !== "POST") {
|
|
2701
|
-
throw new
|
|
4081
|
+
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
2702
4082
|
}
|
|
2703
4083
|
const data = await handleOpenEditor(request, options);
|
|
2704
|
-
|
|
4084
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2705
4085
|
return;
|
|
2706
4086
|
}
|
|
2707
|
-
if (
|
|
4087
|
+
if (path9 === SPOTPATCH_ENDPOINTS4.dataFlowComponentReport) {
|
|
2708
4088
|
if (request.method !== "POST") {
|
|
2709
|
-
throw new
|
|
4089
|
+
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
2710
4090
|
}
|
|
2711
4091
|
const data = await handleComponentDataFlowReport(
|
|
2712
4092
|
request,
|
|
2713
4093
|
dataFlowAnalyzer,
|
|
2714
4094
|
options
|
|
2715
4095
|
);
|
|
2716
|
-
|
|
4096
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2717
4097
|
return;
|
|
2718
4098
|
}
|
|
2719
|
-
if (
|
|
4099
|
+
if (path9 === SPOTPATCH_ENDPOINTS4.dataFlowPageReport) {
|
|
2720
4100
|
if (request.method !== "POST") {
|
|
2721
|
-
throw new
|
|
4101
|
+
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
2722
4102
|
}
|
|
2723
4103
|
const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
|
|
2724
|
-
|
|
4104
|
+
writeJson2(response, 200, { ok: true, data });
|
|
4105
|
+
return;
|
|
4106
|
+
}
|
|
4107
|
+
if (externalAgentRoute !== void 0) {
|
|
4108
|
+
if (!options.options.externalAgent.enabled || externalAgentController === void 0) {
|
|
4109
|
+
throw new SpotPatchError16(ERROR_CODES16.EXTERNAL_HANDOFF_DISABLED);
|
|
4110
|
+
}
|
|
4111
|
+
await externalAgentController.handle(
|
|
4112
|
+
request,
|
|
4113
|
+
response,
|
|
4114
|
+
externalAgentRoute,
|
|
4115
|
+
(target, status, data) => {
|
|
4116
|
+
writeJson2(target, status, { ok: true, data });
|
|
4117
|
+
}
|
|
4118
|
+
);
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
if (externalHandoffRoute !== void 0) {
|
|
4122
|
+
await handleExternalHandoffBrowserRequest(
|
|
4123
|
+
request,
|
|
4124
|
+
response,
|
|
4125
|
+
externalHandoffRoute,
|
|
4126
|
+
{
|
|
4127
|
+
options: options.options,
|
|
4128
|
+
registry: options.registry,
|
|
4129
|
+
root: options.root,
|
|
4130
|
+
...options.externalHandoffService === void 0 ? {} : { service: options.externalHandoffService }
|
|
4131
|
+
},
|
|
4132
|
+
(target, status, data) => {
|
|
4133
|
+
writeJson2(target, status, { ok: true, data });
|
|
4134
|
+
}
|
|
4135
|
+
);
|
|
2725
4136
|
return;
|
|
2726
4137
|
}
|
|
2727
4138
|
if (agentRoute === void 0) {
|
|
2728
|
-
throw new
|
|
4139
|
+
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
2729
4140
|
}
|
|
2730
4141
|
await handleAgentRequest(
|
|
2731
4142
|
request,
|
|
@@ -2733,7 +4144,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
2733
4144
|
options,
|
|
2734
4145
|
agentRoute,
|
|
2735
4146
|
(target, status, data) => {
|
|
2736
|
-
|
|
4147
|
+
writeJson2(target, status, { ok: true, data });
|
|
2737
4148
|
}
|
|
2738
4149
|
);
|
|
2739
4150
|
};
|
|
@@ -2741,12 +4152,17 @@ function createSpotPatchMiddleware(options) {
|
|
|
2741
4152
|
writeError(response, error, options.logger);
|
|
2742
4153
|
});
|
|
2743
4154
|
};
|
|
4155
|
+
return Object.assign(middleware, {
|
|
4156
|
+
dispose() {
|
|
4157
|
+
externalAgentController?.dispose();
|
|
4158
|
+
}
|
|
4159
|
+
});
|
|
2744
4160
|
}
|
|
2745
4161
|
|
|
2746
4162
|
// src/server/source-registration.ts
|
|
2747
|
-
import { timingSafeEqual as
|
|
2748
|
-
import { lstat as
|
|
2749
|
-
import
|
|
4163
|
+
import { timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
4164
|
+
import { lstat as lstat4, realpath as realpath6 } from "fs/promises";
|
|
4165
|
+
import path8 from "path";
|
|
2750
4166
|
import { createSourceFilter } from "@spotpatch/compiler";
|
|
2751
4167
|
import { z as z2 } from "zod";
|
|
2752
4168
|
var REGISTRATION_BODY_LIMIT_BYTES = 4096;
|
|
@@ -2767,16 +4183,16 @@ function identitiesMatch(actual, expected) {
|
|
|
2767
4183
|
}
|
|
2768
4184
|
const actualBytes = Buffer.from(actual);
|
|
2769
4185
|
const expectedBytes = Buffer.from(expected);
|
|
2770
|
-
return actualBytes.byteLength === expectedBytes.byteLength &&
|
|
4186
|
+
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual3(actualBytes, expectedBytes);
|
|
2771
4187
|
}
|
|
2772
4188
|
function isWithinRoot(root, candidate) {
|
|
2773
|
-
const relative =
|
|
2774
|
-
return relative === "" || !relative.startsWith(`..${
|
|
4189
|
+
const relative = path8.relative(root, candidate);
|
|
4190
|
+
return relative === "" || !relative.startsWith(`..${path8.sep}`) && relative !== ".." && !path8.isAbsolute(relative);
|
|
2775
4191
|
}
|
|
2776
4192
|
function hasForbiddenSegment(root, candidate) {
|
|
2777
|
-
return
|
|
4193
|
+
return path8.relative(root, candidate).split(path8.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
|
|
2778
4194
|
}
|
|
2779
|
-
function
|
|
4195
|
+
function writeJson3(response, statusCode, payload) {
|
|
2780
4196
|
const body = JSON.stringify(payload);
|
|
2781
4197
|
response.statusCode = statusCode;
|
|
2782
4198
|
response.setHeader("Cache-Control", "no-store");
|
|
@@ -2796,11 +4212,11 @@ function requestComesFromLoopbackWorker(request) {
|
|
|
2796
4212
|
}
|
|
2797
4213
|
}
|
|
2798
4214
|
async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
|
|
2799
|
-
if (!
|
|
4215
|
+
if (!path8.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
2800
4216
|
return void 0;
|
|
2801
4217
|
}
|
|
2802
4218
|
try {
|
|
2803
|
-
const sourceStat = await
|
|
4219
|
+
const sourceStat = await lstat4(requestedPath);
|
|
2804
4220
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
2805
4221
|
return void 0;
|
|
2806
4222
|
}
|
|
@@ -2826,14 +4242,14 @@ async function createSourceRegistrationService(input) {
|
|
|
2826
4242
|
getSingleHeader3(request, INTERNAL_SECRET_HEADER),
|
|
2827
4243
|
input.internalSecret
|
|
2828
4244
|
)) {
|
|
2829
|
-
|
|
4245
|
+
writeJson3(response, 403, { ok: false });
|
|
2830
4246
|
return;
|
|
2831
4247
|
}
|
|
2832
4248
|
const parsed = registrationRequestSchema.safeParse(
|
|
2833
4249
|
await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
|
|
2834
4250
|
);
|
|
2835
4251
|
if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
|
|
2836
|
-
|
|
4252
|
+
writeJson3(response, 400, { ok: false });
|
|
2837
4253
|
return;
|
|
2838
4254
|
}
|
|
2839
4255
|
const sourcePath = await resolveAuthorizedSource(
|
|
@@ -2842,17 +4258,17 @@ async function createSourceRegistrationService(input) {
|
|
|
2842
4258
|
(absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
|
|
2843
4259
|
);
|
|
2844
4260
|
if (sourcePath === void 0) {
|
|
2845
|
-
|
|
4261
|
+
writeJson3(response, 403, { ok: false });
|
|
2846
4262
|
return;
|
|
2847
4263
|
}
|
|
2848
|
-
|
|
4264
|
+
writeJson3(response, 200, {
|
|
2849
4265
|
epoch: input.registryEpoch,
|
|
2850
4266
|
fileId: input.registry.register(sourcePath)
|
|
2851
4267
|
});
|
|
2852
4268
|
};
|
|
2853
4269
|
void handle().catch(() => {
|
|
2854
4270
|
if (!response.headersSent) {
|
|
2855
|
-
|
|
4271
|
+
writeJson3(response, 400, { ok: false });
|
|
2856
4272
|
} else {
|
|
2857
4273
|
response.destroy();
|
|
2858
4274
|
}
|
|
@@ -2862,11 +4278,11 @@ async function createSourceRegistrationService(input) {
|
|
|
2862
4278
|
}
|
|
2863
4279
|
|
|
2864
4280
|
// src/session/session.ts
|
|
2865
|
-
import { randomBytes as
|
|
4281
|
+
import { randomBytes as randomBytes8 } from "crypto";
|
|
2866
4282
|
function createSession() {
|
|
2867
4283
|
return Object.freeze({
|
|
2868
|
-
id:
|
|
2869
|
-
token:
|
|
4284
|
+
id: randomBytes8(16).toString("base64url"),
|
|
4285
|
+
token: randomBytes8(16).toString("base64url")
|
|
2870
4286
|
});
|
|
2871
4287
|
}
|
|
2872
4288
|
|
|
@@ -2879,6 +4295,7 @@ var OPTION_KEYS = Object.freeze([
|
|
|
2879
4295
|
"dataFlow",
|
|
2880
4296
|
"editor",
|
|
2881
4297
|
"enabled",
|
|
4298
|
+
"externalAgent",
|
|
2882
4299
|
"exclude",
|
|
2883
4300
|
"include",
|
|
2884
4301
|
"locale",
|
|
@@ -2969,6 +4386,7 @@ function serializeResolvedSpotPatchOptions(options) {
|
|
|
2969
4386
|
}) : false,
|
|
2970
4387
|
editor: options.editor,
|
|
2971
4388
|
enabled: options.enabled,
|
|
4389
|
+
externalAgent: options.externalAgent.enabled,
|
|
2972
4390
|
exclude: Object.freeze(options.exclude.map(serializeFilter)),
|
|
2973
4391
|
include: Object.freeze(options.include.map(serializeFilter)),
|
|
2974
4392
|
locale: options.locale,
|
|
@@ -3022,7 +4440,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
3022
4440
|
if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
|
|
3023
4441
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
3024
4442
|
}
|
|
3025
|
-
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)) {
|
|
4443
|
+
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)) {
|
|
3026
4444
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
3027
4445
|
}
|
|
3028
4446
|
try {
|
|
@@ -3034,6 +4452,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
3034
4452
|
dataFlow: parseDataFlow(value.dataFlow),
|
|
3035
4453
|
editor: value.editor,
|
|
3036
4454
|
enabled: value.enabled,
|
|
4455
|
+
externalAgent: value.externalAgent,
|
|
3037
4456
|
exclude: parseFilterList(value.exclude),
|
|
3038
4457
|
include: parseFilterList(value.include),
|
|
3039
4458
|
locale: value.locale,
|
|
@@ -3052,6 +4471,7 @@ export {
|
|
|
3052
4471
|
DEFAULT_OPTIONS,
|
|
3053
4472
|
applyIntegrationPlan,
|
|
3054
4473
|
createAgentJobManager,
|
|
4474
|
+
createExternalHandoffService,
|
|
3055
4475
|
createIntegrationFileChange,
|
|
3056
4476
|
createRuntimeAiConfig,
|
|
3057
4477
|
createRuntimeDataFlowConfig,
|
|
@@ -3068,8 +4488,10 @@ export {
|
|
|
3068
4488
|
readRuntimeBootstrap,
|
|
3069
4489
|
resolveCredentialEnvironment,
|
|
3070
4490
|
resolveEnvironmentAiConfiguration,
|
|
4491
|
+
resolveManagedExecutionValidation,
|
|
3071
4492
|
resolveOptions,
|
|
3072
4493
|
resolveProjectOptions,
|
|
4494
|
+
resolveProjectValidationChecks,
|
|
3073
4495
|
resolveRuntimeBootstrapOptions,
|
|
3074
4496
|
serializeResolvedSpotPatchOptions
|
|
3075
4497
|
};
|