@spotpatch/dev-server 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1776 -331
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -2
- package/dist/index.d.ts +69 -2
- package/dist/index.js +1798 -295
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.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) {
|
|
@@ -742,7 +1839,8 @@ import {
|
|
|
742
1839
|
DEFAULT_AGENT_LIMITS,
|
|
743
1840
|
MAX_ANNOTATION_TARGETS,
|
|
744
1841
|
SPOTPATCH_EDITOR_PREFERENCES,
|
|
745
|
-
SPOTPATCH_LOCALE_PREFERENCES
|
|
1842
|
+
SPOTPATCH_LOCALE_PREFERENCES,
|
|
1843
|
+
DEFAULT_DATA_FLOW_LIMITS
|
|
746
1844
|
} from "@spotpatch/shared";
|
|
747
1845
|
import { z } from "zod";
|
|
748
1846
|
var DEFAULT_EXCLUDE = Object.freeze([
|
|
@@ -753,7 +1851,7 @@ var DEFAULT_EXCLUDE = Object.freeze([
|
|
|
753
1851
|
/(?:^|\/)dist(?:\/|$)/,
|
|
754
1852
|
/(?:^|\/)coverage(?:\/|$)/
|
|
755
1853
|
]);
|
|
756
|
-
var DEFAULT_INCLUDE = Object.freeze([/(?:^|[/\\])src[/\\].+\.(?:jsx|tsx)$/]);
|
|
1854
|
+
var DEFAULT_INCLUDE = Object.freeze([/(?:^|[/\\])src[/\\].+\.(?:js|jsx|ts|tsx)$/]);
|
|
757
1855
|
var DEFAULT_BUDGET = Object.freeze({
|
|
758
1856
|
totalCharacters: 16e3,
|
|
759
1857
|
domCharacters: 3e3,
|
|
@@ -774,7 +1872,13 @@ var DEFAULT_OPTIONS = Object.freeze({
|
|
|
774
1872
|
debug: false,
|
|
775
1873
|
locale: "auto",
|
|
776
1874
|
maxTargets: 8,
|
|
777
|
-
ai: false
|
|
1875
|
+
ai: false,
|
|
1876
|
+
dataFlow: Object.freeze({
|
|
1877
|
+
enabled: false,
|
|
1878
|
+
runtime: "dispatch",
|
|
1879
|
+
limits: DEFAULT_DATA_FLOW_LIMITS
|
|
1880
|
+
}),
|
|
1881
|
+
externalAgent: Object.freeze({ enabled: false })
|
|
778
1882
|
});
|
|
779
1883
|
var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
780
1884
|
var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
@@ -1066,10 +2170,43 @@ function assertPositiveBudget(budget) {
|
|
|
1066
2170
|
}
|
|
1067
2171
|
}
|
|
1068
2172
|
}
|
|
2173
|
+
function resolveDataFlowOptions(options) {
|
|
2174
|
+
if (options === void 0 || options === false) {
|
|
2175
|
+
return DEFAULT_OPTIONS.dataFlow;
|
|
2176
|
+
}
|
|
2177
|
+
const candidate = options;
|
|
2178
|
+
if (typeof candidate !== "object" || candidate === null) {
|
|
2179
|
+
throw new RangeError("SpotPatch dataFlow configuration is invalid.");
|
|
2180
|
+
}
|
|
2181
|
+
const runtime = options.runtime ?? "dispatch";
|
|
2182
|
+
if (runtime !== "dispatch") {
|
|
2183
|
+
throw new RangeError("SpotPatch dataFlow runtime mode is invalid.");
|
|
2184
|
+
}
|
|
2185
|
+
return Object.freeze({
|
|
2186
|
+
enabled: true,
|
|
2187
|
+
runtime,
|
|
2188
|
+
limits: DEFAULT_DATA_FLOW_LIMITS
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
function createRuntimeDataFlowConfig(options) {
|
|
2192
|
+
return Object.freeze({
|
|
2193
|
+
enabled: options.enabled,
|
|
2194
|
+
runtime: options.runtime,
|
|
2195
|
+
limits: Object.freeze({
|
|
2196
|
+
observationMaxEntries: options.limits.observationMaxEntries,
|
|
2197
|
+
observationMaxBytes: options.limits.observationMaxBytes,
|
|
2198
|
+
observationTtlMs: options.limits.observationTtlMs,
|
|
2199
|
+
reportMaxBytes: options.limits.reportMaxBytes
|
|
2200
|
+
})
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
1069
2203
|
function resolveOptions(options = {}, environmentAi) {
|
|
1070
2204
|
if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
|
|
1071
2205
|
throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
|
|
1072
2206
|
}
|
|
2207
|
+
if (options.externalAgent !== void 0 && typeof options.externalAgent !== "boolean") {
|
|
2208
|
+
throw new RangeError("SpotPatch externalAgent must be a boolean.");
|
|
2209
|
+
}
|
|
1073
2210
|
const budget = Object.freeze({
|
|
1074
2211
|
...DEFAULT_OPTIONS.budget,
|
|
1075
2212
|
...options.budget
|
|
@@ -1101,7 +2238,11 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1101
2238
|
debug: options.debug ?? DEFAULT_OPTIONS.debug,
|
|
1102
2239
|
locale,
|
|
1103
2240
|
maxTargets,
|
|
1104
|
-
ai: resolveAiOptions(options.ai ?? environmentAi)
|
|
2241
|
+
ai: resolveAiOptions(options.ai ?? environmentAi),
|
|
2242
|
+
dataFlow: resolveDataFlowOptions(options.dataFlow),
|
|
2243
|
+
externalAgent: Object.freeze({
|
|
2244
|
+
enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
|
|
2245
|
+
})
|
|
1105
2246
|
};
|
|
1106
2247
|
if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
|
|
1107
2248
|
throw new RangeError("SpotPatch shortcut is invalid.");
|
|
@@ -1111,9 +2252,9 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1111
2252
|
|
|
1112
2253
|
// src/project-validation.ts
|
|
1113
2254
|
import { execFile } from "child_process";
|
|
1114
|
-
import { access as access2, lstat as
|
|
2255
|
+
import { access as access2, lstat as lstat3, readFile as readFile2, realpath as realpath2 } from "fs/promises";
|
|
1115
2256
|
import { createRequire } from "module";
|
|
1116
|
-
import
|
|
2257
|
+
import path3 from "path";
|
|
1117
2258
|
import { promisify } from "util";
|
|
1118
2259
|
var execFileAsync = promisify(execFile);
|
|
1119
2260
|
var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
|
|
@@ -1123,14 +2264,14 @@ function isRecord(value) {
|
|
|
1123
2264
|
}
|
|
1124
2265
|
async function isRegularFile(absolutePath) {
|
|
1125
2266
|
try {
|
|
1126
|
-
const metadata = await
|
|
2267
|
+
const metadata = await lstat3(absolutePath);
|
|
1127
2268
|
return metadata.isFile() && !metadata.isSymbolicLink();
|
|
1128
2269
|
} catch {
|
|
1129
2270
|
return false;
|
|
1130
2271
|
}
|
|
1131
2272
|
}
|
|
1132
2273
|
async function readManifest(appRoot) {
|
|
1133
|
-
const manifestPath =
|
|
2274
|
+
const manifestPath = path3.join(appRoot, "package.json");
|
|
1134
2275
|
if (!await isRegularFile(manifestPath)) {
|
|
1135
2276
|
return void 0;
|
|
1136
2277
|
}
|
|
@@ -1159,8 +2300,8 @@ async function findGitRoot(appRoot) {
|
|
|
1159
2300
|
windowsHide: true
|
|
1160
2301
|
});
|
|
1161
2302
|
const root = await realpath2(result.stdout.trim());
|
|
1162
|
-
const relative =
|
|
1163
|
-
if (relative === "" || !relative.startsWith(`..${
|
|
2303
|
+
const relative = path3.relative(root, appRoot);
|
|
2304
|
+
if (relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative)) {
|
|
1164
2305
|
return root;
|
|
1165
2306
|
}
|
|
1166
2307
|
} catch {
|
|
@@ -1169,10 +2310,10 @@ async function findGitRoot(appRoot) {
|
|
|
1169
2310
|
return void 0;
|
|
1170
2311
|
}
|
|
1171
2312
|
async function resolveTypeScriptCli(appRoot) {
|
|
1172
|
-
const resolveFromApplication = createRequire(
|
|
2313
|
+
const resolveFromApplication = createRequire(path3.join(appRoot, "package.json"));
|
|
1173
2314
|
try {
|
|
1174
2315
|
const packagePath = resolveFromApplication.resolve("typescript/package.json");
|
|
1175
|
-
const cliPath =
|
|
2316
|
+
const cliPath = path3.join(path3.dirname(packagePath), "bin", "tsc");
|
|
1176
2317
|
await access2(cliPath);
|
|
1177
2318
|
return await realpath2(cliPath);
|
|
1178
2319
|
} catch {
|
|
@@ -1180,11 +2321,11 @@ async function resolveTypeScriptCli(appRoot) {
|
|
|
1180
2321
|
}
|
|
1181
2322
|
}
|
|
1182
2323
|
function portableRelativePath(from, to) {
|
|
1183
|
-
return
|
|
2324
|
+
return path3.relative(from, to).split(path3.sep).join("/");
|
|
1184
2325
|
}
|
|
1185
2326
|
async function discoverProjectValidationCheck(options) {
|
|
1186
2327
|
const appRoot = await realpath2(options.appRoot);
|
|
1187
|
-
const tsconfigPath =
|
|
2328
|
+
const tsconfigPath = path3.join(appRoot, "tsconfig.json");
|
|
1188
2329
|
const [manifest, projectRoot, hasTsconfig] = await Promise.all([
|
|
1189
2330
|
readManifest(appRoot),
|
|
1190
2331
|
findGitRoot(appRoot),
|
|
@@ -1272,81 +2413,109 @@ async function resolveProjectOptions(input) {
|
|
|
1272
2413
|
}
|
|
1273
2414
|
|
|
1274
2415
|
// src/registry/source-registry.ts
|
|
1275
|
-
import
|
|
2416
|
+
import path4 from "path";
|
|
1276
2417
|
|
|
1277
2418
|
// src/registry/source-id.ts
|
|
1278
|
-
import { randomBytes as
|
|
2419
|
+
import { randomBytes as randomBytes7 } from "crypto";
|
|
1279
2420
|
var SOURCE_ID_BYTES = 8;
|
|
1280
|
-
var createRandomSourceId = () =>
|
|
2421
|
+
var createRandomSourceId = () => randomBytes7(SOURCE_ID_BYTES).toString("base64url");
|
|
1281
2422
|
|
|
1282
2423
|
// src/registry/source-registry.ts
|
|
1283
2424
|
function normalizeAbsolutePath(absolutePath) {
|
|
1284
|
-
return
|
|
2425
|
+
return path4.normalize(path4.resolve(absolutePath));
|
|
1285
2426
|
}
|
|
1286
2427
|
function createSourceRegistry(options = {}) {
|
|
1287
2428
|
const createId = options.createId ?? createRandomSourceId;
|
|
1288
2429
|
const pathToId = /* @__PURE__ */ new Map();
|
|
1289
2430
|
const idToPath = /* @__PURE__ */ new Map();
|
|
2431
|
+
const componentAnchors = /* @__PURE__ */ new Map();
|
|
2432
|
+
const componentIdsByPath = /* @__PURE__ */ new Map();
|
|
2433
|
+
function registerSourcePath(absolutePath) {
|
|
2434
|
+
const normalizedPath = normalizeAbsolutePath(absolutePath);
|
|
2435
|
+
const existingId = pathToId.get(normalizedPath);
|
|
2436
|
+
if (existingId !== void 0) {
|
|
2437
|
+
return existingId;
|
|
2438
|
+
}
|
|
2439
|
+
let fileId = createId();
|
|
2440
|
+
while (idToPath.has(fileId)) fileId = createId();
|
|
2441
|
+
pathToId.set(normalizedPath, fileId);
|
|
2442
|
+
idToPath.set(fileId, normalizedPath);
|
|
2443
|
+
return fileId;
|
|
2444
|
+
}
|
|
1290
2445
|
return Object.freeze({
|
|
1291
2446
|
register(absolutePath) {
|
|
2447
|
+
return registerSourcePath(absolutePath);
|
|
2448
|
+
},
|
|
2449
|
+
registerDataFlowComponents(absolutePath, sourceVersion, components) {
|
|
1292
2450
|
const normalizedPath = normalizeAbsolutePath(absolutePath);
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
2451
|
+
const previousIds = componentIdsByPath.get(normalizedPath);
|
|
2452
|
+
for (const componentSourceId of previousIds ?? []) {
|
|
2453
|
+
componentAnchors.delete(componentSourceId);
|
|
1296
2454
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
2455
|
+
const fileId = registerSourcePath(normalizedPath);
|
|
2456
|
+
const currentIds = /* @__PURE__ */ new Set();
|
|
2457
|
+
for (const component of components) {
|
|
2458
|
+
currentIds.add(component.componentSourceId);
|
|
2459
|
+
componentAnchors.set(
|
|
2460
|
+
component.componentSourceId,
|
|
2461
|
+
Object.freeze({ ...component, fileId, sourceVersion })
|
|
2462
|
+
);
|
|
1300
2463
|
}
|
|
1301
|
-
|
|
1302
|
-
idToPath.set(fileId, normalizedPath);
|
|
1303
|
-
return fileId;
|
|
2464
|
+
componentIdsByPath.set(normalizedPath, currentIds);
|
|
1304
2465
|
},
|
|
1305
2466
|
resolve(fileId) {
|
|
1306
2467
|
return idToPath.get(fileId);
|
|
1307
2468
|
},
|
|
2469
|
+
resolveDataFlowComponent(componentSourceId) {
|
|
2470
|
+
return componentAnchors.get(componentSourceId);
|
|
2471
|
+
},
|
|
1308
2472
|
clear() {
|
|
1309
2473
|
pathToId.clear();
|
|
1310
2474
|
idToPath.clear();
|
|
2475
|
+
componentAnchors.clear();
|
|
2476
|
+
componentIdsByPath.clear();
|
|
1311
2477
|
}
|
|
1312
2478
|
});
|
|
1313
2479
|
}
|
|
1314
2480
|
|
|
1315
2481
|
// src/server/middleware.ts
|
|
1316
2482
|
import {
|
|
1317
|
-
ERROR_CODES as
|
|
2483
|
+
ERROR_CODES as ERROR_CODES15,
|
|
1318
2484
|
SPOTPATCH_API_BASE,
|
|
1319
|
-
SPOTPATCH_ENDPOINTS as
|
|
1320
|
-
SpotPatchError as
|
|
2485
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS3,
|
|
2486
|
+
SpotPatchError as SpotPatchError15,
|
|
1321
2487
|
openEditorRequestSchema,
|
|
1322
2488
|
sourceContextRequestSchema
|
|
1323
2489
|
} from "@spotpatch/shared";
|
|
1324
2490
|
|
|
1325
|
-
// src/
|
|
2491
|
+
// src/external-handoff/browser-http.ts
|
|
1326
2492
|
import {
|
|
1327
|
-
ERROR_CODES as
|
|
2493
|
+
ERROR_CODES as ERROR_CODES10,
|
|
2494
|
+
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS5,
|
|
1328
2495
|
SPOTPATCH_ENDPOINTS,
|
|
1329
|
-
SpotPatchError as
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
2496
|
+
SpotPatchError as SpotPatchError10,
|
|
2497
|
+
externalHandoffCapabilityRequestSchema,
|
|
2498
|
+
externalHandoffPublishRequestSchema,
|
|
2499
|
+
externalHandoffResolveDeliveryRequestSchema,
|
|
2500
|
+
externalHandoffStatusRequestSchema
|
|
1334
2501
|
} from "@spotpatch/shared";
|
|
1335
2502
|
|
|
1336
|
-
// src/server/
|
|
2503
|
+
// src/server/annotation-authorizer.ts
|
|
1337
2504
|
import { realpath as realpath5 } from "fs/promises";
|
|
1338
|
-
import
|
|
2505
|
+
import path7 from "path";
|
|
1339
2506
|
import {
|
|
1340
|
-
ERROR_CODES as
|
|
1341
|
-
SpotPatchError as
|
|
2507
|
+
ERROR_CODES as ERROR_CODES9,
|
|
2508
|
+
SpotPatchError as SpotPatchError9,
|
|
2509
|
+
redactSensitiveText,
|
|
2510
|
+
sanitizeUrl
|
|
1342
2511
|
} from "@spotpatch/shared";
|
|
1343
2512
|
|
|
1344
2513
|
// src/server/source-context.ts
|
|
1345
2514
|
import { readFile as readFile3, realpath as realpath4 } from "fs/promises";
|
|
1346
|
-
import
|
|
2515
|
+
import path6 from "path";
|
|
1347
2516
|
import {
|
|
1348
|
-
ERROR_CODES as
|
|
1349
|
-
SpotPatchError as
|
|
2517
|
+
ERROR_CODES as ERROR_CODES8,
|
|
2518
|
+
SpotPatchError as SpotPatchError8
|
|
1350
2519
|
} from "@spotpatch/shared";
|
|
1351
2520
|
|
|
1352
2521
|
// src/server/extract-code-context.ts
|
|
@@ -1580,15 +2749,8 @@ function extractCodeContext(options) {
|
|
|
1580
2749
|
|
|
1581
2750
|
// src/server/source-file.ts
|
|
1582
2751
|
import { realpath as realpath3, stat as stat2 } from "fs/promises";
|
|
1583
|
-
import
|
|
1584
|
-
import { ERROR_CODES as
|
|
1585
|
-
|
|
1586
|
-
// src/server/constants.ts
|
|
1587
|
-
var MAX_REQUEST_BODY_BYTES = 32 * 1024;
|
|
1588
|
-
var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
|
|
1589
|
-
var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
|
1590
|
-
|
|
1591
|
-
// src/server/source-file.ts
|
|
2752
|
+
import path5 from "path";
|
|
2753
|
+
import { ERROR_CODES as ERROR_CODES7, SpotPatchError as SpotPatchError7 } from "@spotpatch/shared";
|
|
1592
2754
|
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
1593
2755
|
function isMissingFileError(error) {
|
|
1594
2756
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
@@ -1603,51 +2765,51 @@ async function assertInsideRoot(root, candidate) {
|
|
|
1603
2765
|
]);
|
|
1604
2766
|
} catch (error) {
|
|
1605
2767
|
if (isMissingFileError(error)) {
|
|
1606
|
-
throw new
|
|
2768
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND, void 0, {
|
|
1607
2769
|
cause: error
|
|
1608
2770
|
});
|
|
1609
2771
|
}
|
|
1610
2772
|
throw error;
|
|
1611
2773
|
}
|
|
1612
|
-
const relative =
|
|
1613
|
-
const outside = relative.startsWith(`..${
|
|
2774
|
+
const relative = path5.relative(realRoot, realCandidate);
|
|
2775
|
+
const outside = relative.startsWith(`..${path5.sep}`) || relative === ".." || path5.isAbsolute(relative);
|
|
1614
2776
|
if (outside) {
|
|
1615
|
-
throw new
|
|
2777
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_OUTSIDE_ROOT);
|
|
1616
2778
|
}
|
|
1617
2779
|
return realCandidate;
|
|
1618
2780
|
}
|
|
1619
2781
|
async function resolveSourceFile(options) {
|
|
1620
2782
|
const registeredPath = options.registry.resolve(options.fileId);
|
|
1621
2783
|
if (registeredPath === void 0) {
|
|
1622
|
-
throw new
|
|
2784
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
1623
2785
|
}
|
|
1624
2786
|
const sourcePath = await assertInsideRoot(options.root, registeredPath);
|
|
1625
|
-
if (!ALLOWED_EXTENSIONS.has(
|
|
1626
|
-
throw new
|
|
2787
|
+
if (!ALLOWED_EXTENSIONS.has(path5.extname(sourcePath).toLowerCase())) {
|
|
2788
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
1627
2789
|
}
|
|
1628
2790
|
let sourceStat;
|
|
1629
2791
|
try {
|
|
1630
2792
|
sourceStat = await stat2(sourcePath);
|
|
1631
2793
|
} catch (error) {
|
|
1632
2794
|
if (isMissingFileError(error)) {
|
|
1633
|
-
throw new
|
|
2795
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND, void 0, {
|
|
1634
2796
|
cause: error
|
|
1635
2797
|
});
|
|
1636
2798
|
}
|
|
1637
2799
|
throw error;
|
|
1638
2800
|
}
|
|
1639
2801
|
if (!sourceStat.isFile()) {
|
|
1640
|
-
throw new
|
|
2802
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
1641
2803
|
}
|
|
1642
2804
|
if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
|
|
1643
|
-
throw new
|
|
2805
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_TOO_LARGE);
|
|
1644
2806
|
}
|
|
1645
2807
|
return sourcePath;
|
|
1646
2808
|
}
|
|
1647
2809
|
|
|
1648
2810
|
// src/server/source-context.ts
|
|
1649
2811
|
function toDisplayPath(root, sourcePath) {
|
|
1650
|
-
return
|
|
2812
|
+
return path6.relative(root, sourcePath).split(path6.sep).join("/");
|
|
1651
2813
|
}
|
|
1652
2814
|
async function readSourceContext(options) {
|
|
1653
2815
|
const sourcePath = await resolveSourceFile({
|
|
@@ -1660,7 +2822,7 @@ async function readSourceContext(options) {
|
|
|
1660
2822
|
source = await readFile3(sourcePath, "utf8");
|
|
1661
2823
|
} catch (error) {
|
|
1662
2824
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1663
|
-
throw new
|
|
2825
|
+
throw new SpotPatchError8(ERROR_CODES8.SOURCE_NOT_FOUND, void 0, {
|
|
1664
2826
|
cause: error
|
|
1665
2827
|
});
|
|
1666
2828
|
}
|
|
@@ -1668,9 +2830,9 @@ async function readSourceContext(options) {
|
|
|
1668
2830
|
}
|
|
1669
2831
|
const lines = source.split(/\r?\n/);
|
|
1670
2832
|
if (options.request.line > lines.length) {
|
|
1671
|
-
throw new
|
|
2833
|
+
throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
|
|
1672
2834
|
}
|
|
1673
|
-
const extension =
|
|
2835
|
+
const extension = path6.extname(sourcePath).toLowerCase();
|
|
1674
2836
|
return extractCodeContext({
|
|
1675
2837
|
source,
|
|
1676
2838
|
sourcePath,
|
|
@@ -1683,7 +2845,17 @@ async function readSourceContext(options) {
|
|
|
1683
2845
|
});
|
|
1684
2846
|
}
|
|
1685
2847
|
|
|
1686
|
-
// src/server/
|
|
2848
|
+
// src/server/annotation-authorizer.ts
|
|
2849
|
+
function sanitizePageContext(page) {
|
|
2850
|
+
return Object.freeze({
|
|
2851
|
+
url: sanitizeUrl(page.url, page.url),
|
|
2852
|
+
pathname: redactSensitiveText(page.pathname),
|
|
2853
|
+
title: redactSensitiveText(page.title),
|
|
2854
|
+
viewportWidth: page.viewportWidth,
|
|
2855
|
+
viewportHeight: page.viewportHeight,
|
|
2856
|
+
devicePixelRatio: page.devicePixelRatio
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
1687
2859
|
function compactSourceRef(source) {
|
|
1688
2860
|
return Object.freeze({
|
|
1689
2861
|
origin: source.origin,
|
|
@@ -1697,27 +2869,20 @@ function compactSourceRef(source) {
|
|
|
1697
2869
|
async function authorizeSourceRef(source, registry, root) {
|
|
1698
2870
|
const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
|
|
1699
2871
|
if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
|
|
1700
|
-
throw new
|
|
2872
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1701
2873
|
}
|
|
1702
2874
|
if (source.fileId === void 0) {
|
|
1703
2875
|
if (source.origin === "none" && source.relativePath !== void 0) {
|
|
1704
|
-
throw new
|
|
2876
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1705
2877
|
}
|
|
1706
2878
|
return compactSourceRef(source);
|
|
1707
2879
|
}
|
|
1708
|
-
const sourcePath = await resolveSourceFile({
|
|
1709
|
-
|
|
1710
|
-
registry,
|
|
1711
|
-
root
|
|
1712
|
-
});
|
|
1713
|
-
const relativePath = path6.relative(await realpath5(root), sourcePath).split(path6.sep).join("/");
|
|
2880
|
+
const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
|
|
2881
|
+
const relativePath = path7.relative(await realpath5(root), sourcePath).split(path7.sep).join("/");
|
|
1714
2882
|
if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
|
|
1715
|
-
throw new
|
|
2883
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1716
2884
|
}
|
|
1717
|
-
return Object.freeze({
|
|
1718
|
-
...compactSourceRef(source),
|
|
1719
|
-
relativePath
|
|
1720
|
-
});
|
|
2885
|
+
return Object.freeze({ ...compactSourceRef(source), relativePath });
|
|
1721
2886
|
}
|
|
1722
2887
|
function freezeMatchedRule(rule) {
|
|
1723
2888
|
return Object.freeze({
|
|
@@ -1751,7 +2916,7 @@ async function authorizeTarget(target, input) {
|
|
|
1751
2916
|
maxLines: input.options.budget.maxCodeLines
|
|
1752
2917
|
});
|
|
1753
2918
|
if (marker === void 0 && target.code !== void 0) {
|
|
1754
|
-
throw new
|
|
2919
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1755
2920
|
}
|
|
1756
2921
|
const code = marker === void 0 ? void 0 : await readSourceContext({
|
|
1757
2922
|
request: marker,
|
|
@@ -1761,11 +2926,11 @@ async function authorizeTarget(target, input) {
|
|
|
1761
2926
|
maxLines: input.options.budget.maxCodeLines
|
|
1762
2927
|
});
|
|
1763
2928
|
if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
|
|
1764
|
-
throw new
|
|
2929
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1765
2930
|
}
|
|
1766
2931
|
return Object.freeze({
|
|
1767
2932
|
instruction: target.instruction,
|
|
1768
|
-
...target.page === void 0 ? {} : { page:
|
|
2933
|
+
...target.page === void 0 ? {} : { page: sanitizePageContext(target.page) },
|
|
1769
2934
|
source,
|
|
1770
2935
|
react: Object.freeze({
|
|
1771
2936
|
supported: target.react.supported,
|
|
@@ -1793,76 +2958,138 @@ async function authorizeTarget(target, input) {
|
|
|
1793
2958
|
warnings: Object.freeze([...target.warnings])
|
|
1794
2959
|
});
|
|
1795
2960
|
}
|
|
1796
|
-
async function
|
|
1797
|
-
const requestedTargets = input.
|
|
2961
|
+
async function authorizeAnnotation(input) {
|
|
2962
|
+
const requestedTargets = input.annotation.targets;
|
|
1798
2963
|
if (requestedTargets.length > input.options.maxTargets) {
|
|
1799
|
-
throw new
|
|
2964
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1800
2965
|
}
|
|
1801
2966
|
const identities = requestedTargets.map(targetIdentity);
|
|
1802
2967
|
if (new Set(identities).size !== identities.length) {
|
|
1803
|
-
throw new
|
|
2968
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1804
2969
|
}
|
|
1805
2970
|
const targets = Object.freeze(
|
|
1806
2971
|
await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
|
|
1807
2972
|
);
|
|
1808
|
-
|
|
2973
|
+
return Object.freeze({
|
|
1809
2974
|
schemaVersion: 3,
|
|
1810
|
-
id: input.
|
|
1811
|
-
locale: input.
|
|
1812
|
-
page:
|
|
2975
|
+
id: input.annotation.id,
|
|
2976
|
+
locale: input.annotation.locale,
|
|
2977
|
+
page: sanitizePageContext(input.annotation.page),
|
|
1813
2978
|
targets,
|
|
1814
|
-
createdAt: input.
|
|
1815
|
-
});
|
|
1816
|
-
return Object.freeze({
|
|
1817
|
-
annotation,
|
|
1818
|
-
...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
|
|
1819
|
-
providerProfileId: input.request.providerProfileId,
|
|
1820
|
-
modelProfileId: input.request.modelProfileId,
|
|
1821
|
-
providerDataConsent: true,
|
|
1822
|
-
...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
|
|
1823
|
-
workingTreeMode: input.request.workingTreeMode
|
|
2979
|
+
createdAt: input.annotation.createdAt
|
|
1824
2980
|
});
|
|
1825
2981
|
}
|
|
1826
2982
|
|
|
1827
|
-
// src/
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
throw new SpotPatchError5(ERROR_CODES5.INVALID_REQUEST);
|
|
2983
|
+
// src/external-handoff/browser-http.ts
|
|
2984
|
+
function matchExternalHandoffBrowserPath(path9) {
|
|
2985
|
+
if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffCapability) return "capability";
|
|
2986
|
+
if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffPublish) return "publish";
|
|
2987
|
+
if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffStatus) return "status";
|
|
2988
|
+
if (path9 === SPOTPATCH_ENDPOINTS.externalHandoffResolveDelivery) {
|
|
2989
|
+
return "resolve-delivery";
|
|
1835
2990
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2991
|
+
return void 0;
|
|
2992
|
+
}
|
|
2993
|
+
function requireService(options) {
|
|
2994
|
+
if (!options.options.externalAgent.enabled || options.service === void 0) {
|
|
2995
|
+
throw new SpotPatchError10(ERROR_CODES10.EXTERNAL_HANDOFF_DISABLED);
|
|
1839
2996
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2997
|
+
return options.service;
|
|
2998
|
+
}
|
|
2999
|
+
function remapAuthorizationError(error) {
|
|
3000
|
+
if (error instanceof SpotPatchError10) {
|
|
3001
|
+
if (error.code === ERROR_CODES10.SOURCE_NOT_FOUND || error.code === ERROR_CODES10.SOURCE_OUTSIDE_ROOT || error.code === ERROR_CODES10.SOURCE_TOO_LARGE) {
|
|
3002
|
+
throw new SpotPatchError10(ERROR_CODES10.HANDOFF_SOURCE_STALE, void 0, {
|
|
3003
|
+
cause: error
|
|
3004
|
+
});
|
|
1847
3005
|
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
continue;
|
|
3006
|
+
if (error.code === ERROR_CODES10.INVALID_REQUEST) {
|
|
3007
|
+
throw new SpotPatchError10(ERROR_CODES10.HANDOFF_VALIDATION_FAILED, void 0, {
|
|
3008
|
+
cause: error
|
|
3009
|
+
});
|
|
1853
3010
|
}
|
|
1854
|
-
chunks.push(buffer);
|
|
1855
3011
|
}
|
|
1856
|
-
|
|
1857
|
-
|
|
3012
|
+
throw error;
|
|
3013
|
+
}
|
|
3014
|
+
async function handleExternalHandoffBrowserRequest(request, response, route, options, writeSuccess) {
|
|
3015
|
+
if (request.method !== "POST") {
|
|
3016
|
+
throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
1858
3017
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
3018
|
+
const service = requireService(options);
|
|
3019
|
+
if (route === "capability") {
|
|
3020
|
+
const parsed2 = externalHandoffCapabilityRequestSchema.safeParse(
|
|
3021
|
+
await readJsonRequestBody(request)
|
|
3022
|
+
);
|
|
3023
|
+
if (!parsed2.success) throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
3024
|
+
writeSuccess(response, 200, service.capability());
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
if (route === "status") {
|
|
3028
|
+
const parsed2 = externalHandoffStatusRequestSchema.safeParse(
|
|
3029
|
+
await readJsonRequestBody(request)
|
|
3030
|
+
);
|
|
3031
|
+
if (!parsed2.success) throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
3032
|
+
writeSuccess(response, 200, service.status(parsed2.data.cursor));
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
if (route === "resolve-delivery") {
|
|
3036
|
+
const parsed2 = externalHandoffResolveDeliveryRequestSchema.safeParse(
|
|
3037
|
+
await readJsonRequestBody(request)
|
|
3038
|
+
);
|
|
3039
|
+
if (!parsed2.success) throw new SpotPatchError10(ERROR_CODES10.INVALID_REQUEST);
|
|
3040
|
+
writeSuccess(response, 200, service.resolveDelivery(parsed2.data.cursor));
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
const parsed = externalHandoffPublishRequestSchema.safeParse(
|
|
3044
|
+
await readJsonRequestBody(request, EXTERNAL_HANDOFF_LIMITS5.maximumPublishBodyBytes)
|
|
3045
|
+
);
|
|
3046
|
+
if (!parsed.success) {
|
|
3047
|
+
throw new SpotPatchError10(ERROR_CODES10.HANDOFF_VALIDATION_FAILED);
|
|
1865
3048
|
}
|
|
3049
|
+
const result = await service.publish(parsed.data, async (annotation) => {
|
|
3050
|
+
try {
|
|
3051
|
+
return await authorizeAnnotation({
|
|
3052
|
+
annotation,
|
|
3053
|
+
options: options.options,
|
|
3054
|
+
registry: options.registry,
|
|
3055
|
+
root: options.root
|
|
3056
|
+
});
|
|
3057
|
+
} catch (error) {
|
|
3058
|
+
remapAuthorizationError(error);
|
|
3059
|
+
}
|
|
3060
|
+
});
|
|
3061
|
+
writeSuccess(response, result.replayed ? 200 : 201, result);
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
// src/server/agent-http.ts
|
|
3065
|
+
import {
|
|
3066
|
+
ERROR_CODES as ERROR_CODES11,
|
|
3067
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
|
|
3068
|
+
SpotPatchError as SpotPatchError11,
|
|
3069
|
+
agentCapabilityRequestSchema,
|
|
3070
|
+
agentJobActionRequestSchema,
|
|
3071
|
+
agentJobCreateRequestSchema,
|
|
3072
|
+
agentWorkspaceHealthRequestSchema
|
|
3073
|
+
} from "@spotpatch/shared";
|
|
3074
|
+
|
|
3075
|
+
// src/server/agent-request.ts
|
|
3076
|
+
import "@spotpatch/shared";
|
|
3077
|
+
async function authorizeAgentJobRequest(input) {
|
|
3078
|
+
const annotation = await authorizeAnnotation({
|
|
3079
|
+
annotation: input.request.annotation,
|
|
3080
|
+
options: input.options,
|
|
3081
|
+
registry: input.registry,
|
|
3082
|
+
root: input.root
|
|
3083
|
+
});
|
|
3084
|
+
return Object.freeze({
|
|
3085
|
+
annotation,
|
|
3086
|
+
...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
|
|
3087
|
+
providerProfileId: input.request.providerProfileId,
|
|
3088
|
+
modelProfileId: input.request.modelProfileId,
|
|
3089
|
+
providerDataConsent: true,
|
|
3090
|
+
...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
|
|
3091
|
+
workingTreeMode: input.request.workingTreeMode
|
|
3092
|
+
});
|
|
1866
3093
|
}
|
|
1867
3094
|
|
|
1868
3095
|
// src/server/agent-http.ts
|
|
@@ -1882,21 +3109,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
|
|
|
1882
3109
|
"reverted",
|
|
1883
3110
|
"failed"
|
|
1884
3111
|
]);
|
|
1885
|
-
function matchAgentRequestPath(
|
|
1886
|
-
if (
|
|
3112
|
+
function matchAgentRequestPath(path9) {
|
|
3113
|
+
if (path9 === SPOTPATCH_ENDPOINTS2.agentCapability) {
|
|
1887
3114
|
return Object.freeze({ kind: "capability" });
|
|
1888
3115
|
}
|
|
1889
|
-
if (
|
|
3116
|
+
if (path9 === SPOTPATCH_ENDPOINTS2.agentWorkspaceHealth) {
|
|
1890
3117
|
return Object.freeze({ kind: "workspace-health" });
|
|
1891
3118
|
}
|
|
1892
|
-
if (
|
|
3119
|
+
if (path9 === SPOTPATCH_ENDPOINTS2.agentJobs) {
|
|
1893
3120
|
return Object.freeze({ kind: "create-job" });
|
|
1894
3121
|
}
|
|
1895
|
-
const prefix = `${
|
|
1896
|
-
if (!
|
|
3122
|
+
const prefix = `${SPOTPATCH_ENDPOINTS2.agentJobs}/`;
|
|
3123
|
+
if (!path9.startsWith(prefix)) {
|
|
1897
3124
|
return void 0;
|
|
1898
3125
|
}
|
|
1899
|
-
const segments =
|
|
3126
|
+
const segments = path9.slice(prefix.length).split("/");
|
|
1900
3127
|
const jobId = segments[0];
|
|
1901
3128
|
const action = segments[1];
|
|
1902
3129
|
if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
|
|
@@ -1910,7 +3137,7 @@ function matchAgentRequestPath(path8) {
|
|
|
1910
3137
|
}
|
|
1911
3138
|
function requireAgentManager(options) {
|
|
1912
3139
|
if (options.agentManager === void 0 || options.options.ai === false) {
|
|
1913
|
-
throw new
|
|
3140
|
+
throw new SpotPatchError11(ERROR_CODES11.AI_DISABLED);
|
|
1914
3141
|
}
|
|
1915
3142
|
return options.agentManager;
|
|
1916
3143
|
}
|
|
@@ -1963,13 +3190,13 @@ function streamAgentJobEvents(response, manager, jobId) {
|
|
|
1963
3190
|
}
|
|
1964
3191
|
async function handleCapability(request, response, options, writeSuccess) {
|
|
1965
3192
|
if (request.method !== "POST") {
|
|
1966
|
-
throw new
|
|
3193
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
1967
3194
|
}
|
|
1968
3195
|
const parsed = agentCapabilityRequestSchema.safeParse(
|
|
1969
3196
|
await readJsonRequestBody(request)
|
|
1970
3197
|
);
|
|
1971
3198
|
if (!parsed.success) {
|
|
1972
|
-
throw new
|
|
3199
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
1973
3200
|
}
|
|
1974
3201
|
const controller = new AbortController();
|
|
1975
3202
|
const abort = () => {
|
|
@@ -1988,13 +3215,13 @@ async function handleCapability(request, response, options, writeSuccess) {
|
|
|
1988
3215
|
}
|
|
1989
3216
|
async function handleCreateJob(request, response, options, writeSuccess) {
|
|
1990
3217
|
if (request.method !== "POST") {
|
|
1991
|
-
throw new
|
|
3218
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
1992
3219
|
}
|
|
1993
3220
|
const parsed = agentJobCreateRequestSchema.safeParse(
|
|
1994
3221
|
await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
|
|
1995
3222
|
);
|
|
1996
3223
|
if (!parsed.success) {
|
|
1997
|
-
throw new
|
|
3224
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
1998
3225
|
}
|
|
1999
3226
|
const authorizedRequest = await authorizeAgentJobRequest({
|
|
2000
3227
|
request: parsed.data,
|
|
@@ -2007,13 +3234,13 @@ async function handleCreateJob(request, response, options, writeSuccess) {
|
|
|
2007
3234
|
}
|
|
2008
3235
|
async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
2009
3236
|
if (request.method !== "POST") {
|
|
2010
|
-
throw new
|
|
3237
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
2011
3238
|
}
|
|
2012
3239
|
const parsed = agentWorkspaceHealthRequestSchema.safeParse(
|
|
2013
3240
|
await readJsonRequestBody(request)
|
|
2014
3241
|
);
|
|
2015
3242
|
if (!parsed.success) {
|
|
2016
|
-
throw new
|
|
3243
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
2017
3244
|
}
|
|
2018
3245
|
const controller = new AbortController();
|
|
2019
3246
|
const abort = () => {
|
|
@@ -2030,13 +3257,13 @@ async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
|
2030
3257
|
async function handleJobAction(request, response, options, route, writeSuccess) {
|
|
2031
3258
|
const manager = requireAgentManager(options);
|
|
2032
3259
|
if (request.method !== "POST") {
|
|
2033
|
-
throw new
|
|
3260
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
2034
3261
|
}
|
|
2035
3262
|
const parsed = agentJobActionRequestSchema.safeParse(
|
|
2036
3263
|
await readJsonRequestBody(request)
|
|
2037
3264
|
);
|
|
2038
3265
|
if (!parsed.success) {
|
|
2039
|
-
throw new
|
|
3266
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
2040
3267
|
}
|
|
2041
3268
|
if (route.action === "events") {
|
|
2042
3269
|
streamAgentJobEvents(response, manager, route.jobId);
|
|
@@ -2150,21 +3377,197 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
2150
3377
|
}
|
|
2151
3378
|
var launchConfiguredEditor = createEditorLauncher();
|
|
2152
3379
|
|
|
3380
|
+
// src/server/data-flow-http.ts
|
|
3381
|
+
import { createHash as createHash3 } from "crypto";
|
|
3382
|
+
import {
|
|
3383
|
+
createStaticDataFlowAnalyzer
|
|
3384
|
+
} from "@spotpatch/analyzer";
|
|
3385
|
+
import {
|
|
3386
|
+
DATA_FLOW_SCHEMA_VERSION,
|
|
3387
|
+
ERROR_CODES as ERROR_CODES12,
|
|
3388
|
+
SpotPatchError as SpotPatchError12,
|
|
3389
|
+
dataFlowComponentReportRequestSchema,
|
|
3390
|
+
dataFlowPageReportRequestSchema,
|
|
3391
|
+
limitDataFlowReportCollections
|
|
3392
|
+
} from "@spotpatch/shared";
|
|
3393
|
+
function envelopeBytes(report) {
|
|
3394
|
+
return Buffer.byteLength(JSON.stringify({ ok: true, data: report }), "utf8");
|
|
3395
|
+
}
|
|
3396
|
+
function limitDataFlowReportToBytes(report, maximumBytes) {
|
|
3397
|
+
const structurallyLimited = limitDataFlowReportCollections(report);
|
|
3398
|
+
if (envelopeBytes(structurallyLimited) <= maximumBytes) {
|
|
3399
|
+
return structurallyLimited;
|
|
3400
|
+
}
|
|
3401
|
+
let limited = limitDataFlowReportCollections(structurallyLimited, {
|
|
3402
|
+
forceTruncation: true,
|
|
3403
|
+
maximumDependencies: 0,
|
|
3404
|
+
truncatedBy: "bytes"
|
|
3405
|
+
});
|
|
3406
|
+
if (envelopeBytes(limited) > maximumBytes) {
|
|
3407
|
+
throw new SpotPatchError12(ERROR_CODES12.INTERNAL_ERROR);
|
|
3408
|
+
}
|
|
3409
|
+
for (let maximumDependencies = 1; maximumDependencies <= structurallyLimited.dependencies.length; maximumDependencies += 1) {
|
|
3410
|
+
const candidate = limitDataFlowReportCollections(structurallyLimited, {
|
|
3411
|
+
forceTruncation: true,
|
|
3412
|
+
maximumDependencies,
|
|
3413
|
+
truncatedBy: "bytes"
|
|
3414
|
+
});
|
|
3415
|
+
if (envelopeBytes(candidate) > maximumBytes) break;
|
|
3416
|
+
limited = candidate;
|
|
3417
|
+
}
|
|
3418
|
+
return limited;
|
|
3419
|
+
}
|
|
3420
|
+
function createDataFlowAnalyzer(options) {
|
|
3421
|
+
if (!options.options.dataFlow.enabled) return void 0;
|
|
3422
|
+
return createStaticDataFlowAnalyzer({
|
|
3423
|
+
root: options.root,
|
|
3424
|
+
registryEpoch: options.session.id,
|
|
3425
|
+
registerSource: (absolutePath) => options.registry.register(absolutePath),
|
|
3426
|
+
limits: options.options.dataFlow.limits
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
3429
|
+
async function analyzeTarget(request, analyzer, options) {
|
|
3430
|
+
const resolvedRequest = (() => {
|
|
3431
|
+
if ("componentSourceId" in request) {
|
|
3432
|
+
const anchor = options.registry.resolveDataFlowComponent(
|
|
3433
|
+
request.componentSourceId
|
|
3434
|
+
);
|
|
3435
|
+
if (anchor?.sourceVersion !== request.sourceVersion) {
|
|
3436
|
+
throw new SpotPatchError12(ERROR_CODES12.DATA_FLOW_SOURCE_STALE);
|
|
3437
|
+
}
|
|
3438
|
+
return anchor;
|
|
3439
|
+
}
|
|
3440
|
+
return request;
|
|
3441
|
+
})();
|
|
3442
|
+
const absolutePath = await resolveSourceFile({
|
|
3443
|
+
fileId: resolvedRequest.fileId,
|
|
3444
|
+
registry: options.registry,
|
|
3445
|
+
root: options.root
|
|
3446
|
+
});
|
|
3447
|
+
const report = analyzer.analyzeComponent({
|
|
3448
|
+
absolutePath,
|
|
3449
|
+
line: resolvedRequest.line,
|
|
3450
|
+
column: resolvedRequest.column
|
|
3451
|
+
});
|
|
3452
|
+
if (resolvedRequest.sourceVersion !== void 0 && resolvedRequest.sourceVersion !== report.component.source.sourceVersion) {
|
|
3453
|
+
throw new SpotPatchError12(ERROR_CODES12.DATA_FLOW_SOURCE_STALE);
|
|
3454
|
+
}
|
|
3455
|
+
return limitDataFlowReportToBytes(
|
|
3456
|
+
report,
|
|
3457
|
+
options.options.dataFlow.limits.reportMaxBytes
|
|
3458
|
+
);
|
|
3459
|
+
}
|
|
3460
|
+
function requireAnalyzer(analyzer) {
|
|
3461
|
+
if (analyzer === void 0) {
|
|
3462
|
+
throw new SpotPatchError12(ERROR_CODES12.DATA_FLOW_DISABLED);
|
|
3463
|
+
}
|
|
3464
|
+
return analyzer;
|
|
3465
|
+
}
|
|
3466
|
+
async function handleComponentDataFlowReport(request, analyzer, options) {
|
|
3467
|
+
const parsed = dataFlowComponentReportRequestSchema.safeParse(
|
|
3468
|
+
await readJsonRequestBody(
|
|
3469
|
+
request,
|
|
3470
|
+
options.options.dataFlow.limits.protocolRequestMaxBytes
|
|
3471
|
+
)
|
|
3472
|
+
);
|
|
3473
|
+
if (!parsed.success) {
|
|
3474
|
+
throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
|
|
3475
|
+
}
|
|
3476
|
+
return analyzeTarget(parsed.data, requireAnalyzer(analyzer), options);
|
|
3477
|
+
}
|
|
3478
|
+
async function handlePageDataFlowReport(request, analyzer, options) {
|
|
3479
|
+
const parsed = dataFlowPageReportRequestSchema.safeParse(
|
|
3480
|
+
await readJsonRequestBody(
|
|
3481
|
+
request,
|
|
3482
|
+
options.options.dataFlow.limits.protocolRequestMaxBytes
|
|
3483
|
+
)
|
|
3484
|
+
);
|
|
3485
|
+
if (!parsed.success) {
|
|
3486
|
+
throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
|
|
3487
|
+
}
|
|
3488
|
+
const activeAnalyzer = requireAnalyzer(analyzer);
|
|
3489
|
+
const componentReports = await Promise.all(
|
|
3490
|
+
parsed.data.targets.map((target) => analyzeTarget(target, activeAnalyzer, options))
|
|
3491
|
+
);
|
|
3492
|
+
const dependencies = new Map(
|
|
3493
|
+
componentReports.flatMap(
|
|
3494
|
+
(report2) => report2.dependencies.map((dependency) => [dependency.id, dependency])
|
|
3495
|
+
)
|
|
3496
|
+
);
|
|
3497
|
+
const evidence = new Map(
|
|
3498
|
+
componentReports.flatMap(
|
|
3499
|
+
(report2) => report2.evidence.map((entry) => [entry.id, entry])
|
|
3500
|
+
)
|
|
3501
|
+
);
|
|
3502
|
+
const diagnostics = new Map(
|
|
3503
|
+
componentReports.flatMap(
|
|
3504
|
+
(report2) => report2.diagnostics.map((entry) => [entry.id, entry])
|
|
3505
|
+
)
|
|
3506
|
+
);
|
|
3507
|
+
const analyzedVersions = new Set(
|
|
3508
|
+
componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
|
|
3509
|
+
);
|
|
3510
|
+
const reportId = `page_${createHash3("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
|
|
3511
|
+
const complete = componentReports.every((report2) => report2.completeness.complete);
|
|
3512
|
+
const report = Object.freeze({
|
|
3513
|
+
schemaVersion: DATA_FLOW_SCHEMA_VERSION,
|
|
3514
|
+
reportId,
|
|
3515
|
+
baseline: Object.freeze({
|
|
3516
|
+
registryEpoch: options.session.id,
|
|
3517
|
+
analyzerVersion: componentReports[0]?.baseline.analyzerVersion ?? "unavailable",
|
|
3518
|
+
adapterSetHash: componentReports[0]?.baseline.adapterSetHash ?? "unavailable",
|
|
3519
|
+
analyzedSourceVersions: Object.freeze([...analyzedVersions].sort())
|
|
3520
|
+
}),
|
|
3521
|
+
capability: Object.freeze({
|
|
3522
|
+
enabled: true,
|
|
3523
|
+
staticAnalysis: complete ? "available" : "partial",
|
|
3524
|
+
runtimeObservation: "dispatch-only",
|
|
3525
|
+
responseShape: "consumed-fields-only",
|
|
3526
|
+
aiAssistance: "disabled",
|
|
3527
|
+
reasons: Object.freeze(
|
|
3528
|
+
componentReports.flatMap((report2) => report2.capability.reasons)
|
|
3529
|
+
)
|
|
3530
|
+
}),
|
|
3531
|
+
dependencies: Object.freeze([...dependencies.values()]),
|
|
3532
|
+
evidence: Object.freeze([...evidence.values()]),
|
|
3533
|
+
diagnostics: Object.freeze([...diagnostics.values()]),
|
|
3534
|
+
completeness: Object.freeze({
|
|
3535
|
+
complete,
|
|
3536
|
+
visitedModules: componentReports.reduce(
|
|
3537
|
+
(total, report2) => total + report2.completeness.visitedModules,
|
|
3538
|
+
0
|
|
3539
|
+
),
|
|
3540
|
+
visitedCallsites: componentReports.reduce(
|
|
3541
|
+
(total, report2) => total + report2.completeness.visitedCallsites,
|
|
3542
|
+
0
|
|
3543
|
+
),
|
|
3544
|
+
frontierCount: componentReports.reduce(
|
|
3545
|
+
(total, report2) => total + report2.completeness.frontierCount,
|
|
3546
|
+
0
|
|
3547
|
+
)
|
|
3548
|
+
})
|
|
3549
|
+
});
|
|
3550
|
+
return limitDataFlowReportToBytes(
|
|
3551
|
+
report,
|
|
3552
|
+
options.options.dataFlow.limits.reportMaxBytes
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
|
|
2153
3556
|
// src/server/request-security.ts
|
|
2154
|
-
import { timingSafeEqual } from "crypto";
|
|
3557
|
+
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
2155
3558
|
import { isIP } from "net";
|
|
2156
|
-
import { ERROR_CODES as
|
|
3559
|
+
import { ERROR_CODES as ERROR_CODES13, SPOTPATCH_TOKEN_HEADER, SpotPatchError as SpotPatchError13 } from "@spotpatch/shared";
|
|
2157
3560
|
function getSingleHeader(request, name) {
|
|
2158
3561
|
const value = request.headers[name.toLowerCase()];
|
|
2159
3562
|
return Array.isArray(value) ? value[0] : value;
|
|
2160
3563
|
}
|
|
2161
|
-
function
|
|
3564
|
+
function tokensMatch2(actual, expected) {
|
|
2162
3565
|
if (actual === void 0) {
|
|
2163
3566
|
return false;
|
|
2164
3567
|
}
|
|
2165
3568
|
const actualBytes = Buffer.from(actual);
|
|
2166
3569
|
const expectedBytes = Buffer.from(expected);
|
|
2167
|
-
return actualBytes.byteLength === expectedBytes.byteLength &&
|
|
3570
|
+
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual2(actualBytes, expectedBytes);
|
|
2168
3571
|
}
|
|
2169
3572
|
function isLoopbackHostname(hostname) {
|
|
2170
3573
|
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
@@ -2199,33 +3602,33 @@ function parseOrigin(value) {
|
|
|
2199
3602
|
}
|
|
2200
3603
|
function assertRequestAuthorized(request, options) {
|
|
2201
3604
|
const actualToken = getSingleHeader(request, SPOTPATCH_TOKEN_HEADER);
|
|
2202
|
-
if (!
|
|
2203
|
-
throw new
|
|
3605
|
+
if (!tokensMatch2(actualToken, options.sessionToken)) {
|
|
3606
|
+
throw new SpotPatchError13(ERROR_CODES13.INVALID_TOKEN);
|
|
2204
3607
|
}
|
|
2205
3608
|
const hostHeader = getSingleHeader(request, "host");
|
|
2206
3609
|
const originHeader = getSingleHeader(request, "origin");
|
|
2207
3610
|
const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
|
|
2208
3611
|
const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
|
|
2209
3612
|
if (host === void 0 || origin === void 0) {
|
|
2210
|
-
throw new
|
|
3613
|
+
throw new SpotPatchError13(ERROR_CODES13.ORIGIN_NOT_ALLOWED);
|
|
2211
3614
|
}
|
|
2212
3615
|
const hostIsLoopback = isLoopbackHostname(host.hostname);
|
|
2213
3616
|
const originIsLoopback = isLoopbackHostname(origin.hostname);
|
|
2214
3617
|
if (!options.allowLan) {
|
|
2215
3618
|
if (!hostIsLoopback || !originIsLoopback) {
|
|
2216
|
-
throw new
|
|
3619
|
+
throw new SpotPatchError13(ERROR_CODES13.ORIGIN_NOT_ALLOWED);
|
|
2217
3620
|
}
|
|
2218
3621
|
return;
|
|
2219
3622
|
}
|
|
2220
3623
|
if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
|
|
2221
|
-
throw new
|
|
3624
|
+
throw new SpotPatchError13(ERROR_CODES13.ORIGIN_NOT_ALLOWED);
|
|
2222
3625
|
}
|
|
2223
3626
|
}
|
|
2224
3627
|
|
|
2225
3628
|
// src/server/runtime-bootstrap.ts
|
|
2226
3629
|
import {
|
|
2227
|
-
ERROR_CODES as
|
|
2228
|
-
SpotPatchError as
|
|
3630
|
+
ERROR_CODES as ERROR_CODES14,
|
|
3631
|
+
SpotPatchError as SpotPatchError14,
|
|
2229
3632
|
runtimeBootstrapRequestSchema,
|
|
2230
3633
|
runtimeConfigSchema
|
|
2231
3634
|
} from "@spotpatch/shared";
|
|
@@ -2255,7 +3658,7 @@ function resolveRuntimeBootstrapOptions(options) {
|
|
|
2255
3658
|
function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
2256
3659
|
const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
2257
3660
|
if (request.method !== "POST" || contentType !== "application/json") {
|
|
2258
|
-
throw new
|
|
3661
|
+
throw new SpotPatchError14(ERROR_CODES14.INVALID_REQUEST);
|
|
2259
3662
|
}
|
|
2260
3663
|
const host = getSingleHeader2(request, "host");
|
|
2261
3664
|
let hostIsLoopback = false;
|
|
@@ -2267,7 +3670,7 @@ function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
|
2267
3670
|
}
|
|
2268
3671
|
}
|
|
2269
3672
|
if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
|
|
2270
|
-
throw new
|
|
3673
|
+
throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
|
|
2271
3674
|
}
|
|
2272
3675
|
}
|
|
2273
3676
|
async function readRuntimeBootstrap(request, options) {
|
|
@@ -2276,97 +3679,139 @@ async function readRuntimeBootstrap(request, options) {
|
|
|
2276
3679
|
await readJsonRequestBody(request)
|
|
2277
3680
|
);
|
|
2278
3681
|
if (!parsedBody.success) {
|
|
2279
|
-
throw new
|
|
3682
|
+
throw new SpotPatchError14(ERROR_CODES14.INVALID_REQUEST);
|
|
2280
3683
|
}
|
|
2281
3684
|
return options.runtimeConfig;
|
|
2282
3685
|
}
|
|
2283
3686
|
|
|
2284
3687
|
// src/server/middleware.ts
|
|
2285
3688
|
var STATUS_BY_ERROR = Object.freeze({
|
|
2286
|
-
[
|
|
2287
|
-
[
|
|
2288
|
-
[
|
|
2289
|
-
[
|
|
2290
|
-
[
|
|
2291
|
-
[
|
|
2292
|
-
[
|
|
2293
|
-
[
|
|
2294
|
-
[
|
|
2295
|
-
[
|
|
2296
|
-
[
|
|
2297
|
-
[
|
|
2298
|
-
[
|
|
2299
|
-
[
|
|
2300
|
-
[
|
|
2301
|
-
[
|
|
2302
|
-
[
|
|
2303
|
-
[
|
|
2304
|
-
[
|
|
2305
|
-
[
|
|
2306
|
-
[
|
|
2307
|
-
[
|
|
2308
|
-
[
|
|
2309
|
-
[
|
|
2310
|
-
[
|
|
2311
|
-
[
|
|
2312
|
-
[
|
|
2313
|
-
[
|
|
2314
|
-
[
|
|
2315
|
-
[
|
|
2316
|
-
[
|
|
2317
|
-
[
|
|
2318
|
-
[
|
|
3689
|
+
[ERROR_CODES15.INVALID_REQUEST]: 400,
|
|
3690
|
+
[ERROR_CODES15.INVALID_TOKEN]: 401,
|
|
3691
|
+
[ERROR_CODES15.ORIGIN_NOT_ALLOWED]: 403,
|
|
3692
|
+
[ERROR_CODES15.SOURCE_NOT_FOUND]: 404,
|
|
3693
|
+
[ERROR_CODES15.SOURCE_OUTSIDE_ROOT]: 403,
|
|
3694
|
+
[ERROR_CODES15.SOURCE_TOO_LARGE]: 413,
|
|
3695
|
+
[ERROR_CODES15.EDITOR_OPEN_FAILED]: 500,
|
|
3696
|
+
[ERROR_CODES15.DATA_FLOW_DISABLED]: 404,
|
|
3697
|
+
[ERROR_CODES15.DATA_FLOW_SOURCE_STALE]: 409,
|
|
3698
|
+
[ERROR_CODES15.DATA_FLOW_ANALYSIS_CANCELLED]: 409,
|
|
3699
|
+
[ERROR_CODES15.AI_DISABLED]: 404,
|
|
3700
|
+
[ERROR_CODES15.PROVIDER_NOT_CONFIGURED]: 503,
|
|
3701
|
+
[ERROR_CODES15.PROVIDER_AUTH_FAILED]: 502,
|
|
3702
|
+
[ERROR_CODES15.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
|
|
3703
|
+
[ERROR_CODES15.MODEL_NOT_ALLOWED]: 400,
|
|
3704
|
+
[ERROR_CODES15.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
|
|
3705
|
+
[ERROR_CODES15.PROVIDER_RATE_LIMITED]: 429,
|
|
3706
|
+
[ERROR_CODES15.AGENT_BUSY]: 409,
|
|
3707
|
+
[ERROR_CODES15.AGENT_LIMIT_EXCEEDED]: 413,
|
|
3708
|
+
[ERROR_CODES15.AGENT_CANCELLED]: 409,
|
|
3709
|
+
[ERROR_CODES15.EXTERNAL_HANDOFF_DISABLED]: 404,
|
|
3710
|
+
[ERROR_CODES15.EXTERNAL_HANDOFF_UNAVAILABLE]: 503,
|
|
3711
|
+
[ERROR_CODES15.HANDOFF_VALIDATION_FAILED]: 422,
|
|
3712
|
+
[ERROR_CODES15.HANDOFF_SOURCE_STALE]: 409,
|
|
3713
|
+
[ERROR_CODES15.HANDOFF_NOT_FOUND]: 404,
|
|
3714
|
+
[ERROR_CODES15.HANDOFF_EXPIRED]: 410,
|
|
3715
|
+
[ERROR_CODES15.HANDOFF_CURSOR_INVALID]: 409,
|
|
3716
|
+
[ERROR_CODES15.HANDOFF_RESPONSE_TOO_LARGE]: 413,
|
|
3717
|
+
[ERROR_CODES15.BRIDGE_UNAUTHORIZED]: 401,
|
|
3718
|
+
[ERROR_CODES15.BRIDGE_PROTOCOL_MISMATCH]: 409,
|
|
3719
|
+
[ERROR_CODES15.BRIDGE_BUSY]: 429,
|
|
3720
|
+
[ERROR_CODES15.EXTERNAL_AGENT_BUSY]: 409,
|
|
3721
|
+
[ERROR_CODES15.ACTIVE_ADAPTER_CONFLICT]: 409,
|
|
3722
|
+
[ERROR_CODES15.ACTIVE_ADAPTER_LEASE_INVALID]: 409,
|
|
3723
|
+
[ERROR_CODES15.ACTIVE_DISPATCH_INVALID]: 409,
|
|
3724
|
+
[ERROR_CODES15.SESSION_NOT_FOUND]: 404,
|
|
3725
|
+
[ERROR_CODES15.SESSION_AMBIGUOUS]: 409,
|
|
3726
|
+
[ERROR_CODES15.SESSION_CLOSED]: 410,
|
|
3727
|
+
[ERROR_CODES15.WORKTREE_DIRTY]: 409,
|
|
3728
|
+
[ERROR_CODES15.WORKTREE_NOT_REPOSITORY]: 409,
|
|
3729
|
+
[ERROR_CODES15.WORKTREE_OPERATION_IN_PROGRESS]: 409,
|
|
3730
|
+
[ERROR_CODES15.WORKTREE_CONFLICTED]: 409,
|
|
3731
|
+
[ERROR_CODES15.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
|
|
3732
|
+
[ERROR_CODES15.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
|
|
3733
|
+
[ERROR_CODES15.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
|
|
3734
|
+
[ERROR_CODES15.TOOL_DENIED]: 403,
|
|
3735
|
+
[ERROR_CODES15.TOOL_INPUT_INVALID]: 422,
|
|
3736
|
+
[ERROR_CODES15.TOOL_ARGUMENTS_INVALID]: 422,
|
|
3737
|
+
[ERROR_CODES15.TOOL_CALL_ID_CONFLICT]: 422,
|
|
3738
|
+
[ERROR_CODES15.TOOL_PATH_DENIED]: 403,
|
|
3739
|
+
[ERROR_CODES15.PATCH_REJECTED]: 422,
|
|
3740
|
+
[ERROR_CODES15.VALIDATION_FAILED]: 422,
|
|
3741
|
+
[ERROR_CODES15.APPLY_CONFLICT]: 409,
|
|
3742
|
+
[ERROR_CODES15.INTERNAL_ERROR]: 500
|
|
2319
3743
|
});
|
|
2320
3744
|
var PUBLIC_MESSAGES = Object.freeze({
|
|
2321
|
-
[
|
|
2322
|
-
[
|
|
2323
|
-
[
|
|
2324
|
-
[
|
|
2325
|
-
[
|
|
2326
|
-
[
|
|
2327
|
-
[
|
|
2328
|
-
[
|
|
2329
|
-
[
|
|
2330
|
-
[
|
|
2331
|
-
[
|
|
2332
|
-
[
|
|
2333
|
-
[
|
|
2334
|
-
[
|
|
2335
|
-
[
|
|
2336
|
-
[
|
|
2337
|
-
[
|
|
2338
|
-
[
|
|
2339
|
-
[
|
|
2340
|
-
[
|
|
2341
|
-
[
|
|
2342
|
-
[
|
|
2343
|
-
[
|
|
2344
|
-
[
|
|
2345
|
-
[
|
|
2346
|
-
[
|
|
2347
|
-
[
|
|
2348
|
-
[
|
|
2349
|
-
[
|
|
2350
|
-
[
|
|
2351
|
-
[
|
|
2352
|
-
[
|
|
2353
|
-
[
|
|
3745
|
+
[ERROR_CODES15.INVALID_REQUEST]: "The request is invalid.",
|
|
3746
|
+
[ERROR_CODES15.INVALID_TOKEN]: "The session token is invalid.",
|
|
3747
|
+
[ERROR_CODES15.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
|
|
3748
|
+
[ERROR_CODES15.SOURCE_NOT_FOUND]: "The source file is unavailable.",
|
|
3749
|
+
[ERROR_CODES15.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
|
|
3750
|
+
[ERROR_CODES15.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
|
|
3751
|
+
[ERROR_CODES15.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
|
|
3752
|
+
[ERROR_CODES15.DATA_FLOW_DISABLED]: "Component data-flow analysis is not enabled.",
|
|
3753
|
+
[ERROR_CODES15.DATA_FLOW_SOURCE_STALE]: "The selected source version is stale.",
|
|
3754
|
+
[ERROR_CODES15.DATA_FLOW_ANALYSIS_CANCELLED]: "The data-flow analysis was cancelled.",
|
|
3755
|
+
[ERROR_CODES15.AI_DISABLED]: "AI execution is not enabled.",
|
|
3756
|
+
[ERROR_CODES15.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
|
|
3757
|
+
[ERROR_CODES15.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
|
|
3758
|
+
[ERROR_CODES15.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
|
|
3759
|
+
[ERROR_CODES15.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
|
|
3760
|
+
[ERROR_CODES15.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
|
|
3761
|
+
[ERROR_CODES15.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
|
|
3762
|
+
[ERROR_CODES15.AGENT_BUSY]: "Another Agent job is already running.",
|
|
3763
|
+
[ERROR_CODES15.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
|
|
3764
|
+
[ERROR_CODES15.AGENT_CANCELLED]: "The Agent job was cancelled.",
|
|
3765
|
+
[ERROR_CODES15.EXTERNAL_HANDOFF_DISABLED]: "External Agent handoff is not enabled.",
|
|
3766
|
+
[ERROR_CODES15.EXTERNAL_HANDOFF_UNAVAILABLE]: "External Agent handoff is temporarily unavailable.",
|
|
3767
|
+
[ERROR_CODES15.HANDOFF_VALIDATION_FAILED]: "The handoff content is invalid.",
|
|
3768
|
+
[ERROR_CODES15.HANDOFF_SOURCE_STALE]: "The selected source is stale.",
|
|
3769
|
+
[ERROR_CODES15.HANDOFF_NOT_FOUND]: "No current handoff is available.",
|
|
3770
|
+
[ERROR_CODES15.HANDOFF_EXPIRED]: "The handoff has expired.",
|
|
3771
|
+
[ERROR_CODES15.HANDOFF_CURSOR_INVALID]: "The handoff cursor is invalid.",
|
|
3772
|
+
[ERROR_CODES15.HANDOFF_RESPONSE_TOO_LARGE]: "The handoff exceeds the size limit.",
|
|
3773
|
+
[ERROR_CODES15.BRIDGE_UNAUTHORIZED]: "The local bridge request is unauthorized.",
|
|
3774
|
+
[ERROR_CODES15.BRIDGE_PROTOCOL_MISMATCH]: "The local bridge protocol is incompatible.",
|
|
3775
|
+
[ERROR_CODES15.BRIDGE_BUSY]: "The local bridge is busy.",
|
|
3776
|
+
[ERROR_CODES15.EXTERNAL_AGENT_BUSY]: "The connected external Agent is busy.",
|
|
3777
|
+
[ERROR_CODES15.ACTIVE_ADAPTER_CONFLICT]: "Another active Agent adapter is connected.",
|
|
3778
|
+
[ERROR_CODES15.ACTIVE_ADAPTER_LEASE_INVALID]: "The active Agent adapter lease is invalid.",
|
|
3779
|
+
[ERROR_CODES15.ACTIVE_DISPATCH_INVALID]: "The active Agent dispatch transition is invalid.",
|
|
3780
|
+
[ERROR_CODES15.SESSION_NOT_FOUND]: "No active SpotPatch session was found.",
|
|
3781
|
+
[ERROR_CODES15.SESSION_AMBIGUOUS]: "More than one SpotPatch session matches.",
|
|
3782
|
+
[ERROR_CODES15.SESSION_CLOSED]: "The SpotPatch session has closed.",
|
|
3783
|
+
[ERROR_CODES15.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
|
|
3784
|
+
[ERROR_CODES15.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
|
|
3785
|
+
[ERROR_CODES15.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
|
|
3786
|
+
[ERROR_CODES15.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
|
|
3787
|
+
[ERROR_CODES15.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
|
|
3788
|
+
[ERROR_CODES15.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
|
|
3789
|
+
[ERROR_CODES15.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
|
|
3790
|
+
[ERROR_CODES15.TOOL_DENIED]: "The Agent tool request was denied.",
|
|
3791
|
+
[ERROR_CODES15.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
|
|
3792
|
+
[ERROR_CODES15.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
|
|
3793
|
+
[ERROR_CODES15.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
|
|
3794
|
+
[ERROR_CODES15.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
|
|
3795
|
+
[ERROR_CODES15.PATCH_REJECTED]: "The proposed patch was rejected.",
|
|
3796
|
+
[ERROR_CODES15.VALIDATION_FAILED]: "The proposed change failed validation.",
|
|
3797
|
+
[ERROR_CODES15.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
|
|
3798
|
+
[ERROR_CODES15.INTERNAL_ERROR]: "The request could not be completed."
|
|
2354
3799
|
});
|
|
2355
|
-
function
|
|
3800
|
+
function writeJson2(response, status, payload) {
|
|
2356
3801
|
response.statusCode = status;
|
|
2357
3802
|
response.setHeader("Cache-Control", "no-store");
|
|
2358
3803
|
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2359
3804
|
response.end(JSON.stringify(payload));
|
|
2360
3805
|
}
|
|
2361
3806
|
function asSpotPatchError(error) {
|
|
2362
|
-
return error instanceof
|
|
3807
|
+
return error instanceof SpotPatchError15 ? error : new SpotPatchError15(ERROR_CODES15.INTERNAL_ERROR, void 0, { cause: error });
|
|
2363
3808
|
}
|
|
2364
3809
|
function writeError(response, error, logger) {
|
|
2365
3810
|
const normalized = asSpotPatchError(error);
|
|
2366
|
-
if (normalized.code ===
|
|
3811
|
+
if (normalized.code === ERROR_CODES15.INTERNAL_ERROR) {
|
|
2367
3812
|
logger?.warn("[spotpatch:server] Internal request failure.");
|
|
2368
3813
|
}
|
|
2369
|
-
|
|
3814
|
+
writeJson2(response, STATUS_BY_ERROR[normalized.code], {
|
|
2370
3815
|
ok: false,
|
|
2371
3816
|
error: {
|
|
2372
3817
|
code: normalized.code,
|
|
@@ -2386,7 +3831,7 @@ async function handleSourceContext(request, options) {
|
|
|
2386
3831
|
await readJsonRequestBody(request)
|
|
2387
3832
|
);
|
|
2388
3833
|
if (!parsed.success) {
|
|
2389
|
-
throw new
|
|
3834
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2390
3835
|
}
|
|
2391
3836
|
return readSourceContext({
|
|
2392
3837
|
request: parsed.data,
|
|
@@ -2399,7 +3844,7 @@ async function handleSourceContext(request, options) {
|
|
|
2399
3844
|
async function handleOpenEditor(request, options) {
|
|
2400
3845
|
const parsed = openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
|
|
2401
3846
|
if (!parsed.success) {
|
|
2402
|
-
throw new
|
|
3847
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2403
3848
|
}
|
|
2404
3849
|
const body = parsed.data;
|
|
2405
3850
|
const sourcePath = await resolveSourceFile({
|
|
@@ -2416,51 +3861,90 @@ async function handleOpenEditor(request, options) {
|
|
|
2416
3861
|
options.logger?.warn(
|
|
2417
3862
|
`[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
|
|
2418
3863
|
);
|
|
2419
|
-
throw new
|
|
3864
|
+
throw new SpotPatchError15(ERROR_CODES15.EDITOR_OPEN_FAILED, void 0, {
|
|
2420
3865
|
cause: error
|
|
2421
3866
|
});
|
|
2422
3867
|
}
|
|
2423
3868
|
}
|
|
2424
3869
|
function createSpotPatchMiddleware(options) {
|
|
2425
3870
|
const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
|
|
3871
|
+
const dataFlowAnalyzer = createDataFlowAnalyzer(options);
|
|
2426
3872
|
return (request, response, next) => {
|
|
2427
|
-
const
|
|
2428
|
-
const agentRoute = matchAgentRequestPath(
|
|
2429
|
-
|
|
3873
|
+
const path9 = requestPath(request);
|
|
3874
|
+
const agentRoute = matchAgentRequestPath(path9);
|
|
3875
|
+
const externalHandoffRoute = matchExternalHandoffBrowserPath(path9);
|
|
3876
|
+
if (path9 !== SPOTPATCH_ENDPOINTS3.sourceContext && path9 !== SPOTPATCH_ENDPOINTS3.openEditor && path9 !== SPOTPATCH_ENDPOINTS3.dataFlowComponentReport && path9 !== SPOTPATCH_ENDPOINTS3.dataFlowPageReport && agentRoute === void 0 && externalHandoffRoute === void 0 && !path9.startsWith(`${SPOTPATCH_API_BASE}/`)) {
|
|
2430
3877
|
next();
|
|
2431
3878
|
return;
|
|
2432
3879
|
}
|
|
2433
3880
|
const handle = async () => {
|
|
2434
|
-
if (
|
|
3881
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.bootstrap && bootstrap !== void 0) {
|
|
2435
3882
|
const data = await readRuntimeBootstrap(
|
|
2436
3883
|
request,
|
|
2437
3884
|
bootstrap
|
|
2438
3885
|
);
|
|
2439
|
-
|
|
3886
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2440
3887
|
return;
|
|
2441
3888
|
}
|
|
2442
3889
|
assertRequestAuthorized(request, {
|
|
2443
3890
|
allowLan: options.options.allowLan,
|
|
2444
3891
|
sessionToken: options.session.token
|
|
2445
3892
|
});
|
|
2446
|
-
if (
|
|
3893
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.sourceContext) {
|
|
2447
3894
|
if (request.method !== "POST") {
|
|
2448
|
-
throw new
|
|
3895
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2449
3896
|
}
|
|
2450
3897
|
const data = await handleSourceContext(request, options);
|
|
2451
|
-
|
|
3898
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2452
3899
|
return;
|
|
2453
3900
|
}
|
|
2454
|
-
if (
|
|
3901
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.openEditor) {
|
|
2455
3902
|
if (request.method !== "POST") {
|
|
2456
|
-
throw new
|
|
3903
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2457
3904
|
}
|
|
2458
3905
|
const data = await handleOpenEditor(request, options);
|
|
2459
|
-
|
|
3906
|
+
writeJson2(response, 200, { ok: true, data });
|
|
3907
|
+
return;
|
|
3908
|
+
}
|
|
3909
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.dataFlowComponentReport) {
|
|
3910
|
+
if (request.method !== "POST") {
|
|
3911
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
3912
|
+
}
|
|
3913
|
+
const data = await handleComponentDataFlowReport(
|
|
3914
|
+
request,
|
|
3915
|
+
dataFlowAnalyzer,
|
|
3916
|
+
options
|
|
3917
|
+
);
|
|
3918
|
+
writeJson2(response, 200, { ok: true, data });
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.dataFlowPageReport) {
|
|
3922
|
+
if (request.method !== "POST") {
|
|
3923
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
3924
|
+
}
|
|
3925
|
+
const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
|
|
3926
|
+
writeJson2(response, 200, { ok: true, data });
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
if (externalHandoffRoute !== void 0) {
|
|
3930
|
+
await handleExternalHandoffBrowserRequest(
|
|
3931
|
+
request,
|
|
3932
|
+
response,
|
|
3933
|
+
externalHandoffRoute,
|
|
3934
|
+
{
|
|
3935
|
+
options: options.options,
|
|
3936
|
+
registry: options.registry,
|
|
3937
|
+
root: options.root,
|
|
3938
|
+
...options.externalHandoffService === void 0 ? {} : { service: options.externalHandoffService }
|
|
3939
|
+
},
|
|
3940
|
+
(target, status, data) => {
|
|
3941
|
+
writeJson2(target, status, { ok: true, data });
|
|
3942
|
+
}
|
|
3943
|
+
);
|
|
2460
3944
|
return;
|
|
2461
3945
|
}
|
|
2462
3946
|
if (agentRoute === void 0) {
|
|
2463
|
-
throw new
|
|
3947
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2464
3948
|
}
|
|
2465
3949
|
await handleAgentRequest(
|
|
2466
3950
|
request,
|
|
@@ -2468,7 +3952,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
2468
3952
|
options,
|
|
2469
3953
|
agentRoute,
|
|
2470
3954
|
(target, status, data) => {
|
|
2471
|
-
|
|
3955
|
+
writeJson2(target, status, { ok: true, data });
|
|
2472
3956
|
}
|
|
2473
3957
|
);
|
|
2474
3958
|
};
|
|
@@ -2479,9 +3963,9 @@ function createSpotPatchMiddleware(options) {
|
|
|
2479
3963
|
}
|
|
2480
3964
|
|
|
2481
3965
|
// src/server/source-registration.ts
|
|
2482
|
-
import { timingSafeEqual as
|
|
2483
|
-
import { lstat as
|
|
2484
|
-
import
|
|
3966
|
+
import { timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
3967
|
+
import { lstat as lstat4, realpath as realpath6 } from "fs/promises";
|
|
3968
|
+
import path8 from "path";
|
|
2485
3969
|
import { createSourceFilter } from "@spotpatch/compiler";
|
|
2486
3970
|
import { z as z2 } from "zod";
|
|
2487
3971
|
var REGISTRATION_BODY_LIMIT_BYTES = 4096;
|
|
@@ -2502,16 +3986,16 @@ function identitiesMatch(actual, expected) {
|
|
|
2502
3986
|
}
|
|
2503
3987
|
const actualBytes = Buffer.from(actual);
|
|
2504
3988
|
const expectedBytes = Buffer.from(expected);
|
|
2505
|
-
return actualBytes.byteLength === expectedBytes.byteLength &&
|
|
3989
|
+
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual3(actualBytes, expectedBytes);
|
|
2506
3990
|
}
|
|
2507
3991
|
function isWithinRoot(root, candidate) {
|
|
2508
|
-
const relative =
|
|
2509
|
-
return relative === "" || !relative.startsWith(`..${
|
|
3992
|
+
const relative = path8.relative(root, candidate);
|
|
3993
|
+
return relative === "" || !relative.startsWith(`..${path8.sep}`) && relative !== ".." && !path8.isAbsolute(relative);
|
|
2510
3994
|
}
|
|
2511
3995
|
function hasForbiddenSegment(root, candidate) {
|
|
2512
|
-
return
|
|
3996
|
+
return path8.relative(root, candidate).split(path8.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
|
|
2513
3997
|
}
|
|
2514
|
-
function
|
|
3998
|
+
function writeJson3(response, statusCode, payload) {
|
|
2515
3999
|
const body = JSON.stringify(payload);
|
|
2516
4000
|
response.statusCode = statusCode;
|
|
2517
4001
|
response.setHeader("Cache-Control", "no-store");
|
|
@@ -2531,11 +4015,11 @@ function requestComesFromLoopbackWorker(request) {
|
|
|
2531
4015
|
}
|
|
2532
4016
|
}
|
|
2533
4017
|
async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
|
|
2534
|
-
if (!
|
|
4018
|
+
if (!path8.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
2535
4019
|
return void 0;
|
|
2536
4020
|
}
|
|
2537
4021
|
try {
|
|
2538
|
-
const sourceStat = await
|
|
4022
|
+
const sourceStat = await lstat4(requestedPath);
|
|
2539
4023
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
2540
4024
|
return void 0;
|
|
2541
4025
|
}
|
|
@@ -2561,14 +4045,14 @@ async function createSourceRegistrationService(input) {
|
|
|
2561
4045
|
getSingleHeader3(request, INTERNAL_SECRET_HEADER),
|
|
2562
4046
|
input.internalSecret
|
|
2563
4047
|
)) {
|
|
2564
|
-
|
|
4048
|
+
writeJson3(response, 403, { ok: false });
|
|
2565
4049
|
return;
|
|
2566
4050
|
}
|
|
2567
4051
|
const parsed = registrationRequestSchema.safeParse(
|
|
2568
4052
|
await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
|
|
2569
4053
|
);
|
|
2570
4054
|
if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
|
|
2571
|
-
|
|
4055
|
+
writeJson3(response, 400, { ok: false });
|
|
2572
4056
|
return;
|
|
2573
4057
|
}
|
|
2574
4058
|
const sourcePath = await resolveAuthorizedSource(
|
|
@@ -2577,17 +4061,17 @@ async function createSourceRegistrationService(input) {
|
|
|
2577
4061
|
(absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
|
|
2578
4062
|
);
|
|
2579
4063
|
if (sourcePath === void 0) {
|
|
2580
|
-
|
|
4064
|
+
writeJson3(response, 403, { ok: false });
|
|
2581
4065
|
return;
|
|
2582
4066
|
}
|
|
2583
|
-
|
|
4067
|
+
writeJson3(response, 200, {
|
|
2584
4068
|
epoch: input.registryEpoch,
|
|
2585
4069
|
fileId: input.registry.register(sourcePath)
|
|
2586
4070
|
});
|
|
2587
4071
|
};
|
|
2588
4072
|
void handle().catch(() => {
|
|
2589
4073
|
if (!response.headersSent) {
|
|
2590
|
-
|
|
4074
|
+
writeJson3(response, 400, { ok: false });
|
|
2591
4075
|
} else {
|
|
2592
4076
|
response.destroy();
|
|
2593
4077
|
}
|
|
@@ -2597,11 +4081,11 @@ async function createSourceRegistrationService(input) {
|
|
|
2597
4081
|
}
|
|
2598
4082
|
|
|
2599
4083
|
// src/session/session.ts
|
|
2600
|
-
import { randomBytes as
|
|
4084
|
+
import { randomBytes as randomBytes8 } from "crypto";
|
|
2601
4085
|
function createSession() {
|
|
2602
4086
|
return Object.freeze({
|
|
2603
|
-
id:
|
|
2604
|
-
token:
|
|
4087
|
+
id: randomBytes8(16).toString("base64url"),
|
|
4088
|
+
token: randomBytes8(16).toString("base64url")
|
|
2605
4089
|
});
|
|
2606
4090
|
}
|
|
2607
4091
|
|
|
@@ -2611,8 +4095,10 @@ var OPTION_KEYS = Object.freeze([
|
|
|
2611
4095
|
"allowLan",
|
|
2612
4096
|
"budget",
|
|
2613
4097
|
"debug",
|
|
4098
|
+
"dataFlow",
|
|
2614
4099
|
"editor",
|
|
2615
4100
|
"enabled",
|
|
4101
|
+
"externalAgent",
|
|
2616
4102
|
"exclude",
|
|
2617
4103
|
"include",
|
|
2618
4104
|
"locale",
|
|
@@ -2698,8 +4184,12 @@ function serializeResolvedSpotPatchOptions(options) {
|
|
|
2698
4184
|
allowLan: options.allowLan,
|
|
2699
4185
|
budget: options.budget,
|
|
2700
4186
|
debug: options.debug,
|
|
4187
|
+
dataFlow: options.dataFlow.enabled ? Object.freeze({
|
|
4188
|
+
runtime: options.dataFlow.runtime
|
|
4189
|
+
}) : false,
|
|
2701
4190
|
editor: options.editor,
|
|
2702
4191
|
enabled: options.enabled,
|
|
4192
|
+
externalAgent: options.externalAgent.enabled,
|
|
2703
4193
|
exclude: Object.freeze(options.exclude.map(serializeFilter)),
|
|
2704
4194
|
include: Object.freeze(options.include.map(serializeFilter)),
|
|
2705
4195
|
locale: options.locale,
|
|
@@ -2740,11 +4230,20 @@ function parseBudget(value) {
|
|
|
2740
4230
|
);
|
|
2741
4231
|
return Object.freeze(budget);
|
|
2742
4232
|
}
|
|
4233
|
+
function parseDataFlow(value) {
|
|
4234
|
+
if (value === false) return false;
|
|
4235
|
+
if (!isRecord2(value) || !hasExactKeys(value, ["runtime"]) || value.runtime !== "dispatch") {
|
|
4236
|
+
throw new TypeError("The SpotPatch data-flow transport is invalid.");
|
|
4237
|
+
}
|
|
4238
|
+
return Object.freeze({
|
|
4239
|
+
runtime: value.runtime
|
|
4240
|
+
});
|
|
4241
|
+
}
|
|
2743
4242
|
function parseSerializedSpotPatchOptions(value) {
|
|
2744
4243
|
if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
|
|
2745
4244
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
2746
4245
|
}
|
|
2747
|
-
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)) {
|
|
4246
|
+
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)) {
|
|
2748
4247
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
2749
4248
|
}
|
|
2750
4249
|
try {
|
|
@@ -2753,8 +4252,10 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
2753
4252
|
allowLan: value.allowLan,
|
|
2754
4253
|
budget: parseBudget(value.budget),
|
|
2755
4254
|
debug: value.debug,
|
|
4255
|
+
dataFlow: parseDataFlow(value.dataFlow),
|
|
2756
4256
|
editor: value.editor,
|
|
2757
4257
|
enabled: value.enabled,
|
|
4258
|
+
externalAgent: value.externalAgent,
|
|
2758
4259
|
exclude: parseFilterList(value.exclude),
|
|
2759
4260
|
include: parseFilterList(value.include),
|
|
2760
4261
|
locale: value.locale,
|
|
@@ -2773,8 +4274,10 @@ export {
|
|
|
2773
4274
|
DEFAULT_OPTIONS,
|
|
2774
4275
|
applyIntegrationPlan,
|
|
2775
4276
|
createAgentJobManager,
|
|
4277
|
+
createExternalHandoffService,
|
|
2776
4278
|
createIntegrationFileChange,
|
|
2777
4279
|
createRuntimeAiConfig,
|
|
4280
|
+
createRuntimeDataFlowConfig,
|
|
2778
4281
|
createSession,
|
|
2779
4282
|
createSourceRegistrationService,
|
|
2780
4283
|
createSourceRegistry,
|