@spotpatch/dev-server 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1524 -350
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +1529 -306
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -472,6 +472,1103 @@ function createAgentJobManager(options) {
|
|
|
472
472
|
});
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
// src/external-handoff/service.ts
|
|
476
|
+
import {
|
|
477
|
+
ERROR_CODES as ERROR_CODES6,
|
|
478
|
+
EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION as EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION3,
|
|
479
|
+
EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION as EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION2,
|
|
480
|
+
SpotPatchError as SpotPatchError6
|
|
481
|
+
} from "@spotpatch/shared";
|
|
482
|
+
import { computeExternalHandoffProjectKey as computeExternalHandoffProjectKey2 } from "@spotpatch/shared/external-agent-node";
|
|
483
|
+
|
|
484
|
+
// src/external-handoff/active-registry.ts
|
|
485
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
486
|
+
import {
|
|
487
|
+
ERROR_CODES as ERROR_CODES2,
|
|
488
|
+
EXTERNAL_HANDOFF_LIMITS,
|
|
489
|
+
SpotPatchError as SpotPatchError2
|
|
490
|
+
} from "@spotpatch/shared";
|
|
491
|
+
|
|
492
|
+
// src/external-handoff/clock.ts
|
|
493
|
+
import { performance } from "perf_hooks";
|
|
494
|
+
var SYSTEM_EXTERNAL_HANDOFF_CLOCK = Object.freeze({
|
|
495
|
+
monotonicNow: () => performance.now(),
|
|
496
|
+
wallNow: () => Date.now()
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// src/external-handoff/active-registry.ts
|
|
500
|
+
var ALLOWED_TRANSITIONS = Object.freeze({
|
|
501
|
+
queued: ["dispatching", "failed"],
|
|
502
|
+
dispatching: ["dispatched", "working", "failed", "delivery-unknown"],
|
|
503
|
+
dispatched: ["working", "failed", "delivery-unknown"],
|
|
504
|
+
working: ["completed", "failed", "delivery-unknown"],
|
|
505
|
+
completed: [],
|
|
506
|
+
failed: [],
|
|
507
|
+
"delivery-unknown": []
|
|
508
|
+
});
|
|
509
|
+
var TERMINAL_PHASES = /* @__PURE__ */ new Set([
|
|
510
|
+
"completed",
|
|
511
|
+
"failed",
|
|
512
|
+
"delivery-unknown"
|
|
513
|
+
]);
|
|
514
|
+
function defaultRandomId() {
|
|
515
|
+
return randomBytes2(32).toString("base64url");
|
|
516
|
+
}
|
|
517
|
+
function requirePresent(value) {
|
|
518
|
+
if (value === null) throw new SpotPatchError2(ERROR_CODES2.INTERNAL_ERROR);
|
|
519
|
+
return value;
|
|
520
|
+
}
|
|
521
|
+
function createActiveAdapterRegistry(options = {}) {
|
|
522
|
+
const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
|
|
523
|
+
const randomId = options.randomId ?? defaultRandomId;
|
|
524
|
+
let blocked;
|
|
525
|
+
let closed = false;
|
|
526
|
+
let dispatch;
|
|
527
|
+
let lastReleasedToken;
|
|
528
|
+
let lease;
|
|
529
|
+
const nowIso = () => new Date(clock.wallNow()).toISOString();
|
|
530
|
+
const requireOpen = () => {
|
|
531
|
+
if (closed) throw new SpotPatchError2(ERROR_CODES2.SESSION_CLOSED);
|
|
532
|
+
};
|
|
533
|
+
const dispatchSummary = () => dispatch === void 0 ? null : Object.freeze({
|
|
534
|
+
adapterKind: dispatch.adapterKind,
|
|
535
|
+
revision: dispatch.revision,
|
|
536
|
+
phase: dispatch.phase,
|
|
537
|
+
updatedAt: dispatch.updatedAt
|
|
538
|
+
});
|
|
539
|
+
const activeSummary = () => {
|
|
540
|
+
if (blocked !== void 0) {
|
|
541
|
+
return Object.freeze({
|
|
542
|
+
kind: blocked.adapterKind,
|
|
543
|
+
state: "blocked",
|
|
544
|
+
canDispatch: false,
|
|
545
|
+
connectedAt: blocked.connectedAt,
|
|
546
|
+
updatedAt: blocked.updatedAt
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
if (lease === void 0) return null;
|
|
550
|
+
const busy = dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase);
|
|
551
|
+
return Object.freeze({
|
|
552
|
+
kind: lease.adapterKind,
|
|
553
|
+
state: busy ? "busy" : "ready",
|
|
554
|
+
canDispatch: !busy,
|
|
555
|
+
connectedAt: lease.connectedAt,
|
|
556
|
+
updatedAt: lease.updatedAt
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
const state = (cursor) => Object.freeze({
|
|
560
|
+
activeAdapter: activeSummary(),
|
|
561
|
+
dispatch: cursor === void 0 || dispatch?.cursor === cursor ? dispatchSummary() : null
|
|
562
|
+
});
|
|
563
|
+
const enterUnknown = (activeLease) => {
|
|
564
|
+
if (dispatch === void 0) return;
|
|
565
|
+
const updatedAt = nowIso();
|
|
566
|
+
dispatch.phase = "delivery-unknown";
|
|
567
|
+
dispatch.updatedAt = updatedAt;
|
|
568
|
+
blocked = Object.freeze({
|
|
569
|
+
adapterKind: activeLease.adapterKind,
|
|
570
|
+
connectedAt: activeLease.connectedAt,
|
|
571
|
+
updatedAt
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
const endLease = (activeLease) => {
|
|
575
|
+
if (dispatch?.phase === "queued") {
|
|
576
|
+
dispatch.phase = "failed";
|
|
577
|
+
dispatch.updatedAt = nowIso();
|
|
578
|
+
} else if (dispatch?.phase === "dispatching" || dispatch?.phase === "dispatched" || dispatch?.phase === "working") {
|
|
579
|
+
enterUnknown(activeLease);
|
|
580
|
+
}
|
|
581
|
+
lastReleasedToken = activeLease.token;
|
|
582
|
+
lease = void 0;
|
|
583
|
+
};
|
|
584
|
+
const sweep = () => {
|
|
585
|
+
if (lease === void 0) return;
|
|
586
|
+
const monotonicNow = clock.monotonicNow();
|
|
587
|
+
if (monotonicNow >= lease.expiresAtMonotonic || dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase) && monotonicNow >= dispatch.deadlineMonotonic) {
|
|
588
|
+
endLease(lease);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
const assertPublishable = () => {
|
|
592
|
+
requireOpen();
|
|
593
|
+
sweep();
|
|
594
|
+
if (blocked !== void 0 || lease !== void 0 && dispatch !== void 0 && !TERMINAL_PHASES.has(dispatch.phase)) {
|
|
595
|
+
throw new SpotPatchError2(ERROR_CODES2.EXTERNAL_AGENT_BUSY);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
const requireLease = (leaseToken) => {
|
|
599
|
+
requireOpen();
|
|
600
|
+
sweep();
|
|
601
|
+
if (lease?.token !== leaseToken) {
|
|
602
|
+
throw new SpotPatchError2(ERROR_CODES2.ACTIVE_ADAPTER_LEASE_INVALID);
|
|
603
|
+
}
|
|
604
|
+
return lease;
|
|
605
|
+
};
|
|
606
|
+
return Object.freeze({
|
|
607
|
+
assertPublishable,
|
|
608
|
+
claim(adapterKind, connectorInstanceId, baselineCursor) {
|
|
609
|
+
requireOpen();
|
|
610
|
+
sweep();
|
|
611
|
+
if (blocked !== void 0) {
|
|
612
|
+
throw new SpotPatchError2(ERROR_CODES2.EXTERNAL_AGENT_BUSY);
|
|
613
|
+
}
|
|
614
|
+
if (lease !== void 0) {
|
|
615
|
+
if (lease.adapterKind === adapterKind && lease.connectorInstanceId === connectorInstanceId) {
|
|
616
|
+
lease.expiresAtMonotonic = clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
|
|
617
|
+
lease.updatedAt = nowIso();
|
|
618
|
+
return Object.freeze({
|
|
619
|
+
leaseToken: lease.token,
|
|
620
|
+
heartbeatIntervalMs: EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
|
|
621
|
+
baselineCursor: lease.baselineCursor,
|
|
622
|
+
activeAdapter: requirePresent(activeSummary())
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
throw new SpotPatchError2(ERROR_CODES2.ACTIVE_ADAPTER_CONFLICT);
|
|
626
|
+
}
|
|
627
|
+
const timestamp = nowIso();
|
|
628
|
+
lease = {
|
|
629
|
+
adapterKind,
|
|
630
|
+
baselineCursor,
|
|
631
|
+
connectedAt: timestamp,
|
|
632
|
+
connectorInstanceId,
|
|
633
|
+
token: randomId(),
|
|
634
|
+
expiresAtMonotonic: clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs,
|
|
635
|
+
updatedAt: timestamp
|
|
636
|
+
};
|
|
637
|
+
lastReleasedToken = void 0;
|
|
638
|
+
return Object.freeze({
|
|
639
|
+
leaseToken: lease.token,
|
|
640
|
+
heartbeatIntervalMs: EXTERNAL_HANDOFF_LIMITS.activeHeartbeatIntervalMs,
|
|
641
|
+
baselineCursor,
|
|
642
|
+
activeAdapter: requirePresent(activeSummary())
|
|
643
|
+
});
|
|
644
|
+
},
|
|
645
|
+
heartbeat(leaseToken) {
|
|
646
|
+
const activeLease = requireLease(leaseToken);
|
|
647
|
+
activeLease.expiresAtMonotonic = clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeLeaseDurationMs;
|
|
648
|
+
activeLease.updatedAt = nowIso();
|
|
649
|
+
return state();
|
|
650
|
+
},
|
|
651
|
+
report(leaseToken, cursor, phase) {
|
|
652
|
+
const activeLease = requireLease(leaseToken);
|
|
653
|
+
if (dispatch?.cursor !== cursor || dispatch.adapterKind !== activeLease.adapterKind) {
|
|
654
|
+
throw new SpotPatchError2(ERROR_CODES2.ACTIVE_DISPATCH_INVALID);
|
|
655
|
+
}
|
|
656
|
+
if (dispatch.phase === phase) return state(cursor);
|
|
657
|
+
const allowed = ALLOWED_TRANSITIONS[dispatch.phase];
|
|
658
|
+
if (!allowed.includes(phase)) {
|
|
659
|
+
throw new SpotPatchError2(ERROR_CODES2.ACTIVE_DISPATCH_INVALID);
|
|
660
|
+
}
|
|
661
|
+
const updatedAt = nowIso();
|
|
662
|
+
dispatch.phase = phase;
|
|
663
|
+
dispatch.updatedAt = updatedAt;
|
|
664
|
+
activeLease.updatedAt = updatedAt;
|
|
665
|
+
if (phase === "delivery-unknown") {
|
|
666
|
+
blocked = Object.freeze({
|
|
667
|
+
adapterKind: activeLease.adapterKind,
|
|
668
|
+
connectedAt: activeLease.connectedAt,
|
|
669
|
+
updatedAt
|
|
670
|
+
});
|
|
671
|
+
lastReleasedToken = activeLease.token;
|
|
672
|
+
lease = void 0;
|
|
673
|
+
}
|
|
674
|
+
return state(cursor);
|
|
675
|
+
},
|
|
676
|
+
release(leaseToken) {
|
|
677
|
+
requireOpen();
|
|
678
|
+
sweep();
|
|
679
|
+
if (lease === void 0 && lastReleasedToken === leaseToken) return state();
|
|
680
|
+
const activeLease = lease;
|
|
681
|
+
if (activeLease?.token !== leaseToken) {
|
|
682
|
+
throw new SpotPatchError2(ERROR_CODES2.ACTIVE_ADAPTER_LEASE_INVALID);
|
|
683
|
+
}
|
|
684
|
+
endLease(activeLease);
|
|
685
|
+
return state();
|
|
686
|
+
},
|
|
687
|
+
reserve(cursor, revision) {
|
|
688
|
+
assertPublishable();
|
|
689
|
+
if (lease === void 0) return Object.freeze({ mode: "inbox" });
|
|
690
|
+
const updatedAt = nowIso();
|
|
691
|
+
dispatch = {
|
|
692
|
+
adapterKind: lease.adapterKind,
|
|
693
|
+
cursor,
|
|
694
|
+
deadlineMonotonic: clock.monotonicNow() + EXTERNAL_HANDOFF_LIMITS.activeDispatchTimeoutMs,
|
|
695
|
+
revision,
|
|
696
|
+
phase: "queued",
|
|
697
|
+
updatedAt
|
|
698
|
+
};
|
|
699
|
+
lease.updatedAt = updatedAt;
|
|
700
|
+
return Object.freeze({
|
|
701
|
+
mode: "active",
|
|
702
|
+
adapter: requirePresent(activeSummary()),
|
|
703
|
+
dispatch: requirePresent(dispatchSummary())
|
|
704
|
+
});
|
|
705
|
+
},
|
|
706
|
+
resolveDelivery(cursor) {
|
|
707
|
+
requireOpen();
|
|
708
|
+
sweep();
|
|
709
|
+
const activeDispatch = dispatch;
|
|
710
|
+
if (blocked === void 0 || activeDispatch?.cursor !== cursor || activeDispatch.phase !== "delivery-unknown") {
|
|
711
|
+
throw new SpotPatchError2(ERROR_CODES2.ACTIVE_DISPATCH_INVALID);
|
|
712
|
+
}
|
|
713
|
+
blocked = void 0;
|
|
714
|
+
return state(cursor);
|
|
715
|
+
},
|
|
716
|
+
snapshot(cursor) {
|
|
717
|
+
requireOpen();
|
|
718
|
+
sweep();
|
|
719
|
+
return state(cursor);
|
|
720
|
+
},
|
|
721
|
+
close() {
|
|
722
|
+
if (closed) return;
|
|
723
|
+
closed = true;
|
|
724
|
+
blocked = void 0;
|
|
725
|
+
dispatch = void 0;
|
|
726
|
+
lease = void 0;
|
|
727
|
+
lastReleasedToken = void 0;
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/external-handoff/broker.ts
|
|
733
|
+
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
734
|
+
import {
|
|
735
|
+
createServer
|
|
736
|
+
} from "http";
|
|
737
|
+
import {
|
|
738
|
+
ERROR_CODES as ERROR_CODES4,
|
|
739
|
+
EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
|
|
740
|
+
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS2,
|
|
741
|
+
SpotPatchError as SpotPatchError4
|
|
742
|
+
} from "@spotpatch/shared";
|
|
743
|
+
import {
|
|
744
|
+
SPOTPATCH_BRIDGE_PATHS,
|
|
745
|
+
SPOTPATCH_BRIDGE_TOKEN_HEADER,
|
|
746
|
+
bridgeAckRequestSchema,
|
|
747
|
+
bridgeActiveClaimRequestSchema,
|
|
748
|
+
bridgeActiveHeartbeatRequestSchema,
|
|
749
|
+
bridgeActiveReleaseRequestSchema,
|
|
750
|
+
bridgeActiveReportRequestSchema,
|
|
751
|
+
bridgeCurrentRequestSchema,
|
|
752
|
+
bridgeStatusRequestSchema,
|
|
753
|
+
bridgeWaitRequestSchema
|
|
754
|
+
} from "@spotpatch/shared/external-agent-node";
|
|
755
|
+
|
|
756
|
+
// src/server/request-body.ts
|
|
757
|
+
import { ERROR_CODES as ERROR_CODES3, SpotPatchError as SpotPatchError3 } from "@spotpatch/shared";
|
|
758
|
+
|
|
759
|
+
// src/server/constants.ts
|
|
760
|
+
var MAX_REQUEST_BODY_BYTES = 32 * 1024;
|
|
761
|
+
var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
|
|
762
|
+
var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
|
763
|
+
|
|
764
|
+
// src/server/request-body.ts
|
|
765
|
+
function isJsonContentType(value) {
|
|
766
|
+
return value?.split(";", 1)[0]?.trim().toLowerCase() === "application/json";
|
|
767
|
+
}
|
|
768
|
+
async function readJsonRequestBody(request, maximumBytes = MAX_REQUEST_BODY_BYTES) {
|
|
769
|
+
if (!isJsonContentType(request.headers["content-type"])) {
|
|
770
|
+
throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
|
|
771
|
+
}
|
|
772
|
+
const declaredLength = Number(request.headers["content-length"]);
|
|
773
|
+
if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
|
|
774
|
+
throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
|
|
775
|
+
}
|
|
776
|
+
const chunks = [];
|
|
777
|
+
let byteLength = 0;
|
|
778
|
+
let exceededLimit = false;
|
|
779
|
+
for await (const rawChunk of request) {
|
|
780
|
+
const chunk = rawChunk;
|
|
781
|
+
if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
|
|
782
|
+
throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
|
|
783
|
+
}
|
|
784
|
+
const buffer = Buffer.from(chunk);
|
|
785
|
+
byteLength += buffer.byteLength;
|
|
786
|
+
if (byteLength > maximumBytes) {
|
|
787
|
+
exceededLimit = true;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
chunks.push(buffer);
|
|
791
|
+
}
|
|
792
|
+
if (exceededLimit || byteLength === 0) {
|
|
793
|
+
throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST);
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
797
|
+
} catch (error) {
|
|
798
|
+
throw new SpotPatchError3(ERROR_CODES3.INVALID_REQUEST, void 0, {
|
|
799
|
+
cause: error
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/external-handoff/broker.ts
|
|
805
|
+
function singleHeader(request, name) {
|
|
806
|
+
const value = request.headers[name.toLowerCase()];
|
|
807
|
+
return Array.isArray(value) ? void 0 : value;
|
|
808
|
+
}
|
|
809
|
+
function tokensMatch(actual, expected) {
|
|
810
|
+
if (actual === void 0) return false;
|
|
811
|
+
const actualBytes = Buffer.from(actual);
|
|
812
|
+
const expectedBytes = Buffer.from(expected);
|
|
813
|
+
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual(actualBytes, expectedBytes);
|
|
814
|
+
}
|
|
815
|
+
function writeJson(response, status, payload) {
|
|
816
|
+
response.statusCode = status;
|
|
817
|
+
response.setHeader("Cache-Control", "no-store");
|
|
818
|
+
response.setHeader("Connection", "close");
|
|
819
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
820
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
821
|
+
response.end(JSON.stringify(payload));
|
|
822
|
+
}
|
|
823
|
+
function statusForError(code) {
|
|
824
|
+
if (code === ERROR_CODES4.BRIDGE_UNAUTHORIZED) return 401;
|
|
825
|
+
if (code === ERROR_CODES4.HANDOFF_NOT_FOUND) return 404;
|
|
826
|
+
if (code === ERROR_CODES4.HANDOFF_EXPIRED || code === ERROR_CODES4.SESSION_CLOSED) {
|
|
827
|
+
return 410;
|
|
828
|
+
}
|
|
829
|
+
if (code === ERROR_CODES4.BRIDGE_BUSY) return 429;
|
|
830
|
+
if (code === ERROR_CODES4.ACTIVE_ADAPTER_LEASE_INVALID) return 401;
|
|
831
|
+
if (code === ERROR_CODES4.HANDOFF_RESPONSE_TOO_LARGE) return 413;
|
|
832
|
+
if (code === ERROR_CODES4.HANDOFF_CURSOR_INVALID || code === ERROR_CODES4.BRIDGE_PROTOCOL_MISMATCH || code === ERROR_CODES4.EXTERNAL_AGENT_BUSY || code === ERROR_CODES4.ACTIVE_ADAPTER_CONFLICT || code === ERROR_CODES4.ACTIVE_DISPATCH_INVALID) {
|
|
833
|
+
return 409;
|
|
834
|
+
}
|
|
835
|
+
if (code === ERROR_CODES4.INVALID_REQUEST) return 400;
|
|
836
|
+
return 500;
|
|
837
|
+
}
|
|
838
|
+
function normalizeError2(error) {
|
|
839
|
+
return error instanceof SpotPatchError4 ? error : new SpotPatchError4(ERROR_CODES4.INTERNAL_ERROR, void 0, { cause: error });
|
|
840
|
+
}
|
|
841
|
+
function assertAuthorized(request, expectedHost, bridgeToken) {
|
|
842
|
+
if (request.socket.remoteAddress !== "127.0.0.1" || singleHeader(request, "host") !== expectedHost || !tokensMatch(singleHeader(request, SPOTPATCH_BRIDGE_TOKEN_HEADER), bridgeToken)) {
|
|
843
|
+
throw new SpotPatchError4(ERROR_CODES4.BRIDGE_UNAUTHORIZED);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async function closeServer(server, sockets) {
|
|
847
|
+
await new Promise((resolve) => {
|
|
848
|
+
server.close(() => {
|
|
849
|
+
resolve();
|
|
850
|
+
});
|
|
851
|
+
for (const socket of sockets) {
|
|
852
|
+
socket.destroy();
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
async function createExternalHandoffBroker(options) {
|
|
857
|
+
const bridgeToken = randomBytes3(32).toString("base64url");
|
|
858
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
859
|
+
let expectedHost = "";
|
|
860
|
+
const server = createServer(
|
|
861
|
+
{ maxHeaderSize: EXTERNAL_HANDOFF_LIMITS2.maximumBrokerHeaderBytes },
|
|
862
|
+
(request, response) => {
|
|
863
|
+
const handle = async () => {
|
|
864
|
+
assertAuthorized(request, expectedHost, bridgeToken);
|
|
865
|
+
if (request.method !== "POST") {
|
|
866
|
+
throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
867
|
+
}
|
|
868
|
+
const body = await readJsonRequestBody(
|
|
869
|
+
request,
|
|
870
|
+
EXTERNAL_HANDOFF_LIMITS2.maximumBrokerRequestBytes
|
|
871
|
+
);
|
|
872
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.status) {
|
|
873
|
+
const parsed = bridgeStatusRequestSchema.safeParse(body);
|
|
874
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
875
|
+
let current = null;
|
|
876
|
+
try {
|
|
877
|
+
current = options.store.status();
|
|
878
|
+
} catch (error) {
|
|
879
|
+
if (!(error instanceof SpotPatchError4) || error.code !== ERROR_CODES4.HANDOFF_NOT_FOUND) {
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
writeJson(response, 200, {
|
|
884
|
+
ok: true,
|
|
885
|
+
data: Object.freeze({
|
|
886
|
+
brokerProtocolVersion: EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION,
|
|
887
|
+
projectKey: options.projectKey,
|
|
888
|
+
sessionId: options.sessionId,
|
|
889
|
+
framework: options.framework,
|
|
890
|
+
current
|
|
891
|
+
})
|
|
892
|
+
});
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.current) {
|
|
896
|
+
const parsed = bridgeCurrentRequestSchema.safeParse(body);
|
|
897
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
898
|
+
const snapshot2 = options.store.current(parsed.data.cursor);
|
|
899
|
+
writeJson(response, 200, {
|
|
900
|
+
ok: true,
|
|
901
|
+
data: Object.freeze({ outcome: "handoff", snapshot: snapshot2 })
|
|
902
|
+
});
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.ack) {
|
|
906
|
+
const parsed = bridgeAckRequestSchema.safeParse(body);
|
|
907
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
908
|
+
const summary = options.store.ack(
|
|
909
|
+
parsed.data.cursor,
|
|
910
|
+
parsed.data.connectorInstanceId
|
|
911
|
+
);
|
|
912
|
+
writeJson(response, 200, {
|
|
913
|
+
ok: true,
|
|
914
|
+
data: Object.freeze({ summary })
|
|
915
|
+
});
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.wait) {
|
|
919
|
+
const parsed = bridgeWaitRequestSchema.safeParse(body);
|
|
920
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
921
|
+
const controller = new AbortController();
|
|
922
|
+
const abort = () => {
|
|
923
|
+
if (!response.writableEnded) controller.abort("bridge-client-closed");
|
|
924
|
+
};
|
|
925
|
+
response.once("close", abort);
|
|
926
|
+
try {
|
|
927
|
+
const data = await options.store.wait(
|
|
928
|
+
parsed.data.afterCursor,
|
|
929
|
+
parsed.data.timeoutMs,
|
|
930
|
+
controller.signal
|
|
931
|
+
);
|
|
932
|
+
writeJson(response, 200, { ok: true, data });
|
|
933
|
+
} finally {
|
|
934
|
+
response.removeListener("close", abort);
|
|
935
|
+
}
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.activeClaim) {
|
|
939
|
+
const parsed = bridgeActiveClaimRequestSchema.safeParse(body);
|
|
940
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
941
|
+
const data = options.activeRegistry.claim(
|
|
942
|
+
parsed.data.adapterKind,
|
|
943
|
+
parsed.data.connectorInstanceId,
|
|
944
|
+
options.store.currentCursor()
|
|
945
|
+
);
|
|
946
|
+
writeJson(response, 200, { ok: true, data });
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.activeHeartbeat) {
|
|
950
|
+
const parsed = bridgeActiveHeartbeatRequestSchema.safeParse(body);
|
|
951
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
952
|
+
const data = options.activeRegistry.heartbeat(parsed.data.leaseToken);
|
|
953
|
+
writeJson(response, 200, { ok: true, data });
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.activeReport) {
|
|
957
|
+
const parsed = bridgeActiveReportRequestSchema.safeParse(body);
|
|
958
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
959
|
+
const data = options.activeRegistry.report(
|
|
960
|
+
parsed.data.leaseToken,
|
|
961
|
+
parsed.data.cursor,
|
|
962
|
+
parsed.data.phase
|
|
963
|
+
);
|
|
964
|
+
writeJson(response, 200, { ok: true, data });
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (request.url === SPOTPATCH_BRIDGE_PATHS.activeRelease) {
|
|
968
|
+
const parsed = bridgeActiveReleaseRequestSchema.safeParse(body);
|
|
969
|
+
if (!parsed.success) throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
970
|
+
const data = options.activeRegistry.release(parsed.data.leaseToken);
|
|
971
|
+
writeJson(response, 200, { ok: true, data });
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
throw new SpotPatchError4(ERROR_CODES4.INVALID_REQUEST);
|
|
975
|
+
};
|
|
976
|
+
void handle().catch((error) => {
|
|
977
|
+
if (response.writableEnded || response.destroyed) return;
|
|
978
|
+
const normalized = normalizeError2(error);
|
|
979
|
+
if (normalized.code === ERROR_CODES4.BRIDGE_BUSY) {
|
|
980
|
+
response.setHeader("Retry-After", "1");
|
|
981
|
+
}
|
|
982
|
+
writeJson(response, statusForError(normalized.code), {
|
|
983
|
+
ok: false,
|
|
984
|
+
error: {
|
|
985
|
+
code: normalized.code,
|
|
986
|
+
message: "The local SpotPatch bridge request failed."
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
);
|
|
992
|
+
server.maxConnections = EXTERNAL_HANDOFF_LIMITS2.maximumBrokerSockets;
|
|
993
|
+
server.headersTimeout = 5e3;
|
|
994
|
+
server.requestTimeout = EXTERNAL_HANDOFF_LIMITS2.maximumWaitMs + 5e3;
|
|
995
|
+
server.keepAliveTimeout = 1;
|
|
996
|
+
server.on("connection", (socket) => {
|
|
997
|
+
if (sockets.size >= EXTERNAL_HANDOFF_LIMITS2.maximumBrokerSockets) {
|
|
998
|
+
socket.destroy();
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
sockets.add(socket);
|
|
1002
|
+
socket.once("close", () => sockets.delete(socket));
|
|
1003
|
+
});
|
|
1004
|
+
await new Promise((resolve, reject) => {
|
|
1005
|
+
server.once("error", reject);
|
|
1006
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
1007
|
+
});
|
|
1008
|
+
const address = server.address();
|
|
1009
|
+
if (address === null || typeof address === "string" || address.address !== "127.0.0.1") {
|
|
1010
|
+
await closeServer(server, sockets);
|
|
1011
|
+
throw new Error("SpotPatch external Agent broker did not bind IPv4 loopback.");
|
|
1012
|
+
}
|
|
1013
|
+
expectedHost = `127.0.0.1:${String(address.port)}`;
|
|
1014
|
+
let closed = false;
|
|
1015
|
+
let ready = true;
|
|
1016
|
+
server.removeAllListeners("error");
|
|
1017
|
+
server.on("error", () => {
|
|
1018
|
+
ready = false;
|
|
1019
|
+
for (const socket of sockets) socket.destroy();
|
|
1020
|
+
});
|
|
1021
|
+
return Object.freeze({
|
|
1022
|
+
bridgeToken,
|
|
1023
|
+
endpoint: `http://${expectedHost}`,
|
|
1024
|
+
isReady: () => ready && !closed,
|
|
1025
|
+
async close() {
|
|
1026
|
+
if (closed) return;
|
|
1027
|
+
closed = true;
|
|
1028
|
+
ready = false;
|
|
1029
|
+
await closeServer(server, sockets);
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/external-handoff/discovery.ts
|
|
1035
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
1036
|
+
import { lstat, open, rename, unlink } from "fs/promises";
|
|
1037
|
+
import path from "path";
|
|
1038
|
+
import {
|
|
1039
|
+
EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION as EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION2,
|
|
1040
|
+
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS3
|
|
1041
|
+
} from "@spotpatch/shared";
|
|
1042
|
+
import {
|
|
1043
|
+
computeExternalHandoffProjectKey,
|
|
1044
|
+
externalHandoffDescriptorSchema,
|
|
1045
|
+
resolveExternalHandoffRuntimeDirectory
|
|
1046
|
+
} from "@spotpatch/shared/external-agent-node";
|
|
1047
|
+
async function syncDirectory(directory) {
|
|
1048
|
+
const handle = await open(directory, "r");
|
|
1049
|
+
try {
|
|
1050
|
+
await handle.sync();
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
const code = error.code;
|
|
1053
|
+
if (code !== "EINVAL" && code !== "ENOTSUP") {
|
|
1054
|
+
throw error;
|
|
1055
|
+
}
|
|
1056
|
+
} finally {
|
|
1057
|
+
await handle.close();
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
async function publishExternalHandoffDescriptor(options) {
|
|
1061
|
+
const directory = await resolveExternalHandoffRuntimeDirectory(true);
|
|
1062
|
+
const descriptor = externalHandoffDescriptorSchema.parse({
|
|
1063
|
+
schemaVersion: 1,
|
|
1064
|
+
brokerProtocolVersion: EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION2,
|
|
1065
|
+
projectKey: await computeExternalHandoffProjectKey(options.root),
|
|
1066
|
+
sessionId: options.sessionId,
|
|
1067
|
+
framework: options.framework,
|
|
1068
|
+
endpoint: options.endpoint,
|
|
1069
|
+
bridgeToken: options.bridgeToken,
|
|
1070
|
+
pid: process.pid,
|
|
1071
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1072
|
+
});
|
|
1073
|
+
const serialized = JSON.stringify(descriptor);
|
|
1074
|
+
if (Buffer.byteLength(serialized, "utf8") > EXTERNAL_HANDOFF_LIMITS3.maximumDescriptorBytes) {
|
|
1075
|
+
throw new RangeError("SpotPatch external Agent descriptor exceeds its limit.");
|
|
1076
|
+
}
|
|
1077
|
+
const destination = path.join(directory, `${descriptor.sessionId}.json`);
|
|
1078
|
+
const temporary = path.join(
|
|
1079
|
+
directory,
|
|
1080
|
+
`.${descriptor.sessionId}.${randomBytes4(8).toString("hex")}.tmp`
|
|
1081
|
+
);
|
|
1082
|
+
let temporaryExists = false;
|
|
1083
|
+
let published = false;
|
|
1084
|
+
let descriptorIdentity = Object.freeze({
|
|
1085
|
+
device: -1,
|
|
1086
|
+
inode: -1
|
|
1087
|
+
});
|
|
1088
|
+
try {
|
|
1089
|
+
const handle = await open(temporary, "wx", 384);
|
|
1090
|
+
temporaryExists = true;
|
|
1091
|
+
try {
|
|
1092
|
+
await handle.writeFile(serialized, "utf8");
|
|
1093
|
+
await handle.sync();
|
|
1094
|
+
} finally {
|
|
1095
|
+
await handle.close();
|
|
1096
|
+
}
|
|
1097
|
+
await rename(temporary, destination);
|
|
1098
|
+
temporaryExists = false;
|
|
1099
|
+
published = true;
|
|
1100
|
+
const status = await lstat(destination);
|
|
1101
|
+
const uid = process.getuid?.();
|
|
1102
|
+
if (!status.isFile() || status.isSymbolicLink() || uid === void 0 || status.uid !== uid || (status.mode & 63) !== 0) {
|
|
1103
|
+
throw new Error("SpotPatch external Agent descriptor is not private.");
|
|
1104
|
+
}
|
|
1105
|
+
descriptorIdentity = Object.freeze({ device: status.dev, inode: status.ino });
|
|
1106
|
+
await syncDirectory(directory);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
if (temporaryExists) {
|
|
1109
|
+
await unlink(temporary).catch(() => void 0);
|
|
1110
|
+
}
|
|
1111
|
+
if (published) {
|
|
1112
|
+
await unlink(destination).catch(() => void 0);
|
|
1113
|
+
}
|
|
1114
|
+
throw error;
|
|
1115
|
+
}
|
|
1116
|
+
let closed = false;
|
|
1117
|
+
return Object.freeze({
|
|
1118
|
+
descriptor,
|
|
1119
|
+
async close() {
|
|
1120
|
+
if (closed) return;
|
|
1121
|
+
closed = true;
|
|
1122
|
+
await lstat(destination).then(async (status) => {
|
|
1123
|
+
if (status.dev === descriptorIdentity.device && status.ino === descriptorIdentity.inode) {
|
|
1124
|
+
await unlink(destination);
|
|
1125
|
+
}
|
|
1126
|
+
}).catch((error) => {
|
|
1127
|
+
if (error.code !== "ENOENT") {
|
|
1128
|
+
throw error;
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
await syncDirectory(directory);
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/external-handoff/fingerprint.ts
|
|
1137
|
+
import { createHash as createHash2 } from "crypto";
|
|
1138
|
+
function canonicalJson(value) {
|
|
1139
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
1140
|
+
return JSON.stringify(value);
|
|
1141
|
+
}
|
|
1142
|
+
if (typeof value === "number") {
|
|
1143
|
+
if (!Number.isFinite(value)) throw new TypeError("Non-finite JSON number.");
|
|
1144
|
+
return JSON.stringify(value);
|
|
1145
|
+
}
|
|
1146
|
+
if (Array.isArray(value)) {
|
|
1147
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
1148
|
+
}
|
|
1149
|
+
if (typeof value === "object") {
|
|
1150
|
+
const record = value;
|
|
1151
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
1152
|
+
}
|
|
1153
|
+
throw new TypeError("Unsupported JSON value.");
|
|
1154
|
+
}
|
|
1155
|
+
function fingerprintExternalHandoffAnnotation(annotation) {
|
|
1156
|
+
return createHash2("sha256").update(canonicalJson(annotation)).digest("hex");
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// src/external-handoff/store.ts
|
|
1160
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
1161
|
+
import {
|
|
1162
|
+
ERROR_CODES as ERROR_CODES5,
|
|
1163
|
+
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS4,
|
|
1164
|
+
EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
|
|
1165
|
+
SpotPatchError as SpotPatchError5
|
|
1166
|
+
} from "@spotpatch/shared";
|
|
1167
|
+
function defaultRandomId2() {
|
|
1168
|
+
return randomBytes5(24).toString("base64url");
|
|
1169
|
+
}
|
|
1170
|
+
function pageSummary(annotation) {
|
|
1171
|
+
let origin = "[unavailable]";
|
|
1172
|
+
try {
|
|
1173
|
+
const url = new URL(annotation.page.url);
|
|
1174
|
+
origin = url.origin === "null" ? "[unavailable]" : url.origin;
|
|
1175
|
+
} catch {
|
|
1176
|
+
}
|
|
1177
|
+
return Object.freeze({ origin, pathname: annotation.page.pathname });
|
|
1178
|
+
}
|
|
1179
|
+
function summaryOf(current, state) {
|
|
1180
|
+
const snapshot2 = current.snapshot;
|
|
1181
|
+
return Object.freeze({
|
|
1182
|
+
sessionId: snapshot2.session.id,
|
|
1183
|
+
framework: snapshot2.session.framework,
|
|
1184
|
+
revision: snapshot2.revision,
|
|
1185
|
+
cursor: snapshot2.cursor,
|
|
1186
|
+
targetCount: snapshot2.annotation.targets.length,
|
|
1187
|
+
page: pageSummary(snapshot2.annotation),
|
|
1188
|
+
publishedAt: snapshot2.publishedAt,
|
|
1189
|
+
expiresAt: snapshot2.expiresAt,
|
|
1190
|
+
state,
|
|
1191
|
+
pickupCount: current.receipts.size,
|
|
1192
|
+
...current.pickedUpAt === void 0 ? {} : { pickedUpAt: current.pickedUpAt }
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
function replayResult(record) {
|
|
1196
|
+
return Object.freeze({ ...record.result, replayed: true });
|
|
1197
|
+
}
|
|
1198
|
+
function createExternalHandoffStore(options) {
|
|
1199
|
+
const clock = options.clock ?? SYSTEM_EXTERNAL_HANDOFF_CLOCK;
|
|
1200
|
+
const randomId = options.randomId ?? defaultRandomId2;
|
|
1201
|
+
const history = [];
|
|
1202
|
+
const idempotency = /* @__PURE__ */ new Map();
|
|
1203
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
1204
|
+
let closed = false;
|
|
1205
|
+
let current;
|
|
1206
|
+
let revision = 0;
|
|
1207
|
+
const requireOpen = () => {
|
|
1208
|
+
if (closed) throw new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED);
|
|
1209
|
+
};
|
|
1210
|
+
const archive = (state) => {
|
|
1211
|
+
if (current === void 0) return;
|
|
1212
|
+
history.unshift(summaryOf(current, state));
|
|
1213
|
+
history.length = Math.min(
|
|
1214
|
+
history.length,
|
|
1215
|
+
EXTERNAL_HANDOFF_LIMITS4.maximumHistorySummaries
|
|
1216
|
+
);
|
|
1217
|
+
current = void 0;
|
|
1218
|
+
};
|
|
1219
|
+
const sweep = () => {
|
|
1220
|
+
const monotonicNow = clock.monotonicNow();
|
|
1221
|
+
if (current !== void 0 && monotonicNow >= current.expiresAtMonotonic) {
|
|
1222
|
+
archive("expired");
|
|
1223
|
+
}
|
|
1224
|
+
for (const [requestId, record] of idempotency) {
|
|
1225
|
+
if (monotonicNow >= record.expiresAtMonotonic) idempotency.delete(requestId);
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
const knownSummary = (cursor) => {
|
|
1229
|
+
sweep();
|
|
1230
|
+
if (current?.snapshot.cursor === cursor) {
|
|
1231
|
+
return summaryOf(current, "available");
|
|
1232
|
+
}
|
|
1233
|
+
return history.find((summary) => summary.cursor === cursor);
|
|
1234
|
+
};
|
|
1235
|
+
const readCurrent = (cursor) => {
|
|
1236
|
+
requireOpen();
|
|
1237
|
+
sweep();
|
|
1238
|
+
if (current === void 0) {
|
|
1239
|
+
const prior = cursor === void 0 ? void 0 : knownSummary(cursor);
|
|
1240
|
+
throw new SpotPatchError5(
|
|
1241
|
+
prior?.state === "expired" ? ERROR_CODES5.HANDOFF_EXPIRED : cursor === void 0 ? ERROR_CODES5.HANDOFF_NOT_FOUND : ERROR_CODES5.HANDOFF_CURSOR_INVALID
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
if (cursor !== void 0 && cursor !== current.snapshot.cursor) {
|
|
1245
|
+
const prior = knownSummary(cursor);
|
|
1246
|
+
throw new SpotPatchError5(
|
|
1247
|
+
prior?.state === "expired" ? ERROR_CODES5.HANDOFF_EXPIRED : ERROR_CODES5.HANDOFF_CURSOR_INVALID
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
return current.snapshot;
|
|
1251
|
+
};
|
|
1252
|
+
const findReplay = (requestId, fingerprint) => {
|
|
1253
|
+
requireOpen();
|
|
1254
|
+
sweep();
|
|
1255
|
+
const record = idempotency.get(requestId);
|
|
1256
|
+
if (record === void 0) return void 0;
|
|
1257
|
+
if (record.fingerprint !== fingerprint) {
|
|
1258
|
+
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_VALIDATION_FAILED);
|
|
1259
|
+
}
|
|
1260
|
+
return replayResult(record);
|
|
1261
|
+
};
|
|
1262
|
+
const settleWaiters = (result) => {
|
|
1263
|
+
const pending = [...waiters];
|
|
1264
|
+
waiters.clear();
|
|
1265
|
+
for (const waiter of pending) waiter.resolve(result);
|
|
1266
|
+
};
|
|
1267
|
+
return Object.freeze({
|
|
1268
|
+
activeWaitCount: () => waiters.size,
|
|
1269
|
+
replay: findReplay,
|
|
1270
|
+
publish(input) {
|
|
1271
|
+
requireOpen();
|
|
1272
|
+
const replayed = findReplay(input.requestId, input.fingerprint);
|
|
1273
|
+
if (replayed !== void 0) return replayed;
|
|
1274
|
+
if (idempotency.size >= EXTERNAL_HANDOFF_LIMITS4.maximumRequestIdRecords) {
|
|
1275
|
+
throw new SpotPatchError5(ERROR_CODES5.EXTERNAL_HANDOFF_UNAVAILABLE);
|
|
1276
|
+
}
|
|
1277
|
+
const nextRevision = revision + 1;
|
|
1278
|
+
const publishedAtMs = clock.wallNow();
|
|
1279
|
+
const publishedAtMonotonic = clock.monotonicNow();
|
|
1280
|
+
const snapshot2 = Object.freeze({
|
|
1281
|
+
schemaVersion: EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION,
|
|
1282
|
+
cursor: randomId(),
|
|
1283
|
+
session: Object.freeze({ id: options.sessionId, framework: options.framework }),
|
|
1284
|
+
revision: nextRevision,
|
|
1285
|
+
publishedAt: new Date(publishedAtMs).toISOString(),
|
|
1286
|
+
expiresAt: new Date(
|
|
1287
|
+
publishedAtMs + EXTERNAL_HANDOFF_LIMITS4.handoffTtlMs
|
|
1288
|
+
).toISOString(),
|
|
1289
|
+
annotation: input.annotation
|
|
1290
|
+
});
|
|
1291
|
+
if (Buffer.byteLength(JSON.stringify(snapshot2), "utf8") > EXTERNAL_HANDOFF_LIMITS4.maximumSnapshotBytes) {
|
|
1292
|
+
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_RESPONSE_TOO_LARGE);
|
|
1293
|
+
}
|
|
1294
|
+
const delivery = input.reserve(snapshot2.cursor, nextRevision);
|
|
1295
|
+
if (current !== void 0) archive("superseded");
|
|
1296
|
+
revision = nextRevision;
|
|
1297
|
+
current = {
|
|
1298
|
+
expiresAtMonotonic: publishedAtMonotonic + EXTERNAL_HANDOFF_LIMITS4.handoffTtlMs,
|
|
1299
|
+
receipts: /* @__PURE__ */ new Set(),
|
|
1300
|
+
snapshot: snapshot2
|
|
1301
|
+
};
|
|
1302
|
+
const result = Object.freeze({
|
|
1303
|
+
handoff: summaryOf(current, "available"),
|
|
1304
|
+
delivery,
|
|
1305
|
+
replayed: false
|
|
1306
|
+
});
|
|
1307
|
+
idempotency.set(
|
|
1308
|
+
input.requestId,
|
|
1309
|
+
Object.freeze({
|
|
1310
|
+
expiresAtMonotonic: publishedAtMonotonic + EXTERNAL_HANDOFF_LIMITS4.requestIdTtlMs,
|
|
1311
|
+
fingerprint: input.fingerprint,
|
|
1312
|
+
result
|
|
1313
|
+
})
|
|
1314
|
+
);
|
|
1315
|
+
settleWaiters(Object.freeze({ outcome: "handoff", snapshot: snapshot2 }));
|
|
1316
|
+
return result;
|
|
1317
|
+
},
|
|
1318
|
+
current: readCurrent,
|
|
1319
|
+
currentCursor() {
|
|
1320
|
+
requireOpen();
|
|
1321
|
+
sweep();
|
|
1322
|
+
return current?.snapshot.cursor ?? null;
|
|
1323
|
+
},
|
|
1324
|
+
status(cursor) {
|
|
1325
|
+
requireOpen();
|
|
1326
|
+
sweep();
|
|
1327
|
+
if (cursor === void 0) {
|
|
1328
|
+
if (current === void 0) {
|
|
1329
|
+
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_NOT_FOUND);
|
|
1330
|
+
}
|
|
1331
|
+
return summaryOf(current, "available");
|
|
1332
|
+
}
|
|
1333
|
+
const summary = knownSummary(cursor);
|
|
1334
|
+
if (summary === void 0) {
|
|
1335
|
+
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_CURSOR_INVALID);
|
|
1336
|
+
}
|
|
1337
|
+
return summary;
|
|
1338
|
+
},
|
|
1339
|
+
ack(cursor, connectorInstanceId) {
|
|
1340
|
+
const snapshot2 = readCurrent(cursor);
|
|
1341
|
+
if (snapshot2 !== current?.snapshot) {
|
|
1342
|
+
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_CURSOR_INVALID);
|
|
1343
|
+
}
|
|
1344
|
+
if (!current.receipts.has(connectorInstanceId)) {
|
|
1345
|
+
if (current.receipts.size >= EXTERNAL_HANDOFF_LIMITS4.maximumConnectorReceipts) {
|
|
1346
|
+
throw new SpotPatchError5(ERROR_CODES5.BRIDGE_BUSY);
|
|
1347
|
+
}
|
|
1348
|
+
current.receipts.add(connectorInstanceId);
|
|
1349
|
+
current.pickedUpAt = new Date(clock.wallNow()).toISOString();
|
|
1350
|
+
}
|
|
1351
|
+
return summaryOf(current, "available");
|
|
1352
|
+
},
|
|
1353
|
+
async wait(afterCursor, timeoutMs, signal) {
|
|
1354
|
+
requireOpen();
|
|
1355
|
+
sweep();
|
|
1356
|
+
if (afterCursor === void 0 && current !== void 0) {
|
|
1357
|
+
return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
|
|
1358
|
+
}
|
|
1359
|
+
if (afterCursor !== void 0) {
|
|
1360
|
+
const known = knownSummary(afterCursor);
|
|
1361
|
+
if (known === void 0) {
|
|
1362
|
+
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_CURSOR_INVALID);
|
|
1363
|
+
}
|
|
1364
|
+
if (current !== void 0 && current.snapshot.cursor !== afterCursor) {
|
|
1365
|
+
return Object.freeze({ outcome: "handoff", snapshot: current.snapshot });
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (waiters.size >= EXTERNAL_HANDOFF_LIMITS4.maximumWaiters) {
|
|
1369
|
+
throw new SpotPatchError5(ERROR_CODES5.BRIDGE_BUSY);
|
|
1370
|
+
}
|
|
1371
|
+
if (signal.aborted) throw new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED);
|
|
1372
|
+
return new Promise((resolve, reject) => {
|
|
1373
|
+
let settled = false;
|
|
1374
|
+
const finish = () => {
|
|
1375
|
+
if (settled) return false;
|
|
1376
|
+
settled = true;
|
|
1377
|
+
waiters.delete(waiter);
|
|
1378
|
+
clearTimeout(timer);
|
|
1379
|
+
signal.removeEventListener("abort", abort);
|
|
1380
|
+
return true;
|
|
1381
|
+
};
|
|
1382
|
+
const waiter = {
|
|
1383
|
+
reject(error) {
|
|
1384
|
+
if (finish()) reject(error);
|
|
1385
|
+
},
|
|
1386
|
+
resolve(result) {
|
|
1387
|
+
if (finish()) resolve(result);
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
const abort = () => {
|
|
1391
|
+
waiter.reject(new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED));
|
|
1392
|
+
};
|
|
1393
|
+
const timer = setTimeout(() => {
|
|
1394
|
+
waiter.resolve(Object.freeze({ outcome: "timeout" }));
|
|
1395
|
+
}, timeoutMs);
|
|
1396
|
+
timer.unref();
|
|
1397
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1398
|
+
waiters.add(waiter);
|
|
1399
|
+
});
|
|
1400
|
+
},
|
|
1401
|
+
close() {
|
|
1402
|
+
if (closed) return;
|
|
1403
|
+
closed = true;
|
|
1404
|
+
current = void 0;
|
|
1405
|
+
history.length = 0;
|
|
1406
|
+
idempotency.clear();
|
|
1407
|
+
const pending = [...waiters];
|
|
1408
|
+
waiters.clear();
|
|
1409
|
+
for (const waiter of pending) {
|
|
1410
|
+
waiter.reject(new SpotPatchError5(ERROR_CODES5.SESSION_CLOSED));
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/external-handoff/service.ts
|
|
1417
|
+
function asReplay(result) {
|
|
1418
|
+
return Object.freeze({ ...result, replayed: true });
|
|
1419
|
+
}
|
|
1420
|
+
function createExternalHandoffService(options) {
|
|
1421
|
+
const activeRegistry = createActiveAdapterRegistry();
|
|
1422
|
+
const store = createExternalHandoffStore({
|
|
1423
|
+
framework: options.framework,
|
|
1424
|
+
sessionId: options.sessionId
|
|
1425
|
+
});
|
|
1426
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
1427
|
+
let broker;
|
|
1428
|
+
let descriptor;
|
|
1429
|
+
let startPromise;
|
|
1430
|
+
let closePromise;
|
|
1431
|
+
let state = "idle";
|
|
1432
|
+
const isClosed = () => state === "closed";
|
|
1433
|
+
const requireReady = () => {
|
|
1434
|
+
if (state !== "ready" || broker?.isReady() !== true) {
|
|
1435
|
+
throw new SpotPatchError6(
|
|
1436
|
+
state === "closed" ? ERROR_CODES6.SESSION_CLOSED : ERROR_CODES6.EXTERNAL_HANDOFF_UNAVAILABLE
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
const start = async () => {
|
|
1441
|
+
if (state === "ready") return;
|
|
1442
|
+
if (state === "closed") throw new SpotPatchError6(ERROR_CODES6.SESSION_CLOSED);
|
|
1443
|
+
if (startPromise !== void 0) return startPromise;
|
|
1444
|
+
state = "starting";
|
|
1445
|
+
startPromise = (async () => {
|
|
1446
|
+
let createdBroker;
|
|
1447
|
+
let createdDescriptor;
|
|
1448
|
+
try {
|
|
1449
|
+
const projectKey = await computeExternalHandoffProjectKey2(options.root);
|
|
1450
|
+
createdBroker = await createExternalHandoffBroker({
|
|
1451
|
+
activeRegistry,
|
|
1452
|
+
framework: options.framework,
|
|
1453
|
+
projectKey,
|
|
1454
|
+
sessionId: options.sessionId,
|
|
1455
|
+
store
|
|
1456
|
+
});
|
|
1457
|
+
createdDescriptor = await publishExternalHandoffDescriptor({
|
|
1458
|
+
bridgeToken: createdBroker.bridgeToken,
|
|
1459
|
+
endpoint: createdBroker.endpoint,
|
|
1460
|
+
framework: options.framework,
|
|
1461
|
+
root: options.root,
|
|
1462
|
+
sessionId: options.sessionId
|
|
1463
|
+
});
|
|
1464
|
+
if (isClosed()) {
|
|
1465
|
+
await createdDescriptor.close();
|
|
1466
|
+
await createdBroker.close();
|
|
1467
|
+
throw new SpotPatchError6(ERROR_CODES6.SESSION_CLOSED);
|
|
1468
|
+
}
|
|
1469
|
+
broker = createdBroker;
|
|
1470
|
+
descriptor = createdDescriptor;
|
|
1471
|
+
state = "ready";
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
if (createdDescriptor !== void 0 && descriptor !== createdDescriptor) {
|
|
1474
|
+
await createdDescriptor.close().catch(() => void 0);
|
|
1475
|
+
}
|
|
1476
|
+
if (createdBroker !== void 0 && broker !== createdBroker) {
|
|
1477
|
+
await createdBroker.close().catch(() => void 0);
|
|
1478
|
+
}
|
|
1479
|
+
if (!isClosed()) state = "failed";
|
|
1480
|
+
throw error;
|
|
1481
|
+
}
|
|
1482
|
+
})();
|
|
1483
|
+
return startPromise;
|
|
1484
|
+
};
|
|
1485
|
+
return Object.freeze({
|
|
1486
|
+
start,
|
|
1487
|
+
capability() {
|
|
1488
|
+
const currentCursor = store.currentCursor();
|
|
1489
|
+
const active = activeRegistry.snapshot(currentCursor ?? void 0);
|
|
1490
|
+
return Object.freeze({
|
|
1491
|
+
enabled: true,
|
|
1492
|
+
brokerReady: state === "ready" && broker?.isReady() === true,
|
|
1493
|
+
activeWaitCount: store.activeWaitCount(),
|
|
1494
|
+
snapshotSchemaVersion: EXTERNAL_HANDOFF_SNAPSHOT_SCHEMA_VERSION2,
|
|
1495
|
+
brokerProtocolVersion: EXTERNAL_HANDOFF_BROKER_PROTOCOL_VERSION3,
|
|
1496
|
+
activeAdapter: active.activeAdapter,
|
|
1497
|
+
dispatch: currentCursor === null ? null : active.dispatch
|
|
1498
|
+
});
|
|
1499
|
+
},
|
|
1500
|
+
async publish(request, authorize) {
|
|
1501
|
+
requireReady();
|
|
1502
|
+
const fingerprint = fingerprintExternalHandoffAnnotation(request.annotation);
|
|
1503
|
+
const replayed = store.replay(request.requestId, fingerprint);
|
|
1504
|
+
if (replayed !== void 0) return replayed;
|
|
1505
|
+
const pending = inFlight.get(request.requestId);
|
|
1506
|
+
if (pending !== void 0) {
|
|
1507
|
+
if (pending.fingerprint !== fingerprint) {
|
|
1508
|
+
throw new SpotPatchError6(ERROR_CODES6.HANDOFF_VALIDATION_FAILED);
|
|
1509
|
+
}
|
|
1510
|
+
return asReplay(await pending.promise);
|
|
1511
|
+
}
|
|
1512
|
+
activeRegistry.assertPublishable();
|
|
1513
|
+
const promise = (async () => {
|
|
1514
|
+
const annotation = await authorize(request.annotation);
|
|
1515
|
+
return store.publish({
|
|
1516
|
+
annotation,
|
|
1517
|
+
fingerprint,
|
|
1518
|
+
requestId: request.requestId,
|
|
1519
|
+
reserve: activeRegistry.reserve
|
|
1520
|
+
});
|
|
1521
|
+
})();
|
|
1522
|
+
const activePublish = Object.freeze({ fingerprint, promise });
|
|
1523
|
+
inFlight.set(request.requestId, activePublish);
|
|
1524
|
+
try {
|
|
1525
|
+
return await promise;
|
|
1526
|
+
} finally {
|
|
1527
|
+
if (inFlight.get(request.requestId) === activePublish) {
|
|
1528
|
+
inFlight.delete(request.requestId);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
},
|
|
1532
|
+
status(cursor) {
|
|
1533
|
+
requireReady();
|
|
1534
|
+
const handoff = store.status(cursor);
|
|
1535
|
+
const active = activeRegistry.snapshot(cursor ?? handoff.cursor);
|
|
1536
|
+
return Object.freeze({
|
|
1537
|
+
handoff,
|
|
1538
|
+
activeAdapter: active.activeAdapter,
|
|
1539
|
+
dispatch: active.dispatch
|
|
1540
|
+
});
|
|
1541
|
+
},
|
|
1542
|
+
resolveDelivery(cursor) {
|
|
1543
|
+
requireReady();
|
|
1544
|
+
const handoff = store.status(cursor);
|
|
1545
|
+
const active = activeRegistry.resolveDelivery(cursor);
|
|
1546
|
+
return Object.freeze({
|
|
1547
|
+
handoff,
|
|
1548
|
+
activeAdapter: active.activeAdapter,
|
|
1549
|
+
dispatch: active.dispatch
|
|
1550
|
+
});
|
|
1551
|
+
},
|
|
1552
|
+
close() {
|
|
1553
|
+
closePromise ??= (async () => {
|
|
1554
|
+
if (state === "closed") return;
|
|
1555
|
+
state = "closed";
|
|
1556
|
+
activeRegistry.close();
|
|
1557
|
+
store.close();
|
|
1558
|
+
inFlight.clear();
|
|
1559
|
+
await startPromise?.catch(() => void 0);
|
|
1560
|
+
const publishedDescriptor = descriptor;
|
|
1561
|
+
descriptor = void 0;
|
|
1562
|
+
const activeBroker = broker;
|
|
1563
|
+
broker = void 0;
|
|
1564
|
+
await publishedDescriptor?.close().catch(() => void 0);
|
|
1565
|
+
await activeBroker?.close().catch(() => void 0);
|
|
1566
|
+
})();
|
|
1567
|
+
return closePromise;
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
|
|
475
1572
|
// src/environment-ai.ts
|
|
476
1573
|
var AI_ENVIRONMENT_NAMES = Object.freeze({
|
|
477
1574
|
authentication: "SPOTPATCH_AI_AUTHENTICATION",
|
|
@@ -562,32 +1659,32 @@ function resolveEnvironmentAiConfiguration(environment) {
|
|
|
562
1659
|
}
|
|
563
1660
|
|
|
564
1661
|
// src/integration/file-plan.ts
|
|
565
|
-
import { randomBytes as
|
|
1662
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
566
1663
|
import {
|
|
567
1664
|
access,
|
|
568
|
-
lstat,
|
|
1665
|
+
lstat as lstat2,
|
|
569
1666
|
mkdir,
|
|
570
1667
|
readFile,
|
|
571
1668
|
realpath,
|
|
572
|
-
rename,
|
|
1669
|
+
rename as rename2,
|
|
573
1670
|
stat,
|
|
574
|
-
unlink,
|
|
1671
|
+
unlink as unlink2,
|
|
575
1672
|
writeFile
|
|
576
1673
|
} from "fs/promises";
|
|
577
|
-
import
|
|
1674
|
+
import path2 from "path";
|
|
578
1675
|
function isMissingPathError(error) {
|
|
579
1676
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
580
1677
|
}
|
|
581
1678
|
function isPathWithin(root, target) {
|
|
582
|
-
const relative =
|
|
583
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
1679
|
+
const relative = path2.relative(root, target);
|
|
1680
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path2.sep}`) && !path2.isAbsolute(relative);
|
|
584
1681
|
}
|
|
585
1682
|
function relativePathWithin(root, target) {
|
|
586
|
-
const relative =
|
|
1683
|
+
const relative = path2.relative(root, target);
|
|
587
1684
|
if (relative.length === 0 || !isPathWithin(root, target)) {
|
|
588
1685
|
throw new Error("SpotPatch init refuses to modify a path outside the app root.");
|
|
589
1686
|
}
|
|
590
|
-
return relative.split(
|
|
1687
|
+
return relative.split(path2.sep).join("/");
|
|
591
1688
|
}
|
|
592
1689
|
async function integrationPathExists(absolutePath) {
|
|
593
1690
|
try {
|
|
@@ -598,10 +1695,10 @@ async function integrationPathExists(absolutePath) {
|
|
|
598
1695
|
}
|
|
599
1696
|
}
|
|
600
1697
|
async function readIntegrationFile(absolutePath) {
|
|
601
|
-
const metadata = await
|
|
1698
|
+
const metadata = await lstat2(absolutePath);
|
|
602
1699
|
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
603
1700
|
throw new Error(
|
|
604
|
-
`SpotPatch refuses to modify the non-regular file ${
|
|
1701
|
+
`SpotPatch refuses to modify the non-regular file ${path2.basename(absolutePath)}.`
|
|
605
1702
|
);
|
|
606
1703
|
}
|
|
607
1704
|
return readFile(absolutePath, "utf8");
|
|
@@ -610,8 +1707,8 @@ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previou
|
|
|
610
1707
|
if (previousContent === nextContent) {
|
|
611
1708
|
return void 0;
|
|
612
1709
|
}
|
|
613
|
-
const root =
|
|
614
|
-
const target =
|
|
1710
|
+
const root = path2.resolve(appRoot);
|
|
1711
|
+
const target = path2.resolve(absolutePath);
|
|
615
1712
|
return Object.freeze({
|
|
616
1713
|
absolutePath: target,
|
|
617
1714
|
nextContent,
|
|
@@ -620,19 +1717,19 @@ function createIntegrationFileChange(appRoot, absolutePath, nextContent, previou
|
|
|
620
1717
|
});
|
|
621
1718
|
}
|
|
622
1719
|
function temporaryPath(absolutePath, label) {
|
|
623
|
-
return
|
|
624
|
-
|
|
625
|
-
`.${
|
|
1720
|
+
return path2.join(
|
|
1721
|
+
path2.dirname(absolutePath),
|
|
1722
|
+
`.${path2.basename(absolutePath)}.spotpatch-${label}-${String(process.pid)}-${randomBytes6(8).toString("hex")}`
|
|
626
1723
|
);
|
|
627
1724
|
}
|
|
628
1725
|
async function writeAtomic(absolutePath, content, mode) {
|
|
629
|
-
await mkdir(
|
|
1726
|
+
await mkdir(path2.dirname(absolutePath), { recursive: true });
|
|
630
1727
|
const stagedPath = temporaryPath(absolutePath, "stage");
|
|
631
1728
|
try {
|
|
632
1729
|
await writeFile(stagedPath, content, { encoding: "utf8", flag: "wx", mode });
|
|
633
|
-
await
|
|
1730
|
+
await rename2(stagedPath, absolutePath);
|
|
634
1731
|
} catch (error) {
|
|
635
|
-
await
|
|
1732
|
+
await unlink2(stagedPath).catch(() => void 0);
|
|
636
1733
|
throw error;
|
|
637
1734
|
}
|
|
638
1735
|
}
|
|
@@ -644,21 +1741,21 @@ async function rollbackChange(change) {
|
|
|
644
1741
|
);
|
|
645
1742
|
}
|
|
646
1743
|
if (change.previousContent === void 0) {
|
|
647
|
-
await
|
|
1744
|
+
await unlink2(change.absolutePath);
|
|
648
1745
|
return;
|
|
649
1746
|
}
|
|
650
1747
|
const mode = (await stat(change.absolutePath)).mode & 511;
|
|
651
1748
|
await writeAtomic(change.absolutePath, change.previousContent, mode);
|
|
652
1749
|
}
|
|
653
1750
|
async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
654
|
-
const target =
|
|
1751
|
+
const target = path2.resolve(change.absolutePath);
|
|
655
1752
|
const relativePath = relativePathWithin(appRoot, target);
|
|
656
|
-
if (target !== change.absolutePath || relativePath !== change.relativePath ||
|
|
1753
|
+
if (target !== change.absolutePath || relativePath !== change.relativePath || path2.dirname(target) === target) {
|
|
657
1754
|
throw new Error("SpotPatch init received an invalid integration file plan.");
|
|
658
1755
|
}
|
|
659
1756
|
let targetMetadata;
|
|
660
1757
|
try {
|
|
661
|
-
targetMetadata = await
|
|
1758
|
+
targetMetadata = await lstat2(target);
|
|
662
1759
|
} catch (error) {
|
|
663
1760
|
if (!isMissingPathError(error)) {
|
|
664
1761
|
throw error;
|
|
@@ -670,7 +1767,7 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
|
670
1767
|
);
|
|
671
1768
|
}
|
|
672
1769
|
const containmentAnchor = await realpath(
|
|
673
|
-
targetMetadata === void 0 ?
|
|
1770
|
+
targetMetadata === void 0 ? path2.dirname(target) : target
|
|
674
1771
|
);
|
|
675
1772
|
if (!isPathWithin(realAppRoot, containmentAnchor)) {
|
|
676
1773
|
throw new Error("SpotPatch init refuses to modify a path outside the app root.");
|
|
@@ -679,7 +1776,7 @@ async function assertSafeTarget(appRoot, realAppRoot, change) {
|
|
|
679
1776
|
async function assertCurrentBaseline(change) {
|
|
680
1777
|
if (change.previousContent === void 0) {
|
|
681
1778
|
try {
|
|
682
|
-
await
|
|
1779
|
+
await lstat2(change.absolutePath);
|
|
683
1780
|
} catch (error) {
|
|
684
1781
|
if (isMissingPathError(error)) {
|
|
685
1782
|
return;
|
|
@@ -701,7 +1798,7 @@ async function applyIntegrationPlan(plan) {
|
|
|
701
1798
|
if (plan.changes.length === 0) {
|
|
702
1799
|
return;
|
|
703
1800
|
}
|
|
704
|
-
const appRoot =
|
|
1801
|
+
const appRoot = path2.resolve(plan.appRoot);
|
|
705
1802
|
const realAppRoot = await realpath(appRoot);
|
|
706
1803
|
const targets = /* @__PURE__ */ new Set();
|
|
707
1804
|
for (const change of plan.changes) {
|
|
@@ -780,7 +1877,8 @@ var DEFAULT_OPTIONS = Object.freeze({
|
|
|
780
1877
|
enabled: false,
|
|
781
1878
|
runtime: "dispatch",
|
|
782
1879
|
limits: DEFAULT_DATA_FLOW_LIMITS
|
|
783
|
-
})
|
|
1880
|
+
}),
|
|
1881
|
+
externalAgent: Object.freeze({ enabled: false })
|
|
784
1882
|
});
|
|
785
1883
|
var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
786
1884
|
var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
@@ -1106,6 +2204,9 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1106
2204
|
if (options.trustedFastMode !== void 0 && typeof options.trustedFastMode !== "boolean") {
|
|
1107
2205
|
throw new RangeError("SpotPatch trustedFastMode must be a boolean.");
|
|
1108
2206
|
}
|
|
2207
|
+
if (options.externalAgent !== void 0 && typeof options.externalAgent !== "boolean") {
|
|
2208
|
+
throw new RangeError("SpotPatch externalAgent must be a boolean.");
|
|
2209
|
+
}
|
|
1109
2210
|
const budget = Object.freeze({
|
|
1110
2211
|
...DEFAULT_OPTIONS.budget,
|
|
1111
2212
|
...options.budget
|
|
@@ -1138,7 +2239,10 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1138
2239
|
locale,
|
|
1139
2240
|
maxTargets,
|
|
1140
2241
|
ai: resolveAiOptions(options.ai ?? environmentAi),
|
|
1141
|
-
dataFlow: resolveDataFlowOptions(options.dataFlow)
|
|
2242
|
+
dataFlow: resolveDataFlowOptions(options.dataFlow),
|
|
2243
|
+
externalAgent: Object.freeze({
|
|
2244
|
+
enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
|
|
2245
|
+
})
|
|
1142
2246
|
};
|
|
1143
2247
|
if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
|
|
1144
2248
|
throw new RangeError("SpotPatch shortcut is invalid.");
|
|
@@ -1148,9 +2252,9 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
1148
2252
|
|
|
1149
2253
|
// src/project-validation.ts
|
|
1150
2254
|
import { execFile } from "child_process";
|
|
1151
|
-
import { access as access2, lstat as
|
|
2255
|
+
import { access as access2, lstat as lstat3, readFile as readFile2, realpath as realpath2 } from "fs/promises";
|
|
1152
2256
|
import { createRequire } from "module";
|
|
1153
|
-
import
|
|
2257
|
+
import path3 from "path";
|
|
1154
2258
|
import { promisify } from "util";
|
|
1155
2259
|
var execFileAsync = promisify(execFile);
|
|
1156
2260
|
var TYPESCRIPT_CHECK_ID = "spotpatch-typecheck";
|
|
@@ -1160,14 +2264,14 @@ function isRecord(value) {
|
|
|
1160
2264
|
}
|
|
1161
2265
|
async function isRegularFile(absolutePath) {
|
|
1162
2266
|
try {
|
|
1163
|
-
const metadata = await
|
|
2267
|
+
const metadata = await lstat3(absolutePath);
|
|
1164
2268
|
return metadata.isFile() && !metadata.isSymbolicLink();
|
|
1165
2269
|
} catch {
|
|
1166
2270
|
return false;
|
|
1167
2271
|
}
|
|
1168
2272
|
}
|
|
1169
2273
|
async function readManifest(appRoot) {
|
|
1170
|
-
const manifestPath =
|
|
2274
|
+
const manifestPath = path3.join(appRoot, "package.json");
|
|
1171
2275
|
if (!await isRegularFile(manifestPath)) {
|
|
1172
2276
|
return void 0;
|
|
1173
2277
|
}
|
|
@@ -1196,8 +2300,8 @@ async function findGitRoot(appRoot) {
|
|
|
1196
2300
|
windowsHide: true
|
|
1197
2301
|
});
|
|
1198
2302
|
const root = await realpath2(result.stdout.trim());
|
|
1199
|
-
const relative =
|
|
1200
|
-
if (relative === "" || !relative.startsWith(`..${
|
|
2303
|
+
const relative = path3.relative(root, appRoot);
|
|
2304
|
+
if (relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative)) {
|
|
1201
2305
|
return root;
|
|
1202
2306
|
}
|
|
1203
2307
|
} catch {
|
|
@@ -1206,10 +2310,10 @@ async function findGitRoot(appRoot) {
|
|
|
1206
2310
|
return void 0;
|
|
1207
2311
|
}
|
|
1208
2312
|
async function resolveTypeScriptCli(appRoot) {
|
|
1209
|
-
const resolveFromApplication = createRequire(
|
|
2313
|
+
const resolveFromApplication = createRequire(path3.join(appRoot, "package.json"));
|
|
1210
2314
|
try {
|
|
1211
2315
|
const packagePath = resolveFromApplication.resolve("typescript/package.json");
|
|
1212
|
-
const cliPath =
|
|
2316
|
+
const cliPath = path3.join(path3.dirname(packagePath), "bin", "tsc");
|
|
1213
2317
|
await access2(cliPath);
|
|
1214
2318
|
return await realpath2(cliPath);
|
|
1215
2319
|
} catch {
|
|
@@ -1217,11 +2321,11 @@ async function resolveTypeScriptCli(appRoot) {
|
|
|
1217
2321
|
}
|
|
1218
2322
|
}
|
|
1219
2323
|
function portableRelativePath(from, to) {
|
|
1220
|
-
return
|
|
2324
|
+
return path3.relative(from, to).split(path3.sep).join("/");
|
|
1221
2325
|
}
|
|
1222
2326
|
async function discoverProjectValidationCheck(options) {
|
|
1223
2327
|
const appRoot = await realpath2(options.appRoot);
|
|
1224
|
-
const tsconfigPath =
|
|
2328
|
+
const tsconfigPath = path3.join(appRoot, "tsconfig.json");
|
|
1225
2329
|
const [manifest, projectRoot, hasTsconfig] = await Promise.all([
|
|
1226
2330
|
readManifest(appRoot),
|
|
1227
2331
|
findGitRoot(appRoot),
|
|
@@ -1309,16 +2413,16 @@ async function resolveProjectOptions(input) {
|
|
|
1309
2413
|
}
|
|
1310
2414
|
|
|
1311
2415
|
// src/registry/source-registry.ts
|
|
1312
|
-
import
|
|
2416
|
+
import path4 from "path";
|
|
1313
2417
|
|
|
1314
2418
|
// src/registry/source-id.ts
|
|
1315
|
-
import { randomBytes as
|
|
2419
|
+
import { randomBytes as randomBytes7 } from "crypto";
|
|
1316
2420
|
var SOURCE_ID_BYTES = 8;
|
|
1317
|
-
var createRandomSourceId = () =>
|
|
2421
|
+
var createRandomSourceId = () => randomBytes7(SOURCE_ID_BYTES).toString("base64url");
|
|
1318
2422
|
|
|
1319
2423
|
// src/registry/source-registry.ts
|
|
1320
2424
|
function normalizeAbsolutePath(absolutePath) {
|
|
1321
|
-
return
|
|
2425
|
+
return path4.normalize(path4.resolve(absolutePath));
|
|
1322
2426
|
}
|
|
1323
2427
|
function createSourceRegistry(options = {}) {
|
|
1324
2428
|
const createId = options.createId ?? createRandomSourceId;
|
|
@@ -1376,39 +2480,42 @@ function createSourceRegistry(options = {}) {
|
|
|
1376
2480
|
|
|
1377
2481
|
// src/server/middleware.ts
|
|
1378
2482
|
import {
|
|
1379
|
-
ERROR_CODES as
|
|
2483
|
+
ERROR_CODES as ERROR_CODES15,
|
|
1380
2484
|
SPOTPATCH_API_BASE,
|
|
1381
|
-
SPOTPATCH_ENDPOINTS as
|
|
1382
|
-
SpotPatchError as
|
|
2485
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS3,
|
|
2486
|
+
SpotPatchError as SpotPatchError15,
|
|
1383
2487
|
openEditorRequestSchema,
|
|
1384
2488
|
sourceContextRequestSchema
|
|
1385
2489
|
} from "@spotpatch/shared";
|
|
1386
2490
|
|
|
1387
|
-
// src/
|
|
2491
|
+
// src/external-handoff/browser-http.ts
|
|
1388
2492
|
import {
|
|
1389
|
-
ERROR_CODES as
|
|
2493
|
+
ERROR_CODES as ERROR_CODES10,
|
|
2494
|
+
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS5,
|
|
1390
2495
|
SPOTPATCH_ENDPOINTS,
|
|
1391
|
-
SpotPatchError as
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
2496
|
+
SpotPatchError as SpotPatchError10,
|
|
2497
|
+
externalHandoffCapabilityRequestSchema,
|
|
2498
|
+
externalHandoffPublishRequestSchema,
|
|
2499
|
+
externalHandoffResolveDeliveryRequestSchema,
|
|
2500
|
+
externalHandoffStatusRequestSchema
|
|
1396
2501
|
} from "@spotpatch/shared";
|
|
1397
2502
|
|
|
1398
|
-
// src/server/
|
|
2503
|
+
// src/server/annotation-authorizer.ts
|
|
1399
2504
|
import { realpath as realpath5 } from "fs/promises";
|
|
1400
|
-
import
|
|
2505
|
+
import path7 from "path";
|
|
1401
2506
|
import {
|
|
1402
|
-
ERROR_CODES as
|
|
1403
|
-
SpotPatchError as
|
|
2507
|
+
ERROR_CODES as ERROR_CODES9,
|
|
2508
|
+
SpotPatchError as SpotPatchError9,
|
|
2509
|
+
redactSensitiveText,
|
|
2510
|
+
sanitizeUrl
|
|
1404
2511
|
} from "@spotpatch/shared";
|
|
1405
2512
|
|
|
1406
2513
|
// src/server/source-context.ts
|
|
1407
2514
|
import { readFile as readFile3, realpath as realpath4 } from "fs/promises";
|
|
1408
|
-
import
|
|
2515
|
+
import path6 from "path";
|
|
1409
2516
|
import {
|
|
1410
|
-
ERROR_CODES as
|
|
1411
|
-
SpotPatchError as
|
|
2517
|
+
ERROR_CODES as ERROR_CODES8,
|
|
2518
|
+
SpotPatchError as SpotPatchError8
|
|
1412
2519
|
} from "@spotpatch/shared";
|
|
1413
2520
|
|
|
1414
2521
|
// src/server/extract-code-context.ts
|
|
@@ -1642,15 +2749,8 @@ function extractCodeContext(options) {
|
|
|
1642
2749
|
|
|
1643
2750
|
// src/server/source-file.ts
|
|
1644
2751
|
import { realpath as realpath3, stat as stat2 } from "fs/promises";
|
|
1645
|
-
import
|
|
1646
|
-
import { ERROR_CODES as
|
|
1647
|
-
|
|
1648
|
-
// src/server/constants.ts
|
|
1649
|
-
var MAX_REQUEST_BODY_BYTES = 32 * 1024;
|
|
1650
|
-
var MAX_AGENT_REQUEST_BODY_BYTES = 256 * 1024;
|
|
1651
|
-
var MAX_SOURCE_FILE_BYTES = 1024 * 1024;
|
|
1652
|
-
|
|
1653
|
-
// src/server/source-file.ts
|
|
2752
|
+
import path5 from "path";
|
|
2753
|
+
import { ERROR_CODES as ERROR_CODES7, SpotPatchError as SpotPatchError7 } from "@spotpatch/shared";
|
|
1654
2754
|
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
1655
2755
|
function isMissingFileError(error) {
|
|
1656
2756
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
@@ -1665,51 +2765,51 @@ async function assertInsideRoot(root, candidate) {
|
|
|
1665
2765
|
]);
|
|
1666
2766
|
} catch (error) {
|
|
1667
2767
|
if (isMissingFileError(error)) {
|
|
1668
|
-
throw new
|
|
2768
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND, void 0, {
|
|
1669
2769
|
cause: error
|
|
1670
2770
|
});
|
|
1671
2771
|
}
|
|
1672
2772
|
throw error;
|
|
1673
2773
|
}
|
|
1674
|
-
const relative =
|
|
1675
|
-
const outside = relative.startsWith(`..${
|
|
2774
|
+
const relative = path5.relative(realRoot, realCandidate);
|
|
2775
|
+
const outside = relative.startsWith(`..${path5.sep}`) || relative === ".." || path5.isAbsolute(relative);
|
|
1676
2776
|
if (outside) {
|
|
1677
|
-
throw new
|
|
2777
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_OUTSIDE_ROOT);
|
|
1678
2778
|
}
|
|
1679
2779
|
return realCandidate;
|
|
1680
2780
|
}
|
|
1681
2781
|
async function resolveSourceFile(options) {
|
|
1682
2782
|
const registeredPath = options.registry.resolve(options.fileId);
|
|
1683
2783
|
if (registeredPath === void 0) {
|
|
1684
|
-
throw new
|
|
2784
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
1685
2785
|
}
|
|
1686
2786
|
const sourcePath = await assertInsideRoot(options.root, registeredPath);
|
|
1687
|
-
if (!ALLOWED_EXTENSIONS.has(
|
|
1688
|
-
throw new
|
|
2787
|
+
if (!ALLOWED_EXTENSIONS.has(path5.extname(sourcePath).toLowerCase())) {
|
|
2788
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
1689
2789
|
}
|
|
1690
2790
|
let sourceStat;
|
|
1691
2791
|
try {
|
|
1692
2792
|
sourceStat = await stat2(sourcePath);
|
|
1693
2793
|
} catch (error) {
|
|
1694
2794
|
if (isMissingFileError(error)) {
|
|
1695
|
-
throw new
|
|
2795
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND, void 0, {
|
|
1696
2796
|
cause: error
|
|
1697
2797
|
});
|
|
1698
2798
|
}
|
|
1699
2799
|
throw error;
|
|
1700
2800
|
}
|
|
1701
2801
|
if (!sourceStat.isFile()) {
|
|
1702
|
-
throw new
|
|
2802
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
1703
2803
|
}
|
|
1704
2804
|
if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
|
|
1705
|
-
throw new
|
|
2805
|
+
throw new SpotPatchError7(ERROR_CODES7.SOURCE_TOO_LARGE);
|
|
1706
2806
|
}
|
|
1707
2807
|
return sourcePath;
|
|
1708
2808
|
}
|
|
1709
2809
|
|
|
1710
2810
|
// src/server/source-context.ts
|
|
1711
2811
|
function toDisplayPath(root, sourcePath) {
|
|
1712
|
-
return
|
|
2812
|
+
return path6.relative(root, sourcePath).split(path6.sep).join("/");
|
|
1713
2813
|
}
|
|
1714
2814
|
async function readSourceContext(options) {
|
|
1715
2815
|
const sourcePath = await resolveSourceFile({
|
|
@@ -1722,7 +2822,7 @@ async function readSourceContext(options) {
|
|
|
1722
2822
|
source = await readFile3(sourcePath, "utf8");
|
|
1723
2823
|
} catch (error) {
|
|
1724
2824
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1725
|
-
throw new
|
|
2825
|
+
throw new SpotPatchError8(ERROR_CODES8.SOURCE_NOT_FOUND, void 0, {
|
|
1726
2826
|
cause: error
|
|
1727
2827
|
});
|
|
1728
2828
|
}
|
|
@@ -1730,9 +2830,9 @@ async function readSourceContext(options) {
|
|
|
1730
2830
|
}
|
|
1731
2831
|
const lines = source.split(/\r?\n/);
|
|
1732
2832
|
if (options.request.line > lines.length) {
|
|
1733
|
-
throw new
|
|
2833
|
+
throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
|
|
1734
2834
|
}
|
|
1735
|
-
const extension =
|
|
2835
|
+
const extension = path6.extname(sourcePath).toLowerCase();
|
|
1736
2836
|
return extractCodeContext({
|
|
1737
2837
|
source,
|
|
1738
2838
|
sourcePath,
|
|
@@ -1745,7 +2845,17 @@ async function readSourceContext(options) {
|
|
|
1745
2845
|
});
|
|
1746
2846
|
}
|
|
1747
2847
|
|
|
1748
|
-
// 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
|
+
}
|
|
1749
2859
|
function compactSourceRef(source) {
|
|
1750
2860
|
return Object.freeze({
|
|
1751
2861
|
origin: source.origin,
|
|
@@ -1759,27 +2869,20 @@ function compactSourceRef(source) {
|
|
|
1759
2869
|
async function authorizeSourceRef(source, registry, root) {
|
|
1760
2870
|
const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
|
|
1761
2871
|
if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
|
|
1762
|
-
throw new
|
|
2872
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1763
2873
|
}
|
|
1764
2874
|
if (source.fileId === void 0) {
|
|
1765
2875
|
if (source.origin === "none" && source.relativePath !== void 0) {
|
|
1766
|
-
throw new
|
|
2876
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1767
2877
|
}
|
|
1768
2878
|
return compactSourceRef(source);
|
|
1769
2879
|
}
|
|
1770
|
-
const sourcePath = await resolveSourceFile({
|
|
1771
|
-
|
|
1772
|
-
registry,
|
|
1773
|
-
root
|
|
1774
|
-
});
|
|
1775
|
-
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("/");
|
|
1776
2882
|
if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
|
|
1777
|
-
throw new
|
|
2883
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1778
2884
|
}
|
|
1779
|
-
return Object.freeze({
|
|
1780
|
-
...compactSourceRef(source),
|
|
1781
|
-
relativePath
|
|
1782
|
-
});
|
|
2885
|
+
return Object.freeze({ ...compactSourceRef(source), relativePath });
|
|
1783
2886
|
}
|
|
1784
2887
|
function freezeMatchedRule(rule) {
|
|
1785
2888
|
return Object.freeze({
|
|
@@ -1813,7 +2916,7 @@ async function authorizeTarget(target, input) {
|
|
|
1813
2916
|
maxLines: input.options.budget.maxCodeLines
|
|
1814
2917
|
});
|
|
1815
2918
|
if (marker === void 0 && target.code !== void 0) {
|
|
1816
|
-
throw new
|
|
2919
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1817
2920
|
}
|
|
1818
2921
|
const code = marker === void 0 ? void 0 : await readSourceContext({
|
|
1819
2922
|
request: marker,
|
|
@@ -1823,11 +2926,11 @@ async function authorizeTarget(target, input) {
|
|
|
1823
2926
|
maxLines: input.options.budget.maxCodeLines
|
|
1824
2927
|
});
|
|
1825
2928
|
if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
|
|
1826
|
-
throw new
|
|
2929
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1827
2930
|
}
|
|
1828
2931
|
return Object.freeze({
|
|
1829
2932
|
instruction: target.instruction,
|
|
1830
|
-
...target.page === void 0 ? {} : { page:
|
|
2933
|
+
...target.page === void 0 ? {} : { page: sanitizePageContext(target.page) },
|
|
1831
2934
|
source,
|
|
1832
2935
|
react: Object.freeze({
|
|
1833
2936
|
supported: target.react.supported,
|
|
@@ -1855,76 +2958,138 @@ async function authorizeTarget(target, input) {
|
|
|
1855
2958
|
warnings: Object.freeze([...target.warnings])
|
|
1856
2959
|
});
|
|
1857
2960
|
}
|
|
1858
|
-
async function
|
|
1859
|
-
const requestedTargets = input.
|
|
2961
|
+
async function authorizeAnnotation(input) {
|
|
2962
|
+
const requestedTargets = input.annotation.targets;
|
|
1860
2963
|
if (requestedTargets.length > input.options.maxTargets) {
|
|
1861
|
-
throw new
|
|
2964
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1862
2965
|
}
|
|
1863
2966
|
const identities = requestedTargets.map(targetIdentity);
|
|
1864
2967
|
if (new Set(identities).size !== identities.length) {
|
|
1865
|
-
throw new
|
|
2968
|
+
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
1866
2969
|
}
|
|
1867
2970
|
const targets = Object.freeze(
|
|
1868
2971
|
await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
|
|
1869
2972
|
);
|
|
1870
|
-
|
|
2973
|
+
return Object.freeze({
|
|
1871
2974
|
schemaVersion: 3,
|
|
1872
|
-
id: input.
|
|
1873
|
-
locale: input.
|
|
1874
|
-
page:
|
|
2975
|
+
id: input.annotation.id,
|
|
2976
|
+
locale: input.annotation.locale,
|
|
2977
|
+
page: sanitizePageContext(input.annotation.page),
|
|
1875
2978
|
targets,
|
|
1876
|
-
createdAt: input.
|
|
1877
|
-
});
|
|
1878
|
-
return Object.freeze({
|
|
1879
|
-
annotation,
|
|
1880
|
-
...input.request.applyMode === void 0 ? {} : { applyMode: input.request.applyMode },
|
|
1881
|
-
providerProfileId: input.request.providerProfileId,
|
|
1882
|
-
modelProfileId: input.request.modelProfileId,
|
|
1883
|
-
providerDataConsent: true,
|
|
1884
|
-
...input.request.trustedFastModeConsent === true ? { trustedFastModeConsent: true } : {},
|
|
1885
|
-
workingTreeMode: input.request.workingTreeMode
|
|
2979
|
+
createdAt: input.annotation.createdAt
|
|
1886
2980
|
});
|
|
1887
2981
|
}
|
|
1888
2982
|
|
|
1889
|
-
// src/
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
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";
|
|
1897
2990
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
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);
|
|
1901
2996
|
}
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
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
|
+
});
|
|
1909
3005
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
continue;
|
|
3006
|
+
if (error.code === ERROR_CODES10.INVALID_REQUEST) {
|
|
3007
|
+
throw new SpotPatchError10(ERROR_CODES10.HANDOFF_VALIDATION_FAILED, void 0, {
|
|
3008
|
+
cause: error
|
|
3009
|
+
});
|
|
1915
3010
|
}
|
|
1916
|
-
chunks.push(buffer);
|
|
1917
3011
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
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);
|
|
1920
3017
|
}
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
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;
|
|
1927
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);
|
|
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
|
+
});
|
|
1928
3093
|
}
|
|
1929
3094
|
|
|
1930
3095
|
// src/server/agent-http.ts
|
|
@@ -1944,21 +3109,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
|
|
|
1944
3109
|
"reverted",
|
|
1945
3110
|
"failed"
|
|
1946
3111
|
]);
|
|
1947
|
-
function matchAgentRequestPath(
|
|
1948
|
-
if (
|
|
3112
|
+
function matchAgentRequestPath(path9) {
|
|
3113
|
+
if (path9 === SPOTPATCH_ENDPOINTS2.agentCapability) {
|
|
1949
3114
|
return Object.freeze({ kind: "capability" });
|
|
1950
3115
|
}
|
|
1951
|
-
if (
|
|
3116
|
+
if (path9 === SPOTPATCH_ENDPOINTS2.agentWorkspaceHealth) {
|
|
1952
3117
|
return Object.freeze({ kind: "workspace-health" });
|
|
1953
3118
|
}
|
|
1954
|
-
if (
|
|
3119
|
+
if (path9 === SPOTPATCH_ENDPOINTS2.agentJobs) {
|
|
1955
3120
|
return Object.freeze({ kind: "create-job" });
|
|
1956
3121
|
}
|
|
1957
|
-
const prefix = `${
|
|
1958
|
-
if (!
|
|
3122
|
+
const prefix = `${SPOTPATCH_ENDPOINTS2.agentJobs}/`;
|
|
3123
|
+
if (!path9.startsWith(prefix)) {
|
|
1959
3124
|
return void 0;
|
|
1960
3125
|
}
|
|
1961
|
-
const segments =
|
|
3126
|
+
const segments = path9.slice(prefix.length).split("/");
|
|
1962
3127
|
const jobId = segments[0];
|
|
1963
3128
|
const action = segments[1];
|
|
1964
3129
|
if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
|
|
@@ -1972,7 +3137,7 @@ function matchAgentRequestPath(path8) {
|
|
|
1972
3137
|
}
|
|
1973
3138
|
function requireAgentManager(options) {
|
|
1974
3139
|
if (options.agentManager === void 0 || options.options.ai === false) {
|
|
1975
|
-
throw new
|
|
3140
|
+
throw new SpotPatchError11(ERROR_CODES11.AI_DISABLED);
|
|
1976
3141
|
}
|
|
1977
3142
|
return options.agentManager;
|
|
1978
3143
|
}
|
|
@@ -2025,13 +3190,13 @@ function streamAgentJobEvents(response, manager, jobId) {
|
|
|
2025
3190
|
}
|
|
2026
3191
|
async function handleCapability(request, response, options, writeSuccess) {
|
|
2027
3192
|
if (request.method !== "POST") {
|
|
2028
|
-
throw new
|
|
3193
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
2029
3194
|
}
|
|
2030
3195
|
const parsed = agentCapabilityRequestSchema.safeParse(
|
|
2031
3196
|
await readJsonRequestBody(request)
|
|
2032
3197
|
);
|
|
2033
3198
|
if (!parsed.success) {
|
|
2034
|
-
throw new
|
|
3199
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
2035
3200
|
}
|
|
2036
3201
|
const controller = new AbortController();
|
|
2037
3202
|
const abort = () => {
|
|
@@ -2050,13 +3215,13 @@ async function handleCapability(request, response, options, writeSuccess) {
|
|
|
2050
3215
|
}
|
|
2051
3216
|
async function handleCreateJob(request, response, options, writeSuccess) {
|
|
2052
3217
|
if (request.method !== "POST") {
|
|
2053
|
-
throw new
|
|
3218
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
2054
3219
|
}
|
|
2055
3220
|
const parsed = agentJobCreateRequestSchema.safeParse(
|
|
2056
3221
|
await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
|
|
2057
3222
|
);
|
|
2058
3223
|
if (!parsed.success) {
|
|
2059
|
-
throw new
|
|
3224
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
2060
3225
|
}
|
|
2061
3226
|
const authorizedRequest = await authorizeAgentJobRequest({
|
|
2062
3227
|
request: parsed.data,
|
|
@@ -2069,13 +3234,13 @@ async function handleCreateJob(request, response, options, writeSuccess) {
|
|
|
2069
3234
|
}
|
|
2070
3235
|
async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
2071
3236
|
if (request.method !== "POST") {
|
|
2072
|
-
throw new
|
|
3237
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
2073
3238
|
}
|
|
2074
3239
|
const parsed = agentWorkspaceHealthRequestSchema.safeParse(
|
|
2075
3240
|
await readJsonRequestBody(request)
|
|
2076
3241
|
);
|
|
2077
3242
|
if (!parsed.success) {
|
|
2078
|
-
throw new
|
|
3243
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
2079
3244
|
}
|
|
2080
3245
|
const controller = new AbortController();
|
|
2081
3246
|
const abort = () => {
|
|
@@ -2092,13 +3257,13 @@ async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
|
2092
3257
|
async function handleJobAction(request, response, options, route, writeSuccess) {
|
|
2093
3258
|
const manager = requireAgentManager(options);
|
|
2094
3259
|
if (request.method !== "POST") {
|
|
2095
|
-
throw new
|
|
3260
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
2096
3261
|
}
|
|
2097
3262
|
const parsed = agentJobActionRequestSchema.safeParse(
|
|
2098
3263
|
await readJsonRequestBody(request)
|
|
2099
3264
|
);
|
|
2100
3265
|
if (!parsed.success) {
|
|
2101
|
-
throw new
|
|
3266
|
+
throw new SpotPatchError11(ERROR_CODES11.INVALID_REQUEST);
|
|
2102
3267
|
}
|
|
2103
3268
|
if (route.action === "events") {
|
|
2104
3269
|
streamAgentJobEvents(response, manager, route.jobId);
|
|
@@ -2213,14 +3378,14 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
2213
3378
|
var launchConfiguredEditor = createEditorLauncher();
|
|
2214
3379
|
|
|
2215
3380
|
// src/server/data-flow-http.ts
|
|
2216
|
-
import { createHash as
|
|
3381
|
+
import { createHash as createHash3 } from "crypto";
|
|
2217
3382
|
import {
|
|
2218
3383
|
createStaticDataFlowAnalyzer
|
|
2219
3384
|
} from "@spotpatch/analyzer";
|
|
2220
3385
|
import {
|
|
2221
3386
|
DATA_FLOW_SCHEMA_VERSION,
|
|
2222
|
-
ERROR_CODES as
|
|
2223
|
-
SpotPatchError as
|
|
3387
|
+
ERROR_CODES as ERROR_CODES12,
|
|
3388
|
+
SpotPatchError as SpotPatchError12,
|
|
2224
3389
|
dataFlowComponentReportRequestSchema,
|
|
2225
3390
|
dataFlowPageReportRequestSchema,
|
|
2226
3391
|
limitDataFlowReportCollections
|
|
@@ -2239,7 +3404,7 @@ function limitDataFlowReportToBytes(report, maximumBytes) {
|
|
|
2239
3404
|
truncatedBy: "bytes"
|
|
2240
3405
|
});
|
|
2241
3406
|
if (envelopeBytes(limited) > maximumBytes) {
|
|
2242
|
-
throw new
|
|
3407
|
+
throw new SpotPatchError12(ERROR_CODES12.INTERNAL_ERROR);
|
|
2243
3408
|
}
|
|
2244
3409
|
for (let maximumDependencies = 1; maximumDependencies <= structurallyLimited.dependencies.length; maximumDependencies += 1) {
|
|
2245
3410
|
const candidate = limitDataFlowReportCollections(structurallyLimited, {
|
|
@@ -2268,7 +3433,7 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
2268
3433
|
request.componentSourceId
|
|
2269
3434
|
);
|
|
2270
3435
|
if (anchor?.sourceVersion !== request.sourceVersion) {
|
|
2271
|
-
throw new
|
|
3436
|
+
throw new SpotPatchError12(ERROR_CODES12.DATA_FLOW_SOURCE_STALE);
|
|
2272
3437
|
}
|
|
2273
3438
|
return anchor;
|
|
2274
3439
|
}
|
|
@@ -2285,7 +3450,7 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
2285
3450
|
column: resolvedRequest.column
|
|
2286
3451
|
});
|
|
2287
3452
|
if (resolvedRequest.sourceVersion !== void 0 && resolvedRequest.sourceVersion !== report.component.source.sourceVersion) {
|
|
2288
|
-
throw new
|
|
3453
|
+
throw new SpotPatchError12(ERROR_CODES12.DATA_FLOW_SOURCE_STALE);
|
|
2289
3454
|
}
|
|
2290
3455
|
return limitDataFlowReportToBytes(
|
|
2291
3456
|
report,
|
|
@@ -2294,7 +3459,7 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
2294
3459
|
}
|
|
2295
3460
|
function requireAnalyzer(analyzer) {
|
|
2296
3461
|
if (analyzer === void 0) {
|
|
2297
|
-
throw new
|
|
3462
|
+
throw new SpotPatchError12(ERROR_CODES12.DATA_FLOW_DISABLED);
|
|
2298
3463
|
}
|
|
2299
3464
|
return analyzer;
|
|
2300
3465
|
}
|
|
@@ -2306,7 +3471,7 @@ async function handleComponentDataFlowReport(request, analyzer, options) {
|
|
|
2306
3471
|
)
|
|
2307
3472
|
);
|
|
2308
3473
|
if (!parsed.success) {
|
|
2309
|
-
throw new
|
|
3474
|
+
throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
|
|
2310
3475
|
}
|
|
2311
3476
|
return analyzeTarget(parsed.data, requireAnalyzer(analyzer), options);
|
|
2312
3477
|
}
|
|
@@ -2318,7 +3483,7 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
2318
3483
|
)
|
|
2319
3484
|
);
|
|
2320
3485
|
if (!parsed.success) {
|
|
2321
|
-
throw new
|
|
3486
|
+
throw new SpotPatchError12(ERROR_CODES12.INVALID_REQUEST);
|
|
2322
3487
|
}
|
|
2323
3488
|
const activeAnalyzer = requireAnalyzer(analyzer);
|
|
2324
3489
|
const componentReports = await Promise.all(
|
|
@@ -2342,7 +3507,7 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
2342
3507
|
const analyzedVersions = new Set(
|
|
2343
3508
|
componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
|
|
2344
3509
|
);
|
|
2345
|
-
const reportId = `page_${
|
|
3510
|
+
const reportId = `page_${createHash3("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
|
|
2346
3511
|
const complete = componentReports.every((report2) => report2.completeness.complete);
|
|
2347
3512
|
const report = Object.freeze({
|
|
2348
3513
|
schemaVersion: DATA_FLOW_SCHEMA_VERSION,
|
|
@@ -2389,20 +3554,20 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
2389
3554
|
}
|
|
2390
3555
|
|
|
2391
3556
|
// src/server/request-security.ts
|
|
2392
|
-
import { timingSafeEqual } from "crypto";
|
|
3557
|
+
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
2393
3558
|
import { isIP } from "net";
|
|
2394
|
-
import { ERROR_CODES as
|
|
3559
|
+
import { ERROR_CODES as ERROR_CODES13, SPOTPATCH_TOKEN_HEADER, SpotPatchError as SpotPatchError13 } from "@spotpatch/shared";
|
|
2395
3560
|
function getSingleHeader(request, name) {
|
|
2396
3561
|
const value = request.headers[name.toLowerCase()];
|
|
2397
3562
|
return Array.isArray(value) ? value[0] : value;
|
|
2398
3563
|
}
|
|
2399
|
-
function
|
|
3564
|
+
function tokensMatch2(actual, expected) {
|
|
2400
3565
|
if (actual === void 0) {
|
|
2401
3566
|
return false;
|
|
2402
3567
|
}
|
|
2403
3568
|
const actualBytes = Buffer.from(actual);
|
|
2404
3569
|
const expectedBytes = Buffer.from(expected);
|
|
2405
|
-
return actualBytes.byteLength === expectedBytes.byteLength &&
|
|
3570
|
+
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual2(actualBytes, expectedBytes);
|
|
2406
3571
|
}
|
|
2407
3572
|
function isLoopbackHostname(hostname) {
|
|
2408
3573
|
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
@@ -2437,33 +3602,33 @@ function parseOrigin(value) {
|
|
|
2437
3602
|
}
|
|
2438
3603
|
function assertRequestAuthorized(request, options) {
|
|
2439
3604
|
const actualToken = getSingleHeader(request, SPOTPATCH_TOKEN_HEADER);
|
|
2440
|
-
if (!
|
|
2441
|
-
throw new
|
|
3605
|
+
if (!tokensMatch2(actualToken, options.sessionToken)) {
|
|
3606
|
+
throw new SpotPatchError13(ERROR_CODES13.INVALID_TOKEN);
|
|
2442
3607
|
}
|
|
2443
3608
|
const hostHeader = getSingleHeader(request, "host");
|
|
2444
3609
|
const originHeader = getSingleHeader(request, "origin");
|
|
2445
3610
|
const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
|
|
2446
3611
|
const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
|
|
2447
3612
|
if (host === void 0 || origin === void 0) {
|
|
2448
|
-
throw new
|
|
3613
|
+
throw new SpotPatchError13(ERROR_CODES13.ORIGIN_NOT_ALLOWED);
|
|
2449
3614
|
}
|
|
2450
3615
|
const hostIsLoopback = isLoopbackHostname(host.hostname);
|
|
2451
3616
|
const originIsLoopback = isLoopbackHostname(origin.hostname);
|
|
2452
3617
|
if (!options.allowLan) {
|
|
2453
3618
|
if (!hostIsLoopback || !originIsLoopback) {
|
|
2454
|
-
throw new
|
|
3619
|
+
throw new SpotPatchError13(ERROR_CODES13.ORIGIN_NOT_ALLOWED);
|
|
2455
3620
|
}
|
|
2456
3621
|
return;
|
|
2457
3622
|
}
|
|
2458
3623
|
if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
|
|
2459
|
-
throw new
|
|
3624
|
+
throw new SpotPatchError13(ERROR_CODES13.ORIGIN_NOT_ALLOWED);
|
|
2460
3625
|
}
|
|
2461
3626
|
}
|
|
2462
3627
|
|
|
2463
3628
|
// src/server/runtime-bootstrap.ts
|
|
2464
3629
|
import {
|
|
2465
|
-
ERROR_CODES as
|
|
2466
|
-
SpotPatchError as
|
|
3630
|
+
ERROR_CODES as ERROR_CODES14,
|
|
3631
|
+
SpotPatchError as SpotPatchError14,
|
|
2467
3632
|
runtimeBootstrapRequestSchema,
|
|
2468
3633
|
runtimeConfigSchema
|
|
2469
3634
|
} from "@spotpatch/shared";
|
|
@@ -2493,7 +3658,7 @@ function resolveRuntimeBootstrapOptions(options) {
|
|
|
2493
3658
|
function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
2494
3659
|
const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
2495
3660
|
if (request.method !== "POST" || contentType !== "application/json") {
|
|
2496
|
-
throw new
|
|
3661
|
+
throw new SpotPatchError14(ERROR_CODES14.INVALID_REQUEST);
|
|
2497
3662
|
}
|
|
2498
3663
|
const host = getSingleHeader2(request, "host");
|
|
2499
3664
|
let hostIsLoopback = false;
|
|
@@ -2505,7 +3670,7 @@ function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
|
2505
3670
|
}
|
|
2506
3671
|
}
|
|
2507
3672
|
if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
|
|
2508
|
-
throw new
|
|
3673
|
+
throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
|
|
2509
3674
|
}
|
|
2510
3675
|
}
|
|
2511
3676
|
async function readRuntimeBootstrap(request, options) {
|
|
@@ -2514,103 +3679,139 @@ async function readRuntimeBootstrap(request, options) {
|
|
|
2514
3679
|
await readJsonRequestBody(request)
|
|
2515
3680
|
);
|
|
2516
3681
|
if (!parsedBody.success) {
|
|
2517
|
-
throw new
|
|
3682
|
+
throw new SpotPatchError14(ERROR_CODES14.INVALID_REQUEST);
|
|
2518
3683
|
}
|
|
2519
3684
|
return options.runtimeConfig;
|
|
2520
3685
|
}
|
|
2521
3686
|
|
|
2522
3687
|
// src/server/middleware.ts
|
|
2523
3688
|
var STATUS_BY_ERROR = Object.freeze({
|
|
2524
|
-
[
|
|
2525
|
-
[
|
|
2526
|
-
[
|
|
2527
|
-
[
|
|
2528
|
-
[
|
|
2529
|
-
[
|
|
2530
|
-
[
|
|
2531
|
-
[
|
|
2532
|
-
[
|
|
2533
|
-
[
|
|
2534
|
-
[
|
|
2535
|
-
[
|
|
2536
|
-
[
|
|
2537
|
-
[
|
|
2538
|
-
[
|
|
2539
|
-
[
|
|
2540
|
-
[
|
|
2541
|
-
[
|
|
2542
|
-
[
|
|
2543
|
-
[
|
|
2544
|
-
[
|
|
2545
|
-
[
|
|
2546
|
-
[
|
|
2547
|
-
[
|
|
2548
|
-
[
|
|
2549
|
-
[
|
|
2550
|
-
[
|
|
2551
|
-
[
|
|
2552
|
-
[
|
|
2553
|
-
[
|
|
2554
|
-
[
|
|
2555
|
-
[
|
|
2556
|
-
[
|
|
2557
|
-
[
|
|
2558
|
-
[
|
|
2559
|
-
[
|
|
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
|
|
2560
3743
|
});
|
|
2561
3744
|
var PUBLIC_MESSAGES = Object.freeze({
|
|
2562
|
-
[
|
|
2563
|
-
[
|
|
2564
|
-
[
|
|
2565
|
-
[
|
|
2566
|
-
[
|
|
2567
|
-
[
|
|
2568
|
-
[
|
|
2569
|
-
[
|
|
2570
|
-
[
|
|
2571
|
-
[
|
|
2572
|
-
[
|
|
2573
|
-
[
|
|
2574
|
-
[
|
|
2575
|
-
[
|
|
2576
|
-
[
|
|
2577
|
-
[
|
|
2578
|
-
[
|
|
2579
|
-
[
|
|
2580
|
-
[
|
|
2581
|
-
[
|
|
2582
|
-
[
|
|
2583
|
-
[
|
|
2584
|
-
[
|
|
2585
|
-
[
|
|
2586
|
-
[
|
|
2587
|
-
[
|
|
2588
|
-
[
|
|
2589
|
-
[
|
|
2590
|
-
[
|
|
2591
|
-
[
|
|
2592
|
-
[
|
|
2593
|
-
[
|
|
2594
|
-
[
|
|
2595
|
-
[
|
|
2596
|
-
[
|
|
2597
|
-
[
|
|
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."
|
|
2598
3799
|
});
|
|
2599
|
-
function
|
|
3800
|
+
function writeJson2(response, status, payload) {
|
|
2600
3801
|
response.statusCode = status;
|
|
2601
3802
|
response.setHeader("Cache-Control", "no-store");
|
|
2602
3803
|
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2603
3804
|
response.end(JSON.stringify(payload));
|
|
2604
3805
|
}
|
|
2605
3806
|
function asSpotPatchError(error) {
|
|
2606
|
-
return error instanceof
|
|
3807
|
+
return error instanceof SpotPatchError15 ? error : new SpotPatchError15(ERROR_CODES15.INTERNAL_ERROR, void 0, { cause: error });
|
|
2607
3808
|
}
|
|
2608
3809
|
function writeError(response, error, logger) {
|
|
2609
3810
|
const normalized = asSpotPatchError(error);
|
|
2610
|
-
if (normalized.code ===
|
|
3811
|
+
if (normalized.code === ERROR_CODES15.INTERNAL_ERROR) {
|
|
2611
3812
|
logger?.warn("[spotpatch:server] Internal request failure.");
|
|
2612
3813
|
}
|
|
2613
|
-
|
|
3814
|
+
writeJson2(response, STATUS_BY_ERROR[normalized.code], {
|
|
2614
3815
|
ok: false,
|
|
2615
3816
|
error: {
|
|
2616
3817
|
code: normalized.code,
|
|
@@ -2630,7 +3831,7 @@ async function handleSourceContext(request, options) {
|
|
|
2630
3831
|
await readJsonRequestBody(request)
|
|
2631
3832
|
);
|
|
2632
3833
|
if (!parsed.success) {
|
|
2633
|
-
throw new
|
|
3834
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2634
3835
|
}
|
|
2635
3836
|
return readSourceContext({
|
|
2636
3837
|
request: parsed.data,
|
|
@@ -2643,7 +3844,7 @@ async function handleSourceContext(request, options) {
|
|
|
2643
3844
|
async function handleOpenEditor(request, options) {
|
|
2644
3845
|
const parsed = openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
|
|
2645
3846
|
if (!parsed.success) {
|
|
2646
|
-
throw new
|
|
3847
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2647
3848
|
}
|
|
2648
3849
|
const body = parsed.data;
|
|
2649
3850
|
const sourcePath = await resolveSourceFile({
|
|
@@ -2660,7 +3861,7 @@ async function handleOpenEditor(request, options) {
|
|
|
2660
3861
|
options.logger?.warn(
|
|
2661
3862
|
`[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
|
|
2662
3863
|
);
|
|
2663
|
-
throw new
|
|
3864
|
+
throw new SpotPatchError15(ERROR_CODES15.EDITOR_OPEN_FAILED, void 0, {
|
|
2664
3865
|
cause: error
|
|
2665
3866
|
});
|
|
2666
3867
|
}
|
|
@@ -2669,63 +3870,81 @@ function createSpotPatchMiddleware(options) {
|
|
|
2669
3870
|
const bootstrap = options.bootstrap === void 0 ? void 0 : resolveRuntimeBootstrapOptions(options.bootstrap);
|
|
2670
3871
|
const dataFlowAnalyzer = createDataFlowAnalyzer(options);
|
|
2671
3872
|
return (request, response, next) => {
|
|
2672
|
-
const
|
|
2673
|
-
const agentRoute = matchAgentRequestPath(
|
|
2674
|
-
|
|
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}/`)) {
|
|
2675
3877
|
next();
|
|
2676
3878
|
return;
|
|
2677
3879
|
}
|
|
2678
3880
|
const handle = async () => {
|
|
2679
|
-
if (
|
|
3881
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.bootstrap && bootstrap !== void 0) {
|
|
2680
3882
|
const data = await readRuntimeBootstrap(
|
|
2681
3883
|
request,
|
|
2682
3884
|
bootstrap
|
|
2683
3885
|
);
|
|
2684
|
-
|
|
3886
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2685
3887
|
return;
|
|
2686
3888
|
}
|
|
2687
3889
|
assertRequestAuthorized(request, {
|
|
2688
3890
|
allowLan: options.options.allowLan,
|
|
2689
3891
|
sessionToken: options.session.token
|
|
2690
3892
|
});
|
|
2691
|
-
if (
|
|
3893
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.sourceContext) {
|
|
2692
3894
|
if (request.method !== "POST") {
|
|
2693
|
-
throw new
|
|
3895
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2694
3896
|
}
|
|
2695
3897
|
const data = await handleSourceContext(request, options);
|
|
2696
|
-
|
|
3898
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2697
3899
|
return;
|
|
2698
3900
|
}
|
|
2699
|
-
if (
|
|
3901
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.openEditor) {
|
|
2700
3902
|
if (request.method !== "POST") {
|
|
2701
|
-
throw new
|
|
3903
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2702
3904
|
}
|
|
2703
3905
|
const data = await handleOpenEditor(request, options);
|
|
2704
|
-
|
|
3906
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2705
3907
|
return;
|
|
2706
3908
|
}
|
|
2707
|
-
if (
|
|
3909
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.dataFlowComponentReport) {
|
|
2708
3910
|
if (request.method !== "POST") {
|
|
2709
|
-
throw new
|
|
3911
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2710
3912
|
}
|
|
2711
3913
|
const data = await handleComponentDataFlowReport(
|
|
2712
3914
|
request,
|
|
2713
3915
|
dataFlowAnalyzer,
|
|
2714
3916
|
options
|
|
2715
3917
|
);
|
|
2716
|
-
|
|
3918
|
+
writeJson2(response, 200, { ok: true, data });
|
|
2717
3919
|
return;
|
|
2718
3920
|
}
|
|
2719
|
-
if (
|
|
3921
|
+
if (path9 === SPOTPATCH_ENDPOINTS3.dataFlowPageReport) {
|
|
2720
3922
|
if (request.method !== "POST") {
|
|
2721
|
-
throw new
|
|
3923
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2722
3924
|
}
|
|
2723
3925
|
const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
|
|
2724
|
-
|
|
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
|
+
);
|
|
2725
3944
|
return;
|
|
2726
3945
|
}
|
|
2727
3946
|
if (agentRoute === void 0) {
|
|
2728
|
-
throw new
|
|
3947
|
+
throw new SpotPatchError15(ERROR_CODES15.INVALID_REQUEST);
|
|
2729
3948
|
}
|
|
2730
3949
|
await handleAgentRequest(
|
|
2731
3950
|
request,
|
|
@@ -2733,7 +3952,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
2733
3952
|
options,
|
|
2734
3953
|
agentRoute,
|
|
2735
3954
|
(target, status, data) => {
|
|
2736
|
-
|
|
3955
|
+
writeJson2(target, status, { ok: true, data });
|
|
2737
3956
|
}
|
|
2738
3957
|
);
|
|
2739
3958
|
};
|
|
@@ -2744,9 +3963,9 @@ function createSpotPatchMiddleware(options) {
|
|
|
2744
3963
|
}
|
|
2745
3964
|
|
|
2746
3965
|
// src/server/source-registration.ts
|
|
2747
|
-
import { timingSafeEqual as
|
|
2748
|
-
import { lstat as
|
|
2749
|
-
import
|
|
3966
|
+
import { timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
3967
|
+
import { lstat as lstat4, realpath as realpath6 } from "fs/promises";
|
|
3968
|
+
import path8 from "path";
|
|
2750
3969
|
import { createSourceFilter } from "@spotpatch/compiler";
|
|
2751
3970
|
import { z as z2 } from "zod";
|
|
2752
3971
|
var REGISTRATION_BODY_LIMIT_BYTES = 4096;
|
|
@@ -2767,16 +3986,16 @@ function identitiesMatch(actual, expected) {
|
|
|
2767
3986
|
}
|
|
2768
3987
|
const actualBytes = Buffer.from(actual);
|
|
2769
3988
|
const expectedBytes = Buffer.from(expected);
|
|
2770
|
-
return actualBytes.byteLength === expectedBytes.byteLength &&
|
|
3989
|
+
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual3(actualBytes, expectedBytes);
|
|
2771
3990
|
}
|
|
2772
3991
|
function isWithinRoot(root, candidate) {
|
|
2773
|
-
const relative =
|
|
2774
|
-
return relative === "" || !relative.startsWith(`..${
|
|
3992
|
+
const relative = path8.relative(root, candidate);
|
|
3993
|
+
return relative === "" || !relative.startsWith(`..${path8.sep}`) && relative !== ".." && !path8.isAbsolute(relative);
|
|
2775
3994
|
}
|
|
2776
3995
|
function hasForbiddenSegment(root, candidate) {
|
|
2777
|
-
return
|
|
3996
|
+
return path8.relative(root, candidate).split(path8.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
|
|
2778
3997
|
}
|
|
2779
|
-
function
|
|
3998
|
+
function writeJson3(response, statusCode, payload) {
|
|
2780
3999
|
const body = JSON.stringify(payload);
|
|
2781
4000
|
response.statusCode = statusCode;
|
|
2782
4001
|
response.setHeader("Cache-Control", "no-store");
|
|
@@ -2796,11 +4015,11 @@ function requestComesFromLoopbackWorker(request) {
|
|
|
2796
4015
|
}
|
|
2797
4016
|
}
|
|
2798
4017
|
async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
|
|
2799
|
-
if (!
|
|
4018
|
+
if (!path8.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
2800
4019
|
return void 0;
|
|
2801
4020
|
}
|
|
2802
4021
|
try {
|
|
2803
|
-
const sourceStat = await
|
|
4022
|
+
const sourceStat = await lstat4(requestedPath);
|
|
2804
4023
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
2805
4024
|
return void 0;
|
|
2806
4025
|
}
|
|
@@ -2826,14 +4045,14 @@ async function createSourceRegistrationService(input) {
|
|
|
2826
4045
|
getSingleHeader3(request, INTERNAL_SECRET_HEADER),
|
|
2827
4046
|
input.internalSecret
|
|
2828
4047
|
)) {
|
|
2829
|
-
|
|
4048
|
+
writeJson3(response, 403, { ok: false });
|
|
2830
4049
|
return;
|
|
2831
4050
|
}
|
|
2832
4051
|
const parsed = registrationRequestSchema.safeParse(
|
|
2833
4052
|
await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
|
|
2834
4053
|
);
|
|
2835
4054
|
if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
|
|
2836
|
-
|
|
4055
|
+
writeJson3(response, 400, { ok: false });
|
|
2837
4056
|
return;
|
|
2838
4057
|
}
|
|
2839
4058
|
const sourcePath = await resolveAuthorizedSource(
|
|
@@ -2842,17 +4061,17 @@ async function createSourceRegistrationService(input) {
|
|
|
2842
4061
|
(absolutePath) => sourceFilter.shouldTransform(absolutePath, "<")
|
|
2843
4062
|
);
|
|
2844
4063
|
if (sourcePath === void 0) {
|
|
2845
|
-
|
|
4064
|
+
writeJson3(response, 403, { ok: false });
|
|
2846
4065
|
return;
|
|
2847
4066
|
}
|
|
2848
|
-
|
|
4067
|
+
writeJson3(response, 200, {
|
|
2849
4068
|
epoch: input.registryEpoch,
|
|
2850
4069
|
fileId: input.registry.register(sourcePath)
|
|
2851
4070
|
});
|
|
2852
4071
|
};
|
|
2853
4072
|
void handle().catch(() => {
|
|
2854
4073
|
if (!response.headersSent) {
|
|
2855
|
-
|
|
4074
|
+
writeJson3(response, 400, { ok: false });
|
|
2856
4075
|
} else {
|
|
2857
4076
|
response.destroy();
|
|
2858
4077
|
}
|
|
@@ -2862,11 +4081,11 @@ async function createSourceRegistrationService(input) {
|
|
|
2862
4081
|
}
|
|
2863
4082
|
|
|
2864
4083
|
// src/session/session.ts
|
|
2865
|
-
import { randomBytes as
|
|
4084
|
+
import { randomBytes as randomBytes8 } from "crypto";
|
|
2866
4085
|
function createSession() {
|
|
2867
4086
|
return Object.freeze({
|
|
2868
|
-
id:
|
|
2869
|
-
token:
|
|
4087
|
+
id: randomBytes8(16).toString("base64url"),
|
|
4088
|
+
token: randomBytes8(16).toString("base64url")
|
|
2870
4089
|
});
|
|
2871
4090
|
}
|
|
2872
4091
|
|
|
@@ -2879,6 +4098,7 @@ var OPTION_KEYS = Object.freeze([
|
|
|
2879
4098
|
"dataFlow",
|
|
2880
4099
|
"editor",
|
|
2881
4100
|
"enabled",
|
|
4101
|
+
"externalAgent",
|
|
2882
4102
|
"exclude",
|
|
2883
4103
|
"include",
|
|
2884
4104
|
"locale",
|
|
@@ -2969,6 +4189,7 @@ function serializeResolvedSpotPatchOptions(options) {
|
|
|
2969
4189
|
}) : false,
|
|
2970
4190
|
editor: options.editor,
|
|
2971
4191
|
enabled: options.enabled,
|
|
4192
|
+
externalAgent: options.externalAgent.enabled,
|
|
2972
4193
|
exclude: Object.freeze(options.exclude.map(serializeFilter)),
|
|
2973
4194
|
include: Object.freeze(options.include.map(serializeFilter)),
|
|
2974
4195
|
locale: options.locale,
|
|
@@ -3022,7 +4243,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
3022
4243
|
if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
|
|
3023
4244
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
3024
4245
|
}
|
|
3025
|
-
if (typeof value.enabled !== "boolean" || typeof value.redact !== "boolean" || typeof value.allowLan !== "boolean" || typeof value.debug !== "boolean" || typeof value.shortcut !== "string" || typeof value.maxTargets !== "number" || typeof value.editor !== "string" || typeof value.locale !== "string" || value.ai !== false && !isRecord2(value.ai)) {
|
|
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)) {
|
|
3026
4247
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
3027
4248
|
}
|
|
3028
4249
|
try {
|
|
@@ -3034,6 +4255,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
3034
4255
|
dataFlow: parseDataFlow(value.dataFlow),
|
|
3035
4256
|
editor: value.editor,
|
|
3036
4257
|
enabled: value.enabled,
|
|
4258
|
+
externalAgent: value.externalAgent,
|
|
3037
4259
|
exclude: parseFilterList(value.exclude),
|
|
3038
4260
|
include: parseFilterList(value.include),
|
|
3039
4261
|
locale: value.locale,
|
|
@@ -3052,6 +4274,7 @@ export {
|
|
|
3052
4274
|
DEFAULT_OPTIONS,
|
|
3053
4275
|
applyIntegrationPlan,
|
|
3054
4276
|
createAgentJobManager,
|
|
4277
|
+
createExternalHandoffService,
|
|
3055
4278
|
createIntegrationFileChange,
|
|
3056
4279
|
createRuntimeAiConfig,
|
|
3057
4280
|
createRuntimeDataFlowConfig,
|