borgmcp-shared 0.3.0 → 0.4.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.
@@ -2,7 +2,7 @@ import { ErrorCode } from './errors.js';
2
2
  import { PROTOCOL_VERSION, type ProtocolVersion } from './version.js';
3
3
 
4
4
  export const SHARED_PACKAGE_NAME = 'borgmcp-shared' as const;
5
- export const SHARED_PACKAGE_VERSION = '0.3.0' as const;
5
+ export const SHARED_PACKAGE_VERSION = '0.4.0' as const;
6
6
 
7
7
  export const HEALTH_PATH = '/healthz' as const;
8
8
  export const PROTOCOL_INFO_PATH = '/api/protocol' as const;
@@ -11,7 +11,7 @@ export const CUBES_PATH = '/api/cubes' as const;
11
11
 
12
12
  export const PROTOCOL_HTTP_CONTRACT = {
13
13
  health: { method: 'GET', path: HEALTH_PATH, authenticated: false, success_status: 204, bodyless: true },
14
- protocol: { method: 'GET', path: PROTOCOL_INFO_PATH, authenticated: true, success_status: 200 },
14
+ protocol: { method: 'GET', path: PROTOCOL_INFO_PATH, authenticated: false, success_status: 200 },
15
15
  enrollment: { method: 'POST', path: ENROLLMENT_EXCHANGE_PATH, authenticated: 'invitation', success_status: 201 },
16
16
  cubes: { method: 'POST', path: CUBES_PATH, authenticated: true, success_status: 201 },
17
17
  auth_missing_status: 401,
@@ -19,7 +19,6 @@ export const PROTOCOL_HTTP_CONTRACT = {
19
19
  cursor_expired_status: 410,
20
20
  content_too_large_status: 413,
21
21
  unsupported_protocol_status: 426,
22
- unsupported_capability_status: 501,
23
22
  redirect_policy: 'error',
24
23
  } as const;
25
24
 
@@ -30,49 +29,14 @@ export const PROTOCOL_LIMIT_CEILINGS = {
30
29
  max_replay_page_size: 1000,
31
30
  } as const;
32
31
 
33
- export const KNOWN_CAPABILITIES = [
34
- 'coordination.core',
35
- 'auth.bearer',
36
- 'auth.revocation',
37
- 'auth.retry-safe-enrollment',
38
- 'scope.cube-isolation',
39
- 'transport.tls',
40
- 'authority.no-cloud-fallback',
41
- 'log.cursor',
42
- 'stream.sse',
43
- 'stream.replay',
44
- 'acks',
45
- 'claims',
46
- 'decisions',
47
- ] as const;
48
-
49
- export type KnownCapability = (typeof KNOWN_CAPABILITIES)[number];
50
- export type Capability = KnownCapability | (string & {});
51
-
52
- export const REQUIRED_SECURITY_CAPABILITIES = [
53
- 'auth.bearer',
54
- 'auth.revocation',
55
- 'auth.retry-safe-enrollment',
56
- 'scope.cube-isolation',
57
- 'transport.tls',
58
- 'authority.no-cloud-fallback',
59
- ] as const satisfies readonly Capability[];
60
-
61
- export interface ProtocolLimits {
62
- max_request_bytes: number;
63
- max_log_message_bytes: number;
64
- max_read_page_size: number;
65
- max_replay_page_size: number;
66
- }
67
-
68
- export interface ProtocolInfo {
32
+ /**
33
+ * The credential-free protocol-tag preflight body. It carries ONLY the exact
34
+ * protocol tag — no package version, limits, server identity, or other
35
+ * fingerprint surface — so a client can verify pinned TLS and the exact tag
36
+ * before it creates or sends any credential.
37
+ */
38
+ export interface ProtocolTagPreflight {
69
39
  protocol_version: ProtocolVersion;
70
- package: {
71
- name: typeof SHARED_PACKAGE_NAME;
72
- version: string;
73
- };
74
- capabilities: Capability[];
75
- limits: ProtocolLimits;
76
40
  }
77
41
 
78
42
  export interface ProtocolEnvelope<T> {
@@ -89,8 +53,6 @@ export interface ProtocolErrorEnvelope {
89
53
  message: string;
90
54
  details?: string;
91
55
  retry_after?: number;
92
- required_capability?: string;
93
- supported_versions?: readonly string[];
94
56
  };
95
57
  }
96
58
 
@@ -220,17 +182,6 @@ export function utf8ByteLength(value: string): number {
220
182
  return bytes;
221
183
  }
222
184
 
223
- function isSemanticVersion(value: string): boolean {
224
- const match = value.match(
225
- /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/,
226
- );
227
- if (!match) return false;
228
- const prerelease = match[4];
229
- return prerelease === undefined || prerelease.split('.').every((identifier) =>
230
- !/^\d+$/.test(identifier) || identifier === '0' || !identifier.startsWith('0')
231
- );
232
- }
233
-
234
185
  function boundedPositiveInteger(
235
186
  value: unknown,
236
187
  maximum: number,
@@ -266,95 +217,32 @@ function decodeRequestId(value: unknown, path: readonly (string | number)[]): st
266
217
  return decoded;
267
218
  }
268
219
 
269
- function capabilityName(value: unknown, path: readonly (string | number)[]): string {
270
- const decoded = boundedString(value, 1, 64, path);
271
- if (!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(decoded)) {
272
- fail('Capability name contains unsupported characters.', path);
273
- }
274
- return decoded;
220
+ /**
221
+ * Emit the credential-free protocol-tag preflight body. Servers return exactly
222
+ * this — the tag and nothing else — from the unauthenticated `GET /api/protocol`.
223
+ */
224
+ export function createProtocolTagPreflight(): ProtocolTagPreflight {
225
+ return { protocol_version: PROTOCOL_VERSION };
275
226
  }
276
227
 
277
- export function decodeProtocolInfo(value: unknown): ProtocolInfo {
228
+ /**
229
+ * Decode the credential-free, mutation-free protocol-tag preflight. The body must
230
+ * be exactly `{ protocol_version }` and carry the exact expected tag; any other
231
+ * tag, an extra field, or a non-object fails closed before any credential is
232
+ * created or sent. This is the sole acceptance authority — there is no
233
+ * negotiation, capability list, or package/limit surface to inspect.
234
+ */
235
+ export function decodeProtocolTagPreflight(value: unknown): ProtocolTagPreflight {
278
236
  const input = record(value);
279
- exactKeys(input, ['protocol_version', 'package', 'capabilities', 'limits'], [
280
- 'protocol_version',
281
- 'package',
282
- 'capabilities',
283
- 'limits',
284
- ]);
237
+ exactKeys(input, ['protocol_version'], ['protocol_version']);
285
238
  if (input.protocol_version !== PROTOCOL_VERSION) {
286
239
  throw new ProtocolContractError(
287
- `Unsupported protocol version "${String(input.protocol_version)}".`,
240
+ 'Unsupported protocol version.',
288
241
  ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
289
242
  ['protocol_version'],
290
243
  );
291
244
  }
292
-
293
- const packageInfo = record(input.package, ['package']);
294
- exactKeys(packageInfo, ['name', 'version'], ['name', 'version'], ['package']);
295
- if (packageInfo.name !== SHARED_PACKAGE_NAME) {
296
- fail(`Expected package name "${SHARED_PACKAGE_NAME}".`, ['package', 'name']);
297
- }
298
- const packageVersion = boundedString(packageInfo.version, 5, 64, ['package', 'version']);
299
- if (!isSemanticVersion(packageVersion)) {
300
- fail('Expected a semantic package version.', ['package', 'version']);
301
- }
302
-
303
- if (!Array.isArray(input.capabilities)) fail('Expected an array.', ['capabilities']);
304
- const capabilities = input.capabilities.map((capability, index) => {
305
- return capabilityName(capability, ['capabilities', index]) as Capability;
306
- });
307
- if (new Set(capabilities).size !== capabilities.length) {
308
- fail('Capabilities must be unique.', ['capabilities']);
309
- }
310
-
311
- const limits = record(input.limits, ['limits']);
312
- exactKeys(
313
- limits,
314
- [
315
- 'max_request_bytes',
316
- 'max_log_message_bytes',
317
- 'max_read_page_size',
318
- 'max_replay_page_size',
319
- ],
320
- [
321
- 'max_request_bytes',
322
- 'max_log_message_bytes',
323
- 'max_read_page_size',
324
- 'max_replay_page_size',
325
- ],
326
- ['limits'],
327
- );
328
-
329
- return {
330
- protocol_version: PROTOCOL_VERSION,
331
- package: { name: SHARED_PACKAGE_NAME, version: packageVersion },
332
- capabilities,
333
- limits: {
334
- max_request_bytes: boundedPositiveInteger(limits.max_request_bytes, PROTOCOL_LIMIT_CEILINGS.max_request_bytes, ['limits', 'max_request_bytes']),
335
- max_log_message_bytes: boundedPositiveInteger(limits.max_log_message_bytes, PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes, ['limits', 'max_log_message_bytes']),
336
- max_read_page_size: boundedPositiveInteger(limits.max_read_page_size, PROTOCOL_LIMIT_CEILINGS.max_read_page_size, ['limits', 'max_read_page_size']),
337
- max_replay_page_size: boundedPositiveInteger(limits.max_replay_page_size, PROTOCOL_LIMIT_CEILINGS.max_replay_page_size, ['limits', 'max_replay_page_size']),
338
- },
339
- };
340
- }
341
-
342
- export function negotiateProtocol(
343
- value: unknown,
344
- requiredCapabilities: readonly Capability[] = [],
345
- ): ProtocolInfo {
346
- const info = decodeProtocolInfo(value);
347
- const required = [...REQUIRED_SECURITY_CAPABILITIES, ...requiredCapabilities];
348
- for (const capability of new Set(required)) {
349
- if (!info.capabilities.includes(capability)) {
350
- throw new ProtocolContractError(
351
- `Required capability "${capability}" is unavailable.`,
352
- ErrorCode.UNSUPPORTED_CAPABILITY,
353
- ['capabilities'],
354
- );
355
- }
356
- }
357
- return info;
245
+ return { protocol_version: PROTOCOL_VERSION };
358
246
  }
359
247
 
360
248
  export function createProtocolEnvelope<T>(requestId: string, payload: T): ProtocolEnvelope<T> {
@@ -377,7 +265,7 @@ export function decodeProtocolEnvelope<T>(
377
265
  ]);
378
266
  if (input.protocol_version !== PROTOCOL_VERSION) {
379
267
  throw new ProtocolContractError(
380
- `Unsupported protocol version "${String(input.protocol_version)}".`,
268
+ 'Unsupported protocol version.',
381
269
  ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
382
270
  ['protocol_version'],
383
271
  );
@@ -390,16 +278,13 @@ export function decodeProtocolEnvelope<T>(
390
278
  };
391
279
  }
392
280
 
393
- export function decodeProtocolInfoEnvelope(value: unknown): ProtocolEnvelope<ProtocolInfo> {
394
- return decodeProtocolEnvelope(value, decodeProtocolInfo);
395
- }
396
281
 
397
282
  export function decodeProtocolErrorEnvelope(value: unknown): ProtocolErrorEnvelope {
398
283
  const input = record(value);
399
284
  exactKeys(input, ['protocol_version', 'request_id', 'error'], ['protocol_version', 'error']);
400
285
  if (input.protocol_version !== PROTOCOL_VERSION) {
401
286
  throw new ProtocolContractError(
402
- `Unsupported protocol version "${String(input.protocol_version)}".`,
287
+ 'Unsupported protocol version.',
403
288
  ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
404
289
  ['protocol_version'],
405
290
  );
@@ -412,8 +297,6 @@ export function decodeProtocolErrorEnvelope(value: unknown): ProtocolErrorEnvelo
412
297
  'message',
413
298
  'details',
414
299
  'retry_after',
415
- 'required_capability',
416
- 'supported_versions',
417
300
  ],
418
301
  ['code', 'message'],
419
302
  ['error'],
@@ -435,21 +318,6 @@ export function decodeProtocolErrorEnvelope(value: unknown): ProtocolErrorEnvelo
435
318
  if (error.retry_after !== undefined) {
436
319
  decodedError.retry_after = boundedPositiveInteger(error.retry_after, 86_400, ['error', 'retry_after']);
437
320
  }
438
- if (error.required_capability !== undefined) {
439
- decodedError.required_capability = capabilityName(
440
- error.required_capability,
441
- ['error', 'required_capability'],
442
- );
443
- }
444
- if (error.supported_versions !== undefined) {
445
- if (!Array.isArray(error.supported_versions) || error.supported_versions.length === 0 ||
446
- error.supported_versions.length > 16 ||
447
- !error.supported_versions.every((version) => version === PROTOCOL_VERSION) ||
448
- new Set(error.supported_versions).size !== error.supported_versions.length) {
449
- fail('Invalid supported protocol versions.', ['error', 'supported_versions']);
450
- }
451
- decodedError.supported_versions = [...error.supported_versions] as ProtocolVersion[];
452
- }
453
321
  const decodedRequestId = input.request_id === undefined
454
322
  ? undefined
455
323
  : decodeRequestId(input.request_id, ['request_id']);
@@ -722,3 +590,212 @@ export function maxLogCursor(a: LogCursor | null, b: LogCursor | null): LogCurso
722
590
  if (b === null) return decodeLogCursor(a);
723
591
  return compareLogCursor(a, b) >= 0 ? decodeLogCursor(a) : decodeLogCursor(b);
724
592
  }
593
+
594
+ // ── v2 clean-slate wire types ──────────────────────────────────────────────
595
+
596
+ export const ATTACH_PATH = '/api/client/attach' as const;
597
+
598
+ export interface AttachRequest {
599
+ cube_id: string;
600
+ role_id: string;
601
+ session_credential: string;
602
+ prior_drone_id?: string;
603
+ }
604
+
605
+ export interface AttachCube {
606
+ id: string;
607
+ name: string;
608
+ }
609
+
610
+ export type AttachRoleClass = 'queen' | 'worker';
611
+
612
+ export interface AttachRole {
613
+ id: string;
614
+ name: string;
615
+ role_class?: AttachRoleClass;
616
+ is_human_seat?: boolean;
617
+ }
618
+
619
+ export interface AttachDrone {
620
+ id: string;
621
+ label: string;
622
+ }
623
+
624
+ export interface AttachSession {
625
+ id: string;
626
+ expires_at: string;
627
+ }
628
+
629
+ export interface AttachResponse {
630
+ result: 'created' | 'reused';
631
+ cube: AttachCube;
632
+ role: AttachRole;
633
+ drone: AttachDrone;
634
+ session: AttachSession;
635
+ }
636
+
637
+ function decodeAttachCube(value: unknown, path: readonly (string | number)[]): AttachCube {
638
+ const input = record(value, path);
639
+ exactKeys(input, ['id', 'name'], ['id', 'name'], path);
640
+ return {
641
+ id: decodeUuid(input.id, [...path, 'id']),
642
+ name: boundedString(input.name, 1, 128, [...path, 'name']),
643
+ };
644
+ }
645
+
646
+ function decodeAttachRole(value: unknown, path: readonly (string | number)[]): AttachRole {
647
+ const input = record(value, path);
648
+ exactKeys(input, ['id', 'name', 'role_class', 'is_human_seat'], ['id', 'name'], path);
649
+ const result: AttachRole = {
650
+ id: decodeUuid(input.id, [...path, 'id']),
651
+ name: boundedString(input.name, 1, 128, [...path, 'name']),
652
+ };
653
+ if (input.role_class !== undefined) {
654
+ if (input.role_class !== 'queen' && input.role_class !== 'worker') {
655
+ fail('Expected role_class "queen" or "worker".', [...path, 'role_class']);
656
+ }
657
+ result.role_class = input.role_class;
658
+ }
659
+ if (input.is_human_seat !== undefined) {
660
+ if (typeof input.is_human_seat !== 'boolean') {
661
+ fail('Expected a boolean.', [...path, 'is_human_seat']);
662
+ }
663
+ result.is_human_seat = input.is_human_seat;
664
+ }
665
+ return result;
666
+ }
667
+
668
+ function decodeAttachDrone(value: unknown, path: readonly (string | number)[]): AttachDrone {
669
+ const input = record(value, path);
670
+ exactKeys(input, ['id', 'label'], ['id', 'label'], path);
671
+ return {
672
+ id: decodeUuid(input.id, [...path, 'id']),
673
+ label: boundedString(input.label, 1, 128, [...path, 'label']),
674
+ };
675
+ }
676
+
677
+ function decodeAttachSession(value: unknown, path: readonly (string | number)[]): AttachSession {
678
+ const input = record(value, path);
679
+ exactKeys(input, ['id', 'expires_at'], ['id', 'expires_at'], path);
680
+ return {
681
+ id: decodeUuid(input.id, [...path, 'id']),
682
+ expires_at: decodeCanonicalTimestamp(input.expires_at, [...path, 'expires_at']),
683
+ };
684
+ }
685
+
686
+ /**
687
+ * Decode a v2 attach request. Strict: exact keys, bounded sizes,
688
+ * session_credential is token-safe and never echoed in errors.
689
+ */
690
+ export function decodeAttachRequest(value: unknown): AttachRequest {
691
+ const input = record(value);
692
+ exactKeys(input, ['cube_id', 'role_id', 'session_credential', 'prior_drone_id'], [
693
+ 'cube_id',
694
+ 'role_id',
695
+ 'session_credential',
696
+ ]);
697
+ const result: AttachRequest = {
698
+ cube_id: decodeUuid(input.cube_id, ['cube_id']),
699
+ role_id: decodeUuid(input.role_id, ['role_id']),
700
+ session_credential: opaqueToken(input.session_credential, ['session_credential']),
701
+ };
702
+ if (input.prior_drone_id !== undefined) {
703
+ result.prior_drone_id = decodeUuid(input.prior_drone_id, ['prior_drone_id']);
704
+ }
705
+ return result;
706
+ }
707
+
708
+ /**
709
+ * Create a v2 attach request envelope. Stamps the canonical protocol version.
710
+ */
711
+ export function createAttachRequestEnvelope(
712
+ requestId: string,
713
+ payload: AttachRequest,
714
+ ): ProtocolEnvelope<AttachRequest> {
715
+ return {
716
+ protocol_version: PROTOCOL_VERSION,
717
+ request_id: decodeRequestId(requestId, ['request_id']),
718
+ payload,
719
+ };
720
+ }
721
+
722
+ /**
723
+ * Decode a v2 attach request envelope. Verifies protocol_version === PROTOCOL_VERSION
724
+ * BEFORE decoding the payload — a wrong tag never invokes the payload decoder
725
+ * and never exposes or returns the supplied session_credential.
726
+ * Uses a static token-safe diagnostic; does not interpolate attacker-controlled text.
727
+ */
728
+ export function decodeAttachRequestEnvelope(
729
+ value: unknown,
730
+ ): ProtocolEnvelope<AttachRequest> {
731
+ const input = record(value);
732
+ exactKeys(input, ['protocol_version', 'request_id', 'payload'], [
733
+ 'protocol_version',
734
+ 'request_id',
735
+ 'payload',
736
+ ]);
737
+ if (input.protocol_version !== PROTOCOL_VERSION) {
738
+ throw new ProtocolContractError(
739
+ 'Unsupported protocol version.',
740
+ ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
741
+ ['protocol_version'],
742
+ );
743
+ }
744
+ const decodedRequestId = decodeRequestId(input.request_id, ['request_id']);
745
+ return {
746
+ protocol_version: PROTOCOL_VERSION,
747
+ request_id: decodedRequestId,
748
+ payload: decodeAttachRequest(input.payload),
749
+ };
750
+ }
751
+
752
+ /**
753
+ * Decode a v2 attach response. Strict: exact keys, result discriminant,
754
+ * expires_at required non-null finite ISO-8601.
755
+ */
756
+ export function decodeAttachResponse(value: unknown): AttachResponse {
757
+ const input = record(value);
758
+ exactKeys(input, ['result', 'cube', 'role', 'drone', 'session'], [
759
+ 'result',
760
+ 'cube',
761
+ 'role',
762
+ 'drone',
763
+ 'session',
764
+ ]);
765
+ if (input.result !== 'created' && input.result !== 'reused') {
766
+ fail('Expected result "created" or "reused".', ['result']);
767
+ }
768
+ return {
769
+ result: input.result,
770
+ cube: decodeAttachCube(input.cube, ['cube']),
771
+ role: decodeAttachRole(input.role, ['role']),
772
+ drone: decodeAttachDrone(input.drone, ['drone']),
773
+ session: decodeAttachSession(input.session, ['session']),
774
+ };
775
+ }
776
+
777
+ /**
778
+ * Decode a v2 attach response wrapped in a ProtocolEnvelope.
779
+ * Verifies protocol_version === PROTOCOL_VERSION before decoding payload.
780
+ */
781
+ export function decodeAttachResponseEnvelope(value: unknown): ProtocolEnvelope<AttachResponse> {
782
+ const input = record(value);
783
+ exactKeys(input, ['protocol_version', 'request_id', 'payload'], [
784
+ 'protocol_version',
785
+ 'request_id',
786
+ 'payload',
787
+ ]);
788
+ if (input.protocol_version !== PROTOCOL_VERSION) {
789
+ throw new ProtocolContractError(
790
+ 'Unsupported protocol version.',
791
+ ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
792
+ ['protocol_version'],
793
+ );
794
+ }
795
+ const decodedRequestId = decodeRequestId(input.request_id, ['request_id']);
796
+ return {
797
+ protocol_version: PROTOCOL_VERSION,
798
+ request_id: decodedRequestId,
799
+ payload: decodeAttachResponse(input.payload),
800
+ };
801
+ }
@@ -18,10 +18,16 @@ export enum ErrorCode {
18
18
  DRONE_EVICTED = 'DRONE_EVICTED',
19
19
  DRONE_FROZEN = 'DRONE_FROZEN',
20
20
  UNSUPPORTED_PROTOCOL_VERSION = 'UNSUPPORTED_PROTOCOL_VERSION',
21
- UNSUPPORTED_CAPABILITY = 'UNSUPPORTED_CAPABILITY',
22
21
  CURSOR_INVALID = 'CURSOR_INVALID',
23
22
  CURSOR_EXPIRED = 'CURSOR_EXPIRED',
24
23
  SESSION_REVOKED = 'SESSION_REVOKED',
24
+ /**
25
+ * The presented session bearer does not match the seat it targets: a fresh or
26
+ * non-matching bearer against an already-bound active seat. Distinct from
27
+ * SESSION_REVOKED (a formerly valid credential that was explicitly revoked or
28
+ * expired). Carried by the server's typed 401 takeover rejection.
29
+ */
30
+ SESSION_REJECTED = 'SESSION_REJECTED',
25
31
  }
26
32
 
27
33
  /** @deprecated Wire failures use the versioned ProtocolErrorEnvelope. */
@@ -31,6 +37,4 @@ export interface ErrorResponse {
31
37
  details?: string;
32
38
  /** Number of seconds a rate-limited caller should wait. */
33
39
  retryAfter?: number;
34
- requiredCapability?: string;
35
- supportedVersions?: readonly string[];
36
40
  }
@@ -1,36 +1,4 @@
1
- /** Current Borg coordination protocol generation. */
2
- export const PROTOCOL_VERSION = '1' as const;
1
+ /** Current Borg coordination protocol generation. Clean-slate v2. */
2
+ export const PROTOCOL_VERSION = '2' as const;
3
3
 
4
- /** Protocol generations accepted by this package release. */
5
- export const SUPPORTED_PROTOCOL_VERSIONS = [PROTOCOL_VERSION] as const;
6
-
7
- export type ProtocolVersion = (typeof SUPPORTED_PROTOCOL_VERSIONS)[number];
8
-
9
- export interface CompatibilityEntry {
10
- packageRange: string;
11
- protocolVersions: readonly ProtocolVersion[];
12
- notes: string;
13
- }
14
-
15
- /**
16
- * Compatibility table for published package releases. Pre-1.0 package
17
- * releases may add contracts, but do not change an existing wire shape without
18
- * a documented migration path.
19
- */
20
- export const COMPATIBILITY_MATRIX: readonly CompatibilityEntry[] = [
21
- {
22
- packageRange: '>=0.3.0 <0.4.0',
23
- protocolVersions: SUPPORTED_PROTOCOL_VERSIONS,
24
- notes: 'Retry-safe owner enrollment and idempotent multi-cube creation.',
25
- },
26
- {
27
- packageRange: '>=0.2.0 <0.3.0',
28
- protocolVersions: SUPPORTED_PROTOCOL_VERSIONS,
29
- notes: 'Legacy server-generated enrollment credential response.',
30
- },
31
- ];
32
-
33
- export function isProtocolVersionSupported(value: unknown): value is ProtocolVersion {
34
- return typeof value === 'string' &&
35
- (SUPPORTED_PROTOCOL_VERSIONS as readonly string[]).includes(value);
36
- }
4
+ export type ProtocolVersion = typeof PROTOCOL_VERSION;