borgmcp-shared 0.2.2 → 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.
@@ -1,23 +1,24 @@
1
1
  import {
2
2
  ErrorCode,
3
- ProtocolContractError,
4
3
  compareLogCursor,
5
4
  createProtocolEnvelope,
5
+ decodeCreateCubeResponseEnvelope,
6
6
  decodeAppendLogResultEnvelope,
7
7
  decodeDecisionResultEnvelope,
8
8
  decodeDecisionsResultEnvelope,
9
9
  decodeEnrollmentExchangeResponseEnvelope,
10
10
  decodeProtocolErrorEnvelope,
11
- decodeProtocolInfoEnvelope,
11
+ decodeProtocolTagPreflight,
12
12
  decodeReadLogResultEnvelope,
13
13
  decodeSseFrames,
14
- negotiateProtocol,
14
+ PROTOCOL_LIMIT_CEILINGS,
15
+ PROTOCOL_VERSION,
15
16
  utf8ByteLength,
16
- type Capability,
17
+ type CreateCubeResponse,
17
18
  type LogCursor,
18
- type ProtocolInfo,
19
19
  type StreamEvent,
20
20
  } from '../protocol/index.js';
21
+ import { ENROLLMENT_RETRY_CONFORMANCE } from './index.js';
21
22
 
22
23
  export interface ConformanceHttpResponse {
23
24
  status: number;
@@ -32,6 +33,37 @@ export interface ConformanceCube {
32
33
  readonly id: string;
33
34
  }
34
35
 
36
+ export interface ConformanceAuthorityState {
37
+ enrolled_clients: number;
38
+ enrollment_claims: number;
39
+ cubes: number;
40
+ roles: number;
41
+ grants: number;
42
+ server_capabilities: number;
43
+ cube_create_bindings: number;
44
+ }
45
+
46
+ export interface ConformanceCreatedCubeState {
47
+ cube_exists: boolean;
48
+ creator_has_grant: boolean;
49
+ grant_count: number;
50
+ role_count: number;
51
+ human_seat_role_matches: boolean;
52
+ default_worker_role_matches: boolean;
53
+ }
54
+
55
+ export interface ConformanceEnrollmentPrincipalState {
56
+ response_client_matches: boolean;
57
+ active_credential_bindings: number;
58
+ /**
59
+ * The principal's currently-bound credential still equals the one it enrolled
60
+ * with. Since the credential-free tag preflight cannot probe credentials, this
61
+ * out-of-band authority check is what proves a rejected mismatch retry did not
62
+ * overwrite the good credential.
63
+ */
64
+ bound_credential_matches_enrollment: boolean;
65
+ }
66
+
35
67
  export interface ConformanceReplayBarrier {
36
68
  /** Resolves after the replay snapshot is read but before live delivery is active. */
37
69
  readonly reached: Promise<void>;
@@ -52,7 +84,18 @@ export interface ConformanceAdmin {
52
84
  createPrincipal(name: string): Promise<ConformancePrincipal>;
53
85
  createCube(name: string): Promise<ConformanceCube>;
54
86
  grantCube(principal: ConformancePrincipal, cube: ConformanceCube): Promise<void>;
55
- issueSingleUseInvitation(principal: ConformancePrincipal): Promise<string>;
87
+ grantCreateCubeCapability(principal: ConformancePrincipal): Promise<void>;
88
+ issueDroneSession(principal: ConformancePrincipal): Promise<string>;
89
+ issueSingleUseInvitation(principal: ConformancePrincipal, purpose: 'owner' | 'client'): Promise<string>;
90
+ observeAuthorityState(): Promise<ConformanceAuthorityState>;
91
+ inspectCreatedCube(
92
+ creator: ConformancePrincipal,
93
+ response: CreateCubeResponse,
94
+ ): Promise<ConformanceCreatedCubeState>;
95
+ inspectEnrollmentPrincipal(
96
+ principal: ConformancePrincipal,
97
+ responseClientId: string,
98
+ ): Promise<ConformanceEnrollmentPrincipalState>;
56
99
  revokePrincipal(principal: ConformancePrincipal): Promise<void>;
57
100
  expireCursor(cube: ConformanceCube, cursor: LogCursor): Promise<void>;
58
101
  armReplayTransition(): ConformanceReplayBarrier;
@@ -63,6 +106,7 @@ export interface ConformanceOperations {
63
106
  health(): Promise<ConformanceHttpResponse>;
64
107
  protocol(credential: string | null): Promise<ConformanceHttpResponse>;
65
108
  enroll(request: unknown): Promise<ConformanceHttpResponse>;
109
+ createCube(credential: string | null, request: unknown): Promise<ConformanceHttpResponse>;
66
110
  append(
67
111
  credential: string,
68
112
  cube: ConformanceCube,
@@ -108,7 +152,8 @@ export interface ConformanceEnvironment {
108
152
 
109
153
  export const ADAPTER_CONFORMANCE_FIXTURES = [
110
154
  { id: 'http.unauthenticated-liveness', area: 'http' },
111
- { id: 'protocol.enrollment-auth', area: 'protocol' },
155
+ { id: 'protocol.credential-free-preflight', area: 'protocol' },
156
+ { id: 'enrollment.retry-authority', area: 'enrollment' },
112
157
  { id: 'security.adapter-boundary-injection', area: 'security' },
113
158
  { id: 'security.oversize-request', area: 'security' },
114
159
  { id: 'security.cross-cube-isolation', area: 'security' },
@@ -118,7 +163,6 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
118
163
  { id: 'acks.idempotent', area: 'acks' },
119
164
  { id: 'claims.durable-noncursor', area: 'claims' },
120
165
  { id: 'decisions.topic-supersession', area: 'decisions' },
121
- { id: 'capabilities.unsupported-fails-closed', area: 'capabilities' },
122
166
  { id: 'security.active-stream-revocation', area: 'security' },
123
167
  ] as const;
124
168
 
@@ -294,6 +338,50 @@ function same(left: unknown, right: unknown): boolean {
294
338
  return JSON.stringify(left) === JSON.stringify(right);
295
339
  }
296
340
 
341
+ function assertStateDelta(
342
+ before: ConformanceAuthorityState,
343
+ after: ConformanceAuthorityState,
344
+ expected: Partial<ConformanceAuthorityState>,
345
+ description: string,
346
+ ): void {
347
+ for (const key of Object.keys(before) as Array<keyof ConformanceAuthorityState>) {
348
+ invariant(
349
+ after[key] - before[key] === (expected[key] ?? 0),
350
+ `${description} changed ${key} by ${after[key] - before[key]}; expected ${expected[key] ?? 0}.`,
351
+ );
352
+ }
353
+ }
354
+
355
+ function assertEnrollmentErrorIsSecretFree(
356
+ response: ConformanceHttpResponse,
357
+ requests: readonly { invitation: string; retry_key: string; client_credential: string }[],
358
+ description: string,
359
+ ): void {
360
+ const diagnostic = JSON.stringify(response.body);
361
+ const secrets = new Set(requests.flatMap((request) => [
362
+ request.invitation,
363
+ request.retry_key,
364
+ request.client_credential,
365
+ ]));
366
+ for (const secret of secrets) {
367
+ invariant(!diagnostic.includes(secret), `${description} exposed enrollment retry material.`);
368
+ }
369
+ }
370
+
371
+ function expectSecretFreeError(
372
+ response: ConformanceHttpResponse,
373
+ status: number,
374
+ code: ErrorCode,
375
+ operation: string,
376
+ secrets: readonly string[],
377
+ ): void {
378
+ expectError(response, status, code, operation);
379
+ const diagnostic = JSON.stringify(response.body);
380
+ for (const secret of new Set(secrets)) {
381
+ invariant(!diagnostic.includes(secret), `${operation} exposed retry material.`);
382
+ }
383
+ }
384
+
297
385
  export async function runAdapterConformance(
298
386
  environment: ConformanceEnvironment,
299
387
  options: AdapterConformanceOptions = {},
@@ -320,19 +408,13 @@ export async function runAdapterConformance(
320
408
  };
321
409
 
322
410
  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);
411
+ let principalA!: ConformancePrincipal;
412
+ let principalB!: ConformancePrincipal;
413
+ let cubeA!: ConformanceCube;
414
+ let cubeB!: ConformanceCube;
331
415
 
332
416
  let credentialA = '';
333
417
  let credentialB = '';
334
- let protocolBody: unknown;
335
- let protocolInfo: ProtocolInfo | null = null;
336
418
  await record('http.unauthenticated-liveness', async () => {
337
419
  const response = await environment.operations.health();
338
420
  expectStatus(response, 204, 'Unauthenticated liveness');
@@ -340,58 +422,316 @@ export async function runAdapterConformance(
340
422
  return { status: 204, bodyless: true };
341
423
  });
342
424
 
343
- await record('protocol.enrollment-auth', async () => {
344
- expectError(await environment.operations.protocol(null), 401, ErrorCode.AUTH_MISSING, 'Unauthenticated protocol request');
425
+ await record('protocol.credential-free-preflight', async () => {
426
+ // The tag preflight is credential-free (no bearer) and mutation-free: a
427
+ // client verifies pinned TLS and the exact tag before it creates or sends any
428
+ // credential. The server must answer 200 with ONLY the exact tag.
429
+ const before = await environment.admin.observeAuthorityState();
430
+ const response = await environment.operations.protocol(null);
431
+ expectStatus(response, 200, 'Credential-free protocol-tag preflight');
432
+ const preflight = decodeProtocolTagPreflight(response.body);
433
+ invariant(
434
+ Object.keys(preflight).length === 1 && preflight.protocol_version === PROTOCOL_VERSION,
435
+ 'Protocol-tag preflight exposed more than the exact tag.',
436
+ );
437
+ assertStateDelta(before, await environment.admin.observeAuthorityState(), {}, 'Protocol-tag preflight');
438
+ return { authenticated: false, mutation_free: true, protocol_version: preflight.protocol_version };
439
+ });
440
+
441
+ await record('enrollment.retry-authority', async () => {
442
+ const retryVectorErrors: string[] = [];
443
+ for (const [index, vector] of ENROLLMENT_RETRY_CONFORMANCE.entries()) {
444
+ for (const purpose of ['client', 'owner'] as const) {
445
+ await environment.admin.reset();
446
+ try {
447
+ const principal = await environment.admin.createPrincipal(`${purpose}-retry-vector-${index}`);
448
+ const invitation = await environment.admin.issueSingleUseInvitation(principal, purpose);
449
+ const initialPayload = { ...vector.initial, invitation };
450
+ const retryPayload = { ...vector.retry, invitation };
451
+ const beforeInitial = await environment.admin.observeAuthorityState();
452
+ const initialResponse = await environment.operations.enroll(
453
+ createProtocolEnvelope(`retry-${index}-initial`, initialPayload),
454
+ );
455
+ expectStatus(initialResponse, 201, `${vector.name} initial request`);
456
+ const initial = decodeEnrollmentExchangeResponseEnvelope(initialResponse.body).payload;
457
+ invariant(initial.purpose === purpose, `${purpose} ${vector.name} returned the wrong purpose.`);
458
+ invariant(
459
+ same(initial.server_capabilities, purpose === 'owner' ? ['create_cube'] : []),
460
+ `${purpose} ${vector.name} returned incorrect server authority.`,
461
+ );
462
+ const afterInitial = await environment.admin.observeAuthorityState();
463
+ assertStateDelta(
464
+ beforeInitial,
465
+ afterInitial,
466
+ {
467
+ enrolled_clients: 1,
468
+ enrollment_claims: 1,
469
+ server_capabilities: purpose === 'owner' ? 1 : 0,
470
+ },
471
+ `${purpose} ${vector.name} initial request`,
472
+ );
473
+
474
+ const beforeRetry = await environment.admin.observeAuthorityState();
475
+ const retryResponse = await environment.operations.enroll(
476
+ createProtocolEnvelope(`retry-${index}-retry`, retryPayload),
477
+ );
478
+ if (vector.expected.outcome === 'stable_non_secret_identity') {
479
+ expectStatus(retryResponse, 201, `${purpose} ${vector.name}`);
480
+ const retry = decodeEnrollmentExchangeResponseEnvelope(retryResponse.body).payload;
481
+ invariant(same(initial, retry), `${purpose} ${vector.name} returned different identities.`);
482
+ for (const field of vector.expected.forbidden_response_fields) {
483
+ invariant(!(field in retry), `${purpose} ${vector.name} returned forbidden field ${field}.`);
484
+ }
485
+ } else {
486
+ expectError(retryResponse, vector.expected.status, ErrorCode.AUTH_INVALID, `${purpose} ${vector.name}`);
487
+ assertEnrollmentErrorIsSecretFree(
488
+ retryResponse,
489
+ [initialPayload, retryPayload],
490
+ `${purpose} ${vector.name}`,
491
+ );
492
+ }
493
+ assertStateDelta(beforeRetry, await environment.admin.observeAuthorityState(), {}, `${purpose} ${vector.name} retry`);
494
+ invariant(
495
+ same(await environment.admin.inspectEnrollmentPrincipal(principal, initial.client_id), {
496
+ response_client_matches: true,
497
+ active_credential_bindings: 1,
498
+ bound_credential_matches_enrollment: true,
499
+ }),
500
+ `${purpose} ${vector.name} changed enrollment binding ownership.`,
501
+ );
502
+ } catch (error) {
503
+ retryVectorErrors.push(
504
+ `${purpose} ${vector.name}: ${error instanceof Error ? error.message : String(error)}`,
505
+ );
506
+ }
507
+ }
508
+ }
509
+ invariant(retryVectorErrors.length === 0, retryVectorErrors.join(' | '));
510
+
511
+ await environment.admin.reset();
512
+ const ownerPrincipal = await environment.admin.createPrincipal('owner');
513
+ const ordinaryPrincipal = await environment.admin.createPrincipal('ordinary');
514
+ const ownerInvitation = await environment.admin.issueSingleUseInvitation(ownerPrincipal, 'owner');
515
+ const ordinaryInvitation = await environment.admin.issueSingleUseInvitation(ordinaryPrincipal, 'client');
516
+ const ownerCredential = `${'Q'.repeat(42)}U`;
517
+ const ordinaryCredential = `${'Y'.repeat(42)}U`;
518
+ const beforeAuthorityEnrollment = await environment.admin.observeAuthorityState();
519
+ const ownerResponse = await environment.operations.enroll(createProtocolEnvelope('owner-enroll', {
520
+ invitation: ownerInvitation,
521
+ retry_key: '00000000-0000-4000-8000-000000000211',
522
+ client_credential: ownerCredential,
523
+ client_name: 'owner-client',
524
+ }));
525
+ const ordinaryResponse = await environment.operations.enroll(createProtocolEnvelope('ordinary-enroll', {
526
+ invitation: ordinaryInvitation,
527
+ retry_key: '00000000-0000-4000-8000-000000000212',
528
+ client_credential: ordinaryCredential,
529
+ client_name: 'ordinary-client',
530
+ }));
531
+ expectStatus(ownerResponse, 201, 'Owner enrollment');
532
+ expectStatus(ordinaryResponse, 201, 'Ordinary enrollment');
533
+ const owner = decodeEnrollmentExchangeResponseEnvelope(ownerResponse.body).payload;
534
+ const ordinary = decodeEnrollmentExchangeResponseEnvelope(ordinaryResponse.body).payload;
535
+ invariant(owner.purpose === 'owner' && same(owner.server_capabilities, ['create_cube']), 'Owner enrollment lacked exact create-cube authority.');
536
+ invariant(ordinary.purpose === 'client' && ordinary.server_capabilities.length === 0, 'Ordinary enrollment gained authority.');
537
+ assertStateDelta(
538
+ beforeAuthorityEnrollment,
539
+ await environment.admin.observeAuthorityState(),
540
+ { enrolled_clients: 2, enrollment_claims: 2, server_capabilities: 1 },
541
+ 'Owner and ordinary enrollment',
542
+ );
543
+
544
+ const cubeRequest = {
545
+ retry_key: '00000000-0000-4000-8000-000000000213',
546
+ name: 'repository-one',
547
+ template: 'default',
548
+ };
549
+ const droneCredential = await environment.admin.issueDroneSession(ownerPrincipal);
550
+ const beforeDeniedCreate = await environment.admin.observeAuthorityState();
551
+ expectSecretFreeError(
552
+ await environment.operations.createCube(null, createProtocolEnvelope('cube-missing-auth', cubeRequest)),
553
+ 401,
554
+ ErrorCode.AUTH_MISSING,
555
+ 'Missing-auth cube create',
556
+ [cubeRequest.retry_key],
557
+ );
558
+ expectSecretFreeError(
559
+ await environment.operations.createCube('invalid-credential', createProtocolEnvelope('cube-invalid-auth', cubeRequest)),
560
+ 401,
561
+ ErrorCode.AUTH_INVALID,
562
+ 'Invalid-auth cube create',
563
+ [cubeRequest.retry_key],
564
+ );
565
+ expectSecretFreeError(
566
+ await environment.operations.createCube(ordinaryCredential, createProtocolEnvelope('cube-denied', cubeRequest)),
567
+ 403,
568
+ ErrorCode.ACCESS_DENIED,
569
+ 'Ordinary cube create',
570
+ [cubeRequest.retry_key],
571
+ );
572
+ expectSecretFreeError(
573
+ await environment.operations.createCube(droneCredential, createProtocolEnvelope('cube-drone-denied', cubeRequest)),
574
+ 403,
575
+ ErrorCode.ACCESS_DENIED,
576
+ 'Drone-session cube create',
577
+ [cubeRequest.retry_key],
578
+ );
579
+ assertStateDelta(beforeDeniedCreate, await environment.admin.observeAuthorityState(), {}, 'Denied ordinary cube create');
580
+ const beforeCreate = await environment.admin.observeAuthorityState();
581
+ const createdResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-create', cubeRequest));
582
+ expectStatus(createdResponse, 201, 'Owner cube create');
583
+ const created = decodeCreateCubeResponseEnvelope(createdResponse.body).payload;
584
+ assertStateDelta(beforeCreate, await environment.admin.observeAuthorityState(), { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1 }, 'Owner cube create');
585
+ invariant(
586
+ same(await environment.admin.inspectCreatedCube(ownerPrincipal, created), {
587
+ cube_exists: true,
588
+ creator_has_grant: true,
589
+ grant_count: 1,
590
+ role_count: 2,
591
+ human_seat_role_matches: true,
592
+ default_worker_role_matches: true,
593
+ }),
594
+ 'Created cube identities or creator grant did not match persisted authority state.',
595
+ );
596
+ const beforeCreateRetry = await environment.admin.observeAuthorityState();
597
+ const retriedCreateResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-retry', cubeRequest));
598
+ expectStatus(retriedCreateResponse, 201, 'Exact cube-create retry');
599
+ invariant(same(decodeCreateCubeResponseEnvelope(retriedCreateResponse.body).payload, created), 'Exact cube-create retry returned different identities.');
600
+ assertStateDelta(beforeCreateRetry, await environment.admin.observeAuthorityState(), {}, 'Exact cube-create retry');
601
+ const beforeCreateMismatch = await environment.admin.observeAuthorityState();
602
+ expectSecretFreeError(
603
+ await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-mismatch', { ...cubeRequest, name: 'repository-two' })),
604
+ 409,
605
+ ErrorCode.INVALID_INPUT,
606
+ 'Cube-create retry mismatch',
607
+ [cubeRequest.retry_key],
608
+ );
609
+ assertStateDelta(beforeCreateMismatch, await environment.admin.observeAuthorityState(), {}, 'Cube-create retry mismatch');
610
+ await environment.admin.grantCreateCubeCapability(ordinaryPrincipal);
611
+ const crossClientRequest = { ...cubeRequest, name: 'ordinary-repository' };
612
+ const beforeCrossClientCreate = await environment.admin.observeAuthorityState();
613
+ const crossClientResponse = await environment.operations.createCube(
614
+ ordinaryCredential,
615
+ createProtocolEnvelope('cube-cross-client', crossClientRequest),
616
+ );
617
+ expectStatus(crossClientResponse, 201, 'Cross-client cube create with reused retry key');
618
+ const crossClientCreated = decodeCreateCubeResponseEnvelope(crossClientResponse.body).payload;
619
+ invariant(crossClientCreated.cube_id !== created.cube_id, 'Cross-client retry key reused another client\'s cube.');
620
+ assertStateDelta(
621
+ beforeCrossClientCreate,
622
+ await environment.admin.observeAuthorityState(),
623
+ { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1 },
624
+ 'Cross-client cube create',
625
+ );
626
+ invariant(
627
+ (await environment.admin.inspectCreatedCube(ordinaryPrincipal, crossClientCreated)).creator_has_grant,
628
+ 'Cross-client cube creation did not grant its authenticated creator.',
629
+ );
630
+ const beforeCrossClientRetry = await environment.admin.observeAuthorityState();
631
+ const crossClientRetry = await environment.operations.createCube(
632
+ ordinaryCredential,
633
+ createProtocolEnvelope('cube-cross-client-retry', crossClientRequest),
634
+ );
635
+ expectStatus(crossClientRetry, 201, 'Exact cross-client cube-create retry');
636
+ invariant(
637
+ same(decodeCreateCubeResponseEnvelope(crossClientRetry.body).payload, crossClientCreated),
638
+ 'Exact cross-client cube-create retry returned different identities.',
639
+ );
640
+ assertStateDelta(beforeCrossClientRetry, await environment.admin.observeAuthorityState(), {}, 'Exact cross-client cube-create retry');
641
+ const beforeSecondCreate = await environment.admin.observeAuthorityState();
642
+ const secondCreatedResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-create-second', {
643
+ ...cubeRequest,
644
+ retry_key: '00000000-0000-4000-8000-000000000214',
645
+ name: 'repository-two',
646
+ }));
647
+ expectStatus(secondCreatedResponse, 201, 'Second cube create');
648
+ const secondCreated = decodeCreateCubeResponseEnvelope(secondCreatedResponse.body).payload;
649
+ invariant(secondCreated.cube_id !== created.cube_id, 'Fresh cube-create retry key reused an existing cube.');
650
+ assertStateDelta(
651
+ beforeSecondCreate,
652
+ await environment.admin.observeAuthorityState(),
653
+ { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1 },
654
+ 'Second cube create',
655
+ );
656
+ await environment.admin.revokePrincipal(ownerPrincipal);
657
+ const beforeRevokedCreate = await environment.admin.observeAuthorityState();
658
+ expectSecretFreeError(
659
+ await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-revoked', {
660
+ ...cubeRequest,
661
+ retry_key: '00000000-0000-4000-8000-000000000215',
662
+ })),
663
+ 401,
664
+ ErrorCode.SESSION_REVOKED,
665
+ 'Revoked owner cube create',
666
+ ['00000000-0000-4000-8000-000000000215'],
667
+ );
668
+ assertStateDelta(beforeRevokedCreate, await environment.admin.observeAuthorityState(), {}, 'Revoked owner cube create');
669
+
670
+ await environment.admin.reset();
671
+ principalA = await environment.admin.createPrincipal('principal-a');
672
+ principalB = await environment.admin.createPrincipal('principal-b');
673
+ cubeA = await environment.admin.createCube('cube-a');
674
+ cubeB = await environment.admin.createCube('cube-b');
675
+ await environment.admin.grantCube(principalA, cubeA);
676
+ await environment.admin.grantCube(principalB, cubeB);
677
+ const invitationA = await environment.admin.issueSingleUseInvitation(principalA, 'client');
678
+ const invitationB = await environment.admin.issueSingleUseInvitation(principalB, 'client');
679
+ credentialA = 'A'.repeat(43);
680
+ credentialB = 'E'.repeat(43);
345
681
  const enrollmentARequest = createProtocolEnvelope('enroll-a1', {
346
682
  invitation: invitationA,
683
+ retry_key: '00000000-0000-4000-8000-000000000201',
684
+ client_credential: credentialA,
347
685
  client_name: 'conformance-a',
348
686
  });
349
687
  const enrollmentBRequest = createProtocolEnvelope('enroll-b1', {
350
688
  invitation: invitationB,
689
+ retry_key: '00000000-0000-4000-8000-000000000202',
690
+ client_credential: credentialB,
351
691
  client_name: 'conformance-b',
352
692
  });
353
693
  const enrolledAResponse = await environment.operations.enroll(enrollmentARequest);
354
694
  const enrolledBResponse = await environment.operations.enroll(enrollmentBRequest);
355
695
  expectStatus(enrolledAResponse, 201, 'Principal A enrollment');
356
696
  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.');
697
+ const enrolledA = decodeEnrollmentExchangeResponseEnvelope(enrolledAResponse.body).payload;
698
+ const enrolledB = decodeEnrollmentExchangeResponseEnvelope(enrolledBResponse.body).payload;
699
+ invariant(enrolledA.purpose === 'client' && enrolledB.purpose === 'client', 'Ordinary enrollment returned owner authority.');
700
+ invariant(enrolledA.server_capabilities.length === 0 && enrolledB.server_capabilities.length === 0, 'Ordinary enrollment returned a server capability.');
701
+ invariant(!('credential' in enrolledA) && !('credential' in enrolledB), 'Enrollment response returned a bearer.');
702
+ const retriedAResponse = await environment.operations.enroll(enrollmentARequest);
703
+ expectStatus(retriedAResponse, 201, 'Exact enrollment retry');
704
+ invariant(
705
+ JSON.stringify(decodeEnrollmentExchangeResponseEnvelope(retriedAResponse.body).payload) ===
706
+ JSON.stringify(enrolledA),
707
+ 'Exact enrollment retry returned different identities.',
708
+ );
360
709
  expectError(
361
- await environment.operations.enroll(enrollmentARequest),
710
+ await environment.operations.enroll(createProtocolEnvelope('enroll-a-mismatch', {
711
+ ...enrollmentARequest.payload,
712
+ retry_key: '00000000-0000-4000-8000-000000000203',
713
+ })),
362
714
  401,
363
715
  ErrorCode.AUTH_INVALID,
364
- 'Invitation reuse',
365
- );
366
- const protocolResponse = await environment.operations.protocol(credentialA);
367
- expectStatus(protocolResponse, 200, 'Authenticated protocol request');
368
- protocolBody = protocolResponse.body;
369
- protocolInfo = negotiateProtocol(decodeProtocolInfoEnvelope(protocolBody).payload, [
370
- 'log.cursor',
371
- 'stream.sse',
372
- 'stream.replay',
373
- 'acks',
374
- 'claims',
375
- 'decisions',
376
- ]);
716
+ 'Enrollment retry mismatch',
717
+ );
377
718
  return {
378
- unauthenticated: ErrorCode.AUTH_MISSING,
379
719
  enrollment_status: 201,
380
- invitation_reuse: ErrorCode.AUTH_INVALID,
381
- protocol_version: protocolInfo.protocol_version,
720
+ exact_retry_status: 201,
721
+ mismatched_retry: ErrorCode.AUTH_INVALID,
722
+ response_secret_free: true,
382
723
  };
383
724
  });
384
725
 
385
726
  await record('security.adapter-boundary-injection', async () => {
386
- invariant(protocolInfo, 'Protocol fixture did not produce request limits.');
387
727
  const injectedMessage = "'); DROP TABLE log_entries; --\r\ndata: forged-sse-frame";
388
728
  const injectedBody = JSON.stringify(
389
729
  createProtocolEnvelope('inject-b1', { message: injectedMessage }),
390
730
  );
391
731
  invariant(
392
- utf8ByteLength(injectedBody) <= protocolInfo.limits.max_request_bytes &&
393
- utf8ByteLength(injectedMessage) <= protocolInfo.limits.max_log_message_bytes,
394
- 'Injection fixture exceeded an advertised request limit.',
732
+ utf8ByteLength(injectedBody) <= PROTOCOL_LIMIT_CEILINGS.max_request_bytes &&
733
+ utf8ByteLength(injectedMessage) <= PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes,
734
+ 'Injection fixture exceeded the shared request-limit ceiling.',
395
735
  );
396
736
  const injected = await environment.operations.appendRaw(
397
737
  credentialB,
@@ -451,15 +791,14 @@ export async function runAdapterConformance(
451
791
  });
452
792
 
453
793
  await record('security.oversize-request', async () => {
454
- invariant(protocolInfo, 'Protocol fixture did not produce request limits.');
455
794
  const baseBody = JSON.stringify(
456
795
  createProtocolEnvelope('oversize-a1', { message: 'must-not-persist' }),
457
796
  );
458
797
  const oversizedBody = baseBody + ' '.repeat(
459
- Math.max(0, protocolInfo.limits.max_request_bytes - utf8ByteLength(baseBody) + 1),
798
+ Math.max(0, PROTOCOL_LIMIT_CEILINGS.max_request_bytes - utf8ByteLength(baseBody) + 1),
460
799
  );
461
800
  invariant(
462
- utf8ByteLength(oversizedBody) > protocolInfo.limits.max_request_bytes,
801
+ utf8ByteLength(oversizedBody) > PROTOCOL_LIMIT_CEILINGS.max_request_bytes,
463
802
  'Oversize fixture did not exceed max_request_bytes.',
464
803
  );
465
804
  const response = await environment.operations.appendRaw(credentialA, cubeA, oversizedBody);
@@ -646,22 +985,6 @@ export async function runAdapterConformance(
646
985
  return { active_count: 1, active_decision: 'second', supersedes_first: true };
647
986
  });
648
987
 
649
- await record('capabilities.unsupported-fails-closed', async () => {
650
- invariant(protocolBody, 'Protocol fixture did not produce an envelope.');
651
- let code: ErrorCode | null = null;
652
- try {
653
- negotiateProtocol(
654
- decodeProtocolInfoEnvelope(protocolBody).payload,
655
- ['future.required' as Capability],
656
- );
657
- } catch (error) {
658
- if (error instanceof ProtocolContractError) code = error.code;
659
- else throw error;
660
- }
661
- invariant(code === ErrorCode.UNSUPPORTED_CAPABILITY, 'Unsupported capability did not fail closed client-side.');
662
- return { code };
663
- });
664
-
665
988
  await record('security.active-stream-revocation', async () => {
666
989
  invariant(liveCursor, 'Stream fixture did not produce a live cursor.');
667
990
  const opened = await environment.operations.openStream(credentialA, cubeA, liveCursor);