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.
- package/README.md +52 -35
- package/dist/conformance/adapter.d.ts +34 -6
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +215 -54
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/conformance/index.d.ts +49 -0
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +111 -0
- package/dist/conformance/index.js.map +1 -1
- package/dist/protocol/contract.d.ts +81 -28
- package/dist/protocol/contract.d.ts.map +1 -1
- package/dist/protocol/contract.js +213 -128
- package/dist/protocol/contract.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 -13
- package/dist/protocol/version.js.map +1 -1
- package/docs/compatibility.md +44 -25
- package/docs/enrollment.md +167 -0
- package/docs/releasing.md +77 -6
- package/package.json +2 -1
- package/src/conformance/adapter.ts +386 -63
- package/src/conformance/index.ts +139 -0
- package/src/protocol/contract.ts +373 -180
- package/src/protocol/errors.ts +7 -3
- package/src/protocol/version.ts +3 -30
|
@@ -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
|
-
|
|
11
|
+
decodeProtocolTagPreflight,
|
|
12
12
|
decodeReadLogResultEnvelope,
|
|
13
13
|
decodeSseFrames,
|
|
14
|
-
|
|
14
|
+
PROTOCOL_LIMIT_CEILINGS,
|
|
15
|
+
PROTOCOL_VERSION,
|
|
15
16
|
utf8ByteLength,
|
|
16
|
-
type
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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.
|
|
344
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
invariant(
|
|
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(
|
|
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
|
-
'
|
|
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
|
-
|
|
381
|
-
|
|
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) <=
|
|
393
|
-
utf8ByteLength(injectedMessage) <=
|
|
394
|
-
'Injection fixture exceeded
|
|
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,
|
|
798
|
+
Math.max(0, PROTOCOL_LIMIT_CEILINGS.max_request_bytes - utf8ByteLength(baseBody) + 1),
|
|
460
799
|
);
|
|
461
800
|
invariant(
|
|
462
|
-
utf8ByteLength(oversizedBody) >
|
|
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);
|