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.
- package/README.md +8 -56
- package/RELEASES.md +33 -0
- package/dist/conformance/adapter.d.ts +22 -0
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +169 -2
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/conformance/index.d.ts +22 -1
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +78 -1
- package/dist/conformance/index.js.map +1 -1
- package/dist/protocol/contract.d.ts +32 -1
- package/dist/protocol/contract.d.ts.map +1 -1
- package/dist/protocol/contract.js +212 -2
- package/dist/protocol/contract.js.map +1 -1
- package/dist/protocol/errors.d.ts +1 -0
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +1 -0
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/sse.d.ts +6 -2
- package/dist/protocol/sse.d.ts.map +1 -1
- package/dist/protocol/sse.js +8 -2
- package/dist/protocol/sse.js.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/protocol/version.js +1 -1
- package/dist/templates.d.ts +1 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +14 -3
- package/dist/templates.js.map +1 -1
- package/docs/compatibility.md +15 -4
- package/docs/enrollment.md +5 -4
- package/docs/releasing.md +4 -3
- package/package.json +2 -1
- package/src/conformance/adapter.ts +321 -0
- package/src/conformance/index.ts +102 -1
- package/src/protocol/contract.ts +260 -2
- package/src/protocol/errors.ts +1 -0
- package/src/protocol/sse.ts +17 -2
- package/src/protocol/version.ts +2 -2
- 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
|
}
|
package/src/conformance/index.ts
CHANGED
|
@@ -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
|
|
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;
|