borgmcp-shared 0.3.0 → 0.4.2
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 +46 -26
- package/SECURITY.md +6 -5
- package/dist/conformance/adapter.d.ts +62 -11
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +273 -55
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/protocol/contract.d.ts +56 -25
- package/dist/protocol/contract.d.ts.map +1 -1
- package/dist/protocol/contract.js +147 -116
- package/dist/protocol/contract.js.map +1 -1
- package/dist/protocol/coordination.d.ts +25 -0
- package/dist/protocol/coordination.d.ts.map +1 -1
- package/dist/protocol/coordination.js +44 -0
- package/dist/protocol/coordination.js.map +1 -1
- package/dist/protocol/errors.d.ts +2 -4
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +1 -1
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/version.d.ts +2 -10
- package/dist/protocol/version.d.ts.map +1 -1
- package/dist/protocol/version.js +1 -18
- package/dist/protocol/version.js.map +1 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +9 -11
- package/dist/templates.js.map +1 -1
- package/docs/compatibility.md +38 -50
- package/docs/enrollment.md +27 -17
- package/docs/releasing.md +61 -33
- package/package.json +2 -2
- package/src/conformance/adapter.ts +618 -82
- package/src/protocol/contract.ts +250 -160
- package/src/protocol/coordination.ts +82 -0
- package/src/protocol/errors.ts +7 -3
- package/src/protocol/version.ts +3 -35
- package/src/templates.ts +9 -11
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ErrorCode,
|
|
3
|
-
ProtocolContractError,
|
|
4
3
|
compareLogCursor,
|
|
5
4
|
createProtocolEnvelope,
|
|
6
5
|
decodeCreateCubeResponseEnvelope,
|
|
@@ -8,16 +7,19 @@ import {
|
|
|
8
7
|
decodeDecisionResultEnvelope,
|
|
9
8
|
decodeDecisionsResultEnvelope,
|
|
10
9
|
decodeEnrollmentExchangeResponseEnvelope,
|
|
10
|
+
decodeEvictDroneResultEnvelope,
|
|
11
|
+
decodeProtocolEnvelope,
|
|
11
12
|
decodeProtocolErrorEnvelope,
|
|
12
|
-
|
|
13
|
+
decodeProtocolTagPreflight,
|
|
13
14
|
decodeReadLogResultEnvelope,
|
|
15
|
+
decodeReassignDroneResultEnvelope,
|
|
14
16
|
decodeSseFrames,
|
|
15
|
-
|
|
17
|
+
PROTOCOL_LIMIT_CEILINGS,
|
|
18
|
+
PROTOCOL_HTTP_CONTRACT,
|
|
19
|
+
PROTOCOL_VERSION,
|
|
16
20
|
utf8ByteLength,
|
|
17
|
-
type Capability,
|
|
18
21
|
type CreateCubeResponse,
|
|
19
22
|
type LogCursor,
|
|
20
|
-
type ProtocolInfo,
|
|
21
23
|
type StreamEvent,
|
|
22
24
|
} from '../protocol/index.js';
|
|
23
25
|
import { ENROLLMENT_RETRY_CONFORMANCE } from './index.js';
|
|
@@ -35,6 +37,29 @@ export interface ConformanceCube {
|
|
|
35
37
|
readonly id: string;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
export interface ConformanceRole {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ConformanceDrone {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ConformanceCubeAccess = 'read' | 'write' | 'manage';
|
|
49
|
+
|
|
50
|
+
export interface ConformanceCubeManagementState {
|
|
51
|
+
readonly directive: string;
|
|
52
|
+
readonly taxonomy_marker: string | null;
|
|
53
|
+
readonly role_ids: readonly string[];
|
|
54
|
+
readonly active_decision_ids: readonly string[];
|
|
55
|
+
readonly drones: ReadonlyArray<{
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly role_id: string;
|
|
58
|
+
readonly evicted: boolean;
|
|
59
|
+
readonly session_revoked: boolean;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
export interface ConformanceAuthorityState {
|
|
39
64
|
enrolled_clients: number;
|
|
40
65
|
enrollment_claims: number;
|
|
@@ -57,6 +82,13 @@ export interface ConformanceCreatedCubeState {
|
|
|
57
82
|
export interface ConformanceEnrollmentPrincipalState {
|
|
58
83
|
response_client_matches: boolean;
|
|
59
84
|
active_credential_bindings: number;
|
|
85
|
+
/**
|
|
86
|
+
* The principal's currently-bound credential still equals the one it enrolled
|
|
87
|
+
* with. Since the credential-free tag preflight cannot probe credentials, this
|
|
88
|
+
* out-of-band authority check is what proves a rejected mismatch retry did not
|
|
89
|
+
* overwrite the good credential.
|
|
90
|
+
*/
|
|
91
|
+
bound_credential_matches_enrollment: boolean;
|
|
60
92
|
}
|
|
61
93
|
|
|
62
94
|
export interface ConformanceReplayBarrier {
|
|
@@ -78,7 +110,31 @@ export interface ConformanceAdmin {
|
|
|
78
110
|
reset(): Promise<void>;
|
|
79
111
|
createPrincipal(name: string): Promise<ConformancePrincipal>;
|
|
80
112
|
createCube(name: string): Promise<ConformanceCube>;
|
|
81
|
-
|
|
113
|
+
/** Grants the requested cube authority; omitted access defaults to manage. */
|
|
114
|
+
grantCube(
|
|
115
|
+
principal: ConformancePrincipal,
|
|
116
|
+
cube: ConformanceCube,
|
|
117
|
+
access?: ConformanceCubeAccess,
|
|
118
|
+
): Promise<void>;
|
|
119
|
+
createRole(cube: ConformanceCube, input: {
|
|
120
|
+
readonly roleClass: 'queen' | 'worker';
|
|
121
|
+
readonly isHumanSeat: boolean;
|
|
122
|
+
}): Promise<ConformanceRole>;
|
|
123
|
+
createDrone(
|
|
124
|
+
principal: ConformancePrincipal,
|
|
125
|
+
cube: ConformanceCube,
|
|
126
|
+
role: ConformanceRole,
|
|
127
|
+
): Promise<ConformanceDrone>;
|
|
128
|
+
issueManagedDroneSession(drone: ConformanceDrone): Promise<string>;
|
|
129
|
+
revokeManagedDroneSession(drone: ConformanceDrone): Promise<void>;
|
|
130
|
+
expireManagedDroneSession(drone: ConformanceDrone): Promise<void>;
|
|
131
|
+
inspectManagedDrone(drone: ConformanceDrone): Promise<{
|
|
132
|
+
readonly role_id: string;
|
|
133
|
+
readonly evicted: boolean;
|
|
134
|
+
readonly session_revoked: boolean;
|
|
135
|
+
}>;
|
|
136
|
+
/** Observes every cube field mutated by a represented manage-scoped operation. */
|
|
137
|
+
inspectCubeManagementState(cube: ConformanceCube): Promise<ConformanceCubeManagementState>;
|
|
82
138
|
grantCreateCubeCapability(principal: ConformancePrincipal): Promise<void>;
|
|
83
139
|
issueDroneSession(principal: ConformancePrincipal): Promise<string>;
|
|
84
140
|
issueSingleUseInvitation(principal: ConformancePrincipal, purpose: 'owner' | 'client'): Promise<string>;
|
|
@@ -123,6 +179,21 @@ export interface ConformanceOperations {
|
|
|
123
179
|
cube: ConformanceCube,
|
|
124
180
|
request: unknown,
|
|
125
181
|
): Promise<ConformanceHttpResponse>;
|
|
182
|
+
updateCube(
|
|
183
|
+
credential: string,
|
|
184
|
+
cube: ConformanceCube,
|
|
185
|
+
request: unknown,
|
|
186
|
+
): Promise<ConformanceHttpResponse>;
|
|
187
|
+
createRole(
|
|
188
|
+
credential: string,
|
|
189
|
+
cube: ConformanceCube,
|
|
190
|
+
request: unknown,
|
|
191
|
+
): Promise<ConformanceHttpResponse>;
|
|
192
|
+
patchTaxonomy(
|
|
193
|
+
credential: string,
|
|
194
|
+
cube: ConformanceCube,
|
|
195
|
+
request: unknown,
|
|
196
|
+
): Promise<ConformanceHttpResponse>;
|
|
126
197
|
recordDecision(
|
|
127
198
|
credential: string,
|
|
128
199
|
cube: ConformanceCube,
|
|
@@ -133,6 +204,22 @@ export interface ConformanceOperations {
|
|
|
133
204
|
cube: ConformanceCube,
|
|
134
205
|
request: unknown,
|
|
135
206
|
): Promise<ConformanceHttpResponse>;
|
|
207
|
+
listDrones(
|
|
208
|
+
credential: string,
|
|
209
|
+
cube: ConformanceCube,
|
|
210
|
+
): Promise<ConformanceHttpResponse>;
|
|
211
|
+
reassignDrone(
|
|
212
|
+
credential: string,
|
|
213
|
+
cube: ConformanceCube,
|
|
214
|
+
drone: ConformanceDrone,
|
|
215
|
+
request: unknown,
|
|
216
|
+
): Promise<ConformanceHttpResponse>;
|
|
217
|
+
evictDrone(
|
|
218
|
+
credential: string,
|
|
219
|
+
cube: ConformanceCube,
|
|
220
|
+
drone: ConformanceDrone,
|
|
221
|
+
request: unknown,
|
|
222
|
+
): Promise<ConformanceHttpResponse>;
|
|
136
223
|
openStream(
|
|
137
224
|
credential: string,
|
|
138
225
|
cube: ConformanceCube,
|
|
@@ -147,7 +234,8 @@ export interface ConformanceEnvironment {
|
|
|
147
234
|
|
|
148
235
|
export const ADAPTER_CONFORMANCE_FIXTURES = [
|
|
149
236
|
{ id: 'http.unauthenticated-liveness', area: 'http' },
|
|
150
|
-
{ id: 'protocol.
|
|
237
|
+
{ id: 'protocol.credential-free-preflight', area: 'protocol' },
|
|
238
|
+
{ id: 'enrollment.retry-authority', area: 'enrollment' },
|
|
151
239
|
{ id: 'security.adapter-boundary-injection', area: 'security' },
|
|
152
240
|
{ id: 'security.oversize-request', area: 'security' },
|
|
153
241
|
{ id: 'security.cross-cube-isolation', area: 'security' },
|
|
@@ -157,7 +245,12 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
|
|
|
157
245
|
{ id: 'acks.idempotent', area: 'acks' },
|
|
158
246
|
{ id: 'claims.durable-noncursor', area: 'claims' },
|
|
159
247
|
{ id: 'decisions.topic-supersession', area: 'decisions' },
|
|
160
|
-
{ id: '
|
|
248
|
+
{ id: 'security.drone-management-authorization', area: 'security' },
|
|
249
|
+
{ id: 'security.manage-access-matrix', area: 'security' },
|
|
250
|
+
{ id: 'drones.reassign-invariants', area: 'drones' },
|
|
251
|
+
{ id: 'security.cross-cube-drone-management', area: 'security' },
|
|
252
|
+
{ id: 'drones.evict-terminal-signal', area: 'drones' },
|
|
253
|
+
{ id: 'security.drone-session-rejection-causes', area: 'security' },
|
|
161
254
|
{ id: 'security.active-stream-revocation', area: 'security' },
|
|
162
255
|
] as const;
|
|
163
256
|
|
|
@@ -181,13 +274,6 @@ export interface AdapterConformanceReport {
|
|
|
181
274
|
}>;
|
|
182
275
|
}
|
|
183
276
|
|
|
184
|
-
export interface EquivalentAdapterConformanceReport {
|
|
185
|
-
ok: boolean;
|
|
186
|
-
cloud: AdapterConformanceReport;
|
|
187
|
-
local: AdapterConformanceReport;
|
|
188
|
-
equivalent: boolean;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
277
|
export interface AdapterConformanceOptions {
|
|
192
278
|
/** Maximum wait for a replay, live delivery, or revocation close. */
|
|
193
279
|
streamDeadlineMs?: number;
|
|
@@ -333,6 +419,20 @@ function same(left: unknown, right: unknown): boolean {
|
|
|
333
419
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
334
420
|
}
|
|
335
421
|
|
|
422
|
+
function listedDroneIds(response: ConformanceHttpResponse): string[] {
|
|
423
|
+
return decodeProtocolEnvelope(response.body, (payload) => {
|
|
424
|
+
invariant(typeof payload === 'object' && payload !== null, 'Roster payload is not an object.');
|
|
425
|
+
const drones = (payload as { drones?: unknown }).drones;
|
|
426
|
+
invariant(Array.isArray(drones), 'Roster payload omitted drones.');
|
|
427
|
+
return drones.map((drone) => {
|
|
428
|
+
invariant(typeof drone === 'object' && drone !== null, 'Roster contains an invalid drone.');
|
|
429
|
+
const id = (drone as { id?: unknown }).id;
|
|
430
|
+
invariant(typeof id === 'string', 'Roster drone omitted its id.');
|
|
431
|
+
return id;
|
|
432
|
+
});
|
|
433
|
+
}).payload;
|
|
434
|
+
}
|
|
435
|
+
|
|
336
436
|
function assertStateDelta(
|
|
337
437
|
before: ConformanceAuthorityState,
|
|
338
438
|
after: ConformanceAuthorityState,
|
|
@@ -410,8 +510,6 @@ export async function runAdapterConformance(
|
|
|
410
510
|
|
|
411
511
|
let credentialA = '';
|
|
412
512
|
let credentialB = '';
|
|
413
|
-
let protocolBody: unknown;
|
|
414
|
-
let protocolInfo: ProtocolInfo | null = null;
|
|
415
513
|
await record('http.unauthenticated-liveness', async () => {
|
|
416
514
|
const response = await environment.operations.health();
|
|
417
515
|
expectStatus(response, 204, 'Unauthenticated liveness');
|
|
@@ -419,9 +517,23 @@ export async function runAdapterConformance(
|
|
|
419
517
|
return { status: 204, bodyless: true };
|
|
420
518
|
});
|
|
421
519
|
|
|
422
|
-
await record('protocol.
|
|
423
|
-
|
|
520
|
+
await record('protocol.credential-free-preflight', async () => {
|
|
521
|
+
// The tag preflight is credential-free (no bearer) and mutation-free: a
|
|
522
|
+
// client verifies pinned TLS and the exact tag before it creates or sends any
|
|
523
|
+
// credential. The server must answer 200 with ONLY the exact tag.
|
|
524
|
+
const before = await environment.admin.observeAuthorityState();
|
|
525
|
+
const response = await environment.operations.protocol(null);
|
|
526
|
+
expectStatus(response, 200, 'Credential-free protocol-tag preflight');
|
|
527
|
+
const preflight = decodeProtocolTagPreflight(response.body);
|
|
528
|
+
invariant(
|
|
529
|
+
Object.keys(preflight).length === 1 && preflight.protocol_version === PROTOCOL_VERSION,
|
|
530
|
+
'Protocol-tag preflight exposed more than the exact tag.',
|
|
531
|
+
);
|
|
532
|
+
assertStateDelta(before, await environment.admin.observeAuthorityState(), {}, 'Protocol-tag preflight');
|
|
533
|
+
return { authenticated: false, mutation_free: true, protocol_version: preflight.protocol_version };
|
|
534
|
+
});
|
|
424
535
|
|
|
536
|
+
await record('enrollment.retry-authority', async () => {
|
|
425
537
|
const retryVectorErrors: string[] = [];
|
|
426
538
|
for (const [index, vector] of ENROLLMENT_RETRY_CONFORMANCE.entries()) {
|
|
427
539
|
for (const purpose of ['client', 'owner'] as const) {
|
|
@@ -474,23 +586,11 @@ export async function runAdapterConformance(
|
|
|
474
586
|
);
|
|
475
587
|
}
|
|
476
588
|
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
589
|
invariant(
|
|
491
590
|
same(await environment.admin.inspectEnrollmentPrincipal(principal, initial.client_id), {
|
|
492
591
|
response_client_matches: true,
|
|
493
592
|
active_credential_bindings: 1,
|
|
593
|
+
bound_credential_matches_enrollment: true,
|
|
494
594
|
}),
|
|
495
595
|
`${purpose} ${vector.name} changed enrollment binding ownership.`,
|
|
496
596
|
);
|
|
@@ -710,37 +810,23 @@ export async function runAdapterConformance(
|
|
|
710
810
|
ErrorCode.AUTH_INVALID,
|
|
711
811
|
'Enrollment retry mismatch',
|
|
712
812
|
);
|
|
713
|
-
const protocolResponse = await environment.operations.protocol(credentialA);
|
|
714
|
-
expectStatus(protocolResponse, 200, 'Authenticated protocol request');
|
|
715
|
-
protocolBody = protocolResponse.body;
|
|
716
|
-
protocolInfo = negotiateProtocol(decodeProtocolInfoEnvelope(protocolBody).payload, [
|
|
717
|
-
'log.cursor',
|
|
718
|
-
'stream.sse',
|
|
719
|
-
'stream.replay',
|
|
720
|
-
'acks',
|
|
721
|
-
'claims',
|
|
722
|
-
'decisions',
|
|
723
|
-
]);
|
|
724
813
|
return {
|
|
725
|
-
unauthenticated: ErrorCode.AUTH_MISSING,
|
|
726
814
|
enrollment_status: 201,
|
|
727
815
|
exact_retry_status: 201,
|
|
728
816
|
mismatched_retry: ErrorCode.AUTH_INVALID,
|
|
729
817
|
response_secret_free: true,
|
|
730
|
-
protocol_version: protocolInfo.protocol_version,
|
|
731
818
|
};
|
|
732
819
|
});
|
|
733
820
|
|
|
734
821
|
await record('security.adapter-boundary-injection', async () => {
|
|
735
|
-
invariant(protocolInfo, 'Protocol fixture did not produce request limits.');
|
|
736
822
|
const injectedMessage = "'); DROP TABLE log_entries; --\r\ndata: forged-sse-frame";
|
|
737
823
|
const injectedBody = JSON.stringify(
|
|
738
824
|
createProtocolEnvelope('inject-b1', { message: injectedMessage }),
|
|
739
825
|
);
|
|
740
826
|
invariant(
|
|
741
|
-
utf8ByteLength(injectedBody) <=
|
|
742
|
-
utf8ByteLength(injectedMessage) <=
|
|
743
|
-
'Injection fixture exceeded
|
|
827
|
+
utf8ByteLength(injectedBody) <= PROTOCOL_LIMIT_CEILINGS.max_request_bytes &&
|
|
828
|
+
utf8ByteLength(injectedMessage) <= PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes,
|
|
829
|
+
'Injection fixture exceeded the shared request-limit ceiling.',
|
|
744
830
|
);
|
|
745
831
|
const injected = await environment.operations.appendRaw(
|
|
746
832
|
credentialB,
|
|
@@ -800,15 +886,14 @@ export async function runAdapterConformance(
|
|
|
800
886
|
});
|
|
801
887
|
|
|
802
888
|
await record('security.oversize-request', async () => {
|
|
803
|
-
invariant(protocolInfo, 'Protocol fixture did not produce request limits.');
|
|
804
889
|
const baseBody = JSON.stringify(
|
|
805
890
|
createProtocolEnvelope('oversize-a1', { message: 'must-not-persist' }),
|
|
806
891
|
);
|
|
807
892
|
const oversizedBody = baseBody + ' '.repeat(
|
|
808
|
-
Math.max(0,
|
|
893
|
+
Math.max(0, PROTOCOL_LIMIT_CEILINGS.max_request_bytes - utf8ByteLength(baseBody) + 1),
|
|
809
894
|
);
|
|
810
895
|
invariant(
|
|
811
|
-
utf8ByteLength(oversizedBody) >
|
|
896
|
+
utf8ByteLength(oversizedBody) > PROTOCOL_LIMIT_CEILINGS.max_request_bytes,
|
|
812
897
|
'Oversize fixture did not exceed max_request_bytes.',
|
|
813
898
|
);
|
|
814
899
|
const response = await environment.operations.appendRaw(credentialA, cubeA, oversizedBody);
|
|
@@ -995,20 +1080,489 @@ export async function runAdapterConformance(
|
|
|
995
1080
|
return { active_count: 1, active_decision: 'second', supersedes_first: true };
|
|
996
1081
|
});
|
|
997
1082
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1083
|
+
let workerRoleA!: ConformanceRole;
|
|
1084
|
+
let workerRoleB!: ConformanceRole;
|
|
1085
|
+
let managedWorker!: ConformanceDrone;
|
|
1086
|
+
let readCredential = '';
|
|
1087
|
+
let writeCredential = '';
|
|
1088
|
+
let workerSession = '';
|
|
1089
|
+
await record('security.drone-management-authorization', async () => {
|
|
1090
|
+
workerRoleA = await environment.admin.createRole(cubeA, {
|
|
1091
|
+
roleClass: 'worker', isHumanSeat: false,
|
|
1092
|
+
});
|
|
1093
|
+
managedWorker = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
1094
|
+
const deniedCredentials: Array<{ access: 'read' | 'write'; credential: string }> = [];
|
|
1095
|
+
for (const [index, access] of ['read', 'write'].entries()) {
|
|
1096
|
+
const principalName = access === 'read' ? 'Coordinator' : 'write-principal';
|
|
1097
|
+
const principal = await environment.admin.createPrincipal(principalName);
|
|
1098
|
+
await environment.admin.grantCube(principal, cubeA, access as 'read' | 'write');
|
|
1099
|
+
const invitation = await environment.admin.issueSingleUseInvitation(principal, 'client');
|
|
1100
|
+
const credential = `${(access === 'read' ? 'R' : 'W').repeat(42)}Q`;
|
|
1101
|
+
if (access === 'read') readCredential = credential;
|
|
1102
|
+
else writeCredential = credential;
|
|
1103
|
+
const enrollment = await environment.operations.enroll(createProtocolEnvelope(
|
|
1104
|
+
`${access}-principal-enroll`,
|
|
1105
|
+
{
|
|
1106
|
+
invitation,
|
|
1107
|
+
retry_key: `00000000-0000-4000-8000-${String(301 + index).padStart(12, '0')}`,
|
|
1108
|
+
client_credential: credential,
|
|
1109
|
+
client_name: principalName,
|
|
1110
|
+
},
|
|
1111
|
+
));
|
|
1112
|
+
expectStatus(enrollment, 201, `${access} principal enrollment`);
|
|
1113
|
+
deniedCredentials.push({ access: access as 'read' | 'write', credential });
|
|
1114
|
+
}
|
|
1115
|
+
for (const { access, credential } of deniedCredentials) {
|
|
1116
|
+
expectError(
|
|
1117
|
+
await environment.operations.reassignDrone(
|
|
1118
|
+
credential,
|
|
1119
|
+
cubeA,
|
|
1120
|
+
managedWorker,
|
|
1121
|
+
createProtocolEnvelope(`${access}-reassign-denied`, { role_id: workerRoleA.id }),
|
|
1122
|
+
),
|
|
1123
|
+
403,
|
|
1124
|
+
ErrorCode.ACCESS_DENIED,
|
|
1125
|
+
`${access} principal reassignment`,
|
|
1126
|
+
);
|
|
1127
|
+
expectError(
|
|
1128
|
+
await environment.operations.evictDrone(
|
|
1129
|
+
credential,
|
|
1130
|
+
cubeA,
|
|
1131
|
+
managedWorker,
|
|
1132
|
+
createProtocolEnvelope(`${access}-evict-denied`, {}),
|
|
1133
|
+
),
|
|
1134
|
+
403,
|
|
1135
|
+
ErrorCode.ACCESS_DENIED,
|
|
1136
|
+
`${access} principal eviction`,
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
return { read_status: 403, write_status: 403, manage_required: true };
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
await record('security.manage-access-matrix', async () => {
|
|
1143
|
+
const noGrantPrincipal = await environment.admin.createPrincipal('Coordinator role without cube grant');
|
|
1144
|
+
const noGrantInvitation = await environment.admin.issueSingleUseInvitation(noGrantPrincipal, 'client');
|
|
1145
|
+
const noGrantCredential = `${'N'.repeat(42)}Q`;
|
|
1146
|
+
expectStatus(await environment.operations.enroll(createProtocolEnvelope(
|
|
1147
|
+
'no-grant-principal-enroll',
|
|
1148
|
+
{
|
|
1149
|
+
invitation: noGrantInvitation,
|
|
1150
|
+
retry_key: '00000000-0000-4000-8000-000000000303',
|
|
1151
|
+
client_credential: noGrantCredential,
|
|
1152
|
+
client_name: 'Coordinator',
|
|
1153
|
+
},
|
|
1154
|
+
)), 201, 'No-grant principal enrollment');
|
|
1155
|
+
|
|
1156
|
+
const droneCredential = await environment.admin.issueManagedDroneSession(managedWorker);
|
|
1157
|
+
const matrixTargetRole = await environment.admin.createRole(cubeA, {
|
|
1158
|
+
roleClass: 'worker', isHumanSeat: false,
|
|
1159
|
+
});
|
|
1160
|
+
const evictionTarget = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
1161
|
+
await environment.admin.issueManagedDroneSession(evictionTarget);
|
|
1162
|
+
const unknownCube = { id: '00000000-0000-4000-8000-000000000399' };
|
|
1163
|
+
const snapshot = async (): Promise<unknown> => ({
|
|
1164
|
+
cubeA: await environment.admin.inspectCubeManagementState(cubeA),
|
|
1165
|
+
cubeB: await environment.admin.inspectCubeManagementState(cubeB),
|
|
1166
|
+
});
|
|
1167
|
+
const operations: ReadonlyArray<{
|
|
1168
|
+
name: string;
|
|
1169
|
+
successStatus: number;
|
|
1170
|
+
invoke: (credential: string, cube: ConformanceCube) => Promise<ConformanceHttpResponse>;
|
|
1171
|
+
}> = [
|
|
1172
|
+
{
|
|
1173
|
+
name: 'cube-update',
|
|
1174
|
+
successStatus: 200,
|
|
1175
|
+
invoke: (credential, cube) => environment.operations.updateCube(
|
|
1176
|
+
credential,
|
|
1177
|
+
cube,
|
|
1178
|
+
createProtocolEnvelope('matrix-cube-update', { cube_directive: 'matrix-directive' }),
|
|
1179
|
+
),
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
name: 'role-create',
|
|
1183
|
+
successStatus: 201,
|
|
1184
|
+
invoke: (credential, cube) => environment.operations.createRole(
|
|
1185
|
+
credential,
|
|
1186
|
+
cube,
|
|
1187
|
+
createProtocolEnvelope('matrix-role-create', { name: 'Matrix Role' }),
|
|
1188
|
+
),
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
name: 'taxonomy-patch',
|
|
1192
|
+
successStatus: 200,
|
|
1193
|
+
invoke: (credential, cube) => environment.operations.patchTaxonomy(
|
|
1194
|
+
credential,
|
|
1195
|
+
cube,
|
|
1196
|
+
createProtocolEnvelope('matrix-taxonomy-patch', { marker: 'matrix-taxonomy' }),
|
|
1197
|
+
),
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
name: 'decision-record',
|
|
1201
|
+
successStatus: 201,
|
|
1202
|
+
invoke: (credential, cube) => environment.operations.recordDecision(
|
|
1203
|
+
credential,
|
|
1204
|
+
cube,
|
|
1205
|
+
createProtocolEnvelope('matrix-decision-record', {
|
|
1206
|
+
topic: 'matrix-authority', decision: 'manage only',
|
|
1207
|
+
}),
|
|
1208
|
+
),
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
name: 'drone-reassign',
|
|
1212
|
+
successStatus: 200,
|
|
1213
|
+
invoke: (credential, cube) => environment.operations.reassignDrone(
|
|
1214
|
+
credential,
|
|
1215
|
+
cube,
|
|
1216
|
+
managedWorker,
|
|
1217
|
+
createProtocolEnvelope('matrix-drone-reassign', { role_id: matrixTargetRole.id }),
|
|
1218
|
+
),
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
name: 'drone-evict',
|
|
1222
|
+
successStatus: 200,
|
|
1223
|
+
invoke: (credential, cube) => environment.operations.evictDrone(
|
|
1224
|
+
credential,
|
|
1225
|
+
cube,
|
|
1226
|
+
evictionTarget,
|
|
1227
|
+
createProtocolEnvelope('matrix-drone-evict', {}),
|
|
1228
|
+
),
|
|
1229
|
+
},
|
|
1230
|
+
];
|
|
1231
|
+
|
|
1232
|
+
for (const operation of operations) {
|
|
1233
|
+
for (const [kind, credential] of [
|
|
1234
|
+
['read', readCredential],
|
|
1235
|
+
['write', writeCredential],
|
|
1236
|
+
['drone-session', droneCredential],
|
|
1237
|
+
] as const) {
|
|
1238
|
+
const before = await snapshot();
|
|
1239
|
+
expectError(
|
|
1240
|
+
await operation.invoke(credential, cubeA),
|
|
1241
|
+
403,
|
|
1242
|
+
ErrorCode.ACCESS_DENIED,
|
|
1243
|
+
`${operation.name} by ${kind} principal`,
|
|
1244
|
+
);
|
|
1245
|
+
invariant(same(await snapshot(), before), `${operation.name} mutated state after ${kind} denial.`);
|
|
1246
|
+
}
|
|
1247
|
+
for (const [kind, credential, cube] of [
|
|
1248
|
+
['no-grant', noGrantCredential, cubeA],
|
|
1249
|
+
['foreign-principal', credentialB, cubeA],
|
|
1250
|
+
['foreign-cube', credentialA, cubeB],
|
|
1251
|
+
['unknown-cube', credentialA, unknownCube],
|
|
1252
|
+
] as const) {
|
|
1253
|
+
const before = await snapshot();
|
|
1254
|
+
expectError(
|
|
1255
|
+
await operation.invoke(credential, cube),
|
|
1256
|
+
404,
|
|
1257
|
+
ErrorCode.NOT_FOUND,
|
|
1258
|
+
`${operation.name} against ${kind}`,
|
|
1259
|
+
);
|
|
1260
|
+
invariant(same(await snapshot(), before), `${operation.name} mutated state after ${kind} denial.`);
|
|
1261
|
+
}
|
|
1262
|
+
const beforeSuccess = await environment.admin.inspectCubeManagementState(cubeA);
|
|
1263
|
+
const success = await operation.invoke(credentialA, cubeA);
|
|
1264
|
+
expectStatus(success, operation.successStatus, `${operation.name} by managing principal`);
|
|
1265
|
+
invariant(
|
|
1266
|
+
!same(await environment.admin.inspectCubeManagementState(cubeA), beforeSuccess),
|
|
1267
|
+
`${operation.name} managing success did not mutate its declared state.`,
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
operation_count: operations.length,
|
|
1272
|
+
manage_success: true,
|
|
1273
|
+
read_write_status: 403,
|
|
1274
|
+
drone_session_status: 403,
|
|
1275
|
+
hidden_status: 404,
|
|
1276
|
+
denied_mutations: 0,
|
|
1277
|
+
role_labels_authoritative: false,
|
|
1278
|
+
};
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
await record('drones.reassign-invariants', async () => {
|
|
1282
|
+
workerRoleB = await environment.admin.createRole(cubeA, {
|
|
1283
|
+
roleClass: 'worker', isHumanSeat: false,
|
|
1284
|
+
});
|
|
1285
|
+
const humanSourceRole = await environment.admin.createRole(cubeA, {
|
|
1286
|
+
roleClass: 'worker', isHumanSeat: true,
|
|
1287
|
+
});
|
|
1288
|
+
const queenRole = await environment.admin.createRole(cubeA, {
|
|
1289
|
+
roleClass: 'queen', isHumanSeat: false,
|
|
1290
|
+
});
|
|
1291
|
+
const occupiedHumanRole = await environment.admin.createRole(cubeA, {
|
|
1292
|
+
roleClass: 'worker', isHumanSeat: true,
|
|
1293
|
+
});
|
|
1294
|
+
const humanSource = await environment.admin.createDrone(principalA, cubeA, humanSourceRole);
|
|
1295
|
+
await environment.admin.createDrone(principalA, cubeA, occupiedHumanRole);
|
|
1296
|
+
const contender = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
1297
|
+
workerSession = await environment.admin.issueManagedDroneSession(managedWorker);
|
|
1298
|
+
|
|
1299
|
+
const reassigned = await environment.operations.reassignDrone(
|
|
1300
|
+
credentialA,
|
|
1301
|
+
cubeA,
|
|
1302
|
+
managedWorker,
|
|
1303
|
+
createProtocolEnvelope('reassign-worker', { role_id: workerRoleB.id }),
|
|
1304
|
+
);
|
|
1305
|
+
expectStatus(reassigned, 200, 'Worker reassignment');
|
|
1306
|
+
const reassignedDrone = decodeReassignDroneResultEnvelope(reassigned.body).payload.drone;
|
|
1307
|
+
invariant(
|
|
1308
|
+
reassignedDrone.id === managedWorker.id && reassignedDrone.role_id === workerRoleB.id,
|
|
1309
|
+
'Reassignment response did not identify the persisted target role.',
|
|
1310
|
+
);
|
|
1311
|
+
|
|
1312
|
+
expectError(
|
|
1313
|
+
await environment.operations.reassignDrone(
|
|
1314
|
+
workerSession,
|
|
1315
|
+
cubeA,
|
|
1316
|
+
managedWorker,
|
|
1317
|
+
createProtocolEnvelope('reassign-drone-session-denied', { role_id: workerRoleA.id }),
|
|
1318
|
+
),
|
|
1319
|
+
403,
|
|
1320
|
+
ErrorCode.ACCESS_DENIED,
|
|
1321
|
+
'Drone-session reassignment',
|
|
1322
|
+
);
|
|
1323
|
+
expectError(
|
|
1324
|
+
await environment.operations.reassignDrone(
|
|
1325
|
+
credentialA,
|
|
1326
|
+
cubeA,
|
|
1327
|
+
managedWorker,
|
|
1328
|
+
createProtocolEnvelope('reassign-queen-denied', { role_id: queenRole.id }),
|
|
1329
|
+
),
|
|
1330
|
+
403,
|
|
1331
|
+
ErrorCode.ACCESS_DENIED,
|
|
1332
|
+
'Worker-to-queen reassignment',
|
|
1333
|
+
);
|
|
1334
|
+
const promoted = await environment.operations.reassignDrone(
|
|
1335
|
+
credentialA,
|
|
1336
|
+
cubeA,
|
|
1337
|
+
humanSource,
|
|
1338
|
+
createProtocolEnvelope('reassign-queen-allowed', { role_id: queenRole.id }),
|
|
1339
|
+
);
|
|
1340
|
+
expectStatus(promoted, 200, 'Human-seat-to-queen reassignment');
|
|
1341
|
+
invariant(
|
|
1342
|
+
decodeReassignDroneResultEnvelope(promoted.body).payload.drone.role_id === queenRole.id,
|
|
1343
|
+
'Human-seat-to-queen reassignment did not persist.',
|
|
1344
|
+
);
|
|
1345
|
+
expectError(
|
|
1346
|
+
await environment.operations.reassignDrone(
|
|
1347
|
+
credentialA,
|
|
1348
|
+
cubeA,
|
|
1349
|
+
contender,
|
|
1350
|
+
createProtocolEnvelope('reassign-occupied-denied', { role_id: occupiedHumanRole.id }),
|
|
1351
|
+
),
|
|
1352
|
+
409,
|
|
1353
|
+
ErrorCode.ROLE_IN_USE,
|
|
1354
|
+
'Occupied human-seat reassignment',
|
|
1355
|
+
);
|
|
1356
|
+
return {
|
|
1357
|
+
worker_reassigned: true,
|
|
1358
|
+
drone_session_denied: true,
|
|
1359
|
+
queen_requires_human_seat: true,
|
|
1360
|
+
occupied_human_seat_denied: true,
|
|
1361
|
+
};
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
await record('security.cross-cube-drone-management', async () => {
|
|
1365
|
+
const foreignRole = await environment.admin.createRole(cubeB, {
|
|
1366
|
+
roleClass: 'worker', isHumanSeat: false,
|
|
1367
|
+
});
|
|
1368
|
+
const foreignDrone = await environment.admin.createDrone(principalB, cubeB, foreignRole);
|
|
1369
|
+
expectError(
|
|
1370
|
+
await environment.operations.reassignDrone(
|
|
1371
|
+
credentialA,
|
|
1372
|
+
cubeB,
|
|
1373
|
+
foreignDrone,
|
|
1374
|
+
createProtocolEnvelope('cross-cube-reassign', { role_id: foreignRole.id }),
|
|
1375
|
+
),
|
|
1376
|
+
404,
|
|
1377
|
+
ErrorCode.NOT_FOUND,
|
|
1378
|
+
'Cross-cube reassignment',
|
|
1379
|
+
);
|
|
1380
|
+
expectError(
|
|
1381
|
+
await environment.operations.evictDrone(
|
|
1382
|
+
credentialA,
|
|
1383
|
+
cubeB,
|
|
1384
|
+
foreignDrone,
|
|
1385
|
+
createProtocolEnvelope('cross-cube-evict', {}),
|
|
1386
|
+
),
|
|
1387
|
+
404,
|
|
1388
|
+
ErrorCode.NOT_FOUND,
|
|
1389
|
+
'Cross-cube eviction',
|
|
1390
|
+
);
|
|
1391
|
+
expectError(
|
|
1392
|
+
await environment.operations.reassignDrone(
|
|
1393
|
+
credentialA,
|
|
1394
|
+
cubeA,
|
|
1395
|
+
foreignDrone,
|
|
1396
|
+
createProtocolEnvelope('foreign-drone-local-route', { role_id: workerRoleA.id }),
|
|
1397
|
+
),
|
|
1398
|
+
404,
|
|
1399
|
+
ErrorCode.NOT_FOUND,
|
|
1400
|
+
'Foreign drone reassignment through authorized cube route',
|
|
1401
|
+
);
|
|
1402
|
+
expectError(
|
|
1403
|
+
await environment.operations.evictDrone(
|
|
1404
|
+
credentialA,
|
|
1405
|
+
cubeA,
|
|
1406
|
+
foreignDrone,
|
|
1407
|
+
createProtocolEnvelope('foreign-drone-local-evict', {}),
|
|
1408
|
+
),
|
|
1409
|
+
404,
|
|
1410
|
+
ErrorCode.NOT_FOUND,
|
|
1411
|
+
'Foreign drone eviction through authorized cube route',
|
|
1412
|
+
);
|
|
1413
|
+
expectError(
|
|
1414
|
+
await environment.operations.reassignDrone(
|
|
1415
|
+
credentialA,
|
|
1416
|
+
cubeA,
|
|
1417
|
+
managedWorker,
|
|
1418
|
+
createProtocolEnvelope('foreign-role-local-drone', { role_id: foreignRole.id }),
|
|
1419
|
+
),
|
|
1420
|
+
404,
|
|
1421
|
+
ErrorCode.NOT_FOUND,
|
|
1422
|
+
'Foreign role reassignment through authorized cube route',
|
|
1423
|
+
);
|
|
1424
|
+
await environment.admin.grantCube(principalA, cubeB, 'read');
|
|
1425
|
+
const foreignDroneBefore = await environment.admin.inspectManagedDrone(foreignDrone);
|
|
1426
|
+
expectError(
|
|
1427
|
+
await environment.operations.reassignDrone(
|
|
1428
|
+
workerSession,
|
|
1429
|
+
cubeB,
|
|
1430
|
+
foreignDrone,
|
|
1431
|
+
createProtocolEnvelope('bound-drone-cross-cube-reassign', { role_id: foreignRole.id }),
|
|
1432
|
+
),
|
|
1433
|
+
404,
|
|
1434
|
+
ErrorCode.NOT_FOUND,
|
|
1435
|
+
'Bound drone cross-cube reassignment',
|
|
1436
|
+
);
|
|
1437
|
+
invariant(
|
|
1438
|
+
same(await environment.admin.inspectManagedDrone(foreignDrone), foreignDroneBefore),
|
|
1439
|
+
'Bound drone cross-cube reassignment mutated the foreign target.',
|
|
1440
|
+
);
|
|
1441
|
+
expectError(
|
|
1442
|
+
await environment.operations.evictDrone(
|
|
1443
|
+
workerSession,
|
|
1444
|
+
cubeB,
|
|
1445
|
+
foreignDrone,
|
|
1446
|
+
createProtocolEnvelope('bound-drone-cross-cube-evict', {}),
|
|
1447
|
+
),
|
|
1448
|
+
404,
|
|
1449
|
+
ErrorCode.NOT_FOUND,
|
|
1450
|
+
'Bound drone cross-cube eviction',
|
|
1451
|
+
);
|
|
1452
|
+
invariant(
|
|
1453
|
+
same(await environment.admin.inspectManagedDrone(foreignDrone), foreignDroneBefore),
|
|
1454
|
+
'Bound drone cross-cube eviction mutated the foreign target.',
|
|
1455
|
+
);
|
|
1456
|
+
return {
|
|
1457
|
+
unauthorized_cube_status: 404,
|
|
1458
|
+
foreign_drone_status: 404,
|
|
1459
|
+
foreign_role_status: 404,
|
|
1460
|
+
bound_drone_cross_cube_status: 404,
|
|
1461
|
+
code: ErrorCode.NOT_FOUND,
|
|
1462
|
+
};
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
await record('drones.evict-terminal-signal', async () => {
|
|
1466
|
+
const evictedDrone = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
1467
|
+
const evictedCredential = await environment.admin.issueManagedDroneSession(evictedDrone);
|
|
1468
|
+
expectStatus(
|
|
1469
|
+
await environment.operations.read(
|
|
1470
|
+
evictedCredential,
|
|
1471
|
+
cubeA,
|
|
1472
|
+
createProtocolEnvelope('evict-probe-before', { cursor: null, limit: 1 }),
|
|
1473
|
+
),
|
|
1474
|
+
200,
|
|
1475
|
+
'Pre-eviction seat probe',
|
|
1476
|
+
);
|
|
1477
|
+
const before = await environment.operations.listDrones(credentialA, cubeA);
|
|
1478
|
+
expectStatus(before, 200, 'Pre-eviction roster');
|
|
1479
|
+
invariant(listedDroneIds(before).includes(evictedDrone.id), 'Active drone was absent from roster.');
|
|
1480
|
+
|
|
1481
|
+
const evicted = await environment.operations.evictDrone(
|
|
1482
|
+
credentialA,
|
|
1483
|
+
cubeA,
|
|
1484
|
+
evictedDrone,
|
|
1485
|
+
createProtocolEnvelope('evict-managed-drone', {}),
|
|
1486
|
+
);
|
|
1487
|
+
expectStatus(evicted, 200, 'Drone eviction');
|
|
1488
|
+
invariant(
|
|
1489
|
+
same(decodeEvictDroneResultEnvelope(evicted.body).payload, {
|
|
1490
|
+
drone_id: evictedDrone.id,
|
|
1491
|
+
evicted: true,
|
|
1492
|
+
}),
|
|
1493
|
+
'Eviction response did not identify the terminal seat.',
|
|
1494
|
+
);
|
|
1495
|
+
invariant(
|
|
1496
|
+
same(await environment.admin.inspectManagedDrone(evictedDrone), {
|
|
1497
|
+
role_id: workerRoleA.id,
|
|
1498
|
+
evicted: true,
|
|
1499
|
+
session_revoked: true,
|
|
1500
|
+
}),
|
|
1501
|
+
'Eviction did not atomically mark the drone evicted and revoke its session.',
|
|
1502
|
+
);
|
|
1503
|
+
const after = await environment.operations.listDrones(credentialA, cubeA);
|
|
1504
|
+
expectStatus(after, 200, 'Post-eviction roster');
|
|
1505
|
+
invariant(!listedDroneIds(after).includes(evictedDrone.id), 'Evicted drone remained in roster.');
|
|
1506
|
+
expectError(
|
|
1507
|
+
await environment.operations.append(
|
|
1508
|
+
credentialA,
|
|
1509
|
+
cubeA,
|
|
1510
|
+
createProtocolEnvelope('evict-direct-target', {
|
|
1511
|
+
message: 'must-not-fan-out',
|
|
1512
|
+
visibility: 'direct',
|
|
1513
|
+
recipientDroneIds: [evictedDrone.id],
|
|
1514
|
+
}),
|
|
1515
|
+
),
|
|
1516
|
+
404,
|
|
1517
|
+
ErrorCode.NOT_FOUND,
|
|
1518
|
+
'Evicted direct recipient',
|
|
1519
|
+
);
|
|
1520
|
+
expectError(
|
|
1521
|
+
await environment.operations.read(
|
|
1522
|
+
evictedCredential,
|
|
1523
|
+
cubeA,
|
|
1524
|
+
createProtocolEnvelope('evict-probe-after', { cursor: null, limit: 1 }),
|
|
1525
|
+
),
|
|
1526
|
+
PROTOCOL_HTTP_CONTRACT.drone_evicted_status,
|
|
1527
|
+
ErrorCode.DRONE_EVICTED,
|
|
1528
|
+
'Evicted seat probe',
|
|
1529
|
+
);
|
|
1530
|
+
return {
|
|
1531
|
+
eviction_status: 200,
|
|
1532
|
+
roster_visible: false,
|
|
1533
|
+
fanout_reachable: false,
|
|
1534
|
+
old_bearer_status: 410,
|
|
1535
|
+
old_bearer_code: ErrorCode.DRONE_EVICTED,
|
|
1536
|
+
};
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
await record('security.drone-session-rejection-causes', async () => {
|
|
1540
|
+
const revokedDrone = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
1541
|
+
const expiredDrone = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
1542
|
+
const revokedCredential = await environment.admin.issueManagedDroneSession(revokedDrone);
|
|
1543
|
+
const expiredCredential = await environment.admin.issueManagedDroneSession(expiredDrone);
|
|
1544
|
+
await environment.admin.revokeManagedDroneSession(revokedDrone);
|
|
1545
|
+
await environment.admin.expireManagedDroneSession(expiredDrone);
|
|
1546
|
+
for (const [label, credential] of [
|
|
1547
|
+
['revoked', revokedCredential],
|
|
1548
|
+
['expired', expiredCredential],
|
|
1549
|
+
] as const) {
|
|
1550
|
+
expectError(
|
|
1551
|
+
await environment.operations.read(
|
|
1552
|
+
credential,
|
|
1553
|
+
cubeA,
|
|
1554
|
+
createProtocolEnvelope(`${label}-seat-probe`, { cursor: null, limit: 1 }),
|
|
1555
|
+
),
|
|
1556
|
+
401,
|
|
1557
|
+
ErrorCode.SESSION_REVOKED,
|
|
1558
|
+
`${label} seat probe`,
|
|
1005
1559
|
);
|
|
1006
|
-
} catch (error) {
|
|
1007
|
-
if (error instanceof ProtocolContractError) code = error.code;
|
|
1008
|
-
else throw error;
|
|
1009
1560
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1561
|
+
return {
|
|
1562
|
+
revoked_status: 401,
|
|
1563
|
+
expired_status: 401,
|
|
1564
|
+
code: ErrorCode.SESSION_REVOKED,
|
|
1565
|
+
};
|
|
1012
1566
|
});
|
|
1013
1567
|
|
|
1014
1568
|
await record('security.active-stream-revocation', async () => {
|
|
@@ -1046,21 +1600,3 @@ export async function runAdapterConformance(
|
|
|
1046
1600
|
const normalizedTranscript = results.map(({ id, observations }) => ({ id, observations }));
|
|
1047
1601
|
return { ok: results.every((result) => result.ok), results, normalizedTranscript };
|
|
1048
1602
|
}
|
|
1049
|
-
|
|
1050
|
-
export async function runEquivalentAdapterConformance(
|
|
1051
|
-
cloud: ConformanceEnvironment,
|
|
1052
|
-
local: ConformanceEnvironment,
|
|
1053
|
-
options: AdapterConformanceOptions = {},
|
|
1054
|
-
): Promise<EquivalentAdapterConformanceReport> {
|
|
1055
|
-
const [cloudReport, localReport] = await Promise.all([
|
|
1056
|
-
runAdapterConformance(cloud, options),
|
|
1057
|
-
runAdapterConformance(local, options),
|
|
1058
|
-
]);
|
|
1059
|
-
const equivalent = same(cloudReport.normalizedTranscript, localReport.normalizedTranscript);
|
|
1060
|
-
return {
|
|
1061
|
-
ok: cloudReport.ok && localReport.ok && equivalent,
|
|
1062
|
-
cloud: cloudReport,
|
|
1063
|
-
local: localReport,
|
|
1064
|
-
equivalent,
|
|
1065
|
-
};
|
|
1066
|
-
}
|