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 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 compatibility metadata.
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 three shared paths:
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 in a bounded
64
- JSON body over verified TLS and returns the client credential once. Secrets
65
- never belong in a URL, query string, command-line argument, diagnostic, or
66
- serializable domain entity.
67
- - `GET /api/protocol` requires authentication and returns the protocol version,
68
- package version, capabilities, and bounded limits in the shared envelope.
69
-
70
- `negotiateProtocol` requires bearer authentication, revocation, cube isolation,
71
- verified TLS, and no-cloud-fallback capabilities. Missing security capabilities
72
- fail closed before an operation begins.
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
- acks, claims, decisions, and unsupported-capability failures.
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`, `SUPPORTED_PROTOCOL_VERSIONS`, and `COMPATIBILITY_MATRIX`
103
- describe the compatibility contract. During the pre-1.0 package series, new
104
- contracts may be added, but an existing wire shape will not change without a
105
- documented migration path and corresponding conformance coverage. Consumers pin
106
- `^0.2.0` (`>=0.2.0 <0.3.0`): before 1.0, breaking wire changes increment the
107
- minor version while compatible additions and corrections increment the patch.
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 current matrix and
110
- the policy for introducing protocol changes.
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
- After the final source, security, conformance, and packed-artifact gates pass,
115
- `borgmcp-shared@0.2.2` is published publicly from the npm account that owns the
116
- existing `borgmcp` client package. Consumers then pin `borgmcp-shared@^0.2.0`
117
- and commit registry lockfiles. No registry token belongs in this repository,
118
- package metadata, lockfiles, or a committed `.npmrc`; publishing uses protected
119
- external credentials and provenance.
120
-
121
- Before that publication, consumers may temporarily pin an audited, immutable
122
- full commit SHA, using
123
- `git+ssh://git@github.com/Byte-Ventures/borg-mcp-shared.git#<40-hex-SHA>`.
124
- Tag, range, and local-path pins are prohibited. This exception is replaced with
125
- the registry dependency `borgmcp-shared@^0.2.0` after publication. Public release
126
- requires the packed-artifact sensitivity audit, name ownership, account recovery
127
- and 2FA, provenance verification, and explicit release approval.
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
- issueSingleUseInvitation(principal: ConformancePrincipal): Promise<string>;
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.enrollment-auth";
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,EAGf,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IAEvC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IAExE,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;CACtC;AAMD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC7D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACnD,SAAS,CAAC,SAAS,EAAE,oBAAoB,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjF,wBAAwB,CAAC,SAAS,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3E,eAAe,CAAC,SAAS,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,YAAY,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,mBAAmB,IAAI,wBAAwB,CAAC;CACjD;AAGD,MAAM,WAAW,qBAAqB;IACpC,MAAM,IAAI,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC3C,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACtE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC3D,MAAM,CACJ,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAEpC,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,EACrB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACpC,IAAI,CACF,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACpC,GAAG,CACD,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACpC,cAAc,CACZ,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACpC,aAAa,CACX,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACpC,UAAU,CACR,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,GAAG,IAAI,GACvB,OAAO,CAAC,yBAAyB,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;CAC5C;AAED,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAc/B,CAAC;AAEX,MAAM,MAAM,2BAA2B,GACrC,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;AAEtD,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,2BAA2B,CAAC;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,wBAAwB,EAAE,CAAC;IAEpC,oBAAoB,EAAE,aAAa,CAAC;QAClC,EAAE,EAAE,2BAA2B,CAAC;QAChC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACvC,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,kCAAkC;IACjD,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,wBAAwB,CAAC;IAChC,KAAK,EAAE,wBAAwB,CAAC;IAChC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IAExC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AA4ID,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,sBAAsB,EACnC,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,wBAAwB,CAAC,CA+YnC;AAED,wBAAsB,+BAA+B,CACnD,KAAK,EAAE,sBAAsB,EAC7B,KAAK,EAAE,sBAAsB,EAC7B,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,kCAAkC,CAAC,CAY7C"}
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, ProtocolContractError, compareLogCursor, createProtocolEnvelope, decodeAppendLogResultEnvelope, decodeDecisionResultEnvelope, decodeDecisionsResultEnvelope, decodeEnrollmentExchangeResponseEnvelope, decodeProtocolErrorEnvelope, decodeProtocolInfoEnvelope, decodeReadLogResultEnvelope, decodeSseFrames, negotiateProtocol, utf8ByteLength, } from '../protocol/index.js';
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.enrollment-auth', area: '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
- const principalA = await environment.admin.createPrincipal('principal-a');
140
- const principalB = await environment.admin.createPrincipal('principal-b');
141
- const cubeA = await environment.admin.createCube('cube-a');
142
- const cubeB = await environment.admin.createCube('cube-b');
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.enrollment-auth', async () => {
158
- expectError(await environment.operations.protocol(null), 401, ErrorCode.AUTH_MISSING, 'Unauthenticated protocol request');
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
- credentialA = decodeEnrollmentExchangeResponseEnvelope(enrolledAResponse.body).payload.credential;
172
- credentialB = decodeEnrollmentExchangeResponseEnvelope(enrolledBResponse.body).payload.credential;
173
- invariant(credentialA !== credentialB, 'Enrollment issued the same credential to two principals.');
174
- expectError(await environment.operations.enroll(enrollmentARequest), 401, ErrorCode.AUTH_INVALID, 'Invitation reuse');
175
- const protocolResponse = await environment.operations.protocol(credentialA);
176
- expectStatus(protocolResponse, 200, 'Authenticated protocol request');
177
- protocolBody = protocolResponse.body;
178
- protocolInfo = negotiateProtocol(decodeProtocolInfoEnvelope(protocolBody).payload, [
179
- 'log.cursor',
180
- 'stream.sse',
181
- 'stream.replay',
182
- 'acks',
183
- 'claims',
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
- invitation_reuse: ErrorCode.AUTH_INVALID,
190
- protocol_version: protocolInfo.protocol_version,
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) <= protocolInfo.limits.max_request_bytes &&
198
- utf8ByteLength(injectedMessage) <= protocolInfo.limits.max_log_message_bytes, 'Injection fixture exceeded an advertised request limit.');
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, protocolInfo.limits.max_request_bytes - utf8ByteLength(baseBody) + 1));
235
- invariant(utf8ByteLength(oversizedBody) > protocolInfo.limits.max_request_bytes, 'Oversize fixture did not exceed max_request_bytes.');
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);