borgmcp-shared 0.7.0 → 0.8.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.
Files changed (39) hide show
  1. package/README.md +8 -56
  2. package/RELEASES.md +33 -0
  3. package/dist/conformance/adapter.d.ts +22 -0
  4. package/dist/conformance/adapter.d.ts.map +1 -1
  5. package/dist/conformance/adapter.js +169 -2
  6. package/dist/conformance/adapter.js.map +1 -1
  7. package/dist/conformance/index.d.ts +22 -1
  8. package/dist/conformance/index.d.ts.map +1 -1
  9. package/dist/conformance/index.js +78 -1
  10. package/dist/conformance/index.js.map +1 -1
  11. package/dist/protocol/contract.d.ts +32 -1
  12. package/dist/protocol/contract.d.ts.map +1 -1
  13. package/dist/protocol/contract.js +212 -2
  14. package/dist/protocol/contract.js.map +1 -1
  15. package/dist/protocol/errors.d.ts +1 -0
  16. package/dist/protocol/errors.d.ts.map +1 -1
  17. package/dist/protocol/errors.js +1 -0
  18. package/dist/protocol/errors.js.map +1 -1
  19. package/dist/protocol/sse.d.ts +6 -2
  20. package/dist/protocol/sse.d.ts.map +1 -1
  21. package/dist/protocol/sse.js +8 -2
  22. package/dist/protocol/sse.js.map +1 -1
  23. package/dist/protocol/version.d.ts +1 -1
  24. package/dist/protocol/version.js +1 -1
  25. package/dist/templates.d.ts +1 -0
  26. package/dist/templates.d.ts.map +1 -1
  27. package/dist/templates.js +14 -3
  28. package/dist/templates.js.map +1 -1
  29. package/docs/compatibility.md +15 -4
  30. package/docs/enrollment.md +5 -4
  31. package/docs/releasing.md +4 -3
  32. package/package.json +2 -1
  33. package/src/conformance/adapter.ts +321 -0
  34. package/src/conformance/index.ts +102 -1
  35. package/src/protocol/contract.ts +260 -2
  36. package/src/protocol/errors.ts +1 -0
  37. package/src/protocol/sse.ts +17 -2
  38. package/src/protocol/version.ts +2 -2
  39. package/src/templates.ts +15 -3
@@ -4,10 +4,12 @@ import {
4
4
  createProtocolEnvelope,
5
5
  decodeAssociateRepositoryCubeResponseEnvelope,
6
6
  decodeCreateCubeResponseEnvelope,
7
+ decodeDeleteCubeResponseEnvelope,
7
8
  decodeAppendLogResultEnvelope,
8
9
  decodeDecisionResultEnvelope,
9
10
  decodeDecisionsResultEnvelope,
10
11
  decodeEnrollmentExchangeResponseEnvelope,
12
+ decodeInvitationArtifact,
11
13
  decodeAttachResponseEnvelope,
12
14
  decodeDroneRuntimeMetadataPatch,
13
15
  decodeEvictDroneResultEnvelope,
@@ -34,6 +36,7 @@ import {
34
36
  import {
35
37
  CREATE_CUBE_ASSOCIATION_CONFORMANCE,
36
38
  CREATE_CUBE_RETRY_CONFORMANCE,
39
+ INVITATION_ARTIFACT_CONFORMANCE,
37
40
  ENROLLMENT_RETRY_CONFORMANCE,
38
41
  } from './index.js';
39
42
 
@@ -130,6 +133,20 @@ export interface ConformanceCreatedCubeState {
130
133
  default_worker_role_matches: boolean;
131
134
  }
132
135
 
136
+ export interface ConformanceDeletedCubeState {
137
+ readonly cube_exists: boolean;
138
+ readonly role_count: number;
139
+ readonly drone_count: number;
140
+ readonly log_count: number;
141
+ readonly claim_count: number;
142
+ readonly decision_count: number;
143
+ readonly grant_count: number;
144
+ readonly cube_create_binding_count: number;
145
+ readonly repository_association_count: number;
146
+ readonly active_stream_count: number;
147
+ readonly terminal_credential_count: number;
148
+ }
149
+
133
150
  export interface ConformanceRepositoryCubeFixture {
134
151
  cube_id: string;
135
152
  name: string;
@@ -168,6 +185,8 @@ export interface ConformanceStreamResponse extends ConformanceHttpResponse {
168
185
  */
169
186
  export interface ConformanceAdmin {
170
187
  reset(): Promise<void>;
188
+ /** Restarts the authority while preserving durable state. */
189
+ restartAuthority(): Promise<void>;
171
190
  createPrincipal(name: string): Promise<ConformancePrincipal>;
172
191
  createCube(name: string): Promise<ConformanceCube>;
173
192
  /** Grants the requested cube authority; omitted access defaults to manage. */
@@ -205,6 +224,7 @@ export interface ConformanceAdmin {
205
224
  creator: ConformancePrincipal,
206
225
  response: CreateCubeResponse,
207
226
  ): Promise<ConformanceCreatedCubeState>;
227
+ inspectDeletedCube(cube: ConformanceCube): Promise<ConformanceDeletedCubeState>;
208
228
  prepareRepositoryCube(
209
229
  cube: ConformanceCube,
210
230
  input: {
@@ -227,6 +247,11 @@ export interface ConformanceOperations {
227
247
  protocol(credential: string | null): Promise<ConformanceHttpResponse>;
228
248
  enroll(request: unknown): Promise<ConformanceHttpResponse>;
229
249
  createCube(credential: string | null, request: unknown): Promise<ConformanceHttpResponse>;
250
+ deleteCube(
251
+ credential: string,
252
+ cube: ConformanceCube,
253
+ request: unknown,
254
+ ): Promise<ConformanceHttpResponse>;
230
255
  resolveRepositoryCube(credential: string | null, request: unknown): Promise<ConformanceHttpResponse>;
231
256
  associateRepositoryCube(credential: string | null, request: unknown): Promise<ConformanceHttpResponse>;
232
257
  attach(credential: string, request: unknown): Promise<ConformanceHttpResponse>;
@@ -312,6 +337,7 @@ export interface ConformanceEnvironment {
312
337
  export const ADAPTER_CONFORMANCE_FIXTURES = [
313
338
  { id: 'http.unauthenticated-liveness', area: 'http' },
314
339
  { id: 'protocol.credential-free-preflight', area: 'protocol' },
340
+ { id: 'enrollment.invitation-artifact', area: 'enrollment' },
315
341
  { id: 'enrollment.retry-authority', area: 'enrollment' },
316
342
  { id: 'repository.explicit-association', area: 'repository' },
317
343
  { id: 'security.adapter-boundary-injection', area: 'security' },
@@ -337,6 +363,7 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
337
363
  { id: 'security.metadata-noninterference', area: 'security' },
338
364
  { id: 'security.metadata-secret-non-echo', area: 'security' },
339
365
  { id: 'security.active-stream-revocation', area: 'security' },
366
+ { id: 'cubes.delete-terminal-cascade', area: 'cubes' },
340
367
  ] as const;
341
368
 
342
369
  export type AdapterConformanceFixtureId =
@@ -618,6 +645,30 @@ export async function runAdapterConformance(
618
645
  return { authenticated: false, mutation_free: true, protocol_version: preflight.protocol_version };
619
646
  });
620
647
 
648
+ await record('enrollment.invitation-artifact', async () => {
649
+ const errors: string[] = [];
650
+ for (const vector of INVITATION_ARTIFACT_CONFORMANCE) {
651
+ if (vector.expected === null) {
652
+ let rejected = false;
653
+ try {
654
+ decodeInvitationArtifact(vector.input);
655
+ } catch {
656
+ rejected = true;
657
+ }
658
+ if (!rejected) errors.push(`${vector.name}: permissive decoder accepted the invalid artifact.`);
659
+ continue;
660
+ }
661
+ try {
662
+ const decoded = decodeInvitationArtifact(vector.input);
663
+ invariant(same(decoded, vector.expected), `${vector.name} decoded a different artifact.`);
664
+ } catch (error) {
665
+ errors.push(`${vector.name}: ${error instanceof Error ? error.message : String(error)}`);
666
+ }
667
+ }
668
+ invariant(errors.length === 0, errors.join(' | '));
669
+ return { vectors: INVITATION_ARTIFACT_CONFORMANCE.length };
670
+ });
671
+
621
672
  await record('enrollment.retry-authority', async () => {
622
673
  const retryVectorErrors: string[] = [];
623
674
  for (const [index, vector] of ENROLLMENT_RETRY_CONFORMANCE.entries()) {
@@ -2431,6 +2482,276 @@ export async function runAdapterConformance(
2431
2482
  return { stream_terminated: true, subsequent_status: 401, subsequent_code: ErrorCode.SESSION_REVOKED };
2432
2483
  });
2433
2484
 
2485
+ await record('cubes.delete-terminal-cascade', async () => {
2486
+ const enrollParent = async (
2487
+ name: string,
2488
+ marker: string,
2489
+ retrySuffix: string,
2490
+ ): Promise<{ principal: ConformancePrincipal; credential: string }> => {
2491
+ const principal = await environment.admin.createPrincipal(name);
2492
+ const invitation = await environment.admin.issueSingleUseInvitation(principal, 'client');
2493
+ const credential = `${marker.repeat(41)}AQ`;
2494
+ expectStatus(await environment.operations.enroll(createProtocolEnvelope(
2495
+ `delete-${name}-enroll`,
2496
+ {
2497
+ invitation,
2498
+ retry_key: `00000000-0000-4000-8000-${retrySuffix.padStart(12, '0')}`,
2499
+ client_credential: credential,
2500
+ client_name: name,
2501
+ },
2502
+ )), 201, `${name} enrollment`);
2503
+ return { principal, credential };
2504
+ };
2505
+ const creator = await enrollParent('delete-creator', 'C', '701');
2506
+ const manager = await enrollParent('delete-manager', 'M', '702');
2507
+ const reader = await enrollParent('delete-reader', 'R', '703');
2508
+ const writer = await enrollParent('delete-writer', 'W', '704');
2509
+ const outsider = await enrollParent('delete-outsider', 'O', '705');
2510
+ await environment.admin.grantCreateCubeCapability(creator.principal);
2511
+ const createResponse = await environment.operations.createCube(
2512
+ creator.credential,
2513
+ createProtocolEnvelope('delete-cube-create', {
2514
+ retry_key: '00000000-0000-4000-8000-000000000706',
2515
+ name: 'Disposable Cube',
2516
+ working_repo_name: 'disposable-cube',
2517
+ repository: { kind: 'local', value: '00000000-0000-4000-8000-000000000707' },
2518
+ template: 'default',
2519
+ }),
2520
+ );
2521
+ expectStatus(createResponse, 201, 'Disposable cube creation');
2522
+ const created = decodeCreateCubeResponseEnvelope(createResponse.body).payload;
2523
+ const cube = { id: created.cube_id };
2524
+ const role = { id: created.default_worker_role_id };
2525
+ await environment.admin.grantCube(manager.principal, cube, 'manage');
2526
+ await environment.admin.grantCube(reader.principal, cube, 'read');
2527
+ await environment.admin.grantCube(writer.principal, cube, 'write');
2528
+ const drone = await environment.admin.createDrone(creator.principal, cube, role);
2529
+ const droneCredential = await environment.admin.issueManagedDroneSession(drone);
2530
+ const append = await environment.operations.append(
2531
+ creator.credential,
2532
+ cube,
2533
+ createProtocolEnvelope('delete-log', { message: 'deleted with cube' }),
2534
+ );
2535
+ expectStatus(append, 201, 'Deletion fixture log');
2536
+ const entry = decodeAppendLogResultEnvelope(append.body).payload.entry;
2537
+ expectStatus(await environment.operations.ack(
2538
+ creator.credential,
2539
+ cube,
2540
+ createProtocolEnvelope('delete-claim', { entry_id: entry.id, kind: 'claim' }),
2541
+ ), 204, 'Deletion fixture claim');
2542
+ expectStatus(await environment.operations.recordDecision(
2543
+ manager.credential,
2544
+ cube,
2545
+ createProtocolEnvelope('delete-decision', { topic: 'cleanup', decision: 'delete' }),
2546
+ ), 201, 'Deletion fixture decision');
2547
+
2548
+ const beforeDenied = await environment.admin.inspectCubeManagementState(cube);
2549
+ for (const [kind, credential] of [
2550
+ ['read', reader.credential],
2551
+ ['write', writer.credential],
2552
+ ['drone-session', droneCredential],
2553
+ ] as const) {
2554
+ expectError(
2555
+ await environment.operations.deleteCube(
2556
+ credential,
2557
+ cube,
2558
+ createProtocolEnvelope(`delete-${kind}-denied`, {}),
2559
+ ),
2560
+ 403,
2561
+ ErrorCode.ACCESS_DENIED,
2562
+ `${kind} cube deletion`,
2563
+ );
2564
+ invariant(
2565
+ same(await environment.admin.inspectCubeManagementState(cube), beforeDenied),
2566
+ `${kind} cube deletion denial mutated the cube.`,
2567
+ );
2568
+ }
2569
+ expectError(
2570
+ await environment.operations.deleteCube(
2571
+ outsider.credential,
2572
+ cube,
2573
+ createProtocolEnvelope('delete-outsider-denied', {}),
2574
+ ),
2575
+ 404,
2576
+ ErrorCode.NOT_FOUND,
2577
+ 'Never-authorized cube deletion',
2578
+ );
2579
+
2580
+ const parentStream = await environment.operations.openStream(manager.credential, cube, null);
2581
+ const droneStream = await environment.operations.openStream(droneCredential, cube, null);
2582
+ expectStatus(parentStream, 200, 'Deletion parent stream');
2583
+ expectStatus(droneStream, 200, 'Deletion drone stream');
2584
+ invariant(parentStream.stream && droneStream.stream, 'Deletion fixture streams did not open.');
2585
+ const parentEvents = new SseEventReader(parentStream.stream);
2586
+ const droneEvents = new SseEventReader(droneStream.stream);
2587
+ for (const streamReader of [parentEvents, droneEvents]) {
2588
+ invariant((await streamReader.next()).type === 'log', 'Deletion stream omitted replay log.');
2589
+ const bookmark = await streamReader.next();
2590
+ invariant(bookmark.type === 'bookmark' && bookmark.replay_complete, 'Deletion stream omitted bookmark.');
2591
+ }
2592
+ const parentTerminal = parentEvents.next();
2593
+ const droneTerminal = droneEvents.next();
2594
+ await provePending(parentTerminal, 'Parent deletion terminal event', pendingProbeMs);
2595
+ await provePending(droneTerminal, 'Drone deletion terminal event', pendingProbeMs);
2596
+
2597
+ const deleted = await environment.operations.deleteCube(
2598
+ manager.credential,
2599
+ cube,
2600
+ createProtocolEnvelope('delete-managed-cube', {}),
2601
+ );
2602
+ expectStatus(deleted, 200, 'Managed cube deletion');
2603
+ invariant(
2604
+ same(decodeDeleteCubeResponseEnvelope(deleted.body).payload, {
2605
+ cube_id: cube.id,
2606
+ deleted: true,
2607
+ }),
2608
+ 'Cube deletion response did not identify the terminal cube.',
2609
+ );
2610
+ for (const [kind, event] of [
2611
+ ['parent', await within(parentTerminal, 'Parent deletion terminal event', streamDeadlineMs)],
2612
+ ['drone', await within(droneTerminal, 'Drone deletion terminal event', streamDeadlineMs)],
2613
+ ] as const) {
2614
+ invariant(
2615
+ event.type === 'error' && event.error.error.code === ErrorCode.CUBE_DELETED,
2616
+ `${kind} stream did not receive CUBE_DELETED.`,
2617
+ );
2618
+ }
2619
+ for (const streamReader of [parentEvents, droneEvents]) {
2620
+ let closed = false;
2621
+ try {
2622
+ await within(streamReader.next(), 'Deleted cube stream close', streamDeadlineMs);
2623
+ } catch (error) {
2624
+ if (error instanceof Error && error.message.includes('did not settle')) throw error;
2625
+ closed = true;
2626
+ }
2627
+ invariant(closed, 'Deleted cube stream yielded after its terminal error.');
2628
+ await streamReader.close();
2629
+ }
2630
+
2631
+ invariant(
2632
+ same(await environment.admin.inspectDeletedCube(cube), {
2633
+ cube_exists: false,
2634
+ role_count: 0,
2635
+ drone_count: 0,
2636
+ log_count: 0,
2637
+ claim_count: 0,
2638
+ decision_count: 0,
2639
+ grant_count: 0,
2640
+ cube_create_binding_count: 0,
2641
+ repository_association_count: 0,
2642
+ active_stream_count: 0,
2643
+ terminal_credential_count: 5,
2644
+ }),
2645
+ 'Cube deletion did not atomically remove cube-owned state and preserve only terminal credentials.',
2646
+ );
2647
+ for (const [kind, credential] of [
2648
+ ['creator', creator.credential],
2649
+ ['manager', manager.credential],
2650
+ ['reader', reader.credential],
2651
+ ['writer', writer.credential],
2652
+ ['drone', droneCredential],
2653
+ ] as const) {
2654
+ expectError(
2655
+ await environment.operations.read(
2656
+ credential,
2657
+ cube,
2658
+ createProtocolEnvelope(`delete-${kind}-terminal`, { cursor: null, limit: 1 }),
2659
+ ),
2660
+ PROTOCOL_HTTP_CONTRACT.cube_deleted_status,
2661
+ ErrorCode.CUBE_DELETED,
2662
+ `${kind} post-delete request`,
2663
+ );
2664
+ expectError(
2665
+ await environment.operations.deleteCube(
2666
+ credential,
2667
+ cube,
2668
+ createProtocolEnvelope(`delete-${kind}-repeat-terminal`, {}),
2669
+ ),
2670
+ PROTOCOL_HTTP_CONTRACT.cube_deleted_status,
2671
+ ErrorCode.CUBE_DELETED,
2672
+ `${kind} post-delete DELETE`,
2673
+ );
2674
+ }
2675
+ expectError(
2676
+ await environment.operations.deleteCube(
2677
+ outsider.credential,
2678
+ cube,
2679
+ createProtocolEnvelope('delete-outsider-hidden-repeat', {}),
2680
+ ),
2681
+ 404,
2682
+ ErrorCode.NOT_FOUND,
2683
+ 'Never-authorized post-delete DELETE',
2684
+ );
2685
+ expectError(
2686
+ await environment.operations.read(
2687
+ outsider.credential,
2688
+ cube,
2689
+ createProtocolEnvelope('delete-outsider-hidden', { cursor: null, limit: 1 }),
2690
+ ),
2691
+ 404,
2692
+ ErrorCode.NOT_FOUND,
2693
+ 'Never-authorized post-delete request',
2694
+ );
2695
+ await environment.admin.restartAuthority();
2696
+ for (const [kind, credential] of [
2697
+ ['creator', creator.credential],
2698
+ ['manager', manager.credential],
2699
+ ['reader', reader.credential],
2700
+ ['writer', writer.credential],
2701
+ ['drone', droneCredential],
2702
+ ] as const) {
2703
+ expectError(
2704
+ await environment.operations.read(
2705
+ credential,
2706
+ cube,
2707
+ createProtocolEnvelope(`delete-${kind}-after-restart`, { cursor: null, limit: 1 }),
2708
+ ),
2709
+ PROTOCOL_HTTP_CONTRACT.cube_deleted_status,
2710
+ ErrorCode.CUBE_DELETED,
2711
+ `${kind} post-restart deleted-cube request`,
2712
+ );
2713
+ expectError(
2714
+ await environment.operations.deleteCube(
2715
+ credential,
2716
+ cube,
2717
+ createProtocolEnvelope(`delete-${kind}-repeat-after-restart`, {}),
2718
+ ),
2719
+ PROTOCOL_HTTP_CONTRACT.cube_deleted_status,
2720
+ ErrorCode.CUBE_DELETED,
2721
+ `${kind} post-restart DELETE`,
2722
+ );
2723
+ }
2724
+ expectError(
2725
+ await environment.operations.deleteCube(
2726
+ outsider.credential,
2727
+ cube,
2728
+ createProtocolEnvelope('delete-outsider-hidden-repeat-after-restart', {}),
2729
+ ),
2730
+ 404,
2731
+ ErrorCode.NOT_FOUND,
2732
+ 'Never-authorized post-restart DELETE',
2733
+ );
2734
+ expectError(
2735
+ await environment.operations.read(
2736
+ outsider.credential,
2737
+ cube,
2738
+ createProtocolEnvelope('delete-outsider-hidden-after-restart', { cursor: null, limit: 1 }),
2739
+ ),
2740
+ 404,
2741
+ ErrorCode.NOT_FOUND,
2742
+ 'Never-authorized post-restart request',
2743
+ );
2744
+ return {
2745
+ deletion_status: 200,
2746
+ cascade_complete: true,
2747
+ terminal_stream_error: ErrorCode.CUBE_DELETED,
2748
+ terminal_http_status: 410,
2749
+ terminal_state_durable_after_restart: true,
2750
+ unknown_hidden_status: 404,
2751
+ non_member_manager: true,
2752
+ };
2753
+ });
2754
+
2434
2755
  const normalizedTranscript = results.map(({ id, observations }) => ({ id, observations }));
2435
2756
  return { ok: results.every((result) => result.ok), results, normalizedTranscript };
2436
2757
  }
@@ -3,9 +3,14 @@ import type {
3
3
  AttachResponse,
4
4
  AssociateRepositoryCubeRequest,
5
5
  CreateCubeRequest,
6
+ DeleteCubeRequest,
6
7
  EnrollmentExchangeRequest,
7
8
  ResolveRepositoryCubeRequest,
8
9
  } from '../protocol/contract.js';
10
+ import {
11
+ encodeInvitationArtifact,
12
+ type InvitationArtifact,
13
+ } from '../protocol/contract.js';
9
14
  import type { EnrichedStreamEntry } from '../protocol/types.js';
10
15
 
11
16
  export * from './adapter.js';
@@ -229,7 +234,51 @@ export interface EnrollmentRetryConformanceVector {
229
234
  };
230
235
  }
231
236
 
232
- const ENROLLMENT_INVITATION = 'I'.repeat(43);
237
+ const ENROLLMENT_ARTIFACT: InvitationArtifact = {
238
+ version: 2,
239
+ endpoint: 'https://borg.example.test:7091',
240
+ ca_spki_sha256: 'a'.repeat(64),
241
+ authority: 'client',
242
+ secret: 'S'.repeat(43),
243
+ integrity: 'I'.repeat(43),
244
+ };
245
+ const ENROLLMENT_ARTIFACT_TOKEN = encodeInvitationArtifact(ENROLLMENT_ARTIFACT);
246
+
247
+ export const INVITATION_ARTIFACT_CONFORMANCE: readonly ConformanceVector<
248
+ string,
249
+ InvitationArtifact | null
250
+ >[] = [
251
+ {
252
+ name: 'decodes the canonical v2 invitation artifact',
253
+ input: ENROLLMENT_ARTIFACT_TOKEN,
254
+ expected: ENROLLMENT_ARTIFACT,
255
+ },
256
+ {
257
+ name: 'rejects a legacy opaque invitation before use',
258
+ input: 'I'.repeat(43),
259
+ expected: null,
260
+ },
261
+ {
262
+ name: 'rejects a mangled noncanonical invitation token',
263
+ input: `${ENROLLMENT_ARTIFACT_TOKEN}=`,
264
+ expected: null,
265
+ },
266
+ {
267
+ name: 'rejects an invitation token above the outer bound',
268
+ input: 'A'.repeat(1025),
269
+ expected: null,
270
+ },
271
+ {
272
+ name: 'rejects a token with a duplicate trailing field',
273
+ input: `${ENROLLMENT_ARTIFACT_TOKEN}A`,
274
+ expected: null,
275
+ },
276
+ ];
277
+
278
+ const ENROLLMENT_INVITATION = encodeInvitationArtifact({
279
+ ...ENROLLMENT_ARTIFACT,
280
+ authority: 'client',
281
+ });
233
282
  const ENROLLMENT_CREDENTIAL = 'A'.repeat(43);
234
283
  const ENROLLMENT_RETRY_KEY = '00000000-0000-4000-8000-000000000101';
235
284
 
@@ -443,6 +492,58 @@ readonly CreateCubeAssociationConformanceVector[] = [
443
492
  },
444
493
  ];
445
494
 
495
+ export interface DeleteCubeConformanceVector {
496
+ name: string;
497
+ request: DeleteCubeRequest;
498
+ expected: {
499
+ status: 200 | 403 | 404 | 410;
500
+ error?: 'ACCESS_DENIED' | 'NOT_FOUND' | 'CUBE_DELETED';
501
+ response?: { deleted: true };
502
+ terminal_sse?: { event: 'error'; error: 'CUBE_DELETED'; closes_after_event: true };
503
+ durable_after_restart?: true;
504
+ mutation: 'cascade' | 'none';
505
+ };
506
+ }
507
+
508
+ /** Cube deletion is manage-gated, cascading, terminal, and durable across authority restart. */
509
+ export const DELETE_CUBE_CONFORMANCE: readonly DeleteCubeConformanceVector[] = [
510
+ {
511
+ name: 'non-member managing parent deletes the cube atomically',
512
+ request: {},
513
+ expected: { status: 200, response: { deleted: true }, mutation: 'cascade' },
514
+ },
515
+ {
516
+ name: 'known read and write grants and drone sessions cannot delete',
517
+ request: {},
518
+ expected: { status: 403, error: 'ACCESS_DENIED', mutation: 'none' },
519
+ },
520
+ {
521
+ name: 'never-authorized and unknown callers cannot enumerate the cube',
522
+ request: {},
523
+ expected: { status: 404, error: 'NOT_FOUND', mutation: 'none' },
524
+ },
525
+ {
526
+ name: 'connected clients receive one terminal error frame before close',
527
+ request: {},
528
+ expected: {
529
+ status: 410,
530
+ error: 'CUBE_DELETED',
531
+ terminal_sse: { event: 'error', error: 'CUBE_DELETED', closes_after_event: true },
532
+ mutation: 'none',
533
+ },
534
+ },
535
+ {
536
+ name: 'former authorized credentials retain the typed terminal state after restart',
537
+ request: {},
538
+ expected: {
539
+ status: 410,
540
+ error: 'CUBE_DELETED',
541
+ durable_after_restart: true,
542
+ mutation: 'none',
543
+ },
544
+ },
545
+ ];
546
+
446
547
  export interface ResolveRepositoryCubeConformanceVector {
447
548
  name: string;
448
549
  request: ResolveRepositoryCubeRequest;