@tangle-network/agent-provider-tangle 1.1.6 → 1.1.8
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/README.md +5 -0
- package/dist/tangle-contract-safety.d.ts +3 -0
- package/dist/tangle-contract-safety.js +22 -6
- package/dist/tangle-events.js +5 -59
- package/dist/tangle-prompt.js +7 -2
- package/dist/tangle-result-values.js +5 -1
- package/dist/tangle-workspace-branching.js +8 -844
- package/dist/tangle-workspace-markers.d.ts +27 -0
- package/dist/tangle-workspace-markers.js +277 -0
- package/dist/tangle-workspace-recovery.d.ts +68 -0
- package/dist/tangle-workspace-recovery.js +530 -0
- package/package.json +6 -2
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
import { ConfidentialAttestationSchema, ConfidentialExecutionRequestSchema, ForkedEnvironmentRefSchema,
|
|
2
|
-
import { awaitWithSignal, boundedIdentifier, boundedString,
|
|
3
|
-
import { encodeTangleConfidentialAttestationQuote
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
* The values are identity markers, not security evidence. A marker can tell
|
|
8
|
-
* the provider which request produced a resource, but only the Sandbox
|
|
9
|
-
* operation ledger and the external verifier can prove an outcome.
|
|
10
|
-
*/
|
|
11
|
-
const MARKER_PREFIX = "tangle-agent-ws-v1";
|
|
12
|
-
/** Marker namespace used by releases before the 128-byte tag limit. */
|
|
13
|
-
const LEGACY_MARKER_PREFIX = "tangle-agent-sdk:workspace:v1";
|
|
14
|
-
const FORK_METADATA_KEY = "__tangle_agent_workspace_v1";
|
|
15
|
-
const MAX_MARKER_TAG_LENGTH = 128;
|
|
16
|
-
const MARKER_CHUNK_SIZE = 80;
|
|
17
|
-
const LEGACY_MARKER_CHUNK_SIZE = 240;
|
|
18
|
-
const MAX_MARKER_CHUNKS = 512;
|
|
1
|
+
import { ConfidentialAttestationSchema, ConfidentialExecutionRequestSchema, ForkedEnvironmentRefSchema, WorkspaceCheckpointRequestSchema, WorkspaceCleanupAcknowledgementSchema, WorkspaceCleanupRequestSchema, WorkspaceForkRequestSchema, WorkspaceOperationLookupRequestSchema, WorkspaceCheckpointResultSchema, WorkspaceCheckpointLookupResultSchema, WorkspaceForkResultSchema, WorkspaceForkLookupResultSchema, canonicalCandidateDigest, confidentialExecutionVerified, sha256Bytes, } from "@tangle-network/agent-interface";
|
|
2
|
+
import { awaitWithSignal, boundedIdentifier, boundedString, MAX_STRING_LENGTH, cloneJson, safeIdentifier, safeString, } from "./tangle-contract-safety.js";
|
|
3
|
+
import { encodeTangleConfidentialAttestationQuote } from "./tangle-confidential-attestation.js";
|
|
4
|
+
import { checkpointMarkerTags, forkMarkerMetadata, markerBelongsToSource, checkpointMarkerBelongsToSource, checkpointMarkerFromTags, forkMarkerFromMetadata, } from "./tangle-workspace-markers.js";
|
|
5
|
+
import { checkpointRecordFromSnapshot, validSnapshotResult, reconcileCheckpoint, findManagedCheckpoint, findForkByKey, findForkChildById, completeForkChild, findBlockingForks, lookupOutcomeFromSandbox, isoDate, } from "./tangle-workspace-recovery.js";
|
|
19
6
|
const TANGLE_ATTESTATION_NONCE_PATTERN = /^(?:0x)?[0-9a-fA-F]{64}(?:[0-9a-fA-F]{64})?$/;
|
|
20
7
|
function confidentialForkRefusal(request) {
|
|
21
8
|
const confidential = request.confidential;
|
|
@@ -532,30 +519,6 @@ function assertCleanupProvider(request, provider) {
|
|
|
532
519
|
throw new Error("Tangle cleanup provider does not match this provider");
|
|
533
520
|
}
|
|
534
521
|
}
|
|
535
|
-
function checkpointRecordFromSnapshot(request, snapshot) {
|
|
536
|
-
try {
|
|
537
|
-
const createdAt = isoDate(snapshot.createdAt);
|
|
538
|
-
const checkpoint = WorkspaceCheckpointRefSchema.parse({
|
|
539
|
-
checkpointId: boundedIdentifier(snapshot.snapshotId, "Tangle checkpoint id"),
|
|
540
|
-
provider: request.source.provider,
|
|
541
|
-
source: request.source,
|
|
542
|
-
idempotencyKey: request.idempotencyKey,
|
|
543
|
-
requestDigest: request.requestDigest,
|
|
544
|
-
createdAt,
|
|
545
|
-
...(request.metadata === undefined
|
|
546
|
-
? {}
|
|
547
|
-
: { metadata: cloneJson(request.metadata) }),
|
|
548
|
-
});
|
|
549
|
-
return {
|
|
550
|
-
request,
|
|
551
|
-
checkpoint,
|
|
552
|
-
snapshotId: checkpoint.checkpointId,
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
catch {
|
|
556
|
-
return undefined;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
522
|
async function environmentFromChild(request, child, provider, createdAt, verifier, signal) {
|
|
560
523
|
signal?.throwIfAborted();
|
|
561
524
|
const environmentId = safeIdentifier(child.id);
|
|
@@ -621,15 +584,16 @@ async function confidentialAttestationForChild(request, child, provider, verifie
|
|
|
621
584
|
}
|
|
622
585
|
if (!response ||
|
|
623
586
|
response.sandbox_id !== child.id ||
|
|
624
|
-
!
|
|
587
|
+
!response.attestation ||
|
|
625
588
|
safeIdentifier(response.attestationNonce) === undefined ||
|
|
626
589
|
response.attestationNonce !== confidential.nonce) {
|
|
627
590
|
return undefined;
|
|
628
591
|
}
|
|
629
|
-
|
|
592
|
+
// The canonical codec validates the complete report before its bytes are used.
|
|
630
593
|
const quote = encodeTangleConfidentialAttestationQuote(response.attestation);
|
|
631
594
|
if (quote === undefined)
|
|
632
595
|
return undefined;
|
|
596
|
+
const measurement = sha256Bytes(Uint8Array.from(response.attestation.measurement));
|
|
633
597
|
let verifiedAt;
|
|
634
598
|
try {
|
|
635
599
|
verifiedAt = new Date(response.attestation.timestamp * 1_000).toISOString();
|
|
@@ -703,447 +667,6 @@ async function confidentialAttestationForChild(request, child, provider, verifie
|
|
|
703
667
|
? attestation.data
|
|
704
668
|
: undefined;
|
|
705
669
|
}
|
|
706
|
-
function validTeeReport(report) {
|
|
707
|
-
return (!!report &&
|
|
708
|
-
safeIdentifier(report.tee_type) !== undefined &&
|
|
709
|
-
Array.isArray(report.evidence) &&
|
|
710
|
-
report.evidence.length <= MAX_TEE_EVIDENCE_BYTES &&
|
|
711
|
-
report.evidence.every((value) => Number.isInteger(value) && value >= 0 && value <= 255) &&
|
|
712
|
-
Array.isArray(report.measurement) &&
|
|
713
|
-
report.measurement.length <= MAX_TEE_MEASUREMENT_BYTES &&
|
|
714
|
-
report.measurement.every((value) => Number.isInteger(value) && value >= 0 && value <= 255) &&
|
|
715
|
-
Number.isFinite(report.timestamp) &&
|
|
716
|
-
report.timestamp > 0);
|
|
717
|
-
}
|
|
718
|
-
function validSnapshotResult(result) {
|
|
719
|
-
return (!!result &&
|
|
720
|
-
safeIdentifier(result.snapshotId) !== undefined &&
|
|
721
|
-
validDate(result.createdAt) &&
|
|
722
|
-
Array.isArray(result.tags) &&
|
|
723
|
-
result.tags.every((tag) => safeString(tag) !== undefined));
|
|
724
|
-
}
|
|
725
|
-
function validSnapshotInfo(snapshot, sandboxId) {
|
|
726
|
-
return (validSnapshotResult(snapshot) &&
|
|
727
|
-
safeIdentifier(snapshot.sandboxId) !== undefined &&
|
|
728
|
-
(sandboxId === undefined || snapshot.sandboxId === sandboxId));
|
|
729
|
-
}
|
|
730
|
-
function validOperationRecord(value) {
|
|
731
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
732
|
-
}
|
|
733
|
-
function validOperationDate(value) {
|
|
734
|
-
return ((typeof value === "string" || value instanceof Date) && validDate(value));
|
|
735
|
-
}
|
|
736
|
-
function validSnapshotOperationResult(value) {
|
|
737
|
-
return (validOperationRecord(value) &&
|
|
738
|
-
safeIdentifier(value.snapshotId) !== undefined &&
|
|
739
|
-
validOperationDate(value.createdAt));
|
|
740
|
-
}
|
|
741
|
-
function validTaggedSnapshotOperationResult(value) {
|
|
742
|
-
return (validSnapshotOperationResult(value) &&
|
|
743
|
-
Array.isArray(value.tags) &&
|
|
744
|
-
value.tags.every((tag) => safeString(tag) !== undefined));
|
|
745
|
-
}
|
|
746
|
-
function validForkOperationChildResult(value) {
|
|
747
|
-
return (validOperationRecord(value) &&
|
|
748
|
-
safeIdentifier(value.sandboxId ?? value.id) !== undefined &&
|
|
749
|
-
(value.createdAt === undefined ||
|
|
750
|
-
value.createdAt === null ||
|
|
751
|
-
validOperationDate(value.createdAt)));
|
|
752
|
-
}
|
|
753
|
-
function checkpointMarkerTags(request) {
|
|
754
|
-
const marker = {
|
|
755
|
-
version: 1,
|
|
756
|
-
kind: "checkpoint",
|
|
757
|
-
idempotencyKey: request.idempotencyKey,
|
|
758
|
-
requestDigest: request.requestDigest,
|
|
759
|
-
request,
|
|
760
|
-
};
|
|
761
|
-
return markerTags("checkpoint", request.idempotencyKey, request.requestDigest, marker);
|
|
762
|
-
}
|
|
763
|
-
/** Rebuild the exact tags used by the release before the current safe format. */
|
|
764
|
-
function legacyCheckpointMarkerTags(request) {
|
|
765
|
-
const marker = {
|
|
766
|
-
version: 1,
|
|
767
|
-
kind: "checkpoint",
|
|
768
|
-
idempotencyKey: request.idempotencyKey,
|
|
769
|
-
requestDigest: request.requestDigest,
|
|
770
|
-
request,
|
|
771
|
-
};
|
|
772
|
-
const encoded = encodeJson(marker);
|
|
773
|
-
if (encoded === undefined)
|
|
774
|
-
throw new Error("workspace marker is not JSON serializable");
|
|
775
|
-
const base = `${LEGACY_MARKER_PREFIX}:checkpoint`;
|
|
776
|
-
const chunks = splitIntoChunks(encoded, LEGACY_MARKER_CHUNK_SIZE);
|
|
777
|
-
if (chunks.length > MAX_MARKER_CHUNKS) {
|
|
778
|
-
throw new Error("workspace marker exceeds the recovery bound");
|
|
779
|
-
}
|
|
780
|
-
return [
|
|
781
|
-
`${base}:key:${encodeText(request.idempotencyKey)}`,
|
|
782
|
-
`${base}:digest:${request.requestDigest}`,
|
|
783
|
-
...chunks.map((chunk, index) => `${base}:material:${index}:${chunks.length}:${chunk}`),
|
|
784
|
-
];
|
|
785
|
-
}
|
|
786
|
-
function forkMarkerMetadata(request, materialization = "snapshot") {
|
|
787
|
-
if (request.metadata && Object.hasOwn(request.metadata, FORK_METADATA_KEY)) {
|
|
788
|
-
throw new Error(`fork metadata reserves ${FORK_METADATA_KEY}`);
|
|
789
|
-
}
|
|
790
|
-
const marker = {
|
|
791
|
-
version: 1,
|
|
792
|
-
kind: "fork",
|
|
793
|
-
idempotencyKey: request.idempotencyKey,
|
|
794
|
-
requestDigest: request.requestDigest,
|
|
795
|
-
request,
|
|
796
|
-
...(materialization === "snapshot" ? { materialization } : {}),
|
|
797
|
-
};
|
|
798
|
-
assertBoundedJson(marker);
|
|
799
|
-
return {
|
|
800
|
-
...(request.metadata === undefined ? {} : cloneJson(request.metadata)),
|
|
801
|
-
[FORK_METADATA_KEY]: marker,
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Ask the Sandbox operation ledger whether a marked checkpoint settled.
|
|
806
|
-
*
|
|
807
|
-
* A marker only names a candidate resource. Nothing is returned to a caller
|
|
808
|
-
* until the ledger reports the operation succeeded.
|
|
809
|
-
*/
|
|
810
|
-
async function checkpointOperationLookup(box, marker, signal) {
|
|
811
|
-
signal?.throwIfAborted();
|
|
812
|
-
const lookup = await awaitWithSignal(box.getSnapshotOperation?.(marker.idempotencyKey, {
|
|
813
|
-
tags: marker.legacy
|
|
814
|
-
? legacyCheckpointMarkerTags(marker.request)
|
|
815
|
-
: checkpointMarkerTags(marker.request),
|
|
816
|
-
}), signal);
|
|
817
|
-
return lookup;
|
|
818
|
-
}
|
|
819
|
-
/** Read the durable record by its owner-scoped key when no request body remains. */
|
|
820
|
-
async function checkpointOperationLookupByKey(box, idempotencyKey, signal) {
|
|
821
|
-
signal?.throwIfAborted();
|
|
822
|
-
return await awaitWithSignal(box.getSnapshotOperation?.(idempotencyKey), signal);
|
|
823
|
-
}
|
|
824
|
-
async function checkpointOperationSucceeded(box, marker, signal) {
|
|
825
|
-
const lookup = await checkpointOperationLookup(box, marker, signal);
|
|
826
|
-
return (lookup?.outcome === "found" &&
|
|
827
|
-
lookup.kind === "checkpoint" &&
|
|
828
|
-
lookup.state === "succeeded");
|
|
829
|
-
}
|
|
830
|
-
/** Confirm a fork child through its marker or the legacy fork ledger. */
|
|
831
|
-
async function forkOperationLookup(box, marker, signal) {
|
|
832
|
-
if (marker.materialization === "snapshot") {
|
|
833
|
-
return {
|
|
834
|
-
outcome: "found",
|
|
835
|
-
kind: "fork",
|
|
836
|
-
state: "succeeded",
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
const lookup = await awaitWithSignal(box.getForkOperation?.(marker.idempotencyKey, {
|
|
840
|
-
count: 1,
|
|
841
|
-
metadata: forkMarkerMetadata(marker.request, marker.materialization),
|
|
842
|
-
}), signal);
|
|
843
|
-
return lookup;
|
|
844
|
-
}
|
|
845
|
-
async function forkOperationSucceeded(box, marker, signal) {
|
|
846
|
-
const lookup = await forkOperationLookup(box, marker, signal);
|
|
847
|
-
return (lookup?.outcome === "found" &&
|
|
848
|
-
lookup.kind === "fork" &&
|
|
849
|
-
lookup.state === "succeeded");
|
|
850
|
-
}
|
|
851
|
-
function markerTags(kind, idempotencyKey, requestDigest, marker) {
|
|
852
|
-
const encoded = encodeJson(marker);
|
|
853
|
-
if (encoded === undefined)
|
|
854
|
-
throw new Error("workspace marker is not JSON serializable");
|
|
855
|
-
const base = `${MARKER_PREFIX}-${kind}`;
|
|
856
|
-
const chunks = split(encoded);
|
|
857
|
-
if (chunks.length > MAX_MARKER_CHUNKS) {
|
|
858
|
-
throw new Error("workspace marker exceeds the recovery bound");
|
|
859
|
-
}
|
|
860
|
-
return [
|
|
861
|
-
`${base}-key-${markerKeyDigest(idempotencyKey).replace(":", "-")}`,
|
|
862
|
-
`${base}-digest-${requestDigest.replace(":", "-")}`,
|
|
863
|
-
...chunks.map((chunk, index) => `${base}-material-${index}-${chunks.length}-${chunk}`),
|
|
864
|
-
].map((tag) => {
|
|
865
|
-
if (Buffer.byteLength(tag, "utf8") > MAX_MARKER_TAG_LENGTH) {
|
|
866
|
-
throw new Error("workspace marker tag exceeds the platform bound");
|
|
867
|
-
}
|
|
868
|
-
return tag;
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
async function findCheckpointByKey(box, provider, key, signal) {
|
|
872
|
-
let snapshots;
|
|
873
|
-
try {
|
|
874
|
-
signal?.throwIfAborted();
|
|
875
|
-
const listed = await awaitWithSignal(box.listSnapshots?.(), signal);
|
|
876
|
-
if (!Array.isArray(listed))
|
|
877
|
-
return undefined;
|
|
878
|
-
snapshots = listed;
|
|
879
|
-
}
|
|
880
|
-
catch {
|
|
881
|
-
signal?.throwIfAborted();
|
|
882
|
-
return undefined;
|
|
883
|
-
}
|
|
884
|
-
if (!Array.isArray(snapshots) || snapshots.length > MAX_LIST_RESULTS) {
|
|
885
|
-
return undefined;
|
|
886
|
-
}
|
|
887
|
-
const snapshotIds = new Set();
|
|
888
|
-
let found;
|
|
889
|
-
let unresolved = false;
|
|
890
|
-
for (const snapshot of snapshots) {
|
|
891
|
-
if (!validSnapshotInfo(snapshot, box.id))
|
|
892
|
-
return undefined;
|
|
893
|
-
if (snapshotIds.has(snapshot.snapshotId))
|
|
894
|
-
return undefined;
|
|
895
|
-
snapshotIds.add(snapshot.snapshotId);
|
|
896
|
-
const marker = checkpointMarkerFromTags(snapshot.tags, key);
|
|
897
|
-
if (!marker)
|
|
898
|
-
continue;
|
|
899
|
-
if (!checkpointMarkerBelongsToSource(marker, provider, box.id)) {
|
|
900
|
-
return undefined;
|
|
901
|
-
}
|
|
902
|
-
try {
|
|
903
|
-
const lookup = await checkpointOperationLookup(box, marker, signal);
|
|
904
|
-
if (lookup?.outcome === "found" &&
|
|
905
|
-
lookup.kind === "checkpoint" &&
|
|
906
|
-
lookup.state === "succeeded") {
|
|
907
|
-
const authoritative = snapshotFromOperationResult(snapshot, lookup);
|
|
908
|
-
if (authoritative === undefined)
|
|
909
|
-
return undefined;
|
|
910
|
-
if (found !== undefined)
|
|
911
|
-
return undefined;
|
|
912
|
-
found = { state: "found", snapshot: authoritative, marker };
|
|
913
|
-
continue;
|
|
914
|
-
}
|
|
915
|
-
unresolved = true;
|
|
916
|
-
}
|
|
917
|
-
catch {
|
|
918
|
-
signal?.throwIfAborted();
|
|
919
|
-
return undefined;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
if (found !== undefined)
|
|
923
|
-
return found;
|
|
924
|
-
if (unresolved)
|
|
925
|
-
return undefined;
|
|
926
|
-
// Some storage backends retain the snapshot but omit caller tags from a
|
|
927
|
-
// later inventory read. The owner-scoped operation record retains the exact
|
|
928
|
-
// acknowledgement, including those tags. Bind that record to a currently
|
|
929
|
-
// live snapshot id before recovering it; neither record is sufficient alone.
|
|
930
|
-
let lookup;
|
|
931
|
-
try {
|
|
932
|
-
lookup = await checkpointOperationLookupByKey(box, key, signal);
|
|
933
|
-
}
|
|
934
|
-
catch {
|
|
935
|
-
signal?.throwIfAborted();
|
|
936
|
-
return undefined;
|
|
937
|
-
}
|
|
938
|
-
if (lookup?.outcome === "not_found" && lookup.kind === "checkpoint") {
|
|
939
|
-
return null;
|
|
940
|
-
}
|
|
941
|
-
if (lookup?.outcome !== "found" ||
|
|
942
|
-
lookup.kind !== "checkpoint" ||
|
|
943
|
-
lookup.state !== "succeeded") {
|
|
944
|
-
return undefined;
|
|
945
|
-
}
|
|
946
|
-
if (snapshots.length === 0)
|
|
947
|
-
return { state: "retired" };
|
|
948
|
-
if (!validTaggedSnapshotOperationResult(lookup.result))
|
|
949
|
-
return undefined;
|
|
950
|
-
const live = snapshots.filter((snapshot) => snapshot.snapshotId === lookup.result?.snapshotId);
|
|
951
|
-
if (live.length === 0)
|
|
952
|
-
return { state: "retired" };
|
|
953
|
-
if (live.length !== 1)
|
|
954
|
-
return undefined;
|
|
955
|
-
const marker = checkpointMarkerFromTags(lookup.result.tags, key);
|
|
956
|
-
if (!marker ||
|
|
957
|
-
!checkpointMarkerBelongsToSource(marker, provider, box.id)) {
|
|
958
|
-
return undefined;
|
|
959
|
-
}
|
|
960
|
-
const authoritative = snapshotFromOperationResult(live[0], lookup);
|
|
961
|
-
return authoritative === undefined
|
|
962
|
-
? undefined
|
|
963
|
-
: { state: "found", snapshot: authoritative, marker };
|
|
964
|
-
}
|
|
965
|
-
/** Normalize one remote checkpoint recovery attempt for every caller. */
|
|
966
|
-
async function reconcileCheckpoint(box, provider, request, signal) {
|
|
967
|
-
const recovered = await findCheckpointByKey(box, provider, request.idempotencyKey, signal);
|
|
968
|
-
if (recovered === undefined) {
|
|
969
|
-
return { state: "undecided", reason: "inventory_unavailable" };
|
|
970
|
-
}
|
|
971
|
-
if (recovered === null)
|
|
972
|
-
return { state: "absent" };
|
|
973
|
-
if (recovered.state === "retired")
|
|
974
|
-
return recovered;
|
|
975
|
-
if (recovered.marker.requestDigest !== request.requestDigest) {
|
|
976
|
-
return {
|
|
977
|
-
state: "conflict",
|
|
978
|
-
existingRequestDigest: recovered.marker.requestDigest,
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
const record = checkpointRecordFromSnapshot(recovered.marker.request, recovered.snapshot);
|
|
982
|
-
return record === undefined
|
|
983
|
-
? { state: "undecided", reason: "metadata_invalid" }
|
|
984
|
-
: { state: "found", record };
|
|
985
|
-
}
|
|
986
|
-
/**
|
|
987
|
-
* Prefer the durable operation result over inventory metadata.
|
|
988
|
-
*
|
|
989
|
-
* Snapshot inventory and the operation ledger can expose different creation
|
|
990
|
-
* timestamps. The ledger result is the acknowledgement returned by the
|
|
991
|
-
* idempotent operation, so recovery must rebuild the exact checkpoint ref
|
|
992
|
-
* from it when the service provides that result.
|
|
993
|
-
*/
|
|
994
|
-
function snapshotFromOperationResult(snapshot, lookup) {
|
|
995
|
-
if (lookup.result === undefined)
|
|
996
|
-
return snapshot;
|
|
997
|
-
if (!validSnapshotOperationResult(lookup.result) ||
|
|
998
|
-
lookup.result.snapshotId !== snapshot.snapshotId) {
|
|
999
|
-
return undefined;
|
|
1000
|
-
}
|
|
1001
|
-
return { ...snapshot, createdAt: lookup.result.createdAt };
|
|
1002
|
-
}
|
|
1003
|
-
/**
|
|
1004
|
-
* Confirm that one snapshot id is a settled checkpoint this provider created.
|
|
1005
|
-
*
|
|
1006
|
-
* `expected` binds the answer to a specific checkpoint reference. A reference
|
|
1007
|
-
* that does not match its marker is absent, not unknown: the caller supplied a
|
|
1008
|
-
* checkpoint this source never produced.
|
|
1009
|
-
*/
|
|
1010
|
-
async function findManagedCheckpoint(box, provider, id, expected, signal) {
|
|
1011
|
-
try {
|
|
1012
|
-
const snapshots = await awaitWithSignal(box.listSnapshots?.(), signal);
|
|
1013
|
-
if (!Array.isArray(snapshots) || snapshots.length > MAX_LIST_RESULTS) {
|
|
1014
|
-
return "unknown";
|
|
1015
|
-
}
|
|
1016
|
-
const snapshot = snapshots.find((candidate) => candidate.snapshotId === id);
|
|
1017
|
-
if (!snapshot)
|
|
1018
|
-
return false;
|
|
1019
|
-
if (!validSnapshotInfo(snapshot, box.id))
|
|
1020
|
-
return "unknown";
|
|
1021
|
-
const marker = checkpointMarkerFromTags(snapshot.tags, expected?.idempotencyKey);
|
|
1022
|
-
if (!marker)
|
|
1023
|
-
return expected ? false : "unknown";
|
|
1024
|
-
if (marker.request.source.provider !== provider ||
|
|
1025
|
-
marker.request.source.environmentId !== box.id) {
|
|
1026
|
-
return expected ? false : "unknown";
|
|
1027
|
-
}
|
|
1028
|
-
if (expected &&
|
|
1029
|
-
(marker.requestDigest !== expected.requestDigest ||
|
|
1030
|
-
canonicalCandidateDigest(marker.request.source) !==
|
|
1031
|
-
canonicalCandidateDigest(expected.source))) {
|
|
1032
|
-
return false;
|
|
1033
|
-
}
|
|
1034
|
-
return (await checkpointOperationSucceeded(box, marker, signal))
|
|
1035
|
-
? true
|
|
1036
|
-
: "unknown";
|
|
1037
|
-
}
|
|
1038
|
-
catch {
|
|
1039
|
-
signal?.throwIfAborted();
|
|
1040
|
-
return "unknown";
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
async function findForkByKey(client, box, provider, key, signal) {
|
|
1044
|
-
const candidates = await listMarkedForkChildren(client, box, provider, key, signal);
|
|
1045
|
-
if (candidates === undefined)
|
|
1046
|
-
return undefined;
|
|
1047
|
-
let unresolved = false;
|
|
1048
|
-
for (const candidate of candidates) {
|
|
1049
|
-
try {
|
|
1050
|
-
const lookup = await forkOperationLookup(box, candidate.marker, signal);
|
|
1051
|
-
if (lookup?.outcome === "found" &&
|
|
1052
|
-
lookup.kind === "fork" &&
|
|
1053
|
-
lookup.state === "succeeded") {
|
|
1054
|
-
const authoritative = childFromOperationResult(candidate.child, lookup);
|
|
1055
|
-
if (authoritative === undefined)
|
|
1056
|
-
return undefined;
|
|
1057
|
-
return { ...authoritative, marker: candidate.marker };
|
|
1058
|
-
}
|
|
1059
|
-
unresolved = true;
|
|
1060
|
-
}
|
|
1061
|
-
catch {
|
|
1062
|
-
signal?.throwIfAborted();
|
|
1063
|
-
return undefined;
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
return unresolved ? undefined : null;
|
|
1067
|
-
}
|
|
1068
|
-
/**
|
|
1069
|
-
* Prefer the durable fork result over account-inventory metadata.
|
|
1070
|
-
*
|
|
1071
|
-
* Fork inventory can report a child timestamp from a later registry read. The
|
|
1072
|
-
* operation ledger stores the original child acknowledgement, which is the
|
|
1073
|
-
* stable value required to replay one exact fork reference after a restart.
|
|
1074
|
-
* Some Sandbox responses omit that timestamp, so the validated inventory
|
|
1075
|
-
* record supplies it only when the operation result does not.
|
|
1076
|
-
*/
|
|
1077
|
-
function childFromOperationResult(child, lookup) {
|
|
1078
|
-
if (lookup.result === undefined) {
|
|
1079
|
-
return { child, createdAt: child.createdAt };
|
|
1080
|
-
}
|
|
1081
|
-
const result = lookup.result;
|
|
1082
|
-
if (!validOperationRecord(result))
|
|
1083
|
-
return undefined;
|
|
1084
|
-
const children = result.children;
|
|
1085
|
-
if (!Array.isArray(children))
|
|
1086
|
-
return undefined;
|
|
1087
|
-
const operationChild = children.find((candidate) => validForkOperationChildResult(candidate) &&
|
|
1088
|
-
(candidate.sandboxId ?? candidate.id) === child.id);
|
|
1089
|
-
if (!operationChild)
|
|
1090
|
-
return undefined;
|
|
1091
|
-
const createdAt = operationChild.createdAt ?? child.createdAt;
|
|
1092
|
-
if (!validOperationDate(createdAt))
|
|
1093
|
-
return undefined;
|
|
1094
|
-
return { child, createdAt };
|
|
1095
|
-
}
|
|
1096
|
-
async function findForkChildById(client, box, provider, id, signal) {
|
|
1097
|
-
try {
|
|
1098
|
-
if (typeof client.get !== "function")
|
|
1099
|
-
return undefined;
|
|
1100
|
-
const child = await awaitWithSignal(client.get(id, signal ? { signal } : undefined), signal);
|
|
1101
|
-
if (child === null)
|
|
1102
|
-
return null;
|
|
1103
|
-
if (child.id !== id)
|
|
1104
|
-
return undefined;
|
|
1105
|
-
const marker = forkMarkerFromMetadata(child.metadata);
|
|
1106
|
-
if (!marker || !markerBelongsToSource(marker, provider, box.id))
|
|
1107
|
-
return undefined;
|
|
1108
|
-
return (await forkOperationSucceeded(box, marker, signal))
|
|
1109
|
-
? child
|
|
1110
|
-
: undefined;
|
|
1111
|
-
}
|
|
1112
|
-
catch {
|
|
1113
|
-
signal?.throwIfAborted();
|
|
1114
|
-
return undefined;
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
/**
|
|
1118
|
-
* Resolve a complete child identity when an acknowledgement omits durable data.
|
|
1119
|
-
*
|
|
1120
|
-
* A branch response can precede a richer registry read during a rolling
|
|
1121
|
-
* deployment. Recover the exact child when its creation time or provider
|
|
1122
|
-
* marker is absent. Never invent either field from the request.
|
|
1123
|
-
*/
|
|
1124
|
-
async function completeForkChild(client, child, signal) {
|
|
1125
|
-
if (child.createdAt !== undefined &&
|
|
1126
|
-
forkMarkerFromMetadata(child.metadata) !== undefined) {
|
|
1127
|
-
return child;
|
|
1128
|
-
}
|
|
1129
|
-
if (typeof client.get !== "function" ||
|
|
1130
|
-
safeIdentifier(child.id) === undefined) {
|
|
1131
|
-
return undefined;
|
|
1132
|
-
}
|
|
1133
|
-
try {
|
|
1134
|
-
const resolved = await awaitWithSignal(client.get(child.id, signal ? { signal } : undefined), signal);
|
|
1135
|
-
if (!resolved ||
|
|
1136
|
-
resolved.id !== child.id ||
|
|
1137
|
-
resolved.createdAt === undefined) {
|
|
1138
|
-
return undefined;
|
|
1139
|
-
}
|
|
1140
|
-
return resolved;
|
|
1141
|
-
}
|
|
1142
|
-
catch {
|
|
1143
|
-
signal?.throwIfAborted();
|
|
1144
|
-
return undefined;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
670
|
/** Remove only a child this exact create call confirmed it created. */
|
|
1148
671
|
async function compensateCreatedForkChild(child, outcome, signal) {
|
|
1149
672
|
if (outcome !== "created")
|
|
@@ -1164,255 +687,6 @@ async function compensateCreatedForkChild(child, outcome, signal) {
|
|
|
1164
687
|
return "unconfirmed";
|
|
1165
688
|
}
|
|
1166
689
|
}
|
|
1167
|
-
async function findBlockingForks(box, client, provider, checkpointId, signal) {
|
|
1168
|
-
const candidates = await listMarkedForkChildren(client, box, provider, undefined, signal);
|
|
1169
|
-
if (candidates === undefined)
|
|
1170
|
-
return undefined;
|
|
1171
|
-
const blocking = new Set();
|
|
1172
|
-
for (const { child, marker } of candidates) {
|
|
1173
|
-
if (marker.request.checkpoint.checkpointId !== checkpointId)
|
|
1174
|
-
continue;
|
|
1175
|
-
try {
|
|
1176
|
-
// A candidate that cannot be confirmed leaves the dependency set
|
|
1177
|
-
// unknown, so cleanup must not proceed on a partial answer.
|
|
1178
|
-
if (!(await forkOperationSucceeded(box, marker, signal)))
|
|
1179
|
-
return undefined;
|
|
1180
|
-
blocking.add(child.id);
|
|
1181
|
-
}
|
|
1182
|
-
catch {
|
|
1183
|
-
signal?.throwIfAborted();
|
|
1184
|
-
return undefined;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
return [...blocking].sort();
|
|
1188
|
-
}
|
|
1189
|
-
/**
|
|
1190
|
-
* Read the complete account inventory through Sandbox offset pages.
|
|
1191
|
-
*
|
|
1192
|
-
* Sandbox returns only an array, so a short page is the terminal marker. A
|
|
1193
|
-
* full page requires another request; stopping there would make recovery
|
|
1194
|
-
* report a false absence. Duplicate ids or an inventory above the safety
|
|
1195
|
-
* bound make completeness unknowable and therefore fail closed.
|
|
1196
|
-
*/
|
|
1197
|
-
async function listAllSandboxChildren(client, signal) {
|
|
1198
|
-
if (typeof client.list !== "function")
|
|
1199
|
-
return undefined;
|
|
1200
|
-
const children = [];
|
|
1201
|
-
const seen = new Set();
|
|
1202
|
-
let offset = 0;
|
|
1203
|
-
while (true) {
|
|
1204
|
-
signal?.throwIfAborted();
|
|
1205
|
-
let page;
|
|
1206
|
-
try {
|
|
1207
|
-
const listed = await awaitWithSignal(client.list({
|
|
1208
|
-
scope: "all",
|
|
1209
|
-
limit: SANDBOX_LIST_PAGE_SIZE,
|
|
1210
|
-
offset,
|
|
1211
|
-
}), signal);
|
|
1212
|
-
if (!Array.isArray(listed) || listed.length > SANDBOX_LIST_PAGE_SIZE) {
|
|
1213
|
-
return undefined;
|
|
1214
|
-
}
|
|
1215
|
-
page = listed;
|
|
1216
|
-
}
|
|
1217
|
-
catch {
|
|
1218
|
-
signal?.throwIfAborted();
|
|
1219
|
-
return undefined;
|
|
1220
|
-
}
|
|
1221
|
-
for (const child of page) {
|
|
1222
|
-
if (!child ||
|
|
1223
|
-
typeof child !== "object" ||
|
|
1224
|
-
safeIdentifier(child.id) === undefined ||
|
|
1225
|
-
seen.has(child.id)) {
|
|
1226
|
-
return undefined;
|
|
1227
|
-
}
|
|
1228
|
-
seen.add(child.id);
|
|
1229
|
-
}
|
|
1230
|
-
if (children.length + page.length > MAX_LIST_RESULTS)
|
|
1231
|
-
return undefined;
|
|
1232
|
-
children.push(...page);
|
|
1233
|
-
if (page.length < SANDBOX_LIST_PAGE_SIZE)
|
|
1234
|
-
return children;
|
|
1235
|
-
if (offset > Number.MAX_SAFE_INTEGER - SANDBOX_LIST_PAGE_SIZE) {
|
|
1236
|
-
return undefined;
|
|
1237
|
-
}
|
|
1238
|
-
offset += SANDBOX_LIST_PAGE_SIZE;
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
/**
|
|
1242
|
-
* Read every account child that carries a fork marker this source produced.
|
|
1243
|
-
*
|
|
1244
|
-
* The scan is the shared front half of fork recovery and cleanup. It returns
|
|
1245
|
-
* undefined when the inventory itself cannot be trusted, so both callers fail
|
|
1246
|
-
* closed on the same condition.
|
|
1247
|
-
*/
|
|
1248
|
-
async function listMarkedForkChildren(client, box, provider, key, signal) {
|
|
1249
|
-
const children = await listAllSandboxChildren(client, signal);
|
|
1250
|
-
if (children === undefined)
|
|
1251
|
-
return undefined;
|
|
1252
|
-
const marked = [];
|
|
1253
|
-
for (const child of children) {
|
|
1254
|
-
if (!child ||
|
|
1255
|
-
typeof child !== "object" ||
|
|
1256
|
-
safeIdentifier(child.id) === undefined) {
|
|
1257
|
-
return undefined;
|
|
1258
|
-
}
|
|
1259
|
-
if (child.id === box.id)
|
|
1260
|
-
continue;
|
|
1261
|
-
const marker = forkMarkerFromMetadata(child.metadata, key);
|
|
1262
|
-
if (!marker || !markerBelongsToSource(marker, provider, box.id))
|
|
1263
|
-
continue;
|
|
1264
|
-
marked.push({ child, marker });
|
|
1265
|
-
}
|
|
1266
|
-
return marked;
|
|
1267
|
-
}
|
|
1268
|
-
function markerBelongsToSource(marker, provider, sourceEnvironmentId) {
|
|
1269
|
-
return (marker.request.checkpoint.provider === provider &&
|
|
1270
|
-
marker.request.checkpoint.source.environmentId === sourceEnvironmentId);
|
|
1271
|
-
}
|
|
1272
|
-
function checkpointMarkerBelongsToSource(marker, provider, sourceEnvironmentId) {
|
|
1273
|
-
return (marker.request.source.provider === provider &&
|
|
1274
|
-
marker.request.source.environmentId === sourceEnvironmentId);
|
|
1275
|
-
}
|
|
1276
|
-
function checkpointMarkerFromTags(tags, key) {
|
|
1277
|
-
if (!Array.isArray(tags) ||
|
|
1278
|
-
tags.length > MAX_MARKER_CHUNKS + 3 ||
|
|
1279
|
-
!tags.every((tag) => safeString(tag) !== undefined)) {
|
|
1280
|
-
return undefined;
|
|
1281
|
-
}
|
|
1282
|
-
const currentBase = `${MARKER_PREFIX}-checkpoint`;
|
|
1283
|
-
const legacyBase = `${LEGACY_MARKER_PREFIX}:checkpoint`;
|
|
1284
|
-
const hasCurrentTags = tags.some((tag) => tag.startsWith(`${currentBase}-`));
|
|
1285
|
-
const hasLegacyTags = tags.some((tag) => tag.startsWith(`${legacyBase}:`));
|
|
1286
|
-
if (hasCurrentTags === hasLegacyTags)
|
|
1287
|
-
return undefined;
|
|
1288
|
-
if (hasLegacyTags)
|
|
1289
|
-
return legacyCheckpointMarkerFromTags(tags, key, legacyBase);
|
|
1290
|
-
if (tags.some((tag) => Buffer.byteLength(tag, "utf8") > MAX_MARKER_TAG_LENGTH)) {
|
|
1291
|
-
return undefined;
|
|
1292
|
-
}
|
|
1293
|
-
return currentCheckpointMarkerFromTags(tags, key, currentBase);
|
|
1294
|
-
}
|
|
1295
|
-
function currentCheckpointMarkerFromTags(tags, key, base) {
|
|
1296
|
-
const keyTag = tags.find((tag) => tag.startsWith(`${base}-key-`));
|
|
1297
|
-
if (keyTag &&
|
|
1298
|
-
key !== undefined &&
|
|
1299
|
-
keyTag.slice(`${base}-key-`.length) !==
|
|
1300
|
-
markerKeyDigest(key).replace(":", "-")) {
|
|
1301
|
-
return undefined;
|
|
1302
|
-
}
|
|
1303
|
-
const chunks = tags
|
|
1304
|
-
.map((tag) => {
|
|
1305
|
-
const match = tag.match(new RegExp(`^${escapeRegExp(base)}-material-(\\d+)-(\\d+)-([A-Za-z0-9_-]+)$`));
|
|
1306
|
-
return match
|
|
1307
|
-
? { index: Number(match[1]), total: Number(match[2]), chunk: match[3] }
|
|
1308
|
-
: undefined;
|
|
1309
|
-
})
|
|
1310
|
-
.filter((value) => value !== undefined)
|
|
1311
|
-
.sort((left, right) => left.index - right.index);
|
|
1312
|
-
if (chunks.length === 0 ||
|
|
1313
|
-
chunks[0].total < 1 ||
|
|
1314
|
-
chunks[0].total > MAX_MARKER_CHUNKS ||
|
|
1315
|
-
chunks[0].total !== chunks.length ||
|
|
1316
|
-
chunks.some((chunk, index) => !Number.isSafeInteger(chunk.index) ||
|
|
1317
|
-
!Number.isSafeInteger(chunk.total) ||
|
|
1318
|
-
chunk.index !== index ||
|
|
1319
|
-
chunk.total !== chunks[0].total)) {
|
|
1320
|
-
return undefined;
|
|
1321
|
-
}
|
|
1322
|
-
const decoded = decodeJson(chunks.map((chunk) => chunk.chunk).join(""));
|
|
1323
|
-
return checkpointMarkerFromUnknown(decoded, key);
|
|
1324
|
-
}
|
|
1325
|
-
function legacyCheckpointMarkerFromTags(tags, key, base) {
|
|
1326
|
-
const keyTag = tags.find((tag) => tag.startsWith(`${base}:key:`));
|
|
1327
|
-
if (keyTag &&
|
|
1328
|
-
key !== undefined &&
|
|
1329
|
-
decodeText(keyTag.slice(`${base}:key:`.length)) !== key) {
|
|
1330
|
-
return undefined;
|
|
1331
|
-
}
|
|
1332
|
-
const chunks = tags
|
|
1333
|
-
.map((tag) => {
|
|
1334
|
-
const match = tag.match(new RegExp(`^${escapeRegExp(base)}:material:(\\d+):(\\d+):([A-Za-z0-9_-]+)$`));
|
|
1335
|
-
return match
|
|
1336
|
-
? { index: Number(match[1]), total: Number(match[2]), chunk: match[3] }
|
|
1337
|
-
: undefined;
|
|
1338
|
-
})
|
|
1339
|
-
.filter((value) => value !== undefined)
|
|
1340
|
-
.sort((left, right) => left.index - right.index);
|
|
1341
|
-
if (chunks.length === 0 ||
|
|
1342
|
-
chunks[0].total < 1 ||
|
|
1343
|
-
chunks[0].total > MAX_MARKER_CHUNKS ||
|
|
1344
|
-
chunks[0].total !== chunks.length ||
|
|
1345
|
-
chunks.some((chunk, index) => !Number.isSafeInteger(chunk.index) ||
|
|
1346
|
-
!Number.isSafeInteger(chunk.total) ||
|
|
1347
|
-
chunk.index !== index ||
|
|
1348
|
-
chunk.total !== chunks[0].total)) {
|
|
1349
|
-
return undefined;
|
|
1350
|
-
}
|
|
1351
|
-
const decoded = decodeJson(chunks.map((chunk) => chunk.chunk).join(""));
|
|
1352
|
-
return checkpointMarkerFromUnknown(decoded, key, true);
|
|
1353
|
-
}
|
|
1354
|
-
function checkpointMarkerFromUnknown(value, key, legacy = false) {
|
|
1355
|
-
if (!value || typeof value !== "object")
|
|
1356
|
-
return undefined;
|
|
1357
|
-
const parsed = value;
|
|
1358
|
-
if (parsed.version !== 1 ||
|
|
1359
|
-
parsed.kind !== "checkpoint" ||
|
|
1360
|
-
typeof parsed.idempotencyKey !== "string" ||
|
|
1361
|
-
typeof parsed.requestDigest !== "string")
|
|
1362
|
-
return undefined;
|
|
1363
|
-
if (key !== undefined && parsed.idempotencyKey !== key)
|
|
1364
|
-
return undefined;
|
|
1365
|
-
const request = WorkspaceCheckpointRequestSchema.safeParse(parsed.request);
|
|
1366
|
-
if (!request.success ||
|
|
1367
|
-
request.data.idempotencyKey !== parsed.idempotencyKey ||
|
|
1368
|
-
request.data.requestDigest !== parsed.requestDigest)
|
|
1369
|
-
return undefined;
|
|
1370
|
-
return {
|
|
1371
|
-
version: 1,
|
|
1372
|
-
kind: "checkpoint",
|
|
1373
|
-
idempotencyKey: parsed.idempotencyKey,
|
|
1374
|
-
requestDigest: parsed.requestDigest,
|
|
1375
|
-
request: request.data,
|
|
1376
|
-
...(legacy ? { legacy: true } : {}),
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
function forkMarkerFromMetadata(metadata, key) {
|
|
1380
|
-
if (!metadata ||
|
|
1381
|
-
typeof metadata !== "object" ||
|
|
1382
|
-
!Object.hasOwn(metadata, FORK_METADATA_KEY)) {
|
|
1383
|
-
return undefined;
|
|
1384
|
-
}
|
|
1385
|
-
const value = metadata[FORK_METADATA_KEY];
|
|
1386
|
-
if (!value || typeof value !== "object")
|
|
1387
|
-
return undefined;
|
|
1388
|
-
const parsed = value;
|
|
1389
|
-
if (parsed.version !== 1 ||
|
|
1390
|
-
parsed.kind !== "fork" ||
|
|
1391
|
-
typeof parsed.idempotencyKey !== "string" ||
|
|
1392
|
-
typeof parsed.requestDigest !== "string")
|
|
1393
|
-
return undefined;
|
|
1394
|
-
if (parsed.materialization !== undefined &&
|
|
1395
|
-
parsed.materialization !== "snapshot") {
|
|
1396
|
-
return undefined;
|
|
1397
|
-
}
|
|
1398
|
-
if (key !== undefined && parsed.idempotencyKey !== key)
|
|
1399
|
-
return undefined;
|
|
1400
|
-
const request = WorkspaceForkRequestSchema.safeParse(parsed.request);
|
|
1401
|
-
if (!request.success ||
|
|
1402
|
-
request.data.idempotencyKey !== parsed.idempotencyKey ||
|
|
1403
|
-
request.data.requestDigest !== parsed.requestDigest)
|
|
1404
|
-
return undefined;
|
|
1405
|
-
return {
|
|
1406
|
-
version: 1,
|
|
1407
|
-
kind: "fork",
|
|
1408
|
-
idempotencyKey: parsed.idempotencyKey,
|
|
1409
|
-
requestDigest: parsed.requestDigest,
|
|
1410
|
-
request: request.data,
|
|
1411
|
-
...(parsed.materialization === "snapshot"
|
|
1412
|
-
? { materialization: "snapshot" }
|
|
1413
|
-
: {}),
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
690
|
/**
|
|
1417
691
|
* Turn a failed create into a conflict only from provider-owned material.
|
|
1418
692
|
*
|
|
@@ -1450,38 +724,6 @@ async function forkConflictFromRemote(client, box, provider, request, verifier,
|
|
|
1450
724
|
? undefined
|
|
1451
725
|
: forkSuccess(request, environment, "replayed");
|
|
1452
726
|
}
|
|
1453
|
-
/**
|
|
1454
|
-
* Read a fork ledger answer for a key that left no marked resource behind.
|
|
1455
|
-
*
|
|
1456
|
-
* `absent` is the settled answer: a decided operation with no inventory marker
|
|
1457
|
-
* means the child was cleaned after creation, and the provider must not
|
|
1458
|
-
* resurrect it from the ledger. Every other state is undecided for the caller.
|
|
1459
|
-
*/
|
|
1460
|
-
function lookupOutcomeFromSandbox(lookup, kind) {
|
|
1461
|
-
if (!lookup || lookup.kind !== kind) {
|
|
1462
|
-
return {
|
|
1463
|
-
absent: false,
|
|
1464
|
-
message: `Sandbox returned no ${kind} lookup`,
|
|
1465
|
-
retryable: true,
|
|
1466
|
-
};
|
|
1467
|
-
}
|
|
1468
|
-
if (lookup.outcome === "conflict") {
|
|
1469
|
-
return {
|
|
1470
|
-
absent: false,
|
|
1471
|
-
message: "Sandbox found a conflicting operation without provider identity",
|
|
1472
|
-
retryable: false,
|
|
1473
|
-
};
|
|
1474
|
-
}
|
|
1475
|
-
if (lookup.outcome !== "not_found" &&
|
|
1476
|
-
(lookup.outcome === "unknown" || lookup.state !== "succeeded")) {
|
|
1477
|
-
return {
|
|
1478
|
-
absent: false,
|
|
1479
|
-
message: `Sandbox ${kind} operation is not decided`,
|
|
1480
|
-
retryable: true,
|
|
1481
|
-
};
|
|
1482
|
-
}
|
|
1483
|
-
return { absent: true };
|
|
1484
|
-
}
|
|
1485
727
|
function checkpointSuccess(request, checkpoint, status) {
|
|
1486
728
|
return WorkspaceCheckpointResultSchema.parse({
|
|
1487
729
|
status,
|
|
@@ -1640,85 +882,7 @@ function cleanupTransportFailure(request, message, retryable) {
|
|
|
1640
882
|
retryable,
|
|
1641
883
|
});
|
|
1642
884
|
}
|
|
1643
|
-
function isoDate(value) {
|
|
1644
|
-
const date = value instanceof Date ? value : new Date(value);
|
|
1645
|
-
if (!Number.isFinite(date.getTime()))
|
|
1646
|
-
throw new Error("Sandbox returned an invalid workspace timestamp");
|
|
1647
|
-
return date.toISOString();
|
|
1648
|
-
}
|
|
1649
|
-
function validDate(value) {
|
|
1650
|
-
if (value === undefined)
|
|
1651
|
-
return false;
|
|
1652
|
-
const date = value instanceof Date ? value : new Date(value);
|
|
1653
|
-
return Number.isFinite(date.getTime());
|
|
1654
|
-
}
|
|
1655
|
-
function cloneJson(value) {
|
|
1656
|
-
assertBoundedJson(value);
|
|
1657
|
-
return structuredClone(value);
|
|
1658
|
-
}
|
|
1659
|
-
function safeIdentifier(value) {
|
|
1660
|
-
if (typeof value !== "string" ||
|
|
1661
|
-
value.length === 0 ||
|
|
1662
|
-
value.length > 512 ||
|
|
1663
|
-
value.trim() !== value)
|
|
1664
|
-
return undefined;
|
|
1665
|
-
return value;
|
|
1666
|
-
}
|
|
1667
|
-
function safeString(value) {
|
|
1668
|
-
if (typeof value !== "string" ||
|
|
1669
|
-
value.length === 0 ||
|
|
1670
|
-
value.length > MAX_STRING_LENGTH)
|
|
1671
|
-
return undefined;
|
|
1672
|
-
return value;
|
|
1673
|
-
}
|
|
1674
885
|
function safeError(error) {
|
|
1675
886
|
const message = error instanceof Error ? error.message : "transport error";
|
|
1676
887
|
return message.slice(0, MAX_STRING_LENGTH);
|
|
1677
888
|
}
|
|
1678
|
-
function encodeText(value) {
|
|
1679
|
-
return Buffer.from(value, "utf8").toString("base64url");
|
|
1680
|
-
}
|
|
1681
|
-
function decodeText(value) {
|
|
1682
|
-
try {
|
|
1683
|
-
const decoded = Buffer.from(value, "base64url").toString("utf8");
|
|
1684
|
-
return encodeText(decoded) === value ? decoded : undefined;
|
|
1685
|
-
}
|
|
1686
|
-
catch {
|
|
1687
|
-
return undefined;
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
function markerKeyDigest(value) {
|
|
1691
|
-
return sha256Bytes(Buffer.from(value, "utf8"));
|
|
1692
|
-
}
|
|
1693
|
-
function encodeJson(value) {
|
|
1694
|
-
try {
|
|
1695
|
-
const serialized = JSON.stringify(value);
|
|
1696
|
-
if (serialized === undefined)
|
|
1697
|
-
return undefined;
|
|
1698
|
-
return encodeText(serialized);
|
|
1699
|
-
}
|
|
1700
|
-
catch {
|
|
1701
|
-
return undefined;
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
function decodeJson(value) {
|
|
1705
|
-
try {
|
|
1706
|
-
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
1707
|
-
}
|
|
1708
|
-
catch {
|
|
1709
|
-
return undefined;
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
function split(value) {
|
|
1713
|
-
return splitIntoChunks(value, MARKER_CHUNK_SIZE);
|
|
1714
|
-
}
|
|
1715
|
-
function splitIntoChunks(value, chunkSize) {
|
|
1716
|
-
const chunks = [];
|
|
1717
|
-
for (let index = 0; index < value.length; index += chunkSize) {
|
|
1718
|
-
chunks.push(value.slice(index, index + chunkSize));
|
|
1719
|
-
}
|
|
1720
|
-
return chunks;
|
|
1721
|
-
}
|
|
1722
|
-
function escapeRegExp(value) {
|
|
1723
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1724
|
-
}
|