borgmcp 2.1.0 → 2.2.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/README.md +2 -2
- package/dist/assimilate-cmd.d.ts +11 -2
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +60 -85
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +26 -8
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +7 -5
- package/dist/cli-help.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/regen-format.d.ts +1 -6
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +9 -8
- package/dist/regen-format.js.map +1 -1
- package/dist/remote-client.js +1 -1
- package/dist/remote-client.js.map +1 -1
- package/dist/repository-cube-init.d.ts +16 -1
- package/dist/repository-cube-init.d.ts.map +1 -1
- package/dist/repository-cube-init.js +167 -22
- package/dist/repository-cube-init.js.map +1 -1
- package/dist/server-errors.d.ts +11 -0
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js +20 -0
- package/dist/server-errors.js.map +1 -1
- package/dist/server-facade.d.ts +2 -1
- package/dist/server-facade.d.ts.map +1 -1
- package/dist/server-facade.js +16 -10
- package/dist/server-facade.js.map +1 -1
- package/dist/server-handshake.d.ts +32 -2
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +161 -8
- package/dist/server-handshake.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +9 -7
- package/docs/LOCAL_SERVER.md +19 -13
- package/docs/RELEASING.md +21 -5
- package/package.json +2 -2
- package/src/assimilate-cmd.ts +85 -69
- package/src/assimilate-deps.ts +37 -7
- package/src/cli-help.ts +7 -5
- package/src/index.ts +5 -1
- package/src/regen-format.ts +11 -9
- package/src/remote-client.ts +1 -1
- package/src/repository-cube-init.ts +202 -23
- package/src/server-errors.ts +27 -0
- package/src/server-facade.ts +16 -10
- package/src/server-handshake.ts +226 -8
package/src/server-handshake.ts
CHANGED
|
@@ -4,8 +4,12 @@ import {
|
|
|
4
4
|
ENROLLMENT_EXCHANGE_PATH,
|
|
5
5
|
HEALTH_PATH,
|
|
6
6
|
PROTOCOL_INFO_PATH,
|
|
7
|
+
REPOSITORY_CUBE_ASSOCIATION_PATH,
|
|
8
|
+
REPOSITORY_CUBE_RESOLVE_PATH,
|
|
7
9
|
createAttachRequestEnvelope,
|
|
8
10
|
createProtocolEnvelope,
|
|
11
|
+
decodeAssociateRepositoryCubeRequest,
|
|
12
|
+
decodeAssociateRepositoryCubeResponseEnvelope,
|
|
9
13
|
decodeAttachResponseEnvelope,
|
|
10
14
|
decodeCreateCubeRequest,
|
|
11
15
|
decodeCreateCubeResponseEnvelope,
|
|
@@ -13,12 +17,16 @@ import {
|
|
|
13
17
|
decodeEnrollmentExchangeResponseEnvelope,
|
|
14
18
|
decodeProtocolErrorEnvelope,
|
|
15
19
|
decodeProtocolTagPreflight,
|
|
20
|
+
decodeResolveRepositoryCubeRequest,
|
|
21
|
+
decodeResolveRepositoryCubeResponseEnvelope,
|
|
16
22
|
ErrorCode,
|
|
23
|
+
type AssociateRepositoryCubeResponse,
|
|
17
24
|
type CreateCubeRepository,
|
|
18
25
|
type CreateCubeResponse,
|
|
19
26
|
type CubeTemplate,
|
|
20
27
|
type DroneRuntimeMetadata,
|
|
21
28
|
type ProtocolTagPreflight,
|
|
29
|
+
type ResolveRepositoryCubeResponse,
|
|
22
30
|
type ServerCapability,
|
|
23
31
|
} from 'borgmcp-shared/protocol';
|
|
24
32
|
import { createHash, randomUUID } from 'node:crypto';
|
|
@@ -49,6 +57,9 @@ import {
|
|
|
49
57
|
BorgServerUnreachableError,
|
|
50
58
|
CubeCreationConfirmationError,
|
|
51
59
|
CubeCreationOutcomeUnknownError,
|
|
60
|
+
RepositoryAssociationOperationError,
|
|
61
|
+
RepositoryAssociationOutcomeUnknownError,
|
|
62
|
+
RepositoryAssociationResolutionError,
|
|
52
63
|
} from './server-errors.js';
|
|
53
64
|
import { DroneEvictedError, DRONE_EVICTED_CODE } from './drone-lifecycle.js';
|
|
54
65
|
import { readBoundedResponseBody } from './server-response.js';
|
|
@@ -185,7 +196,7 @@ export interface ServerAttachResult {
|
|
|
185
196
|
}
|
|
186
197
|
|
|
187
198
|
/**
|
|
188
|
-
* Attach an enrolled client principal to one granted cube/role over protocol
|
|
199
|
+
* Attach an enrolled client principal to one granted cube/role over protocol v5.
|
|
189
200
|
* The client CSPRNG-generates the session bearer and persists it PENDING in the
|
|
190
201
|
* OS keychain (keyed by the stable per-seat identity) BEFORE this request, so an
|
|
191
202
|
* interrupted/lost response is recovered by re-sending the exact same bearer —
|
|
@@ -558,6 +569,210 @@ export async function resumeBorgServerEnrollment(
|
|
|
558
569
|
});
|
|
559
570
|
}
|
|
560
571
|
|
|
572
|
+
async function requireMatchingServerCredential(
|
|
573
|
+
origin: string,
|
|
574
|
+
trustIdentity: string,
|
|
575
|
+
parentCredential: string,
|
|
576
|
+
loadCredentialRecord: typeof getServerCredentialRecord,
|
|
577
|
+
): Promise<void> {
|
|
578
|
+
const active = await loadCredentialRecord(origin, trustIdentity);
|
|
579
|
+
if (!active || active.credential !== parentCredential || !active.clientId) {
|
|
580
|
+
throw new BorgServerError('CREDENTIAL_REJECTED', 'stored Borg server credential was rejected');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function protocolErrorCode(response: Response): Promise<ErrorCode | undefined> {
|
|
585
|
+
try {
|
|
586
|
+
return decodeProtocolErrorEnvelope(
|
|
587
|
+
JSON.parse(await readHandshakeBodyWithTimeout(response)),
|
|
588
|
+
).error.code;
|
|
589
|
+
} catch {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Resolve one client-scoped repository association without mutation. */
|
|
595
|
+
export async function resolveBorgServerRepositoryCube(
|
|
596
|
+
origin: string,
|
|
597
|
+
trustIdentity: string,
|
|
598
|
+
parentCredential: string,
|
|
599
|
+
input: { workingRepoName: string; repository: CreateCubeRepository },
|
|
600
|
+
deps: {
|
|
601
|
+
fetchImpl?: FetchLike;
|
|
602
|
+
loadCredentialRecord?: typeof getServerCredentialRecord;
|
|
603
|
+
} = {},
|
|
604
|
+
): Promise<ResolveRepositoryCubeResponse> {
|
|
605
|
+
await requireMatchingServerCredential(
|
|
606
|
+
origin,
|
|
607
|
+
trustIdentity,
|
|
608
|
+
parentCredential,
|
|
609
|
+
deps.loadCredentialRecord ?? getServerCredentialRecord,
|
|
610
|
+
);
|
|
611
|
+
const request = decodeResolveRepositoryCubeRequest({
|
|
612
|
+
working_repo_name: input.workingRepoName,
|
|
613
|
+
repository: input.repository,
|
|
614
|
+
});
|
|
615
|
+
const controller = new AbortController();
|
|
616
|
+
const timeout = setTimeout(() => controller.abort(), HANDSHAKE_TIMEOUT_MS);
|
|
617
|
+
let response: Response;
|
|
618
|
+
try {
|
|
619
|
+
response = await (deps.fetchImpl ?? fetch)(handshakeUrl(origin, REPOSITORY_CUBE_RESOLVE_PATH), {
|
|
620
|
+
method: 'POST',
|
|
621
|
+
redirect: 'error',
|
|
622
|
+
signal: controller.signal,
|
|
623
|
+
headers: {
|
|
624
|
+
Accept: 'application/json',
|
|
625
|
+
'Content-Type': 'application/json',
|
|
626
|
+
Authorization: `Bearer ${parentCredential}`,
|
|
627
|
+
},
|
|
628
|
+
body: JSON.stringify(createProtocolEnvelope(randomUUID(), request)),
|
|
629
|
+
});
|
|
630
|
+
} catch (error) {
|
|
631
|
+
throw new RepositoryAssociationResolutionError();
|
|
632
|
+
} finally {
|
|
633
|
+
clearTimeout(timeout);
|
|
634
|
+
}
|
|
635
|
+
if (response.status === 401 || response.status === 403) {
|
|
636
|
+
throw new BorgServerError('CREDENTIAL_REJECTED', 'Borg server enrollment was rejected');
|
|
637
|
+
}
|
|
638
|
+
if (response.status !== 200) {
|
|
639
|
+
throw new RepositoryAssociationResolutionError();
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
const decoded = decodeResolveRepositoryCubeResponseEnvelope(
|
|
643
|
+
JSON.parse(await readHandshakeBodyWithTimeout(response)),
|
|
644
|
+
).payload;
|
|
645
|
+
if (
|
|
646
|
+
decoded.result === 'resolved' &&
|
|
647
|
+
(decoded.repository.kind !== request.repository.kind ||
|
|
648
|
+
decoded.repository.value !== request.repository.value ||
|
|
649
|
+
decoded.working_repo_name !== request.working_repo_name)
|
|
650
|
+
) {
|
|
651
|
+
throw new Error('association mismatch');
|
|
652
|
+
}
|
|
653
|
+
return decoded;
|
|
654
|
+
} catch {
|
|
655
|
+
throw new RepositoryAssociationResolutionError();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export async function resolveLocalBorgServerRepositoryCube(
|
|
660
|
+
origin: string,
|
|
661
|
+
trustIdentity: string,
|
|
662
|
+
parentCredential: string,
|
|
663
|
+
input: { workingRepoName: string; repository: CreateCubeRepository },
|
|
664
|
+
deps: { loadTrust?: typeof loadBorgServerTrust } = {},
|
|
665
|
+
): Promise<ResolveRepositoryCubeResponse> {
|
|
666
|
+
const trust = await (deps.loadTrust ?? loadBorgServerTrust)(origin);
|
|
667
|
+
if (trust.identity !== trustIdentity) {
|
|
668
|
+
throw new BorgServerTrustError('Borg server trust identity changed; refusing repository resolution');
|
|
669
|
+
}
|
|
670
|
+
return resolveBorgServerRepositoryCube(origin, trustIdentity, parentCredential, input, {
|
|
671
|
+
fetchImpl: trust.fetchImpl,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Atomically associate an explicit accessible cube after local confirmation. */
|
|
676
|
+
export async function associateBorgServerRepositoryCube(
|
|
677
|
+
origin: string,
|
|
678
|
+
trustIdentity: string,
|
|
679
|
+
parentCredential: string,
|
|
680
|
+
input: { cubeId: string; workingRepoName: string; repository: CreateCubeRepository },
|
|
681
|
+
deps: {
|
|
682
|
+
fetchImpl?: FetchLike;
|
|
683
|
+
loadCredentialRecord?: typeof getServerCredentialRecord;
|
|
684
|
+
} = {},
|
|
685
|
+
): Promise<AssociateRepositoryCubeResponse> {
|
|
686
|
+
await requireMatchingServerCredential(
|
|
687
|
+
origin,
|
|
688
|
+
trustIdentity,
|
|
689
|
+
parentCredential,
|
|
690
|
+
deps.loadCredentialRecord ?? getServerCredentialRecord,
|
|
691
|
+
);
|
|
692
|
+
const request = decodeAssociateRepositoryCubeRequest({
|
|
693
|
+
cube_id: input.cubeId,
|
|
694
|
+
working_repo_name: input.workingRepoName,
|
|
695
|
+
repository: input.repository,
|
|
696
|
+
});
|
|
697
|
+
const controller = new AbortController();
|
|
698
|
+
const timeout = setTimeout(() => controller.abort(), HANDSHAKE_TIMEOUT_MS);
|
|
699
|
+
let response: Response;
|
|
700
|
+
try {
|
|
701
|
+
response = await (deps.fetchImpl ?? fetch)(handshakeUrl(origin, REPOSITORY_CUBE_ASSOCIATION_PATH), {
|
|
702
|
+
method: 'PUT',
|
|
703
|
+
redirect: 'error',
|
|
704
|
+
signal: controller.signal,
|
|
705
|
+
headers: {
|
|
706
|
+
Accept: 'application/json',
|
|
707
|
+
'Content-Type': 'application/json',
|
|
708
|
+
Authorization: `Bearer ${parentCredential}`,
|
|
709
|
+
},
|
|
710
|
+
body: JSON.stringify(createProtocolEnvelope(randomUUID(), request)),
|
|
711
|
+
});
|
|
712
|
+
} catch {
|
|
713
|
+
throw new RepositoryAssociationOutcomeUnknownError();
|
|
714
|
+
} finally {
|
|
715
|
+
clearTimeout(timeout);
|
|
716
|
+
}
|
|
717
|
+
if (response.status === 401) {
|
|
718
|
+
throw new BorgServerError('CREDENTIAL_REJECTED', 'Borg server enrollment was rejected');
|
|
719
|
+
}
|
|
720
|
+
if (response.status === 403) {
|
|
721
|
+
if (await protocolErrorCode(response) !== ErrorCode.ACCESS_DENIED) {
|
|
722
|
+
throw new RepositoryAssociationOutcomeUnknownError();
|
|
723
|
+
}
|
|
724
|
+
throw new RepositoryAssociationOperationError('access-denied');
|
|
725
|
+
}
|
|
726
|
+
if (response.status === 409) {
|
|
727
|
+
const code = await protocolErrorCode(response);
|
|
728
|
+
if (code === ErrorCode.REPOSITORY_ALREADY_ASSOCIATED) {
|
|
729
|
+
throw new RepositoryAssociationOperationError('repository-already-associated');
|
|
730
|
+
}
|
|
731
|
+
if (code === ErrorCode.CUBE_ALREADY_ASSOCIATED) {
|
|
732
|
+
throw new RepositoryAssociationOperationError('cube-already-associated');
|
|
733
|
+
}
|
|
734
|
+
if (code === ErrorCode.INVALID_INPUT) {
|
|
735
|
+
throw new RepositoryAssociationOperationError('invalid-cube');
|
|
736
|
+
}
|
|
737
|
+
throw new RepositoryAssociationOutcomeUnknownError();
|
|
738
|
+
}
|
|
739
|
+
if (response.status !== 200) {
|
|
740
|
+
throw new RepositoryAssociationOutcomeUnknownError();
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
const decoded = decodeAssociateRepositoryCubeResponseEnvelope(
|
|
744
|
+
JSON.parse(await readHandshakeBodyWithTimeout(response)),
|
|
745
|
+
).payload;
|
|
746
|
+
if (
|
|
747
|
+
decoded.cube_id !== request.cube_id ||
|
|
748
|
+
decoded.repository.kind !== request.repository.kind ||
|
|
749
|
+
decoded.repository.value !== request.repository.value ||
|
|
750
|
+
decoded.working_repo_name !== request.working_repo_name
|
|
751
|
+
) {
|
|
752
|
+
throw new Error('association mismatch');
|
|
753
|
+
}
|
|
754
|
+
return decoded;
|
|
755
|
+
} catch (error) {
|
|
756
|
+
throw new RepositoryAssociationOutcomeUnknownError();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export async function associateLocalBorgServerRepositoryCube(
|
|
761
|
+
origin: string,
|
|
762
|
+
trustIdentity: string,
|
|
763
|
+
parentCredential: string,
|
|
764
|
+
input: { cubeId: string; workingRepoName: string; repository: CreateCubeRepository },
|
|
765
|
+
deps: { loadTrust?: typeof loadBorgServerTrust } = {},
|
|
766
|
+
): Promise<AssociateRepositoryCubeResponse> {
|
|
767
|
+
const trust = await (deps.loadTrust ?? loadBorgServerTrust)(origin);
|
|
768
|
+
if (trust.identity !== trustIdentity) {
|
|
769
|
+
throw new BorgServerTrustError('Borg server trust identity changed; refusing repository association');
|
|
770
|
+
}
|
|
771
|
+
return associateBorgServerRepositoryCube(origin, trustIdentity, parentCredential, input, {
|
|
772
|
+
fetchImpl: trust.fetchImpl,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
561
776
|
/**
|
|
562
777
|
* Create one repository cube through the narrow owner capability. The retry
|
|
563
778
|
* key is persisted in the OS keychain before network I/O and reused exactly
|
|
@@ -652,16 +867,15 @@ export async function createBorgServerCube(
|
|
|
652
867
|
throw new CubeCreationConfirmationError('The Borg server rejected the repository cube operation identity.');
|
|
653
868
|
}
|
|
654
869
|
if (response.status !== 201) {
|
|
655
|
-
throw new
|
|
870
|
+
throw new CubeCreationOutcomeUnknownError();
|
|
656
871
|
}
|
|
657
872
|
let decoded: ReturnType<typeof decodeCreateCubeResponseEnvelope>;
|
|
658
873
|
try {
|
|
659
874
|
decoded = decodeCreateCubeResponseEnvelope(
|
|
660
875
|
JSON.parse(await readHandshakeBodyWithTimeout(response)),
|
|
661
876
|
);
|
|
662
|
-
} catch
|
|
663
|
-
|
|
664
|
-
throw new Error('Borg server returned an invalid cube creation envelope');
|
|
877
|
+
} catch {
|
|
878
|
+
throw new CubeCreationOutcomeUnknownError();
|
|
665
879
|
}
|
|
666
880
|
if (
|
|
667
881
|
decoded.payload.repository.kind !== pending.repository.kind ||
|
|
@@ -669,9 +883,13 @@ export async function createBorgServerCube(
|
|
|
669
883
|
) {
|
|
670
884
|
throw new CubeCreationConfirmationError();
|
|
671
885
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
886
|
+
try {
|
|
887
|
+
await (deps.clearCubeCreation ?? clearPendingServerCubeCreation)(
|
|
888
|
+
pending as PendingServerCubeCreationRecord,
|
|
889
|
+
);
|
|
890
|
+
} catch {
|
|
891
|
+
throw new CubeCreationConfirmationError('The server confirmed the cube, but local retry state could not be finalized.');
|
|
892
|
+
}
|
|
675
893
|
return decoded.payload;
|
|
676
894
|
}
|
|
677
895
|
|