borgmcp-shared 0.2.2 → 0.3.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 +30 -21
- package/dist/conformance/adapter.d.ts +29 -2
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +202 -14
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/conformance/index.d.ts +49 -0
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +111 -0
- package/dist/conformance/index.js.map +1 -1
- package/dist/protocol/contract.d.ts +41 -6
- package/dist/protocol/contract.d.ts.map +1 -1
- package/dist/protocol/contract.js +82 -15
- package/dist/protocol/contract.js.map +1 -1
- package/dist/protocol/version.d.ts.map +1 -1
- package/dist/protocol/version.js +6 -1
- package/dist/protocol/version.js.map +1 -1
- package/docs/compatibility.md +40 -9
- package/docs/enrollment.md +160 -0
- package/docs/releasing.md +71 -6
- package/package.json +2 -1
- package/src/conformance/adapter.ts +364 -15
- package/src/conformance/index.ts +139 -0
- package/src/protocol/contract.ts +139 -23
- package/src/protocol/version.ts +6 -1
package/src/protocol/contract.ts
CHANGED
|
@@ -2,16 +2,18 @@ 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.
|
|
5
|
+
export const SHARED_PACKAGE_VERSION = '0.3.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;
|
|
9
9
|
export const ENROLLMENT_EXCHANGE_PATH = '/api/enrollment/exchange' as const;
|
|
10
|
+
export const CUBES_PATH = '/api/cubes' as const;
|
|
10
11
|
|
|
11
12
|
export const PROTOCOL_HTTP_CONTRACT = {
|
|
12
13
|
health: { method: 'GET', path: HEALTH_PATH, authenticated: false, success_status: 204, bodyless: true },
|
|
13
14
|
protocol: { method: 'GET', path: PROTOCOL_INFO_PATH, authenticated: true, success_status: 200 },
|
|
14
15
|
enrollment: { method: 'POST', path: ENROLLMENT_EXCHANGE_PATH, authenticated: 'invitation', success_status: 201 },
|
|
16
|
+
cubes: { method: 'POST', path: CUBES_PATH, authenticated: true, success_status: 201 },
|
|
15
17
|
auth_missing_status: 401,
|
|
16
18
|
auth_invalid_status: 401,
|
|
17
19
|
cursor_expired_status: 410,
|
|
@@ -32,6 +34,7 @@ export const KNOWN_CAPABILITIES = [
|
|
|
32
34
|
'coordination.core',
|
|
33
35
|
'auth.bearer',
|
|
34
36
|
'auth.revocation',
|
|
37
|
+
'auth.retry-safe-enrollment',
|
|
35
38
|
'scope.cube-isolation',
|
|
36
39
|
'transport.tls',
|
|
37
40
|
'authority.no-cloud-fallback',
|
|
@@ -49,6 +52,7 @@ export type Capability = KnownCapability | (string & {});
|
|
|
49
52
|
export const REQUIRED_SECURITY_CAPABILITIES = [
|
|
50
53
|
'auth.bearer',
|
|
51
54
|
'auth.revocation',
|
|
55
|
+
'auth.retry-safe-enrollment',
|
|
52
56
|
'scope.cube-isolation',
|
|
53
57
|
'transport.tls',
|
|
54
58
|
'authority.no-cloud-fallback',
|
|
@@ -90,17 +94,49 @@ export interface ProtocolErrorEnvelope {
|
|
|
90
94
|
};
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
/**
|
|
97
|
+
/** All secret values are generated and persisted pending by the client before send. */
|
|
94
98
|
export interface EnrollmentExchangeRequest {
|
|
95
99
|
invitation: string;
|
|
100
|
+
retry_key: string;
|
|
101
|
+
client_credential: string;
|
|
96
102
|
client_name?: string;
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
export
|
|
105
|
+
export const SERVER_CAPABILITIES = ['create_cube'] as const;
|
|
106
|
+
export type ServerCapability = (typeof SERVER_CAPABILITIES)[number];
|
|
107
|
+
|
|
108
|
+
/** Ordinary enrollment creates an ungranted client and never returns a bearer. */
|
|
109
|
+
export interface ClientEnrollmentExchangeResponse {
|
|
110
|
+
purpose: 'client';
|
|
111
|
+
client_id: string;
|
|
112
|
+
server_capabilities: [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Owner enrollment grants only the narrow authority to create cubes. */
|
|
116
|
+
export interface OwnerEnrollmentExchangeResponse {
|
|
117
|
+
purpose: 'owner';
|
|
101
118
|
client_id: string;
|
|
102
|
-
|
|
103
|
-
|
|
119
|
+
server_capabilities: ['create_cube'];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type EnrollmentExchangeResponse =
|
|
123
|
+
| ClientEnrollmentExchangeResponse
|
|
124
|
+
| OwnerEnrollmentExchangeResponse;
|
|
125
|
+
|
|
126
|
+
export const CUBE_TEMPLATES = ['default'] as const;
|
|
127
|
+
export type CubeTemplate = (typeof CUBE_TEMPLATES)[number];
|
|
128
|
+
|
|
129
|
+
export interface CreateCubeRequest {
|
|
130
|
+
retry_key: string;
|
|
131
|
+
name: string;
|
|
132
|
+
template: CubeTemplate;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface CreateCubeResponse {
|
|
136
|
+
cube_id: string;
|
|
137
|
+
human_seat_role_id: string;
|
|
138
|
+
default_worker_role_id: string;
|
|
139
|
+
access: 'manage';
|
|
104
140
|
}
|
|
105
141
|
|
|
106
142
|
export interface AckLogRequest {
|
|
@@ -424,15 +460,29 @@ export function decodeProtocolErrorEnvelope(value: unknown): ProtocolErrorEnvelo
|
|
|
424
460
|
|
|
425
461
|
export function decodeEnrollmentExchangeRequest(value: unknown): EnrollmentExchangeRequest {
|
|
426
462
|
const input = record(value);
|
|
427
|
-
exactKeys(
|
|
463
|
+
exactKeys(
|
|
464
|
+
input,
|
|
465
|
+
['invitation', 'retry_key', 'client_credential', 'client_name'],
|
|
466
|
+
['invitation', 'retry_key', 'client_credential'],
|
|
467
|
+
);
|
|
428
468
|
const invitation = opaqueToken(input.invitation, ['invitation']);
|
|
469
|
+
const retryKey = decodeUuid(input.retry_key, ['retry_key']);
|
|
470
|
+
const clientCredential = decodeEnrollmentClientCredential(
|
|
471
|
+
input.client_credential,
|
|
472
|
+
['client_credential'],
|
|
473
|
+
);
|
|
429
474
|
const clientName = input.client_name === undefined
|
|
430
475
|
? undefined
|
|
431
476
|
: boundedString(input.client_name, 1, 120, ['client_name']);
|
|
432
477
|
if (clientName !== undefined && !/^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(clientName)) {
|
|
433
478
|
fail('Client name contains unsupported characters.', ['client_name']);
|
|
434
479
|
}
|
|
435
|
-
|
|
480
|
+
const request = {
|
|
481
|
+
invitation,
|
|
482
|
+
retry_key: retryKey,
|
|
483
|
+
client_credential: clientCredential,
|
|
484
|
+
};
|
|
485
|
+
return clientName === undefined ? request : { ...request, client_name: clientName };
|
|
436
486
|
}
|
|
437
487
|
|
|
438
488
|
export function decodeEnrollmentExchangeRequestEnvelope(
|
|
@@ -443,21 +493,23 @@ export function decodeEnrollmentExchangeRequestEnvelope(
|
|
|
443
493
|
|
|
444
494
|
export function decodeEnrollmentExchangeResponse(value: unknown): EnrollmentExchangeResponse {
|
|
445
495
|
const input = record(value);
|
|
446
|
-
|
|
447
|
-
input,
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
return
|
|
459
|
-
|
|
460
|
-
:
|
|
496
|
+
if (input.purpose === 'client') {
|
|
497
|
+
exactKeys(input, ['purpose', 'client_id', 'server_capabilities'], ['purpose', 'client_id', 'server_capabilities']);
|
|
498
|
+
decodeExactServerCapabilities(input.server_capabilities, [], ['server_capabilities']);
|
|
499
|
+
return {
|
|
500
|
+
purpose: 'client',
|
|
501
|
+
client_id: decodeUuid(input.client_id, ['client_id']),
|
|
502
|
+
server_capabilities: [],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
if (input.purpose !== 'owner') fail('Invalid enrollment purpose.', ['purpose']);
|
|
506
|
+
exactKeys(input, ['purpose', 'client_id', 'server_capabilities'], ['purpose', 'client_id', 'server_capabilities']);
|
|
507
|
+
decodeExactServerCapabilities(input.server_capabilities, ['create_cube'], ['server_capabilities']);
|
|
508
|
+
return {
|
|
509
|
+
purpose: 'owner',
|
|
510
|
+
client_id: decodeUuid(input.client_id, ['client_id']),
|
|
511
|
+
server_capabilities: ['create_cube'],
|
|
512
|
+
};
|
|
461
513
|
}
|
|
462
514
|
|
|
463
515
|
export function decodeEnrollmentExchangeResponseEnvelope(
|
|
@@ -466,6 +518,58 @@ export function decodeEnrollmentExchangeResponseEnvelope(
|
|
|
466
518
|
return decodeProtocolEnvelope(value, decodeEnrollmentExchangeResponse);
|
|
467
519
|
}
|
|
468
520
|
|
|
521
|
+
function decodeExactServerCapabilities(
|
|
522
|
+
value: unknown,
|
|
523
|
+
expected: readonly ServerCapability[],
|
|
524
|
+
path: readonly (string | number)[],
|
|
525
|
+
): void {
|
|
526
|
+
if (!Array.isArray(value) || value.length !== expected.length ||
|
|
527
|
+
value.some((capability, index) => capability !== expected[index])) {
|
|
528
|
+
fail(`Expected server capabilities [${expected.join(', ')}].`, path);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function decodeCreateCubeRequest(value: unknown): CreateCubeRequest {
|
|
533
|
+
const input = record(value);
|
|
534
|
+
exactKeys(input, ['retry_key', 'name', 'template'], ['retry_key', 'name', 'template']);
|
|
535
|
+
const name = boundedString(input.name, 1, 120, ['name']);
|
|
536
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(name)) {
|
|
537
|
+
fail('Cube name contains unsupported characters.', ['name']);
|
|
538
|
+
}
|
|
539
|
+
if (!CUBE_TEMPLATES.includes(input.template as CubeTemplate)) {
|
|
540
|
+
fail('Unsupported cube template.', ['template']);
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
retry_key: decodeUuid(input.retry_key, ['retry_key']),
|
|
544
|
+
name,
|
|
545
|
+
template: input.template as CubeTemplate,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function decodeCreateCubeRequestEnvelope(value: unknown): ProtocolEnvelope<CreateCubeRequest> {
|
|
550
|
+
return decodeProtocolEnvelope(value, decodeCreateCubeRequest);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function decodeCreateCubeResponse(value: unknown): CreateCubeResponse {
|
|
554
|
+
const input = record(value);
|
|
555
|
+
exactKeys(
|
|
556
|
+
input,
|
|
557
|
+
['cube_id', 'human_seat_role_id', 'default_worker_role_id', 'access'],
|
|
558
|
+
['cube_id', 'human_seat_role_id', 'default_worker_role_id', 'access'],
|
|
559
|
+
);
|
|
560
|
+
if (input.access !== 'manage') fail('Created cube access must be manage.', ['access']);
|
|
561
|
+
return {
|
|
562
|
+
cube_id: decodeUuid(input.cube_id, ['cube_id']),
|
|
563
|
+
human_seat_role_id: decodeUuid(input.human_seat_role_id, ['human_seat_role_id']),
|
|
564
|
+
default_worker_role_id: decodeUuid(input.default_worker_role_id, ['default_worker_role_id']),
|
|
565
|
+
access: 'manage',
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function decodeCreateCubeResponseEnvelope(value: unknown): ProtocolEnvelope<CreateCubeResponse> {
|
|
570
|
+
return decodeProtocolEnvelope(value, decodeCreateCubeResponse);
|
|
571
|
+
}
|
|
572
|
+
|
|
469
573
|
export function decodeAppendLogRequest(value: unknown): import('./types.js').AppendLogRequest {
|
|
470
574
|
const input = record(value);
|
|
471
575
|
exactKeys(
|
|
@@ -577,6 +681,17 @@ export function decodeUuid(value: unknown, path: readonly (string | number)[] =
|
|
|
577
681
|
return id.toLowerCase();
|
|
578
682
|
}
|
|
579
683
|
|
|
684
|
+
export function decodeEnrollmentClientCredential(
|
|
685
|
+
value: unknown,
|
|
686
|
+
path: readonly (string | number)[] = [],
|
|
687
|
+
): string {
|
|
688
|
+
const credential = boundedString(value, 43, 43, path);
|
|
689
|
+
if (!/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/.test(credential)) {
|
|
690
|
+
fail('Expected an unpadded base64url encoding of exactly 256 bits.', path);
|
|
691
|
+
}
|
|
692
|
+
return credential;
|
|
693
|
+
}
|
|
694
|
+
|
|
580
695
|
export function decodeOpaqueIdentifier(
|
|
581
696
|
value: unknown,
|
|
582
697
|
path: readonly (string | number)[] = [],
|
|
@@ -586,6 +701,7 @@ export function decodeOpaqueIdentifier(
|
|
|
586
701
|
|
|
587
702
|
export function redactProtocolDiagnostic(value: string): string {
|
|
588
703
|
return value
|
|
704
|
+
.replace(/(\bretry[_-]?key\b["']?\s*(?:=|:)\s*["']?)[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi, '$1<REDACTED>')
|
|
589
705
|
.replace(/[\u0000-\u001f\u007f-\u009f]/g, (character) =>
|
|
590
706
|
`\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
|
|
591
707
|
)
|
package/src/protocol/version.ts
CHANGED
|
@@ -18,10 +18,15 @@ export interface CompatibilityEntry {
|
|
|
18
18
|
* a documented migration path.
|
|
19
19
|
*/
|
|
20
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
|
+
},
|
|
21
26
|
{
|
|
22
27
|
packageRange: '>=0.2.0 <0.3.0',
|
|
23
28
|
protocolVersions: SUPPORTED_PROTOCOL_VERSIONS,
|
|
24
|
-
notes: '
|
|
29
|
+
notes: 'Legacy server-generated enrollment credential response.',
|
|
25
30
|
},
|
|
26
31
|
];
|
|
27
32
|
|