borgmcp 2.10.2 → 2.12.1

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.
Files changed (86) hide show
  1. package/README.md +7 -3
  2. package/dist/assimilate-cmd.d.ts +5 -2
  3. package/dist/assimilate-cmd.d.ts.map +1 -1
  4. package/dist/assimilate-cmd.js +3 -1
  5. package/dist/assimilate-cmd.js.map +1 -1
  6. package/dist/assimilate-deps.d.ts.map +1 -1
  7. package/dist/assimilate-deps.js +2 -2
  8. package/dist/assimilate-deps.js.map +1 -1
  9. package/dist/claude.js +3 -3
  10. package/dist/claude.js.map +1 -1
  11. package/dist/cli-help.js +3 -3
  12. package/dist/cli-help.js.map +1 -1
  13. package/dist/cli-platform.d.ts +1 -0
  14. package/dist/cli-platform.d.ts.map +1 -1
  15. package/dist/cli-platform.js +10 -1
  16. package/dist/cli-platform.js.map +1 -1
  17. package/dist/cli-tool-approval.d.ts +3 -1
  18. package/dist/cli-tool-approval.d.ts.map +1 -1
  19. package/dist/cli-tool-approval.js +8 -14
  20. package/dist/cli-tool-approval.js.map +1 -1
  21. package/dist/config.d.ts +34 -9
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +487 -105
  24. package/dist/config.js.map +1 -1
  25. package/dist/enrollment-lock.d.ts +10 -0
  26. package/dist/enrollment-lock.d.ts.map +1 -0
  27. package/dist/enrollment-lock.js +49 -0
  28. package/dist/enrollment-lock.js.map +1 -0
  29. package/dist/enrollment-types.d.ts +47 -0
  30. package/dist/enrollment-types.d.ts.map +1 -0
  31. package/dist/enrollment-types.js +2 -0
  32. package/dist/enrollment-types.js.map +1 -0
  33. package/dist/index.js +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/invitation-artifact.d.ts +3 -1
  36. package/dist/invitation-artifact.d.ts.map +1 -1
  37. package/dist/invitation-artifact.js +3 -1
  38. package/dist/invitation-artifact.js.map +1 -1
  39. package/dist/parse-assimilate-args.d.ts +1 -1
  40. package/dist/parse-assimilate-args.d.ts.map +1 -1
  41. package/dist/parse-assimilate-args.js +5 -2
  42. package/dist/parse-assimilate-args.js.map +1 -1
  43. package/dist/recover-enrollment-cmd.d.ts.map +1 -1
  44. package/dist/recover-enrollment-cmd.js +34 -15
  45. package/dist/recover-enrollment-cmd.js.map +1 -1
  46. package/dist/remote-client.d.ts +3 -2
  47. package/dist/remote-client.d.ts.map +1 -1
  48. package/dist/remote-client.js +32 -11
  49. package/dist/remote-client.js.map +1 -1
  50. package/dist/server-errors.d.ts +4 -0
  51. package/dist/server-errors.d.ts.map +1 -1
  52. package/dist/server-errors.js +8 -0
  53. package/dist/server-errors.js.map +1 -1
  54. package/dist/server-handshake.d.ts +11 -3
  55. package/dist/server-handshake.d.ts.map +1 -1
  56. package/dist/server-handshake.js +103 -45
  57. package/dist/server-handshake.js.map +1 -1
  58. package/dist/server-trust.d.ts +14 -1
  59. package/dist/server-trust.d.ts.map +1 -1
  60. package/dist/server-trust.js +363 -66
  61. package/dist/server-trust.js.map +1 -1
  62. package/dist/tool-manifest.d.ts.map +1 -1
  63. package/dist/tool-manifest.js +4 -10
  64. package/dist/tool-manifest.js.map +1 -1
  65. package/docs/EXTRACTION_PROVENANCE.md +5 -5
  66. package/docs/LOCAL_SERVER.md +1 -1
  67. package/docs/RELEASING.md +10 -5
  68. package/package.json +3 -3
  69. package/src/assimilate-cmd.ts +10 -3
  70. package/src/assimilate-deps.ts +3 -2
  71. package/src/claude.ts +4 -3
  72. package/src/cli-help.ts +3 -3
  73. package/src/cli-platform.ts +16 -2
  74. package/src/cli-tool-approval.ts +9 -16
  75. package/src/config.ts +544 -101
  76. package/src/enrollment-lock.ts +53 -0
  77. package/src/enrollment-types.ts +51 -0
  78. package/src/index.ts +1 -1
  79. package/src/invitation-artifact.ts +7 -1
  80. package/src/parse-assimilate-args.ts +4 -2
  81. package/src/recover-enrollment-cmd.ts +39 -15
  82. package/src/remote-client.ts +36 -10
  83. package/src/server-errors.ts +7 -0
  84. package/src/server-handshake.ts +130 -52
  85. package/src/server-trust.ts +392 -66
  86. package/src/tool-manifest.ts +4 -10
@@ -32,6 +32,7 @@ import {
32
32
  type ServerCapability,
33
33
  } from 'borgmcp-shared/protocol';
34
34
  import { createHash, randomUUID } from 'node:crypto';
35
+ import { withEnrollmentOriginLock } from './enrollment-lock.js';
35
36
  import {
36
37
  activatePendingServerEnrollment,
37
38
  clearPendingServerCubeCreation,
@@ -40,9 +41,12 @@ import {
40
41
  getServerCredentialRecord,
41
42
  hasServerCredentialForOrigin,
42
43
  getPendingServerEnrollment,
44
+ findPendingServerEnrollment,
43
45
  getOrCreatePendingServerCubeCreation,
44
46
  getOrCreatePendingServerEnrollment,
47
+ createPendingServerEnrollmentWithReplacementConsent,
45
48
  type PendingServerCubeCreationRecord,
49
+ type PendingServerEnrollmentRecord,
46
50
  } from './config.js';
47
51
  import {
48
52
  activateAndBindSeat,
@@ -69,6 +73,7 @@ import { readBoundedResponseBody } from './server-response.js';
69
73
  import {
70
74
  loadBorgServerTrust,
71
75
  loadBorgServerTrustFromPresentedChain,
76
+ loadStagedBorgServerTrust,
72
77
  type BorgServerTrust,
73
78
  type StagedBorgServerTrust,
74
79
  } from './server-trust.js';
@@ -79,6 +84,7 @@ import {
79
84
  InvitationArtifactTrustError,
80
85
  decodeAndVerifyInvitationArtifact,
81
86
  } from './invitation-artifact.js';
87
+ import type { EnrollmentArtifactBinding } from './enrollment-types.js';
82
88
 
83
89
  const HANDSHAKE_BODY_LIMIT = 64 * 1024;
84
90
  const HANDSHAKE_TIMEOUT_MS = 5_000;
@@ -462,6 +468,7 @@ export async function enrollBorgServer(
462
468
  deps: {
463
469
  fetchImpl?: FetchLike;
464
470
  prepareEnrollment?: typeof getOrCreatePendingServerEnrollment;
471
+ prepareReplacementEnrollment?: typeof createPendingServerEnrollmentWithReplacementConsent;
465
472
  activateEnrollment?: typeof activatePendingServerEnrollment;
466
473
  clearPendingEnrollment?: typeof clearPendingServerEnrollment;
467
474
  loadCredentialRecord?: typeof getServerCredentialRecord;
@@ -469,8 +476,10 @@ export async function enrollBorgServer(
469
476
  confirmReplacement?: () => Promise<boolean>;
470
477
  clientName?: string;
471
478
  expectedAuthority?: InvitationArtifact['authority'];
472
- commitTrust?: (activate: () => Promise<void>) => Promise<void>;
479
+ artifactBinding?: EnrollmentArtifactBinding;
480
+ commitTrust?: StagedBorgServerTrust['commitTrust'];
473
481
  discardTrust?: () => Promise<void>;
482
+ resumePending?: PendingServerEnrollmentRecord;
474
483
  } = {},
475
484
  ): Promise<NewServerEnrollment> {
476
485
  const fetchImpl = deps.fetchImpl ?? fetch;
@@ -485,15 +494,13 @@ export async function enrollBorgServer(
485
494
  throw error;
486
495
  }
487
496
 
488
- const existingCredential = await (deps.loadCredentialRecord ?? getServerCredentialRecord)(
489
- origin,
490
- trustIdentity,
491
- );
492
- const existingOriginCredential = existingCredential !== null || await (
497
+ const existingCredential = deps.resumePending ? null : await (
498
+ deps.loadCredentialRecord ?? getServerCredentialRecord
499
+ )(origin, trustIdentity);
500
+ const existingOriginCredential = deps.resumePending ? false : existingCredential !== null || await (
493
501
  deps.hasCredentialForOrigin ?? hasServerCredentialForOrigin
494
502
  )(origin);
495
503
  let replacementConfirmed = false;
496
- let replacementCapability: string | undefined;
497
504
  if (existingOriginCredential) {
498
505
  const confirmed = deps.confirmReplacement !== undefined
499
506
  ? await deps.confirmReplacement()
@@ -506,16 +513,33 @@ export async function enrollBorgServer(
506
513
  );
507
514
  }
508
515
  replacementConfirmed = true;
509
- replacementCapability = randomUUID();
510
516
  }
511
517
 
512
- const pending = await (deps.prepareEnrollment ?? getOrCreatePendingServerEnrollment)({
513
- origin,
514
- trustIdentity,
515
- invitation,
516
- ...(deps.clientName ? { clientName: deps.clientName } : {}),
517
- ...(replacementCapability === undefined ? {} : { replacementCapability }),
518
- });
518
+ if (replacementConfirmed && deps.artifactBinding === undefined) {
519
+ await deps.discardTrust?.();
520
+ throw new BorgServerError(
521
+ 'LOCAL_CREDENTIAL_EXISTS',
522
+ 'replacement requires a current verified enrollment invitation',
523
+ );
524
+ }
525
+ let pending = deps.resumePending;
526
+ if (!pending) {
527
+ const common = {
528
+ origin,
529
+ trustIdentity,
530
+ invitation,
531
+ ...(deps.clientName ? { clientName: deps.clientName } : {}),
532
+ };
533
+ pending = replacementConfirmed
534
+ ? await (deps.prepareReplacementEnrollment ?? createPendingServerEnrollmentWithReplacementConsent)({
535
+ ...common,
536
+ artifactBinding: deps.artifactBinding!,
537
+ })
538
+ : await (deps.prepareEnrollment ?? getOrCreatePendingServerEnrollment)({
539
+ ...common,
540
+ ...(deps.artifactBinding === undefined ? {} : { artifactBinding: deps.artifactBinding }),
541
+ });
542
+ }
519
543
  const request = decodeEnrollmentExchangeRequest({
520
544
  invitation: pending.invitation,
521
545
  retry_key: pending.retryKey,
@@ -545,7 +569,6 @@ export async function enrollBorgServer(
545
569
  }
546
570
  }
547
571
  if (!response) {
548
- await deps.discardTrust?.();
549
572
  throw lastTransportError;
550
573
  }
551
574
 
@@ -562,7 +585,6 @@ export async function enrollBorgServer(
562
585
  );
563
586
  }
564
587
  if (response.status !== 201) {
565
- await deps.discardTrust?.();
566
588
  throw new Error(`Borg server enrollment failed (HTTP ${response.status})`);
567
589
  }
568
590
  let decoded: ReturnType<typeof decodeEnrollmentExchangeResponseEnvelope>;
@@ -571,7 +593,6 @@ export async function enrollBorgServer(
571
593
  JSON.parse(await readHandshakeBodyWithTimeout(response)),
572
594
  );
573
595
  } catch (error) {
574
- await deps.discardTrust?.();
575
596
  if (error instanceof Error && error.message.includes('response limit')) throw error;
576
597
  throw new Error('Borg server returned an invalid enrollment envelope');
577
598
  }
@@ -589,16 +610,18 @@ export async function enrollBorgServer(
589
610
  }
590
611
  }
591
612
 
592
- const activate = () => (deps.activateEnrollment ?? activatePendingServerEnrollment)({
613
+ const activate = (context?: { generationId: string; previousPointer: import('./enrollment-types.js').EnrollmentTrustPointer | null }) =>
614
+ (deps.activateEnrollment ?? activatePendingServerEnrollment)({
593
615
  origin,
594
616
  trustIdentity,
595
617
  retryKey: pending.retryKey,
596
618
  credential: pending.credential,
597
619
  clientId: decoded.payload.client_id,
598
620
  serverCapabilities: decoded.payload.server_capabilities,
599
- ...(replacementConfirmed || pending.replacementCapability !== undefined
600
- ? { allowReplacement: true, ...(pending.replacementCapability === undefined ? {} : { replacementCapability: pending.replacementCapability }) }
601
- : {}),
621
+ ...(context === undefined ? {} : context),
622
+ ...(pending.replacementCapability === undefined
623
+ ? {}
624
+ : { replacementCapabilityToken: pending.replacementCapability.token }),
602
625
  });
603
626
  if (deps.commitTrust) await deps.commitTrust(activate);
604
627
  else await activate();
@@ -621,6 +644,8 @@ export async function resumeBorgServerEnrollment(
621
644
  activateEnrollment?: typeof activatePendingServerEnrollment;
622
645
  clearPendingEnrollment?: typeof clearPendingServerEnrollment;
623
646
  onPending?: () => void;
647
+ commitTrust?: StagedBorgServerTrust['commitTrust'];
648
+ discardTrust?: () => Promise<void>;
624
649
  } = {},
625
650
  ): Promise<NewServerEnrollment | null> {
626
651
  const pending = await (deps.loadPendingEnrollment ?? getPendingServerEnrollment)(
@@ -628,10 +653,21 @@ export async function resumeBorgServerEnrollment(
628
653
  trustIdentity,
629
654
  );
630
655
  if (!pending) return null;
656
+ if (pending.artifactBinding) {
657
+ const verified = decodeAndVerifyInvitationArtifact(pending.invitation);
658
+ const digest = createHash('sha256').update(pending.invitation).digest('hex');
659
+ if (
660
+ digest !== pending.artifactBinding.artifactDigest ||
661
+ verified.version !== pending.artifactBinding.artifactFormatVersion ||
662
+ verified.endpoint !== pending.artifactBinding.endpoint ||
663
+ verified.ca_spki_sha256 !== pending.artifactBinding.caSpkiSha256 ||
664
+ verified.authority !== pending.artifactBinding.expectedAuthority
665
+ ) throw new InvitationArtifactTrustError();
666
+ }
631
667
  deps.onPending?.();
632
668
  return enrollBorgServer(origin, trustIdentity, pending.invitation, {
633
669
  ...(deps.fetchImpl === undefined ? {} : { fetchImpl: deps.fetchImpl }),
634
- prepareEnrollment: async () => pending,
670
+ resumePending: pending,
635
671
  ...(deps.activateEnrollment === undefined
636
672
  ? {}
637
673
  : { activateEnrollment: deps.activateEnrollment }),
@@ -639,6 +675,14 @@ export async function resumeBorgServerEnrollment(
639
675
  ? {}
640
676
  : { clearPendingEnrollment: deps.clearPendingEnrollment }),
641
677
  ...(pending.clientName === undefined ? {} : { clientName: pending.clientName }),
678
+ ...(pending.artifactBinding === undefined
679
+ ? {}
680
+ : {
681
+ artifactBinding: pending.artifactBinding,
682
+ expectedAuthority: pending.artifactBinding.expectedAuthority,
683
+ }),
684
+ ...(deps.commitTrust === undefined ? {} : { commitTrust: deps.commitTrust }),
685
+ ...(deps.discardTrust === undefined ? {} : { discardTrust: deps.discardTrust }),
642
686
  });
643
687
  }
644
688
 
@@ -1037,23 +1081,25 @@ export async function resolveBorgServerAuthority(
1037
1081
  loadCredentialRecord?: typeof getServerCredentialRecord;
1038
1082
  } = {},
1039
1083
  ): Promise<ResolvedServerAuthority> {
1040
- const trust = await (deps.loadTrust ?? loadBorgServerTrust)(origin);
1041
- const active = deps.loadCredentialRecord
1042
- ? await deps.loadCredentialRecord(origin, trust.identity)
1043
- : deps.loadCredential
1044
- ? null
1045
- : await getServerCredentialRecord(origin, trust.identity);
1046
- const token = active?.credential ?? await (deps.loadCredential ?? getServerCredential)(
1047
- origin,
1048
- trust.identity,
1049
- );
1050
- if (!token) {
1051
- throw new BorgServerError(
1052
- 'NOT_ENROLLED',
1053
- 'no enrolled credential is stored for this Borg server identity',
1084
+ return withEnrollmentOriginLock(origin, async () => {
1085
+ const trust = await (deps.loadTrust ?? loadBorgServerTrust)(origin);
1086
+ const active = deps.loadCredentialRecord
1087
+ ? await deps.loadCredentialRecord(origin, trust.identity)
1088
+ : deps.loadCredential
1089
+ ? null
1090
+ : await getServerCredentialRecord(origin, trust.identity);
1091
+ const token = active?.credential ?? await (deps.loadCredential ?? getServerCredential)(
1092
+ origin,
1093
+ trust.identity,
1054
1094
  );
1055
- }
1056
- return { ...trust, token };
1095
+ if (!token) {
1096
+ throw new BorgServerError(
1097
+ 'NOT_ENROLLED',
1098
+ 'no enrolled credential is stored for this Borg server identity',
1099
+ );
1100
+ }
1101
+ return { ...trust, token };
1102
+ });
1057
1103
  }
1058
1104
 
1059
1105
  /** Load pinned trust, then prove the stored credential against /api/protocol. */
@@ -1065,13 +1111,15 @@ export async function connectLocalBorgServer(
1065
1111
  loadCredentialRecord?: typeof getServerCredentialRecord;
1066
1112
  } = {},
1067
1113
  ): Promise<EnrolledServerConnection> {
1068
- const trust = await (deps.loadTrust ?? loadBorgServerTrust)(origin);
1069
- return connectEnrolledBorgServer(origin, trust.identity, {
1070
- fetchImpl: trust.fetchImpl,
1071
- ...(deps.loadCredential === undefined ? {} : { loadCredential: deps.loadCredential }),
1072
- ...(deps.loadCredentialRecord === undefined
1073
- ? {}
1074
- : { loadCredentialRecord: deps.loadCredentialRecord }),
1114
+ return withEnrollmentOriginLock(origin, async () => {
1115
+ const trust = await (deps.loadTrust ?? loadBorgServerTrust)(origin);
1116
+ return connectEnrolledBorgServer(origin, trust.identity, {
1117
+ fetchImpl: trust.fetchImpl,
1118
+ ...(deps.loadCredential === undefined ? {} : { loadCredential: deps.loadCredential }),
1119
+ ...(deps.loadCredentialRecord === undefined
1120
+ ? {}
1121
+ : { loadCredentialRecord: deps.loadCredentialRecord }),
1122
+ });
1075
1123
  });
1076
1124
  }
1077
1125
 
@@ -1116,6 +1164,7 @@ export async function enrollLocalBorgServerArtifact(
1116
1164
  clientName?: string;
1117
1165
  loadTrustFromPresentedChain?: typeof loadBorgServerTrustFromPresentedChain;
1118
1166
  prepareEnrollment?: typeof getOrCreatePendingServerEnrollment;
1167
+ prepareReplacementEnrollment?: typeof createPendingServerEnrollmentWithReplacementConsent;
1119
1168
  activateEnrollment?: typeof activatePendingServerEnrollment;
1120
1169
  clearPendingEnrollment?: typeof clearPendingServerEnrollment;
1121
1170
  loadCredentialRecord?: typeof getServerCredentialRecord;
@@ -1141,16 +1190,29 @@ export async function enrollLocalBorgServerArtifact(
1141
1190
  if (error instanceof InvitationArtifactStorageError) throw error;
1142
1191
  throw new InvitationArtifactTransportError();
1143
1192
  }
1193
+ const artifactBinding: EnrollmentArtifactBinding = {
1194
+ artifactFormatVersion: verifiedArtifact.version,
1195
+ artifactDigest: createHash('sha256').update(opaqueArtifact).digest('hex'),
1196
+ endpoint: verifiedArtifact.endpoint,
1197
+ caSpkiSha256: verifiedArtifact.ca_spki_sha256,
1198
+ trustIdentity: trust.identity,
1199
+ expectedAuthority: verifiedArtifact.authority,
1200
+ stagedGenerationId: trust.generationId,
1201
+ };
1144
1202
  return enrollBorgServer(verifiedArtifact.endpoint, trust.identity, opaqueArtifact, {
1145
1203
  fetchImpl: trust.fetchImpl,
1146
1204
  ...(deps.confirmReplacement === undefined ? {} : { confirmReplacement: deps.confirmReplacement }),
1147
1205
  ...(deps.clientName === undefined ? {} : { clientName: deps.clientName }),
1148
1206
  ...(deps.prepareEnrollment === undefined ? {} : { prepareEnrollment: deps.prepareEnrollment }),
1207
+ ...(deps.prepareReplacementEnrollment === undefined
1208
+ ? {}
1209
+ : { prepareReplacementEnrollment: deps.prepareReplacementEnrollment }),
1149
1210
  ...(deps.activateEnrollment === undefined ? {} : { activateEnrollment: deps.activateEnrollment }),
1150
1211
  ...(deps.clearPendingEnrollment === undefined ? {} : { clearPendingEnrollment: deps.clearPendingEnrollment }),
1151
1212
  ...(deps.loadCredentialRecord === undefined ? {} : { loadCredentialRecord: deps.loadCredentialRecord }),
1152
1213
  ...(deps.hasCredentialForOrigin === undefined ? {} : { hasCredentialForOrigin: deps.hasCredentialForOrigin }),
1153
1214
  expectedAuthority: verifiedArtifact.authority,
1215
+ artifactBinding,
1154
1216
  commitTrust: trust.commitTrust,
1155
1217
  discardTrust: trust.discardTrust,
1156
1218
  });
@@ -1161,26 +1223,42 @@ export async function resumeLocalBorgServerEnrollment(
1161
1223
  origin: string,
1162
1224
  deps: {
1163
1225
  loadTrust?: typeof loadBorgServerTrust;
1226
+ loadStagedTrust?: typeof loadStagedBorgServerTrust;
1164
1227
  loadPendingEnrollment?: typeof getPendingServerEnrollment;
1165
1228
  onPending?: () => void;
1166
1229
  } = {},
1167
1230
  ): Promise<NewServerEnrollment | null> {
1168
- let trust: BorgServerTrust;
1231
+ const discovered = await findPendingServerEnrollment();
1232
+ const pending = discovered?.origin === origin
1233
+ ? await (deps.loadPendingEnrollment ?? getPendingServerEnrollment)(
1234
+ origin,
1235
+ discovered.trustIdentity,
1236
+ )
1237
+ : null;
1238
+ if (!pending) return null;
1239
+ let trust: BorgServerTrust | StagedBorgServerTrust;
1169
1240
  try {
1170
- trust = await (deps.loadTrust ?? loadBorgServerTrust)(origin);
1241
+ trust = pending.artifactBinding
1242
+ ? await (deps.loadStagedTrust ?? loadStagedBorgServerTrust)(origin, pending.artifactBinding)
1243
+ : await (deps.loadTrust ?? loadBorgServerTrust)(origin);
1171
1244
  } catch (error) {
1172
- if (error instanceof Error && /trust files were not found|trust metadata/i.test(error.message)) return null;
1173
1245
  throw error;
1174
1246
  }
1175
1247
  return resumeBorgServerEnrollment(origin, trust.identity, {
1176
1248
  fetchImpl: trust.fetchImpl,
1177
- ...(deps.loadPendingEnrollment === undefined
1178
- ? {}
1179
- : { loadPendingEnrollment: deps.loadPendingEnrollment }),
1249
+ loadPendingEnrollment: async () => pending,
1180
1250
  ...(deps.onPending === undefined ? {} : { onPending: deps.onPending }),
1251
+ ...(isStagedTrust(trust) ? {
1252
+ commitTrust: trust.commitTrust,
1253
+ discardTrust: trust.discardTrust,
1254
+ } : {}),
1181
1255
  });
1182
1256
  }
1183
1257
 
1258
+ function isStagedTrust(trust: BorgServerTrust | StagedBorgServerTrust): trust is StagedBorgServerTrust {
1259
+ return 'generationId' in trust && 'commitTrust' in trust && 'discardTrust' in trust;
1260
+ }
1261
+
1184
1262
  /** Advisory discovery that still verifies the server-owned CA. */
1185
1263
  export async function probeLocalBorgServer(origin: string): Promise<boolean> {
1186
1264
  try {