borgmcp 2.0.11 → 2.1.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 +11 -3
- package/dist/assimilate-cmd.d.ts +13 -4
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +134 -110
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts +7 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +44 -18
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/claude.d.ts +3 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +26 -12
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +22 -5
- package/dist/cli-help.js.map +1 -1
- package/dist/config.d.ts +7 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -4
- package/dist/config.js.map +1 -1
- package/dist/remote-client.js +1 -1
- package/dist/repository-cube-init.d.ts +57 -0
- package/dist/repository-cube-init.d.ts.map +1 -0
- package/dist/repository-cube-init.js +196 -0
- package/dist/repository-cube-init.js.map +1 -0
- package/dist/repository-identity.d.ts +29 -0
- package/dist/repository-identity.d.ts.map +1 -0
- package/dist/repository-identity.js +143 -0
- package/dist/repository-identity.js.map +1 -0
- package/dist/server-errors.d.ts +6 -0
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js +12 -0
- package/dist/server-errors.js.map +1 -1
- package/dist/server-facade.d.ts +11 -1
- package/dist/server-facade.d.ts.map +1 -1
- package/dist/server-facade.js +33 -3
- package/dist/server-facade.js.map +1 -1
- package/dist/server-handshake.d.ts +8 -4
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +15 -6
- package/dist/server-handshake.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +7 -7
- package/docs/LOCAL_SERVER.md +35 -6
- package/docs/RELEASING.md +11 -3
- package/package.json +2 -2
- package/src/assimilate-cmd.ts +166 -140
- package/src/assimilate-deps.ts +59 -17
- package/src/claude.ts +34 -14
- package/src/cli-help.ts +25 -5
- package/src/config.ts +30 -8
- package/src/remote-client.ts +1 -1
- package/src/repository-cube-init.ts +273 -0
- package/src/repository-identity.ts +199 -0
- package/src/server-errors.ts +14 -0
- package/src/server-facade.ts +47 -2
- package/src/server-handshake.ts +33 -7
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createHmac, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { basename, isAbsolute, join } from 'node:path';
|
|
4
|
+
import { realpath } from 'node:fs/promises';
|
|
5
|
+
import type { CreateCubeRepository, CubeTemplate } from 'borgmcp-shared/protocol';
|
|
6
|
+
import { canonicalizeWorkingRepoIdentity } from './working-repo.js';
|
|
7
|
+
import { normalizeCubeName } from './cube-name.js';
|
|
8
|
+
import { borgConfigRoot, ensurePrivateBorgConfigRoot } from './private-root.js';
|
|
9
|
+
import { atomicWrite0600, readStoreFile, withStoreLock } from './seat-store.js';
|
|
10
|
+
|
|
11
|
+
const STORE_VERSION = 1;
|
|
12
|
+
const SECRET_FILE = 'repository-identity.key';
|
|
13
|
+
const STATE_FILE = 'repository-identities.json';
|
|
14
|
+
const LOCK_FILE = 'repository-identities.lock';
|
|
15
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
16
|
+
const DISPLAY_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/;
|
|
17
|
+
|
|
18
|
+
export interface GitRepositoryContext {
|
|
19
|
+
root: string;
|
|
20
|
+
commonDir: string;
|
|
21
|
+
derivedName: string;
|
|
22
|
+
publicRepository: Extract<CreateCubeRepository, { kind: 'origin' }> | null;
|
|
23
|
+
publicRepositoryName: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RepositoryAssociation {
|
|
27
|
+
cubeId: string;
|
|
28
|
+
name: string;
|
|
29
|
+
workingRepoName: string;
|
|
30
|
+
template: CubeTemplate;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface RepositoryIdentityState {
|
|
34
|
+
version: 1;
|
|
35
|
+
localIdentities: Record<string, string>;
|
|
36
|
+
associations: Record<string, RepositoryAssociation>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RepositoryIdentityDeps {
|
|
40
|
+
runGit?: (cwd: string, args: string[]) => { status: number | null; stdout?: string | null };
|
|
41
|
+
canonicalPath?: (path: string) => Promise<string>;
|
|
42
|
+
root?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defaultRunGit(cwd: string, args: string[]) {
|
|
46
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
47
|
+
return { status: result.status, stdout: result.stdout };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function output(result: { status: number | null; stdout?: string | null }): string | null {
|
|
51
|
+
const value = result.status === 0 ? result.stdout?.trim() : null;
|
|
52
|
+
return value || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function resolveGitRepositoryContext(
|
|
56
|
+
cwd: string,
|
|
57
|
+
deps: RepositoryIdentityDeps = {},
|
|
58
|
+
): Promise<GitRepositoryContext | null> {
|
|
59
|
+
const runGit = deps.runGit ?? defaultRunGit;
|
|
60
|
+
const canonicalPath = deps.canonicalPath ?? realpath;
|
|
61
|
+
const rootRaw = output(runGit(cwd, ['rev-parse', '--show-toplevel']));
|
|
62
|
+
if (!rootRaw || !isAbsolute(rootRaw)) return null;
|
|
63
|
+
const bare = output(runGit(cwd, ['rev-parse', '--is-bare-repository']));
|
|
64
|
+
if (bare === 'true') throw new Error('BARE_REPOSITORY');
|
|
65
|
+
const commonRaw = output(runGit(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir']));
|
|
66
|
+
if (!commonRaw || !isAbsolute(commonRaw)) return null;
|
|
67
|
+
const root = await canonicalPath(rootRaw);
|
|
68
|
+
const commonDir = await canonicalPath(commonRaw);
|
|
69
|
+
const derivedName = normalizeCubeName(basename(root));
|
|
70
|
+
if (!derivedName) return null;
|
|
71
|
+
|
|
72
|
+
const originRaw = output(runGit(root, ['config', '--get', 'remote.origin.url']));
|
|
73
|
+
const canonical = originRaw ? canonicalizeWorkingRepoIdentity(originRaw) : null;
|
|
74
|
+
return {
|
|
75
|
+
root,
|
|
76
|
+
commonDir,
|
|
77
|
+
derivedName,
|
|
78
|
+
publicRepository: canonical?.origin
|
|
79
|
+
? { kind: 'origin', value: canonical.origin }
|
|
80
|
+
: null,
|
|
81
|
+
publicRepositoryName: canonical?.name ?? null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseSecret(raw: string | null): Buffer | null {
|
|
86
|
+
if (raw === null) return null;
|
|
87
|
+
if (!/^[a-f0-9]{64}$/.test(raw.trim())) {
|
|
88
|
+
throw new Error('Borg repository identity secret is malformed');
|
|
89
|
+
}
|
|
90
|
+
return Buffer.from(raw.trim(), 'hex');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function emptyState(): RepositoryIdentityState {
|
|
94
|
+
return { version: STORE_VERSION, localIdentities: {}, associations: {} };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseState(raw: string | null): RepositoryIdentityState {
|
|
98
|
+
if (raw === null) return emptyState();
|
|
99
|
+
const parsed = JSON.parse(raw) as Partial<RepositoryIdentityState>;
|
|
100
|
+
if (
|
|
101
|
+
parsed.version !== STORE_VERSION ||
|
|
102
|
+
!parsed.localIdentities || typeof parsed.localIdentities !== 'object' || Array.isArray(parsed.localIdentities) ||
|
|
103
|
+
!parsed.associations || typeof parsed.associations !== 'object' || Array.isArray(parsed.associations)
|
|
104
|
+
) {
|
|
105
|
+
throw new Error('Borg repository identity store is malformed or unsupported');
|
|
106
|
+
}
|
|
107
|
+
for (const [key, value] of Object.entries(parsed.localIdentities)) {
|
|
108
|
+
if (!/^[a-f0-9]{64}$/.test(key) || typeof value !== 'string' || !UUID_RE.test(value)) {
|
|
109
|
+
throw new Error('Borg repository identity store is malformed or unsupported');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const [key, value] of Object.entries(parsed.associations)) {
|
|
113
|
+
if (
|
|
114
|
+
!/^[a-f0-9]{64}$/.test(key) ||
|
|
115
|
+
!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
116
|
+
typeof value.cubeId !== 'string' || !UUID_RE.test(value.cubeId) ||
|
|
117
|
+
typeof value.name !== 'string' || Buffer.byteLength(value.name, 'utf8') > 120 ||
|
|
118
|
+
!DISPLAY_NAME_RE.test(value.name) ||
|
|
119
|
+
typeof value.workingRepoName !== 'string' || Buffer.byteLength(value.workingRepoName, 'utf8') > 120 ||
|
|
120
|
+
!DISPLAY_NAME_RE.test(value.workingRepoName) ||
|
|
121
|
+
(value.template !== 'software-dev' && value.template !== 'starter' && value.template !== 'default')
|
|
122
|
+
) {
|
|
123
|
+
throw new Error('Borg repository identity store is malformed or unsupported');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return parsed as RepositoryIdentityState;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function digest(secret: Buffer, purpose: string, value: string): string {
|
|
130
|
+
return createHmac('sha256', secret).update(purpose).update('\0').update(value).digest('hex');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function withIdentityState<T>(
|
|
134
|
+
operation: (secret: Buffer, state: RepositoryIdentityState) => Promise<{ result: T; changed?: boolean }>,
|
|
135
|
+
deps: RepositoryIdentityDeps = {},
|
|
136
|
+
): Promise<T> {
|
|
137
|
+
const root = deps.root ?? borgConfigRoot();
|
|
138
|
+
await ensurePrivateBorgConfigRoot(root);
|
|
139
|
+
const options = { secureRoot: root, rootMode: 'private' as const };
|
|
140
|
+
return withStoreLock(join(root, LOCK_FILE), async () => {
|
|
141
|
+
const secretPath = join(root, SECRET_FILE);
|
|
142
|
+
let secret = parseSecret(await readStoreFile(secretPath, options));
|
|
143
|
+
if (!secret) {
|
|
144
|
+
secret = randomBytes(32);
|
|
145
|
+
await atomicWrite0600(secretPath, `${secret.toString('hex')}\n`, options);
|
|
146
|
+
}
|
|
147
|
+
const statePath = join(root, STATE_FILE);
|
|
148
|
+
const state = parseState(await readStoreFile(statePath, options));
|
|
149
|
+
const outcome = await operation(secret, state);
|
|
150
|
+
if (outcome.changed) {
|
|
151
|
+
await atomicWrite0600(statePath, `${JSON.stringify(state, null, 2)}\n`, options);
|
|
152
|
+
}
|
|
153
|
+
return outcome.result;
|
|
154
|
+
}, options);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function getOrCreateRepositoryIdentity(
|
|
158
|
+
context: GitRepositoryContext,
|
|
159
|
+
deps: RepositoryIdentityDeps = {},
|
|
160
|
+
): Promise<CreateCubeRepository> {
|
|
161
|
+
if (context.publicRepository) return context.publicRepository;
|
|
162
|
+
return withIdentityState(async (secret, state) => {
|
|
163
|
+
const key = digest(secret, 'git-common-dir', context.commonDir);
|
|
164
|
+
let value = state.localIdentities[key];
|
|
165
|
+
let changed = false;
|
|
166
|
+
if (!value) {
|
|
167
|
+
value = randomUUID();
|
|
168
|
+
state.localIdentities[key] = value;
|
|
169
|
+
changed = true;
|
|
170
|
+
}
|
|
171
|
+
return { result: { kind: 'local' as const, value }, changed };
|
|
172
|
+
}, deps);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function associationBinding(secret: Buffer, trustIdentity: string, repository: CreateCubeRepository): string {
|
|
176
|
+
return digest(secret, 'association', `${trustIdentity}\0${repository.kind}\0${repository.value}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function getRepositoryAssociation(
|
|
180
|
+
trustIdentity: string,
|
|
181
|
+
repository: CreateCubeRepository,
|
|
182
|
+
deps: RepositoryIdentityDeps = {},
|
|
183
|
+
): Promise<RepositoryAssociation | null> {
|
|
184
|
+
return withIdentityState(async (secret, state) => ({
|
|
185
|
+
result: state.associations[associationBinding(secret, trustIdentity, repository)] ?? null,
|
|
186
|
+
}), deps);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function saveRepositoryAssociation(
|
|
190
|
+
trustIdentity: string,
|
|
191
|
+
repository: CreateCubeRepository,
|
|
192
|
+
association: RepositoryAssociation,
|
|
193
|
+
deps: RepositoryIdentityDeps = {},
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
await withIdentityState(async (secret, state) => {
|
|
196
|
+
state.associations[associationBinding(secret, trustIdentity, repository)] = association;
|
|
197
|
+
return { result: undefined, changed: true };
|
|
198
|
+
}, deps);
|
|
199
|
+
}
|
package/src/server-errors.ts
CHANGED
|
@@ -92,6 +92,20 @@ export class BorgServerUnreachableError extends Error {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
export class CubeCreationOutcomeUnknownError extends Error {
|
|
96
|
+
constructor() {
|
|
97
|
+
super('Cube creation outcome is unknown.');
|
|
98
|
+
this.name = 'CubeCreationOutcomeUnknownError';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class CubeCreationConfirmationError extends Error {
|
|
103
|
+
constructor(message = 'The Borg server returned conflicting repository cube state.') {
|
|
104
|
+
super(message);
|
|
105
|
+
this.name = 'CubeCreationConfirmationError';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
/** Exact retired TTL-replacement state: two saved bearers and no safe implicit winner. */
|
|
96
110
|
export class LegacySessionCredentialCollisionError extends Error {
|
|
97
111
|
constructor(public readonly origin: string) {
|
package/src/server-facade.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { spawn as spawnChild, type SpawnOptions } from 'node:child_process';
|
|
2
2
|
import { constants } from 'node:os';
|
|
3
|
-
import { serverHelpText } from './cli-help.js';
|
|
3
|
+
import { cubeInitHelpText, isHelpFlag, serverHelpText } from './cli-help.js';
|
|
4
4
|
|
|
5
5
|
export const SERVER_LIFECYCLE_COMMANDS = ['setup', 'start', 'stop', 'status', 'update', 'invite'] as const;
|
|
6
6
|
export type ServerLifecycleCommand = typeof SERVER_LIFECYCLE_COMMANDS[number];
|
|
7
7
|
|
|
8
8
|
export type ParsedServerFacadeArgs =
|
|
9
9
|
| { kind: 'help' }
|
|
10
|
+
| { kind: 'cube-init-help' }
|
|
11
|
+
| { kind: 'cube-init'; args: string[] }
|
|
10
12
|
| { kind: 'command'; command: ServerLifecycleCommand; args: string[] }
|
|
11
13
|
| { kind: 'error'; reason: 'unknown-command'; command: string };
|
|
12
14
|
|
|
@@ -15,6 +17,12 @@ export function parseServerFacadeArgs(args: readonly string[]): ParsedServerFaca
|
|
|
15
17
|
if (command === undefined || command === '--help' || command === '-h') {
|
|
16
18
|
return { kind: 'help' };
|
|
17
19
|
}
|
|
20
|
+
if (command === 'cube' && rest[0] === 'init') {
|
|
21
|
+
const args = rest.slice(1);
|
|
22
|
+
return args.some(isHelpFlag)
|
|
23
|
+
? { kind: 'cube-init-help' }
|
|
24
|
+
: { kind: 'cube-init', args };
|
|
25
|
+
}
|
|
18
26
|
if (!(SERVER_LIFECYCLE_COMMANDS as readonly string[]).includes(command)) {
|
|
19
27
|
return { kind: 'error', reason: 'unknown-command', command };
|
|
20
28
|
}
|
|
@@ -49,6 +57,12 @@ export interface ServerFacadeOutputDeps {
|
|
|
49
57
|
writeStderr(text: string): void;
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
export interface ServerFacadeClientDeps {
|
|
61
|
+
cubeInit(args: readonly string[]): Promise<number>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type AssimilateDepsBuilder = typeof import('./assimilate-deps.js').buildDefaultAssimilateDeps;
|
|
65
|
+
|
|
52
66
|
export type ServerFacadeProcessResult =
|
|
53
67
|
| { kind: 'exited'; code: number }
|
|
54
68
|
| { kind: 'signaled'; signal: NodeJS.Signals }
|
|
@@ -65,6 +79,31 @@ const defaultOutputDeps: ServerFacadeOutputDeps = {
|
|
|
65
79
|
writeStderr: (text) => process.stderr.write(text),
|
|
66
80
|
};
|
|
67
81
|
|
|
82
|
+
export function buildDefaultServerFacadeClientDeps(
|
|
83
|
+
buildDeps?: AssimilateDepsBuilder,
|
|
84
|
+
): ServerFacadeClientDeps {
|
|
85
|
+
return {
|
|
86
|
+
cubeInit: async (args) => {
|
|
87
|
+
const [{ parseAssimilateArgs }, { buildDefaultAssimilateDeps }, { runAssimilate }] = await Promise.all([
|
|
88
|
+
import('./parse-assimilate-args.js'),
|
|
89
|
+
import('./assimilate-deps.js'),
|
|
90
|
+
import('./assimilate-cmd.js'),
|
|
91
|
+
]);
|
|
92
|
+
const parsed = parseAssimilateArgs([...args]);
|
|
93
|
+
if (!parsed.ok || parsed.role !== undefined) {
|
|
94
|
+
process.stderr.write(`${parsed.ok ? 'borg server cube init does not accept a role' : parsed.error}\n`);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
return runAssimilate(
|
|
98
|
+
{ role: undefined, flags: parsed.flags, mode: 'cube-init' },
|
|
99
|
+
(buildDeps ?? buildDefaultAssimilateDeps)(),
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const defaultClientDeps = buildDefaultServerFacadeClientDeps();
|
|
106
|
+
|
|
68
107
|
const MAX_RENDERED_COMMAND_CODE_POINTS = 80;
|
|
69
108
|
|
|
70
109
|
function inertCommand(command: string): string {
|
|
@@ -86,7 +125,7 @@ function inertCommand(command: string): string {
|
|
|
86
125
|
export function unknownServerCommandText(command: string): string {
|
|
87
126
|
return (
|
|
88
127
|
`Unknown server command: ${inertCommand(command)}.\n` +
|
|
89
|
-
`Available commands: setup, start, stop, status, update, invite.\n` +
|
|
128
|
+
`Available commands: setup, start, stop, status, update, invite, cube init.\n` +
|
|
90
129
|
`Next: run borg server --help.\n`
|
|
91
130
|
);
|
|
92
131
|
}
|
|
@@ -168,6 +207,7 @@ export async function runEarlyServerFacade(
|
|
|
168
207
|
argv: readonly string[],
|
|
169
208
|
deps: ServerFacadeProcessDeps = defaultProcessDeps,
|
|
170
209
|
output: ServerFacadeOutputDeps = defaultOutputDeps,
|
|
210
|
+
client: ServerFacadeClientDeps = defaultClientDeps,
|
|
171
211
|
): Promise<number | null> {
|
|
172
212
|
if (argv[2] !== 'server') return null;
|
|
173
213
|
const parsed = parseServerFacadeArgs(argv.slice(3));
|
|
@@ -175,10 +215,15 @@ export async function runEarlyServerFacade(
|
|
|
175
215
|
output.writeStdout(serverHelpText());
|
|
176
216
|
return 0;
|
|
177
217
|
}
|
|
218
|
+
if (parsed.kind === 'cube-init-help') {
|
|
219
|
+
output.writeStdout(cubeInitHelpText());
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
178
222
|
if (parsed.kind === 'error') {
|
|
179
223
|
output.writeStderr(unknownServerCommandText(parsed.command));
|
|
180
224
|
return 1;
|
|
181
225
|
}
|
|
226
|
+
if (parsed.kind === 'cube-init') return client.cubeInit(parsed.args);
|
|
182
227
|
|
|
183
228
|
const result = await runServerFacadeProcess(parsed, deps);
|
|
184
229
|
if (result.kind === 'spawn-error') {
|
package/src/server-handshake.ts
CHANGED
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
decodeProtocolErrorEnvelope,
|
|
15
15
|
decodeProtocolTagPreflight,
|
|
16
16
|
ErrorCode,
|
|
17
|
+
type CreateCubeRepository,
|
|
17
18
|
type CreateCubeResponse,
|
|
19
|
+
type CubeTemplate,
|
|
18
20
|
type DroneRuntimeMetadata,
|
|
19
21
|
type ProtocolTagPreflight,
|
|
20
22
|
type ServerCapability,
|
|
@@ -45,6 +47,8 @@ import {
|
|
|
45
47
|
BorgServerError,
|
|
46
48
|
BorgServerTrustError,
|
|
47
49
|
BorgServerUnreachableError,
|
|
50
|
+
CubeCreationConfirmationError,
|
|
51
|
+
CubeCreationOutcomeUnknownError,
|
|
48
52
|
} from './server-errors.js';
|
|
49
53
|
import { DroneEvictedError, DRONE_EVICTED_CODE } from './drone-lifecycle.js';
|
|
50
54
|
import { readBoundedResponseBody } from './server-response.js';
|
|
@@ -181,7 +185,7 @@ export interface ServerAttachResult {
|
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
/**
|
|
184
|
-
* Attach an enrolled client principal to one granted cube/role over protocol
|
|
188
|
+
* Attach an enrolled client principal to one granted cube/role over protocol v4.
|
|
185
189
|
* The client CSPRNG-generates the session bearer and persists it PENDING in the
|
|
186
190
|
* OS keychain (keyed by the stable per-seat identity) BEFORE this request, so an
|
|
187
191
|
* interrupted/lost response is recovered by re-sending the exact same bearer —
|
|
@@ -564,7 +568,12 @@ export async function createBorgServerCube(
|
|
|
564
568
|
origin: string,
|
|
565
569
|
trustIdentity: string,
|
|
566
570
|
parentCredential: string,
|
|
567
|
-
input: {
|
|
571
|
+
input: {
|
|
572
|
+
name: string;
|
|
573
|
+
workingRepoName: string;
|
|
574
|
+
repository: CreateCubeRepository;
|
|
575
|
+
template: CubeTemplate;
|
|
576
|
+
},
|
|
568
577
|
deps: {
|
|
569
578
|
fetchImpl?: FetchLike;
|
|
570
579
|
loadCredentialRecord?: typeof getServerCredentialRecord;
|
|
@@ -589,13 +598,16 @@ export async function createBorgServerCube(
|
|
|
589
598
|
origin,
|
|
590
599
|
trustIdentity,
|
|
591
600
|
clientId: active.clientId,
|
|
592
|
-
projectRoot: input.projectRoot,
|
|
593
601
|
name: input.name,
|
|
594
|
-
|
|
602
|
+
workingRepoName: input.workingRepoName,
|
|
603
|
+
repository: input.repository,
|
|
604
|
+
template: input.template,
|
|
595
605
|
});
|
|
596
606
|
const request = decodeCreateCubeRequest({
|
|
597
607
|
retry_key: pending.retryKey,
|
|
598
608
|
name: pending.name,
|
|
609
|
+
working_repo_name: pending.workingRepoName,
|
|
610
|
+
repository: pending.repository,
|
|
599
611
|
template: pending.template,
|
|
600
612
|
});
|
|
601
613
|
|
|
@@ -623,7 +635,10 @@ export async function createBorgServerCube(
|
|
|
623
635
|
clearTimeout(timeout);
|
|
624
636
|
}
|
|
625
637
|
}
|
|
626
|
-
if (!response)
|
|
638
|
+
if (!response) {
|
|
639
|
+
void lastTransportError;
|
|
640
|
+
throw new CubeCreationOutcomeUnknownError();
|
|
641
|
+
}
|
|
627
642
|
if (response.status === 401 || response.status === 403) {
|
|
628
643
|
throw new BorgServerError('CREDENTIAL_REJECTED', 'Borg server enrollment was rejected');
|
|
629
644
|
}
|
|
@@ -634,7 +649,7 @@ export async function createBorgServerCube(
|
|
|
634
649
|
);
|
|
635
650
|
}
|
|
636
651
|
if (response.status === 409) {
|
|
637
|
-
throw new
|
|
652
|
+
throw new CubeCreationConfirmationError('The Borg server rejected the repository cube operation identity.');
|
|
638
653
|
}
|
|
639
654
|
if (response.status !== 201) {
|
|
640
655
|
throw new Error(`Borg server cube creation failed (HTTP ${response.status})`);
|
|
@@ -648,6 +663,12 @@ export async function createBorgServerCube(
|
|
|
648
663
|
if (error instanceof Error && error.message.includes('response limit')) throw error;
|
|
649
664
|
throw new Error('Borg server returned an invalid cube creation envelope');
|
|
650
665
|
}
|
|
666
|
+
if (
|
|
667
|
+
decoded.payload.repository.kind !== pending.repository.kind ||
|
|
668
|
+
decoded.payload.repository.value !== pending.repository.value
|
|
669
|
+
) {
|
|
670
|
+
throw new CubeCreationConfirmationError();
|
|
671
|
+
}
|
|
651
672
|
await (deps.clearCubeCreation ?? clearPendingServerCubeCreation)(
|
|
652
673
|
pending as PendingServerCubeCreationRecord,
|
|
653
674
|
);
|
|
@@ -658,7 +679,12 @@ export async function createLocalBorgServerCube(
|
|
|
658
679
|
origin: string,
|
|
659
680
|
trustIdentity: string,
|
|
660
681
|
parentCredential: string,
|
|
661
|
-
input: {
|
|
682
|
+
input: {
|
|
683
|
+
name: string;
|
|
684
|
+
workingRepoName: string;
|
|
685
|
+
repository: CreateCubeRepository;
|
|
686
|
+
template: CubeTemplate;
|
|
687
|
+
},
|
|
662
688
|
deps: { loadTrust?: typeof loadBorgServerTrust } = {},
|
|
663
689
|
): Promise<CreateCubeResponse> {
|
|
664
690
|
const trust = await (deps.loadTrust ?? loadBorgServerTrust)(origin);
|