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.
@@ -3,6 +3,7 @@ import {
3
3
  ProtocolContractError,
4
4
  compareLogCursor,
5
5
  createProtocolEnvelope,
6
+ decodeCreateCubeResponseEnvelope,
6
7
  decodeAppendLogResultEnvelope,
7
8
  decodeDecisionResultEnvelope,
8
9
  decodeDecisionsResultEnvelope,
@@ -14,10 +15,12 @@ import {
14
15
  negotiateProtocol,
15
16
  utf8ByteLength,
16
17
  type Capability,
18
+ type CreateCubeResponse,
17
19
  type LogCursor,
18
20
  type ProtocolInfo,
19
21
  type StreamEvent,
20
22
  } from '../protocol/index.js';
23
+ import { ENROLLMENT_RETRY_CONFORMANCE } from './index.js';
21
24
 
22
25
  export interface ConformanceHttpResponse {
23
26
  status: number;
@@ -32,6 +35,30 @@ export interface ConformanceCube {
32
35
  readonly id: string;
33
36
  }
34
37
 
38
+ export interface ConformanceAuthorityState {
39
+ enrolled_clients: number;
40
+ enrollment_claims: number;
41
+ cubes: number;
42
+ roles: number;
43
+ grants: number;
44
+ server_capabilities: number;
45
+ cube_create_bindings: number;
46
+ }
47
+
48
+ export interface ConformanceCreatedCubeState {
49
+ cube_exists: boolean;
50
+ creator_has_grant: boolean;
51
+ grant_count: number;
52
+ role_count: number;
53
+ human_seat_role_matches: boolean;
54
+ default_worker_role_matches: boolean;
55
+ }
56
+
57
+ export interface ConformanceEnrollmentPrincipalState {
58
+ response_client_matches: boolean;
59
+ active_credential_bindings: number;
60
+ }
61
+
35
62
  export interface ConformanceReplayBarrier {
36
63
  /** Resolves after the replay snapshot is read but before live delivery is active. */
37
64
  readonly reached: Promise<void>;
@@ -52,7 +79,18 @@ export interface ConformanceAdmin {
52
79
  createPrincipal(name: string): Promise<ConformancePrincipal>;
53
80
  createCube(name: string): Promise<ConformanceCube>;
54
81
  grantCube(principal: ConformancePrincipal, cube: ConformanceCube): Promise<void>;
55
- issueSingleUseInvitation(principal: ConformancePrincipal): Promise<string>;
82
+ grantCreateCubeCapability(principal: ConformancePrincipal): Promise<void>;
83
+ issueDroneSession(principal: ConformancePrincipal): Promise<string>;
84
+ issueSingleUseInvitation(principal: ConformancePrincipal, purpose: 'owner' | 'client'): Promise<string>;
85
+ observeAuthorityState(): Promise<ConformanceAuthorityState>;
86
+ inspectCreatedCube(
87
+ creator: ConformancePrincipal,
88
+ response: CreateCubeResponse,
89
+ ): Promise<ConformanceCreatedCubeState>;
90
+ inspectEnrollmentPrincipal(
91
+ principal: ConformancePrincipal,
92
+ responseClientId: string,
93
+ ): Promise<ConformanceEnrollmentPrincipalState>;
56
94
  revokePrincipal(principal: ConformancePrincipal): Promise<void>;
57
95
  expireCursor(cube: ConformanceCube, cursor: LogCursor): Promise<void>;
58
96
  armReplayTransition(): ConformanceReplayBarrier;
@@ -63,6 +101,7 @@ export interface ConformanceOperations {
63
101
  health(): Promise<ConformanceHttpResponse>;
64
102
  protocol(credential: string | null): Promise<ConformanceHttpResponse>;
65
103
  enroll(request: unknown): Promise<ConformanceHttpResponse>;
104
+ createCube(credential: string | null, request: unknown): Promise<ConformanceHttpResponse>;
66
105
  append(
67
106
  credential: string,
68
107
  cube: ConformanceCube,
@@ -294,6 +333,50 @@ function same(left: unknown, right: unknown): boolean {
294
333
  return JSON.stringify(left) === JSON.stringify(right);
295
334
  }
296
335
 
336
+ function assertStateDelta(
337
+ before: ConformanceAuthorityState,
338
+ after: ConformanceAuthorityState,
339
+ expected: Partial<ConformanceAuthorityState>,
340
+ description: string,
341
+ ): void {
342
+ for (const key of Object.keys(before) as Array<keyof ConformanceAuthorityState>) {
343
+ invariant(
344
+ after[key] - before[key] === (expected[key] ?? 0),
345
+ `${description} changed ${key} by ${after[key] - before[key]}; expected ${expected[key] ?? 0}.`,
346
+ );
347
+ }
348
+ }
349
+
350
+ function assertEnrollmentErrorIsSecretFree(
351
+ response: ConformanceHttpResponse,
352
+ requests: readonly { invitation: string; retry_key: string; client_credential: string }[],
353
+ description: string,
354
+ ): void {
355
+ const diagnostic = JSON.stringify(response.body);
356
+ const secrets = new Set(requests.flatMap((request) => [
357
+ request.invitation,
358
+ request.retry_key,
359
+ request.client_credential,
360
+ ]));
361
+ for (const secret of secrets) {
362
+ invariant(!diagnostic.includes(secret), `${description} exposed enrollment retry material.`);
363
+ }
364
+ }
365
+
366
+ function expectSecretFreeError(
367
+ response: ConformanceHttpResponse,
368
+ status: number,
369
+ code: ErrorCode,
370
+ operation: string,
371
+ secrets: readonly string[],
372
+ ): void {
373
+ expectError(response, status, code, operation);
374
+ const diagnostic = JSON.stringify(response.body);
375
+ for (const secret of new Set(secrets)) {
376
+ invariant(!diagnostic.includes(secret), `${operation} exposed retry material.`);
377
+ }
378
+ }
379
+
297
380
  export async function runAdapterConformance(
298
381
  environment: ConformanceEnvironment,
299
382
  options: AdapterConformanceOptions = {},
@@ -320,14 +403,10 @@ export async function runAdapterConformance(
320
403
  };
321
404
 
322
405
  await environment.admin.reset();
323
- const principalA = await environment.admin.createPrincipal('principal-a');
324
- const principalB = await environment.admin.createPrincipal('principal-b');
325
- const cubeA = await environment.admin.createCube('cube-a');
326
- const cubeB = await environment.admin.createCube('cube-b');
327
- await environment.admin.grantCube(principalA, cubeA);
328
- await environment.admin.grantCube(principalB, cubeB);
329
- const invitationA = await environment.admin.issueSingleUseInvitation(principalA);
330
- const invitationB = await environment.admin.issueSingleUseInvitation(principalB);
406
+ let principalA!: ConformancePrincipal;
407
+ let principalB!: ConformancePrincipal;
408
+ let cubeA!: ConformanceCube;
409
+ let cubeB!: ConformanceCube;
331
410
 
332
411
  let credentialA = '';
333
412
  let credentialB = '';
@@ -342,26 +421,294 @@ export async function runAdapterConformance(
342
421
 
343
422
  await record('protocol.enrollment-auth', async () => {
344
423
  expectError(await environment.operations.protocol(null), 401, ErrorCode.AUTH_MISSING, 'Unauthenticated protocol request');
424
+
425
+ const retryVectorErrors: string[] = [];
426
+ for (const [index, vector] of ENROLLMENT_RETRY_CONFORMANCE.entries()) {
427
+ for (const purpose of ['client', 'owner'] as const) {
428
+ await environment.admin.reset();
429
+ try {
430
+ const principal = await environment.admin.createPrincipal(`${purpose}-retry-vector-${index}`);
431
+ const invitation = await environment.admin.issueSingleUseInvitation(principal, purpose);
432
+ const initialPayload = { ...vector.initial, invitation };
433
+ const retryPayload = { ...vector.retry, invitation };
434
+ const beforeInitial = await environment.admin.observeAuthorityState();
435
+ const initialResponse = await environment.operations.enroll(
436
+ createProtocolEnvelope(`retry-${index}-initial`, initialPayload),
437
+ );
438
+ expectStatus(initialResponse, 201, `${vector.name} initial request`);
439
+ const initial = decodeEnrollmentExchangeResponseEnvelope(initialResponse.body).payload;
440
+ invariant(initial.purpose === purpose, `${purpose} ${vector.name} returned the wrong purpose.`);
441
+ invariant(
442
+ same(initial.server_capabilities, purpose === 'owner' ? ['create_cube'] : []),
443
+ `${purpose} ${vector.name} returned incorrect server authority.`,
444
+ );
445
+ const afterInitial = await environment.admin.observeAuthorityState();
446
+ assertStateDelta(
447
+ beforeInitial,
448
+ afterInitial,
449
+ {
450
+ enrolled_clients: 1,
451
+ enrollment_claims: 1,
452
+ server_capabilities: purpose === 'owner' ? 1 : 0,
453
+ },
454
+ `${purpose} ${vector.name} initial request`,
455
+ );
456
+
457
+ const beforeRetry = await environment.admin.observeAuthorityState();
458
+ const retryResponse = await environment.operations.enroll(
459
+ createProtocolEnvelope(`retry-${index}-retry`, retryPayload),
460
+ );
461
+ if (vector.expected.outcome === 'stable_non_secret_identity') {
462
+ expectStatus(retryResponse, 201, `${purpose} ${vector.name}`);
463
+ const retry = decodeEnrollmentExchangeResponseEnvelope(retryResponse.body).payload;
464
+ invariant(same(initial, retry), `${purpose} ${vector.name} returned different identities.`);
465
+ for (const field of vector.expected.forbidden_response_fields) {
466
+ invariant(!(field in retry), `${purpose} ${vector.name} returned forbidden field ${field}.`);
467
+ }
468
+ } else {
469
+ expectError(retryResponse, vector.expected.status, ErrorCode.AUTH_INVALID, `${purpose} ${vector.name}`);
470
+ assertEnrollmentErrorIsSecretFree(
471
+ retryResponse,
472
+ [initialPayload, retryPayload],
473
+ `${purpose} ${vector.name}`,
474
+ );
475
+ }
476
+ assertStateDelta(beforeRetry, await environment.admin.observeAuthorityState(), {}, `${purpose} ${vector.name} retry`);
477
+ expectStatus(
478
+ await environment.operations.protocol(vector.initial.client_credential),
479
+ 200,
480
+ `${purpose} ${vector.name} original credential continuity`,
481
+ );
482
+ if (vector.retry.client_credential !== vector.initial.client_credential) {
483
+ expectError(
484
+ await environment.operations.protocol(vector.retry.client_credential),
485
+ 401,
486
+ ErrorCode.AUTH_INVALID,
487
+ `${purpose} ${vector.name} mismatched credential rejection`,
488
+ );
489
+ }
490
+ invariant(
491
+ same(await environment.admin.inspectEnrollmentPrincipal(principal, initial.client_id), {
492
+ response_client_matches: true,
493
+ active_credential_bindings: 1,
494
+ }),
495
+ `${purpose} ${vector.name} changed enrollment binding ownership.`,
496
+ );
497
+ } catch (error) {
498
+ retryVectorErrors.push(
499
+ `${purpose} ${vector.name}: ${error instanceof Error ? error.message : String(error)}`,
500
+ );
501
+ }
502
+ }
503
+ }
504
+ invariant(retryVectorErrors.length === 0, retryVectorErrors.join(' | '));
505
+
506
+ await environment.admin.reset();
507
+ const ownerPrincipal = await environment.admin.createPrincipal('owner');
508
+ const ordinaryPrincipal = await environment.admin.createPrincipal('ordinary');
509
+ const ownerInvitation = await environment.admin.issueSingleUseInvitation(ownerPrincipal, 'owner');
510
+ const ordinaryInvitation = await environment.admin.issueSingleUseInvitation(ordinaryPrincipal, 'client');
511
+ const ownerCredential = `${'Q'.repeat(42)}U`;
512
+ const ordinaryCredential = `${'Y'.repeat(42)}U`;
513
+ const beforeAuthorityEnrollment = await environment.admin.observeAuthorityState();
514
+ const ownerResponse = await environment.operations.enroll(createProtocolEnvelope('owner-enroll', {
515
+ invitation: ownerInvitation,
516
+ retry_key: '00000000-0000-4000-8000-000000000211',
517
+ client_credential: ownerCredential,
518
+ client_name: 'owner-client',
519
+ }));
520
+ const ordinaryResponse = await environment.operations.enroll(createProtocolEnvelope('ordinary-enroll', {
521
+ invitation: ordinaryInvitation,
522
+ retry_key: '00000000-0000-4000-8000-000000000212',
523
+ client_credential: ordinaryCredential,
524
+ client_name: 'ordinary-client',
525
+ }));
526
+ expectStatus(ownerResponse, 201, 'Owner enrollment');
527
+ expectStatus(ordinaryResponse, 201, 'Ordinary enrollment');
528
+ const owner = decodeEnrollmentExchangeResponseEnvelope(ownerResponse.body).payload;
529
+ const ordinary = decodeEnrollmentExchangeResponseEnvelope(ordinaryResponse.body).payload;
530
+ invariant(owner.purpose === 'owner' && same(owner.server_capabilities, ['create_cube']), 'Owner enrollment lacked exact create-cube authority.');
531
+ invariant(ordinary.purpose === 'client' && ordinary.server_capabilities.length === 0, 'Ordinary enrollment gained authority.');
532
+ assertStateDelta(
533
+ beforeAuthorityEnrollment,
534
+ await environment.admin.observeAuthorityState(),
535
+ { enrolled_clients: 2, enrollment_claims: 2, server_capabilities: 1 },
536
+ 'Owner and ordinary enrollment',
537
+ );
538
+
539
+ const cubeRequest = {
540
+ retry_key: '00000000-0000-4000-8000-000000000213',
541
+ name: 'repository-one',
542
+ template: 'default',
543
+ };
544
+ const droneCredential = await environment.admin.issueDroneSession(ownerPrincipal);
545
+ const beforeDeniedCreate = await environment.admin.observeAuthorityState();
546
+ expectSecretFreeError(
547
+ await environment.operations.createCube(null, createProtocolEnvelope('cube-missing-auth', cubeRequest)),
548
+ 401,
549
+ ErrorCode.AUTH_MISSING,
550
+ 'Missing-auth cube create',
551
+ [cubeRequest.retry_key],
552
+ );
553
+ expectSecretFreeError(
554
+ await environment.operations.createCube('invalid-credential', createProtocolEnvelope('cube-invalid-auth', cubeRequest)),
555
+ 401,
556
+ ErrorCode.AUTH_INVALID,
557
+ 'Invalid-auth cube create',
558
+ [cubeRequest.retry_key],
559
+ );
560
+ expectSecretFreeError(
561
+ await environment.operations.createCube(ordinaryCredential, createProtocolEnvelope('cube-denied', cubeRequest)),
562
+ 403,
563
+ ErrorCode.ACCESS_DENIED,
564
+ 'Ordinary cube create',
565
+ [cubeRequest.retry_key],
566
+ );
567
+ expectSecretFreeError(
568
+ await environment.operations.createCube(droneCredential, createProtocolEnvelope('cube-drone-denied', cubeRequest)),
569
+ 403,
570
+ ErrorCode.ACCESS_DENIED,
571
+ 'Drone-session cube create',
572
+ [cubeRequest.retry_key],
573
+ );
574
+ assertStateDelta(beforeDeniedCreate, await environment.admin.observeAuthorityState(), {}, 'Denied ordinary cube create');
575
+ const beforeCreate = await environment.admin.observeAuthorityState();
576
+ const createdResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-create', cubeRequest));
577
+ expectStatus(createdResponse, 201, 'Owner cube create');
578
+ const created = decodeCreateCubeResponseEnvelope(createdResponse.body).payload;
579
+ assertStateDelta(beforeCreate, await environment.admin.observeAuthorityState(), { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1 }, 'Owner cube create');
580
+ invariant(
581
+ same(await environment.admin.inspectCreatedCube(ownerPrincipal, created), {
582
+ cube_exists: true,
583
+ creator_has_grant: true,
584
+ grant_count: 1,
585
+ role_count: 2,
586
+ human_seat_role_matches: true,
587
+ default_worker_role_matches: true,
588
+ }),
589
+ 'Created cube identities or creator grant did not match persisted authority state.',
590
+ );
591
+ const beforeCreateRetry = await environment.admin.observeAuthorityState();
592
+ const retriedCreateResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-retry', cubeRequest));
593
+ expectStatus(retriedCreateResponse, 201, 'Exact cube-create retry');
594
+ invariant(same(decodeCreateCubeResponseEnvelope(retriedCreateResponse.body).payload, created), 'Exact cube-create retry returned different identities.');
595
+ assertStateDelta(beforeCreateRetry, await environment.admin.observeAuthorityState(), {}, 'Exact cube-create retry');
596
+ const beforeCreateMismatch = await environment.admin.observeAuthorityState();
597
+ expectSecretFreeError(
598
+ await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-mismatch', { ...cubeRequest, name: 'repository-two' })),
599
+ 409,
600
+ ErrorCode.INVALID_INPUT,
601
+ 'Cube-create retry mismatch',
602
+ [cubeRequest.retry_key],
603
+ );
604
+ assertStateDelta(beforeCreateMismatch, await environment.admin.observeAuthorityState(), {}, 'Cube-create retry mismatch');
605
+ await environment.admin.grantCreateCubeCapability(ordinaryPrincipal);
606
+ const crossClientRequest = { ...cubeRequest, name: 'ordinary-repository' };
607
+ const beforeCrossClientCreate = await environment.admin.observeAuthorityState();
608
+ const crossClientResponse = await environment.operations.createCube(
609
+ ordinaryCredential,
610
+ createProtocolEnvelope('cube-cross-client', crossClientRequest),
611
+ );
612
+ expectStatus(crossClientResponse, 201, 'Cross-client cube create with reused retry key');
613
+ const crossClientCreated = decodeCreateCubeResponseEnvelope(crossClientResponse.body).payload;
614
+ invariant(crossClientCreated.cube_id !== created.cube_id, 'Cross-client retry key reused another client\'s cube.');
615
+ assertStateDelta(
616
+ beforeCrossClientCreate,
617
+ await environment.admin.observeAuthorityState(),
618
+ { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1 },
619
+ 'Cross-client cube create',
620
+ );
621
+ invariant(
622
+ (await environment.admin.inspectCreatedCube(ordinaryPrincipal, crossClientCreated)).creator_has_grant,
623
+ 'Cross-client cube creation did not grant its authenticated creator.',
624
+ );
625
+ const beforeCrossClientRetry = await environment.admin.observeAuthorityState();
626
+ const crossClientRetry = await environment.operations.createCube(
627
+ ordinaryCredential,
628
+ createProtocolEnvelope('cube-cross-client-retry', crossClientRequest),
629
+ );
630
+ expectStatus(crossClientRetry, 201, 'Exact cross-client cube-create retry');
631
+ invariant(
632
+ same(decodeCreateCubeResponseEnvelope(crossClientRetry.body).payload, crossClientCreated),
633
+ 'Exact cross-client cube-create retry returned different identities.',
634
+ );
635
+ assertStateDelta(beforeCrossClientRetry, await environment.admin.observeAuthorityState(), {}, 'Exact cross-client cube-create retry');
636
+ const beforeSecondCreate = await environment.admin.observeAuthorityState();
637
+ const secondCreatedResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-create-second', {
638
+ ...cubeRequest,
639
+ retry_key: '00000000-0000-4000-8000-000000000214',
640
+ name: 'repository-two',
641
+ }));
642
+ expectStatus(secondCreatedResponse, 201, 'Second cube create');
643
+ const secondCreated = decodeCreateCubeResponseEnvelope(secondCreatedResponse.body).payload;
644
+ invariant(secondCreated.cube_id !== created.cube_id, 'Fresh cube-create retry key reused an existing cube.');
645
+ assertStateDelta(
646
+ beforeSecondCreate,
647
+ await environment.admin.observeAuthorityState(),
648
+ { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1 },
649
+ 'Second cube create',
650
+ );
651
+ await environment.admin.revokePrincipal(ownerPrincipal);
652
+ const beforeRevokedCreate = await environment.admin.observeAuthorityState();
653
+ expectSecretFreeError(
654
+ await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-revoked', {
655
+ ...cubeRequest,
656
+ retry_key: '00000000-0000-4000-8000-000000000215',
657
+ })),
658
+ 401,
659
+ ErrorCode.SESSION_REVOKED,
660
+ 'Revoked owner cube create',
661
+ ['00000000-0000-4000-8000-000000000215'],
662
+ );
663
+ assertStateDelta(beforeRevokedCreate, await environment.admin.observeAuthorityState(), {}, 'Revoked owner cube create');
664
+
665
+ await environment.admin.reset();
666
+ principalA = await environment.admin.createPrincipal('principal-a');
667
+ principalB = await environment.admin.createPrincipal('principal-b');
668
+ cubeA = await environment.admin.createCube('cube-a');
669
+ cubeB = await environment.admin.createCube('cube-b');
670
+ await environment.admin.grantCube(principalA, cubeA);
671
+ await environment.admin.grantCube(principalB, cubeB);
672
+ const invitationA = await environment.admin.issueSingleUseInvitation(principalA, 'client');
673
+ const invitationB = await environment.admin.issueSingleUseInvitation(principalB, 'client');
674
+ credentialA = 'A'.repeat(43);
675
+ credentialB = 'E'.repeat(43);
345
676
  const enrollmentARequest = createProtocolEnvelope('enroll-a1', {
346
677
  invitation: invitationA,
678
+ retry_key: '00000000-0000-4000-8000-000000000201',
679
+ client_credential: credentialA,
347
680
  client_name: 'conformance-a',
348
681
  });
349
682
  const enrollmentBRequest = createProtocolEnvelope('enroll-b1', {
350
683
  invitation: invitationB,
684
+ retry_key: '00000000-0000-4000-8000-000000000202',
685
+ client_credential: credentialB,
351
686
  client_name: 'conformance-b',
352
687
  });
353
688
  const enrolledAResponse = await environment.operations.enroll(enrollmentARequest);
354
689
  const enrolledBResponse = await environment.operations.enroll(enrollmentBRequest);
355
690
  expectStatus(enrolledAResponse, 201, 'Principal A enrollment');
356
691
  expectStatus(enrolledBResponse, 201, 'Principal B enrollment');
357
- credentialA = decodeEnrollmentExchangeResponseEnvelope(enrolledAResponse.body).payload.credential;
358
- credentialB = decodeEnrollmentExchangeResponseEnvelope(enrolledBResponse.body).payload.credential;
359
- invariant(credentialA !== credentialB, 'Enrollment issued the same credential to two principals.');
692
+ const enrolledA = decodeEnrollmentExchangeResponseEnvelope(enrolledAResponse.body).payload;
693
+ const enrolledB = decodeEnrollmentExchangeResponseEnvelope(enrolledBResponse.body).payload;
694
+ invariant(enrolledA.purpose === 'client' && enrolledB.purpose === 'client', 'Ordinary enrollment returned owner authority.');
695
+ invariant(enrolledA.server_capabilities.length === 0 && enrolledB.server_capabilities.length === 0, 'Ordinary enrollment returned a server capability.');
696
+ invariant(!('credential' in enrolledA) && !('credential' in enrolledB), 'Enrollment response returned a bearer.');
697
+ const retriedAResponse = await environment.operations.enroll(enrollmentARequest);
698
+ expectStatus(retriedAResponse, 201, 'Exact enrollment retry');
699
+ invariant(
700
+ JSON.stringify(decodeEnrollmentExchangeResponseEnvelope(retriedAResponse.body).payload) ===
701
+ JSON.stringify(enrolledA),
702
+ 'Exact enrollment retry returned different identities.',
703
+ );
360
704
  expectError(
361
- await environment.operations.enroll(enrollmentARequest),
705
+ await environment.operations.enroll(createProtocolEnvelope('enroll-a-mismatch', {
706
+ ...enrollmentARequest.payload,
707
+ retry_key: '00000000-0000-4000-8000-000000000203',
708
+ })),
362
709
  401,
363
710
  ErrorCode.AUTH_INVALID,
364
- 'Invitation reuse',
711
+ 'Enrollment retry mismatch',
365
712
  );
366
713
  const protocolResponse = await environment.operations.protocol(credentialA);
367
714
  expectStatus(protocolResponse, 200, 'Authenticated protocol request');
@@ -377,7 +724,9 @@ export async function runAdapterConformance(
377
724
  return {
378
725
  unauthenticated: ErrorCode.AUTH_MISSING,
379
726
  enrollment_status: 201,
380
- invitation_reuse: ErrorCode.AUTH_INVALID,
727
+ exact_retry_status: 201,
728
+ mismatched_retry: ErrorCode.AUTH_INVALID,
729
+ response_secret_free: true,
381
730
  protocol_version: protocolInfo.protocol_version,
382
731
  };
383
732
  });
@@ -1,4 +1,5 @@
1
1
  import type { BroadcastHwm } from '../log-stream-hwm.js';
2
+ import type { EnrollmentExchangeRequest } from '../protocol/contract.js';
2
3
 
3
4
  export * from './adapter.js';
4
5
 
@@ -66,3 +67,141 @@ export const ROLE_SECTION_ROUND_TRIP_CONFORMANCE: readonly string[] = [
66
67
  'Preamble.\n\nWorkflow:\n- step one\n\nProject conventions:\n- TDD.\n',
67
68
  '**Markdown heading:**\nMust remain part of the preamble.\n',
68
69
  ];
70
+
71
+ export interface EnrollmentRetryConformanceVector {
72
+ name: string;
73
+ initial: EnrollmentExchangeRequest;
74
+ retry: EnrollmentExchangeRequest;
75
+ expected:
76
+ | {
77
+ outcome: 'stable_non_secret_identity';
78
+ status: 201;
79
+ forbidden_response_fields: readonly [
80
+ 'credential',
81
+ 'client_credential',
82
+ 'invitation',
83
+ 'retry_key',
84
+ ];
85
+ }
86
+ | {
87
+ outcome: 'uniform_auth_invalid';
88
+ status: 401;
89
+ error: 'AUTH_INVALID';
90
+ };
91
+ }
92
+
93
+ const ENROLLMENT_INVITATION = 'I'.repeat(43);
94
+ const ENROLLMENT_CREDENTIAL = 'A'.repeat(43);
95
+ const ENROLLMENT_RETRY_KEY = '00000000-0000-4000-8000-000000000101';
96
+
97
+ /** Stateful vectors: adapters must compare the complete canonical retry tuple. */
98
+ export const ENROLLMENT_RETRY_CONFORMANCE: readonly EnrollmentRetryConformanceVector[] = [
99
+ {
100
+ name: 'exact credential-proven retry returns stable non-secret identities',
101
+ initial: {
102
+ invitation: ENROLLMENT_INVITATION,
103
+ retry_key: ENROLLMENT_RETRY_KEY,
104
+ client_credential: ENROLLMENT_CREDENTIAL,
105
+ client_name: 'operator-laptop',
106
+ },
107
+ retry: {
108
+ invitation: ENROLLMENT_INVITATION,
109
+ retry_key: ENROLLMENT_RETRY_KEY,
110
+ client_credential: ENROLLMENT_CREDENTIAL,
111
+ client_name: 'operator-laptop',
112
+ },
113
+ expected: {
114
+ outcome: 'stable_non_secret_identity',
115
+ status: 201,
116
+ forbidden_response_fields: [
117
+ 'credential',
118
+ 'client_credential',
119
+ 'invitation',
120
+ 'retry_key',
121
+ ],
122
+ },
123
+ },
124
+ {
125
+ name: 'retry-key mismatch is uniformly invalid',
126
+ initial: {
127
+ invitation: ENROLLMENT_INVITATION,
128
+ retry_key: ENROLLMENT_RETRY_KEY,
129
+ client_credential: ENROLLMENT_CREDENTIAL,
130
+ },
131
+ retry: {
132
+ invitation: ENROLLMENT_INVITATION,
133
+ retry_key: '00000000-0000-4000-8000-000000000102',
134
+ client_credential: ENROLLMENT_CREDENTIAL,
135
+ },
136
+ expected: { outcome: 'uniform_auth_invalid', status: 401, error: 'AUTH_INVALID' },
137
+ },
138
+ {
139
+ name: 'credential mismatch is uniformly invalid',
140
+ initial: {
141
+ invitation: ENROLLMENT_INVITATION,
142
+ retry_key: ENROLLMENT_RETRY_KEY,
143
+ client_credential: ENROLLMENT_CREDENTIAL,
144
+ },
145
+ retry: {
146
+ invitation: ENROLLMENT_INVITATION,
147
+ retry_key: ENROLLMENT_RETRY_KEY,
148
+ client_credential: 'E'.repeat(43),
149
+ },
150
+ expected: { outcome: 'uniform_auth_invalid', status: 401, error: 'AUTH_INVALID' },
151
+ },
152
+ {
153
+ name: 'client-name mismatch is uniformly invalid',
154
+ initial: {
155
+ invitation: ENROLLMENT_INVITATION,
156
+ retry_key: ENROLLMENT_RETRY_KEY,
157
+ client_credential: ENROLLMENT_CREDENTIAL,
158
+ client_name: 'operator-laptop',
159
+ },
160
+ retry: {
161
+ invitation: ENROLLMENT_INVITATION,
162
+ retry_key: ENROLLMENT_RETRY_KEY,
163
+ client_credential: ENROLLMENT_CREDENTIAL,
164
+ client_name: 'different-client',
165
+ },
166
+ expected: { outcome: 'uniform_auth_invalid', status: 401, error: 'AUTH_INVALID' },
167
+ },
168
+ ];
169
+
170
+ export const ENROLLMENT_AUTHORITY_CONFORMANCE = [
171
+ {
172
+ name: 'ordinary enrollment creates no authority or cube state',
173
+ response: {
174
+ purpose: 'client',
175
+ client_id: '00000000-0000-4000-8000-000000000111',
176
+ server_capabilities: [],
177
+ },
178
+ expected_state_delta: { cubes: 0, roles: 0, grants: 0, server_capabilities: 0 },
179
+ },
180
+ {
181
+ name: 'owner enrollment grants create-cube authority without cube state',
182
+ response: {
183
+ purpose: 'owner',
184
+ client_id: '00000000-0000-4000-8000-000000000111',
185
+ server_capabilities: ['create_cube'],
186
+ },
187
+ expected_state_delta: { cubes: 0, roles: 0, grants: 0, server_capabilities: 1 },
188
+ },
189
+ ] as const;
190
+
191
+ export const ENROLLMENT_REDACTION_CONFORMANCE: readonly ConformanceVector<string, string>[] = [
192
+ {
193
+ name: 'redacts invitation and client credential from diagnostics',
194
+ input: `invitation=${ENROLLMENT_INVITATION} client_credential=${ENROLLMENT_CREDENTIAL}`,
195
+ expected: 'invitation=<REDACTED> client_credential=<REDACTED>',
196
+ },
197
+ {
198
+ name: 'redacts a contextual enrollment retry key',
199
+ input: `retry_key=${ENROLLMENT_RETRY_KEY}`,
200
+ expected: 'retry_key=<REDACTED>',
201
+ },
202
+ {
203
+ name: 'preserves unrelated public UUIDs',
204
+ input: `cube_id=${ENROLLMENT_RETRY_KEY}`,
205
+ expected: `cube_id=${ENROLLMENT_RETRY_KEY}`,
206
+ },
207
+ ];