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
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ import { ADAPTER_CONFORMANCE_FIXTURES } from 'borgmcp-shared/conformance';
|
|
|
41
41
|
The supported subpaths are:
|
|
42
42
|
|
|
43
43
|
- `borgmcp-shared/protocol`: wire entities, requests, responses, errors, and
|
|
44
|
-
protocol
|
|
44
|
+
the exact protocol version tag.
|
|
45
45
|
- `borgmcp-shared/domain`: pure role-section, address, and high-water-mark
|
|
46
46
|
helpers plus shared domain types.
|
|
47
47
|
- `borgmcp-shared/conformance`: test-runner-independent behavior vectors and
|
|
@@ -56,26 +56,40 @@ functions and contracts include API documentation in their TypeScript sources.
|
|
|
56
56
|
|
|
57
57
|
## Handshake
|
|
58
58
|
|
|
59
|
-
The first-slice HTTP contract has
|
|
59
|
+
The first-slice HTTP contract has four shared paths:
|
|
60
60
|
|
|
61
61
|
- `GET /healthz` is the only unauthenticated liveness probe. Success is `204`
|
|
62
62
|
with no body or identifying metadata.
|
|
63
|
-
- `POST /api/enrollment/exchange` accepts a single-use invitation
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
- `POST /api/enrollment/exchange` accepts a single-use invitation, canonical
|
|
64
|
+
retry key, and client-generated 256-bit bearer in a bounded JSON body over
|
|
65
|
+
verified TLS. It returns only stable non-secret client identity and server
|
|
66
|
+
capabilities; owner enrollment grants `create_cube` but creates no cube. An exact
|
|
67
|
+
credential-proven retry is non-mutating, and a mismatched replay fails
|
|
68
|
+
uniformly. Secrets never belong in a response, URL, query string, command-line
|
|
69
|
+
argument, diagnostic, or serializable domain entity.
|
|
70
|
+
- `GET /api/protocol` is credential-free and mutation-free. It returns ONLY the
|
|
71
|
+
exact protocol tag — no package version, limits, server identity, or other
|
|
72
|
+
fingerprint surface — so a client verifies pinned TLS and the exact tag before
|
|
73
|
+
it creates or sends any credential.
|
|
74
|
+
- `POST /api/cubes` requires an active parent client with `create_cube`. Its
|
|
75
|
+
strict, idempotent request selects a server-owned template; one atomic success
|
|
76
|
+
creates a cube, two initial roles, and the creator's cube-scoped `manage`
|
|
77
|
+
grant. Exact retries return the same non-secret identities without mutation.
|
|
78
|
+
|
|
79
|
+
`decodeProtocolTagPreflight` fails closed on any tag other than the exact
|
|
80
|
+
expected version, on any extra field, or on a non-object body — before any
|
|
81
|
+
credential is created or sent. The exact protocol tag is the sole acceptance
|
|
82
|
+
authority: there is no capability negotiation, and client and server ship and
|
|
83
|
+
update together as one clean-slate product. The attach request envelope still
|
|
84
|
+
decodes its version before any payload as defense in depth.
|
|
73
85
|
|
|
74
86
|
Every JSON coordination request and successful JSON response is carried inside
|
|
75
87
|
`ProtocolEnvelope<T>`. Failures use `ProtocolErrorEnvelope`. The only bodyless
|
|
76
88
|
exceptions are the `204` liveness and acknowledgement responses. Payload codecs
|
|
77
89
|
are exported separately so adapters can validate the envelope first and then
|
|
78
90
|
validate the operation-specific payload without accepting ambiguous fields.
|
|
91
|
+
See [docs/enrollment.md](docs/enrollment.md) for purpose-bound owner enrollment,
|
|
92
|
+
ordinary ungranted enrollment, cube creation, pending-keychain, and retry contracts.
|
|
79
93
|
|
|
80
94
|
## Conformance
|
|
81
95
|
|
|
@@ -84,7 +98,8 @@ Server and client implementations should run the vectors exported from
|
|
|
84
98
|
readonly data rather than Vitest-specific helpers, so they work with any test
|
|
85
99
|
runner and in any JavaScript runtime. Cases cover HTTP and canonical errors,
|
|
86
100
|
credential misuse, isolation and revocation, SSE framing/replay/cursor ordering,
|
|
87
|
-
|
|
101
|
+
executable enrollment authority/retry/mismatch/redaction and cube-create
|
|
102
|
+
idempotency, acks, claims, and decisions.
|
|
88
103
|
|
|
89
104
|
Implement `AdapterConformanceDriver` with raw responses from the target adapter,
|
|
90
105
|
then call `runAdapterConformance`. The runner creates and decodes envelopes,
|
|
@@ -99,32 +114,34 @@ response shapes.
|
|
|
99
114
|
|
|
100
115
|
## Compatibility
|
|
101
116
|
|
|
102
|
-
`PROTOCOL_VERSION
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
`PROTOCOL_VERSION` is the sole acceptance authority: every envelope carries the
|
|
118
|
+
protocol tag, and each decoder fails closed on any value other than the exact
|
|
119
|
+
expected tag. There is no capability negotiation, supported-version list,
|
|
120
|
+
compatibility matrix, or version-range fallback. The client and server are one
|
|
121
|
+
clean-slate product — a wire change increments the tag and both adopt it
|
|
122
|
+
together in a coordinated release, with no mixed-version window.
|
|
108
123
|
|
|
109
|
-
See [docs/compatibility.md](docs/compatibility.md) for the
|
|
110
|
-
the
|
|
124
|
+
See [docs/compatibility.md](docs/compatibility.md) for the exact-tag policy and
|
|
125
|
+
the coordinated rollout order for introducing protocol changes.
|
|
111
126
|
|
|
112
127
|
## Distribution
|
|
113
128
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
`borgmcp-shared@0.3.0` is the published v1 baseline on the registry (its latest
|
|
130
|
+
release) and is immutable. This source now identifies the still-unpublished
|
|
131
|
+
`0.4.0` candidate for the breaking clean-slate v2 contract — its package
|
|
132
|
+
manifest, lockfile, exported identity, and the SBOM / packed-artifact verifiers
|
|
133
|
+
all read `0.4.0`. This reviewed version bump grants no tag or publish authority
|
|
134
|
+
and must not republish `0.3.0`; creating the annotated `v0.4.0` tag and the
|
|
135
|
+
registry publication remain separate, independently gated steps. After that
|
|
136
|
+
separately authorized registry release, consumers adopting this contract pin
|
|
137
|
+
`borgmcp-shared@^0.4.0` and commit registry lockfiles. No registry token belongs
|
|
138
|
+
in this repository, package metadata, lockfiles, or a committed `.npmrc`;
|
|
139
|
+
publishing uses protected external credentials and provenance.
|
|
140
|
+
|
|
141
|
+
The first client and server releases must consume the reviewed registry release,
|
|
142
|
+
not a Git, tag, or local-path dependency. Public release requires the
|
|
143
|
+
packed-artifact sensitivity audit, ownership and Trusted Publisher checks,
|
|
144
|
+
provenance verification, and explicit release approval.
|
|
128
145
|
|
|
129
146
|
## Security Posture
|
|
130
147
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type LogCursor } from '../protocol/index.js';
|
|
1
|
+
import { type CreateCubeResponse, type LogCursor } from '../protocol/index.js';
|
|
2
2
|
export interface ConformanceHttpResponse {
|
|
3
3
|
status: number;
|
|
4
4
|
body: unknown;
|
|
@@ -9,6 +9,28 @@ export interface ConformancePrincipal {
|
|
|
9
9
|
export interface ConformanceCube {
|
|
10
10
|
readonly id: string;
|
|
11
11
|
}
|
|
12
|
+
export interface ConformanceAuthorityState {
|
|
13
|
+
enrolled_clients: number;
|
|
14
|
+
enrollment_claims: number;
|
|
15
|
+
cubes: number;
|
|
16
|
+
roles: number;
|
|
17
|
+
grants: number;
|
|
18
|
+
server_capabilities: number;
|
|
19
|
+
cube_create_bindings: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ConformanceCreatedCubeState {
|
|
22
|
+
cube_exists: boolean;
|
|
23
|
+
creator_has_grant: boolean;
|
|
24
|
+
grant_count: number;
|
|
25
|
+
role_count: number;
|
|
26
|
+
human_seat_role_matches: boolean;
|
|
27
|
+
default_worker_role_matches: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface ConformanceEnrollmentPrincipalState {
|
|
30
|
+
response_client_matches: boolean;
|
|
31
|
+
active_credential_bindings: number;
|
|
32
|
+
bound_credential_matches_enrollment: boolean;
|
|
33
|
+
}
|
|
12
34
|
export interface ConformanceReplayBarrier {
|
|
13
35
|
readonly reached: Promise<void>;
|
|
14
36
|
release(): void;
|
|
@@ -21,7 +43,12 @@ export interface ConformanceAdmin {
|
|
|
21
43
|
createPrincipal(name: string): Promise<ConformancePrincipal>;
|
|
22
44
|
createCube(name: string): Promise<ConformanceCube>;
|
|
23
45
|
grantCube(principal: ConformancePrincipal, cube: ConformanceCube): Promise<void>;
|
|
24
|
-
|
|
46
|
+
grantCreateCubeCapability(principal: ConformancePrincipal): Promise<void>;
|
|
47
|
+
issueDroneSession(principal: ConformancePrincipal): Promise<string>;
|
|
48
|
+
issueSingleUseInvitation(principal: ConformancePrincipal, purpose: 'owner' | 'client'): Promise<string>;
|
|
49
|
+
observeAuthorityState(): Promise<ConformanceAuthorityState>;
|
|
50
|
+
inspectCreatedCube(creator: ConformancePrincipal, response: CreateCubeResponse): Promise<ConformanceCreatedCubeState>;
|
|
51
|
+
inspectEnrollmentPrincipal(principal: ConformancePrincipal, responseClientId: string): Promise<ConformanceEnrollmentPrincipalState>;
|
|
25
52
|
revokePrincipal(principal: ConformancePrincipal): Promise<void>;
|
|
26
53
|
expireCursor(cube: ConformanceCube, cursor: LogCursor): Promise<void>;
|
|
27
54
|
armReplayTransition(): ConformanceReplayBarrier;
|
|
@@ -30,6 +57,7 @@ export interface ConformanceOperations {
|
|
|
30
57
|
health(): Promise<ConformanceHttpResponse>;
|
|
31
58
|
protocol(credential: string | null): Promise<ConformanceHttpResponse>;
|
|
32
59
|
enroll(request: unknown): Promise<ConformanceHttpResponse>;
|
|
60
|
+
createCube(credential: string | null, request: unknown): Promise<ConformanceHttpResponse>;
|
|
33
61
|
append(credential: string, cube: ConformanceCube, request: unknown): Promise<ConformanceHttpResponse>;
|
|
34
62
|
appendRaw(credential: string, cube: ConformanceCube, body: string): Promise<ConformanceHttpResponse>;
|
|
35
63
|
read(credential: string, cube: ConformanceCube, request: unknown): Promise<ConformanceHttpResponse>;
|
|
@@ -46,8 +74,11 @@ export declare const ADAPTER_CONFORMANCE_FIXTURES: readonly [{
|
|
|
46
74
|
readonly id: "http.unauthenticated-liveness";
|
|
47
75
|
readonly area: "http";
|
|
48
76
|
}, {
|
|
49
|
-
readonly id: "protocol.
|
|
77
|
+
readonly id: "protocol.credential-free-preflight";
|
|
50
78
|
readonly area: "protocol";
|
|
79
|
+
}, {
|
|
80
|
+
readonly id: "enrollment.retry-authority";
|
|
81
|
+
readonly area: "enrollment";
|
|
51
82
|
}, {
|
|
52
83
|
readonly id: "security.adapter-boundary-injection";
|
|
53
84
|
readonly area: "security";
|
|
@@ -75,9 +106,6 @@ export declare const ADAPTER_CONFORMANCE_FIXTURES: readonly [{
|
|
|
75
106
|
}, {
|
|
76
107
|
readonly id: "decisions.topic-supersession";
|
|
77
108
|
readonly area: "decisions";
|
|
78
|
-
}, {
|
|
79
|
-
readonly id: "capabilities.unsupported-fails-closed";
|
|
80
|
-
readonly area: "capabilities";
|
|
81
109
|
}, {
|
|
82
110
|
readonly id: "security.active-stream-revocation";
|
|
83
111
|
readonly area: "security";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/conformance/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAgBL,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/conformance/adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ErrorCode,
|
|
1
|
+
import { ErrorCode, compareLogCursor, createProtocolEnvelope, decodeCreateCubeResponseEnvelope, decodeAppendLogResultEnvelope, decodeDecisionResultEnvelope, decodeDecisionsResultEnvelope, decodeEnrollmentExchangeResponseEnvelope, decodeProtocolErrorEnvelope, decodeProtocolTagPreflight, decodeReadLogResultEnvelope, decodeSseFrames, PROTOCOL_LIMIT_CEILINGS, PROTOCOL_VERSION, utf8ByteLength, } from '../protocol/index.js';
|
|
2
|
+
import { ENROLLMENT_RETRY_CONFORMANCE } from './index.js';
|
|
2
3
|
export const ADAPTER_CONFORMANCE_FIXTURES = [
|
|
3
4
|
{ id: 'http.unauthenticated-liveness', area: 'http' },
|
|
4
|
-
{ id: 'protocol.
|
|
5
|
+
{ id: 'protocol.credential-free-preflight', area: 'protocol' },
|
|
6
|
+
{ id: 'enrollment.retry-authority', area: 'enrollment' },
|
|
5
7
|
{ id: 'security.adapter-boundary-injection', area: 'security' },
|
|
6
8
|
{ id: 'security.oversize-request', area: 'security' },
|
|
7
9
|
{ id: 'security.cross-cube-isolation', area: 'security' },
|
|
@@ -11,7 +13,6 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
|
|
|
11
13
|
{ id: 'acks.idempotent', area: 'acks' },
|
|
12
14
|
{ id: 'claims.durable-noncursor', area: 'claims' },
|
|
13
15
|
{ id: 'decisions.topic-supersession', area: 'decisions' },
|
|
14
|
-
{ id: 'capabilities.unsupported-fails-closed', area: 'capabilities' },
|
|
15
16
|
{ id: 'security.active-stream-revocation', area: 'security' },
|
|
16
17
|
];
|
|
17
18
|
const DEFAULT_STREAM_DEADLINE_MS = 5_000;
|
|
@@ -116,6 +117,29 @@ function logEvent(event, description) {
|
|
|
116
117
|
function same(left, right) {
|
|
117
118
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
118
119
|
}
|
|
120
|
+
function assertStateDelta(before, after, expected, description) {
|
|
121
|
+
for (const key of Object.keys(before)) {
|
|
122
|
+
invariant(after[key] - before[key] === (expected[key] ?? 0), `${description} changed ${key} by ${after[key] - before[key]}; expected ${expected[key] ?? 0}.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function assertEnrollmentErrorIsSecretFree(response, requests, description) {
|
|
126
|
+
const diagnostic = JSON.stringify(response.body);
|
|
127
|
+
const secrets = new Set(requests.flatMap((request) => [
|
|
128
|
+
request.invitation,
|
|
129
|
+
request.retry_key,
|
|
130
|
+
request.client_credential,
|
|
131
|
+
]));
|
|
132
|
+
for (const secret of secrets) {
|
|
133
|
+
invariant(!diagnostic.includes(secret), `${description} exposed enrollment retry material.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function expectSecretFreeError(response, status, code, operation, secrets) {
|
|
137
|
+
expectError(response, status, code, operation);
|
|
138
|
+
const diagnostic = JSON.stringify(response.body);
|
|
139
|
+
for (const secret of new Set(secrets)) {
|
|
140
|
+
invariant(!diagnostic.includes(secret), `${operation} exposed retry material.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
119
143
|
export async function runAdapterConformance(environment, options = {}) {
|
|
120
144
|
const streamDeadlineMs = options.streamDeadlineMs ?? DEFAULT_STREAM_DEADLINE_MS;
|
|
121
145
|
const pendingProbeMs = options.pendingProbeMs ?? DEFAULT_PENDING_PROBE_MS;
|
|
@@ -136,66 +160,219 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
136
160
|
}
|
|
137
161
|
};
|
|
138
162
|
await environment.admin.reset();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
await environment.admin.grantCube(principalA, cubeA);
|
|
144
|
-
await environment.admin.grantCube(principalB, cubeB);
|
|
145
|
-
const invitationA = await environment.admin.issueSingleUseInvitation(principalA);
|
|
146
|
-
const invitationB = await environment.admin.issueSingleUseInvitation(principalB);
|
|
163
|
+
let principalA;
|
|
164
|
+
let principalB;
|
|
165
|
+
let cubeA;
|
|
166
|
+
let cubeB;
|
|
147
167
|
let credentialA = '';
|
|
148
168
|
let credentialB = '';
|
|
149
|
-
let protocolBody;
|
|
150
|
-
let protocolInfo = null;
|
|
151
169
|
await record('http.unauthenticated-liveness', async () => {
|
|
152
170
|
const response = await environment.operations.health();
|
|
153
171
|
expectStatus(response, 204, 'Unauthenticated liveness');
|
|
154
172
|
invariant(response.body === '' || response.body === undefined, 'Unauthenticated liveness exposed a response body.');
|
|
155
173
|
return { status: 204, bodyless: true };
|
|
156
174
|
});
|
|
157
|
-
await record('protocol.
|
|
158
|
-
|
|
175
|
+
await record('protocol.credential-free-preflight', async () => {
|
|
176
|
+
const before = await environment.admin.observeAuthorityState();
|
|
177
|
+
const response = await environment.operations.protocol(null);
|
|
178
|
+
expectStatus(response, 200, 'Credential-free protocol-tag preflight');
|
|
179
|
+
const preflight = decodeProtocolTagPreflight(response.body);
|
|
180
|
+
invariant(Object.keys(preflight).length === 1 && preflight.protocol_version === PROTOCOL_VERSION, 'Protocol-tag preflight exposed more than the exact tag.');
|
|
181
|
+
assertStateDelta(before, await environment.admin.observeAuthorityState(), {}, 'Protocol-tag preflight');
|
|
182
|
+
return { authenticated: false, mutation_free: true, protocol_version: preflight.protocol_version };
|
|
183
|
+
});
|
|
184
|
+
await record('enrollment.retry-authority', async () => {
|
|
185
|
+
const retryVectorErrors = [];
|
|
186
|
+
for (const [index, vector] of ENROLLMENT_RETRY_CONFORMANCE.entries()) {
|
|
187
|
+
for (const purpose of ['client', 'owner']) {
|
|
188
|
+
await environment.admin.reset();
|
|
189
|
+
try {
|
|
190
|
+
const principal = await environment.admin.createPrincipal(`${purpose}-retry-vector-${index}`);
|
|
191
|
+
const invitation = await environment.admin.issueSingleUseInvitation(principal, purpose);
|
|
192
|
+
const initialPayload = { ...vector.initial, invitation };
|
|
193
|
+
const retryPayload = { ...vector.retry, invitation };
|
|
194
|
+
const beforeInitial = await environment.admin.observeAuthorityState();
|
|
195
|
+
const initialResponse = await environment.operations.enroll(createProtocolEnvelope(`retry-${index}-initial`, initialPayload));
|
|
196
|
+
expectStatus(initialResponse, 201, `${vector.name} initial request`);
|
|
197
|
+
const initial = decodeEnrollmentExchangeResponseEnvelope(initialResponse.body).payload;
|
|
198
|
+
invariant(initial.purpose === purpose, `${purpose} ${vector.name} returned the wrong purpose.`);
|
|
199
|
+
invariant(same(initial.server_capabilities, purpose === 'owner' ? ['create_cube'] : []), `${purpose} ${vector.name} returned incorrect server authority.`);
|
|
200
|
+
const afterInitial = await environment.admin.observeAuthorityState();
|
|
201
|
+
assertStateDelta(beforeInitial, afterInitial, {
|
|
202
|
+
enrolled_clients: 1,
|
|
203
|
+
enrollment_claims: 1,
|
|
204
|
+
server_capabilities: purpose === 'owner' ? 1 : 0,
|
|
205
|
+
}, `${purpose} ${vector.name} initial request`);
|
|
206
|
+
const beforeRetry = await environment.admin.observeAuthorityState();
|
|
207
|
+
const retryResponse = await environment.operations.enroll(createProtocolEnvelope(`retry-${index}-retry`, retryPayload));
|
|
208
|
+
if (vector.expected.outcome === 'stable_non_secret_identity') {
|
|
209
|
+
expectStatus(retryResponse, 201, `${purpose} ${vector.name}`);
|
|
210
|
+
const retry = decodeEnrollmentExchangeResponseEnvelope(retryResponse.body).payload;
|
|
211
|
+
invariant(same(initial, retry), `${purpose} ${vector.name} returned different identities.`);
|
|
212
|
+
for (const field of vector.expected.forbidden_response_fields) {
|
|
213
|
+
invariant(!(field in retry), `${purpose} ${vector.name} returned forbidden field ${field}.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
expectError(retryResponse, vector.expected.status, ErrorCode.AUTH_INVALID, `${purpose} ${vector.name}`);
|
|
218
|
+
assertEnrollmentErrorIsSecretFree(retryResponse, [initialPayload, retryPayload], `${purpose} ${vector.name}`);
|
|
219
|
+
}
|
|
220
|
+
assertStateDelta(beforeRetry, await environment.admin.observeAuthorityState(), {}, `${purpose} ${vector.name} retry`);
|
|
221
|
+
invariant(same(await environment.admin.inspectEnrollmentPrincipal(principal, initial.client_id), {
|
|
222
|
+
response_client_matches: true,
|
|
223
|
+
active_credential_bindings: 1,
|
|
224
|
+
bound_credential_matches_enrollment: true,
|
|
225
|
+
}), `${purpose} ${vector.name} changed enrollment binding ownership.`);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
retryVectorErrors.push(`${purpose} ${vector.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
invariant(retryVectorErrors.length === 0, retryVectorErrors.join(' | '));
|
|
233
|
+
await environment.admin.reset();
|
|
234
|
+
const ownerPrincipal = await environment.admin.createPrincipal('owner');
|
|
235
|
+
const ordinaryPrincipal = await environment.admin.createPrincipal('ordinary');
|
|
236
|
+
const ownerInvitation = await environment.admin.issueSingleUseInvitation(ownerPrincipal, 'owner');
|
|
237
|
+
const ordinaryInvitation = await environment.admin.issueSingleUseInvitation(ordinaryPrincipal, 'client');
|
|
238
|
+
const ownerCredential = `${'Q'.repeat(42)}U`;
|
|
239
|
+
const ordinaryCredential = `${'Y'.repeat(42)}U`;
|
|
240
|
+
const beforeAuthorityEnrollment = await environment.admin.observeAuthorityState();
|
|
241
|
+
const ownerResponse = await environment.operations.enroll(createProtocolEnvelope('owner-enroll', {
|
|
242
|
+
invitation: ownerInvitation,
|
|
243
|
+
retry_key: '00000000-0000-4000-8000-000000000211',
|
|
244
|
+
client_credential: ownerCredential,
|
|
245
|
+
client_name: 'owner-client',
|
|
246
|
+
}));
|
|
247
|
+
const ordinaryResponse = await environment.operations.enroll(createProtocolEnvelope('ordinary-enroll', {
|
|
248
|
+
invitation: ordinaryInvitation,
|
|
249
|
+
retry_key: '00000000-0000-4000-8000-000000000212',
|
|
250
|
+
client_credential: ordinaryCredential,
|
|
251
|
+
client_name: 'ordinary-client',
|
|
252
|
+
}));
|
|
253
|
+
expectStatus(ownerResponse, 201, 'Owner enrollment');
|
|
254
|
+
expectStatus(ordinaryResponse, 201, 'Ordinary enrollment');
|
|
255
|
+
const owner = decodeEnrollmentExchangeResponseEnvelope(ownerResponse.body).payload;
|
|
256
|
+
const ordinary = decodeEnrollmentExchangeResponseEnvelope(ordinaryResponse.body).payload;
|
|
257
|
+
invariant(owner.purpose === 'owner' && same(owner.server_capabilities, ['create_cube']), 'Owner enrollment lacked exact create-cube authority.');
|
|
258
|
+
invariant(ordinary.purpose === 'client' && ordinary.server_capabilities.length === 0, 'Ordinary enrollment gained authority.');
|
|
259
|
+
assertStateDelta(beforeAuthorityEnrollment, await environment.admin.observeAuthorityState(), { enrolled_clients: 2, enrollment_claims: 2, server_capabilities: 1 }, 'Owner and ordinary enrollment');
|
|
260
|
+
const cubeRequest = {
|
|
261
|
+
retry_key: '00000000-0000-4000-8000-000000000213',
|
|
262
|
+
name: 'repository-one',
|
|
263
|
+
template: 'default',
|
|
264
|
+
};
|
|
265
|
+
const droneCredential = await environment.admin.issueDroneSession(ownerPrincipal);
|
|
266
|
+
const beforeDeniedCreate = await environment.admin.observeAuthorityState();
|
|
267
|
+
expectSecretFreeError(await environment.operations.createCube(null, createProtocolEnvelope('cube-missing-auth', cubeRequest)), 401, ErrorCode.AUTH_MISSING, 'Missing-auth cube create', [cubeRequest.retry_key]);
|
|
268
|
+
expectSecretFreeError(await environment.operations.createCube('invalid-credential', createProtocolEnvelope('cube-invalid-auth', cubeRequest)), 401, ErrorCode.AUTH_INVALID, 'Invalid-auth cube create', [cubeRequest.retry_key]);
|
|
269
|
+
expectSecretFreeError(await environment.operations.createCube(ordinaryCredential, createProtocolEnvelope('cube-denied', cubeRequest)), 403, ErrorCode.ACCESS_DENIED, 'Ordinary cube create', [cubeRequest.retry_key]);
|
|
270
|
+
expectSecretFreeError(await environment.operations.createCube(droneCredential, createProtocolEnvelope('cube-drone-denied', cubeRequest)), 403, ErrorCode.ACCESS_DENIED, 'Drone-session cube create', [cubeRequest.retry_key]);
|
|
271
|
+
assertStateDelta(beforeDeniedCreate, await environment.admin.observeAuthorityState(), {}, 'Denied ordinary cube create');
|
|
272
|
+
const beforeCreate = await environment.admin.observeAuthorityState();
|
|
273
|
+
const createdResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-create', cubeRequest));
|
|
274
|
+
expectStatus(createdResponse, 201, 'Owner cube create');
|
|
275
|
+
const created = decodeCreateCubeResponseEnvelope(createdResponse.body).payload;
|
|
276
|
+
assertStateDelta(beforeCreate, await environment.admin.observeAuthorityState(), { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1 }, 'Owner cube create');
|
|
277
|
+
invariant(same(await environment.admin.inspectCreatedCube(ownerPrincipal, created), {
|
|
278
|
+
cube_exists: true,
|
|
279
|
+
creator_has_grant: true,
|
|
280
|
+
grant_count: 1,
|
|
281
|
+
role_count: 2,
|
|
282
|
+
human_seat_role_matches: true,
|
|
283
|
+
default_worker_role_matches: true,
|
|
284
|
+
}), 'Created cube identities or creator grant did not match persisted authority state.');
|
|
285
|
+
const beforeCreateRetry = await environment.admin.observeAuthorityState();
|
|
286
|
+
const retriedCreateResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-retry', cubeRequest));
|
|
287
|
+
expectStatus(retriedCreateResponse, 201, 'Exact cube-create retry');
|
|
288
|
+
invariant(same(decodeCreateCubeResponseEnvelope(retriedCreateResponse.body).payload, created), 'Exact cube-create retry returned different identities.');
|
|
289
|
+
assertStateDelta(beforeCreateRetry, await environment.admin.observeAuthorityState(), {}, 'Exact cube-create retry');
|
|
290
|
+
const beforeCreateMismatch = await environment.admin.observeAuthorityState();
|
|
291
|
+
expectSecretFreeError(await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-mismatch', { ...cubeRequest, name: 'repository-two' })), 409, ErrorCode.INVALID_INPUT, 'Cube-create retry mismatch', [cubeRequest.retry_key]);
|
|
292
|
+
assertStateDelta(beforeCreateMismatch, await environment.admin.observeAuthorityState(), {}, 'Cube-create retry mismatch');
|
|
293
|
+
await environment.admin.grantCreateCubeCapability(ordinaryPrincipal);
|
|
294
|
+
const crossClientRequest = { ...cubeRequest, name: 'ordinary-repository' };
|
|
295
|
+
const beforeCrossClientCreate = await environment.admin.observeAuthorityState();
|
|
296
|
+
const crossClientResponse = await environment.operations.createCube(ordinaryCredential, createProtocolEnvelope('cube-cross-client', crossClientRequest));
|
|
297
|
+
expectStatus(crossClientResponse, 201, 'Cross-client cube create with reused retry key');
|
|
298
|
+
const crossClientCreated = decodeCreateCubeResponseEnvelope(crossClientResponse.body).payload;
|
|
299
|
+
invariant(crossClientCreated.cube_id !== created.cube_id, 'Cross-client retry key reused another client\'s cube.');
|
|
300
|
+
assertStateDelta(beforeCrossClientCreate, await environment.admin.observeAuthorityState(), { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1 }, 'Cross-client cube create');
|
|
301
|
+
invariant((await environment.admin.inspectCreatedCube(ordinaryPrincipal, crossClientCreated)).creator_has_grant, 'Cross-client cube creation did not grant its authenticated creator.');
|
|
302
|
+
const beforeCrossClientRetry = await environment.admin.observeAuthorityState();
|
|
303
|
+
const crossClientRetry = await environment.operations.createCube(ordinaryCredential, createProtocolEnvelope('cube-cross-client-retry', crossClientRequest));
|
|
304
|
+
expectStatus(crossClientRetry, 201, 'Exact cross-client cube-create retry');
|
|
305
|
+
invariant(same(decodeCreateCubeResponseEnvelope(crossClientRetry.body).payload, crossClientCreated), 'Exact cross-client cube-create retry returned different identities.');
|
|
306
|
+
assertStateDelta(beforeCrossClientRetry, await environment.admin.observeAuthorityState(), {}, 'Exact cross-client cube-create retry');
|
|
307
|
+
const beforeSecondCreate = await environment.admin.observeAuthorityState();
|
|
308
|
+
const secondCreatedResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-create-second', {
|
|
309
|
+
...cubeRequest,
|
|
310
|
+
retry_key: '00000000-0000-4000-8000-000000000214',
|
|
311
|
+
name: 'repository-two',
|
|
312
|
+
}));
|
|
313
|
+
expectStatus(secondCreatedResponse, 201, 'Second cube create');
|
|
314
|
+
const secondCreated = decodeCreateCubeResponseEnvelope(secondCreatedResponse.body).payload;
|
|
315
|
+
invariant(secondCreated.cube_id !== created.cube_id, 'Fresh cube-create retry key reused an existing cube.');
|
|
316
|
+
assertStateDelta(beforeSecondCreate, await environment.admin.observeAuthorityState(), { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1 }, 'Second cube create');
|
|
317
|
+
await environment.admin.revokePrincipal(ownerPrincipal);
|
|
318
|
+
const beforeRevokedCreate = await environment.admin.observeAuthorityState();
|
|
319
|
+
expectSecretFreeError(await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-revoked', {
|
|
320
|
+
...cubeRequest,
|
|
321
|
+
retry_key: '00000000-0000-4000-8000-000000000215',
|
|
322
|
+
})), 401, ErrorCode.SESSION_REVOKED, 'Revoked owner cube create', ['00000000-0000-4000-8000-000000000215']);
|
|
323
|
+
assertStateDelta(beforeRevokedCreate, await environment.admin.observeAuthorityState(), {}, 'Revoked owner cube create');
|
|
324
|
+
await environment.admin.reset();
|
|
325
|
+
principalA = await environment.admin.createPrincipal('principal-a');
|
|
326
|
+
principalB = await environment.admin.createPrincipal('principal-b');
|
|
327
|
+
cubeA = await environment.admin.createCube('cube-a');
|
|
328
|
+
cubeB = await environment.admin.createCube('cube-b');
|
|
329
|
+
await environment.admin.grantCube(principalA, cubeA);
|
|
330
|
+
await environment.admin.grantCube(principalB, cubeB);
|
|
331
|
+
const invitationA = await environment.admin.issueSingleUseInvitation(principalA, 'client');
|
|
332
|
+
const invitationB = await environment.admin.issueSingleUseInvitation(principalB, 'client');
|
|
333
|
+
credentialA = 'A'.repeat(43);
|
|
334
|
+
credentialB = 'E'.repeat(43);
|
|
159
335
|
const enrollmentARequest = createProtocolEnvelope('enroll-a1', {
|
|
160
336
|
invitation: invitationA,
|
|
337
|
+
retry_key: '00000000-0000-4000-8000-000000000201',
|
|
338
|
+
client_credential: credentialA,
|
|
161
339
|
client_name: 'conformance-a',
|
|
162
340
|
});
|
|
163
341
|
const enrollmentBRequest = createProtocolEnvelope('enroll-b1', {
|
|
164
342
|
invitation: invitationB,
|
|
343
|
+
retry_key: '00000000-0000-4000-8000-000000000202',
|
|
344
|
+
client_credential: credentialB,
|
|
165
345
|
client_name: 'conformance-b',
|
|
166
346
|
});
|
|
167
347
|
const enrolledAResponse = await environment.operations.enroll(enrollmentARequest);
|
|
168
348
|
const enrolledBResponse = await environment.operations.enroll(enrollmentBRequest);
|
|
169
349
|
expectStatus(enrolledAResponse, 201, 'Principal A enrollment');
|
|
170
350
|
expectStatus(enrolledBResponse, 201, 'Principal B enrollment');
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
invariant(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
'
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
'
|
|
183
|
-
|
|
184
|
-
'decisions',
|
|
185
|
-
]);
|
|
351
|
+
const enrolledA = decodeEnrollmentExchangeResponseEnvelope(enrolledAResponse.body).payload;
|
|
352
|
+
const enrolledB = decodeEnrollmentExchangeResponseEnvelope(enrolledBResponse.body).payload;
|
|
353
|
+
invariant(enrolledA.purpose === 'client' && enrolledB.purpose === 'client', 'Ordinary enrollment returned owner authority.');
|
|
354
|
+
invariant(enrolledA.server_capabilities.length === 0 && enrolledB.server_capabilities.length === 0, 'Ordinary enrollment returned a server capability.');
|
|
355
|
+
invariant(!('credential' in enrolledA) && !('credential' in enrolledB), 'Enrollment response returned a bearer.');
|
|
356
|
+
const retriedAResponse = await environment.operations.enroll(enrollmentARequest);
|
|
357
|
+
expectStatus(retriedAResponse, 201, 'Exact enrollment retry');
|
|
358
|
+
invariant(JSON.stringify(decodeEnrollmentExchangeResponseEnvelope(retriedAResponse.body).payload) ===
|
|
359
|
+
JSON.stringify(enrolledA), 'Exact enrollment retry returned different identities.');
|
|
360
|
+
expectError(await environment.operations.enroll(createProtocolEnvelope('enroll-a-mismatch', {
|
|
361
|
+
...enrollmentARequest.payload,
|
|
362
|
+
retry_key: '00000000-0000-4000-8000-000000000203',
|
|
363
|
+
})), 401, ErrorCode.AUTH_INVALID, 'Enrollment retry mismatch');
|
|
186
364
|
return {
|
|
187
|
-
unauthenticated: ErrorCode.AUTH_MISSING,
|
|
188
365
|
enrollment_status: 201,
|
|
189
|
-
|
|
190
|
-
|
|
366
|
+
exact_retry_status: 201,
|
|
367
|
+
mismatched_retry: ErrorCode.AUTH_INVALID,
|
|
368
|
+
response_secret_free: true,
|
|
191
369
|
};
|
|
192
370
|
});
|
|
193
371
|
await record('security.adapter-boundary-injection', async () => {
|
|
194
|
-
invariant(protocolInfo, 'Protocol fixture did not produce request limits.');
|
|
195
372
|
const injectedMessage = "'); DROP TABLE log_entries; --\r\ndata: forged-sse-frame";
|
|
196
373
|
const injectedBody = JSON.stringify(createProtocolEnvelope('inject-b1', { message: injectedMessage }));
|
|
197
|
-
invariant(utf8ByteLength(injectedBody) <=
|
|
198
|
-
utf8ByteLength(injectedMessage) <=
|
|
374
|
+
invariant(utf8ByteLength(injectedBody) <= PROTOCOL_LIMIT_CEILINGS.max_request_bytes &&
|
|
375
|
+
utf8ByteLength(injectedMessage) <= PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes, 'Injection fixture exceeded the shared request-limit ceiling.');
|
|
199
376
|
const injected = await environment.operations.appendRaw(credentialB, cubeB, injectedBody);
|
|
200
377
|
expectStatus(injected, 201, 'Adapter-boundary injection append');
|
|
201
378
|
const injectedEntry = decodeAppendLogResultEnvelope(injected.body).payload.entry;
|
|
@@ -229,10 +406,9 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
229
406
|
};
|
|
230
407
|
});
|
|
231
408
|
await record('security.oversize-request', async () => {
|
|
232
|
-
invariant(protocolInfo, 'Protocol fixture did not produce request limits.');
|
|
233
409
|
const baseBody = JSON.stringify(createProtocolEnvelope('oversize-a1', { message: 'must-not-persist' }));
|
|
234
|
-
const oversizedBody = baseBody + ' '.repeat(Math.max(0,
|
|
235
|
-
invariant(utf8ByteLength(oversizedBody) >
|
|
410
|
+
const oversizedBody = baseBody + ' '.repeat(Math.max(0, PROTOCOL_LIMIT_CEILINGS.max_request_bytes - utf8ByteLength(baseBody) + 1));
|
|
411
|
+
invariant(utf8ByteLength(oversizedBody) > PROTOCOL_LIMIT_CEILINGS.max_request_bytes, 'Oversize fixture did not exceed max_request_bytes.');
|
|
236
412
|
const response = await environment.operations.appendRaw(credentialA, cubeA, oversizedBody);
|
|
237
413
|
expectError(response, 413, ErrorCode.CONTENT_TOO_LARGE, 'Oversized append request');
|
|
238
414
|
const read = await environment.operations.read(credentialA, cubeA, createProtocolEnvelope('oversize-read', { cursor: null, limit: 10 }));
|
|
@@ -353,21 +529,6 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
353
529
|
invariant(second.supersedes === first.id, 'Superseding decision did not reference its predecessor.');
|
|
354
530
|
return { active_count: 1, active_decision: 'second', supersedes_first: true };
|
|
355
531
|
});
|
|
356
|
-
await record('capabilities.unsupported-fails-closed', async () => {
|
|
357
|
-
invariant(protocolBody, 'Protocol fixture did not produce an envelope.');
|
|
358
|
-
let code = null;
|
|
359
|
-
try {
|
|
360
|
-
negotiateProtocol(decodeProtocolInfoEnvelope(protocolBody).payload, ['future.required']);
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
if (error instanceof ProtocolContractError)
|
|
364
|
-
code = error.code;
|
|
365
|
-
else
|
|
366
|
-
throw error;
|
|
367
|
-
}
|
|
368
|
-
invariant(code === ErrorCode.UNSUPPORTED_CAPABILITY, 'Unsupported capability did not fail closed client-side.');
|
|
369
|
-
return { code };
|
|
370
|
-
});
|
|
371
532
|
await record('security.active-stream-revocation', async () => {
|
|
372
533
|
invariant(liveCursor, 'Stream fixture did not produce a live cursor.');
|
|
373
534
|
const opened = await environment.operations.openStream(credentialA, cubeA, liveCursor);
|