borgmcp 5.4.1 → 5.6.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 +15 -0
- package/dist/assimilate-cmd.d.ts +8 -1
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +54 -21
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +30 -0
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +1 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +45 -0
- package/dist/cli-help.js.map +1 -1
- package/dist/docs-sections.d.ts.map +1 -1
- package/dist/docs-sections.js +8 -0
- package/dist/docs-sections.js.map +1 -1
- package/dist/local-server-cursor.d.ts +10 -1
- package/dist/local-server-cursor.d.ts.map +1 -1
- package/dist/local-server-cursor.js +64 -5
- package/dist/local-server-cursor.js.map +1 -1
- package/dist/log-stream.d.ts +13 -0
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +45 -13
- package/dist/log-stream.js.map +1 -1
- package/dist/remote-client.d.ts +17 -0
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +37 -15
- package/dist/remote-client.js.map +1 -1
- package/dist/representative-cmd.d.ts +94 -0
- package/dist/representative-cmd.d.ts.map +1 -0
- package/dist/representative-cmd.js +290 -0
- package/dist/representative-cmd.js.map +1 -0
- package/dist/representative-core.d.ts +243 -0
- package/dist/representative-core.d.ts.map +1 -0
- package/dist/representative-core.js +679 -0
- package/dist/representative-core.js.map +1 -0
- package/dist/representative-delivery-store.d.ts +79 -0
- package/dist/representative-delivery-store.d.ts.map +1 -0
- package/dist/representative-delivery-store.js +233 -0
- package/dist/representative-delivery-store.js.map +1 -0
- package/dist/representative-listener-store.d.ts +46 -0
- package/dist/representative-listener-store.d.ts.map +1 -0
- package/dist/representative-listener-store.js +119 -0
- package/dist/representative-listener-store.js.map +1 -0
- package/dist/representative-listener.d.ts +32 -0
- package/dist/representative-listener.d.ts.map +1 -0
- package/dist/representative-listener.js +293 -0
- package/dist/representative-listener.js.map +1 -0
- package/dist/representative-mcp.d.ts +37 -0
- package/dist/representative-mcp.d.ts.map +1 -0
- package/dist/representative-mcp.js +211 -0
- package/dist/representative-mcp.js.map +1 -0
- package/dist/representative-owner.d.ts +10 -0
- package/dist/representative-owner.d.ts.map +1 -0
- package/dist/representative-owner.js +107 -0
- package/dist/representative-owner.js.map +1 -0
- package/dist/representative-store.d.ts +68 -0
- package/dist/representative-store.d.ts.map +1 -0
- package/dist/representative-store.js +173 -0
- package/dist/representative-store.js.map +1 -0
- package/dist/seat-probe.d.ts +1 -0
- package/dist/seat-probe.d.ts.map +1 -1
- package/dist/seat-probe.js +1 -1
- package/dist/seat-probe.js.map +1 -1
- package/dist/seat-store.d.ts +13 -0
- package/dist/seat-store.d.ts.map +1 -1
- package/dist/seat-store.js +55 -10
- package/dist/seat-store.js.map +1 -1
- package/dist/server-trust.d.ts +10 -0
- package/dist/server-trust.d.ts.map +1 -1
- package/dist/server-trust.js +23 -6
- package/dist/server-trust.js.map +1 -1
- package/dist/stream-owner.d.ts +10 -0
- package/dist/stream-owner.d.ts.map +1 -1
- package/dist/stream-owner.js +129 -21
- package/dist/stream-owner.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +1 -0
- package/dist/unknown-subcommand.js.map +1 -1
- package/docs/HUMAN_REPRESENTATIVE.md +434 -0
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +73 -22
- package/src/claude.ts +30 -0
- package/src/cli-help.ts +48 -0
- package/src/docs-sections.ts +8 -0
- package/src/local-server-cursor.ts +56 -4
- package/src/log-stream.ts +47 -14
- package/src/remote-client.ts +54 -13
- package/src/representative-cmd.ts +369 -0
- package/src/representative-core.ts +908 -0
- package/src/representative-delivery-store.ts +235 -0
- package/src/representative-listener-store.ts +115 -0
- package/src/representative-listener.ts +215 -0
- package/src/representative-mcp.ts +250 -0
- package/src/representative-owner.ts +105 -0
- package/src/representative-store.ts +224 -0
- package/src/seat-probe.ts +1 -1
- package/src/seat-store.ts +61 -10
- package/src/server-trust.ts +25 -6
- package/src/stream-owner.ts +129 -20
- package/src/unknown-subcommand.ts +1 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Lifecycle adapter for the existing stream lease; no separate lock protocol. */
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { borgConfigRoot, borgHomeRoot } from './private-root.js';
|
|
5
|
+
import { acquireStreamLease, readOwnershipSnapshot, STREAM_OWNER_STALE_MS, } from './stream-owner.js';
|
|
6
|
+
import { RepresentativeError } from './representative-core.js';
|
|
7
|
+
export function representativeOwnerDeps(binding) {
|
|
8
|
+
const authority = createHash('sha256').update(JSON.stringify([binding.origin, binding.trustIdentity])).digest('hex');
|
|
9
|
+
return {
|
|
10
|
+
// Separate from stream-locks, which borg_stream-status inspects. One lease
|
|
11
|
+
// across all worktrees using this authority/cube/drone, never one per host.
|
|
12
|
+
locksDir: join(borgConfigRoot(), 'representative-host-locks', authority),
|
|
13
|
+
privateRoot: { root: borgConfigRoot(), boundary: borgHomeRoot() },
|
|
14
|
+
worktree: binding.worktree, droneLabel: binding.representativeLabel, cubeName: binding.cubeName,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function representativeOwnership(binding) {
|
|
18
|
+
return privateStorage(() => readOwnershipSnapshot(binding.cubeId, binding.representativeDroneId, representativeOwnerDeps(binding)));
|
|
19
|
+
}
|
|
20
|
+
async function privateStorage(operation) {
|
|
21
|
+
try {
|
|
22
|
+
return await operation();
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error instanceof RepresentativeError)
|
|
26
|
+
throw error;
|
|
27
|
+
const message = error instanceof Error ? error.message : 'invalid private state';
|
|
28
|
+
// Only this exact directory-mode refusal permits manual permission recovery.
|
|
29
|
+
// Symlink, ownership and other failures must not suggest chmod through a path.
|
|
30
|
+
const looseDirectory = /^Borg credential store root (.+) has insecure permissions; expected 0700$/s.exec(message);
|
|
31
|
+
const detail = looseDirectory
|
|
32
|
+
? `directory ${looseDirectory[1]} has insecure permissions; expected 0700. ` +
|
|
33
|
+
'Check that the named path is a real directory you own and not a symlink, then set it to 0700 and retry. ' +
|
|
34
|
+
'Restart a process that had already lost ownership.'
|
|
35
|
+
: message.replace(/^Borg credential store /, '');
|
|
36
|
+
throw new RepresentativeError('REPRESENTATIVE_OWNERSHIP_REQUIRED', 'Representative lease storage refused: ' + detail);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function createRepresentativeOwner(heartbeatIntervalMs = 20_000) {
|
|
40
|
+
let lease = null;
|
|
41
|
+
let selected;
|
|
42
|
+
let deps;
|
|
43
|
+
let lost = false, closed = false;
|
|
44
|
+
let timer;
|
|
45
|
+
let tail = Promise.resolve();
|
|
46
|
+
const processNonce = randomUUID();
|
|
47
|
+
// Serialize lease mutations, including heartbeats, but NOT tool execution:
|
|
48
|
+
// overlapping sends still use the existing atomic request reservation.
|
|
49
|
+
const serial = (op) => {
|
|
50
|
+
const result = tail.then(op);
|
|
51
|
+
tail = result.catch(() => { });
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
const snapshot = (binding) => privateStorage(() => readOwnershipSnapshot(binding.cubeId, binding.representativeDroneId, { ...representativeOwnerDeps(binding), processNonce }));
|
|
55
|
+
const refuse = async (binding) => {
|
|
56
|
+
const owner = await snapshot(binding);
|
|
57
|
+
throw new RepresentativeError('REPRESENTATIVE_OWNERSHIP_REQUIRED', `This representative is owned by another MCP process (pid ${owner.pid ?? 'unknown'}, started ${owner.startedAt ?? 'unknown'}), ` +
|
|
58
|
+
'or this process lost its lease. Use the owning host; after it exits or its lease expires, retry from the intended host. ' +
|
|
59
|
+
'Restart a process that lost ownership before using it again. Status remains available.', { owner });
|
|
60
|
+
};
|
|
61
|
+
const refresh = async () => {
|
|
62
|
+
try {
|
|
63
|
+
if (!lease || !await lease.refresh())
|
|
64
|
+
lost = true;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
lost = true;
|
|
68
|
+
}
|
|
69
|
+
if (lost && timer) {
|
|
70
|
+
clearInterval(timer);
|
|
71
|
+
timer = undefined;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
snapshot,
|
|
76
|
+
ensure: (binding) => serial(async () => {
|
|
77
|
+
if (selected && (selected.origin !== binding.origin || selected.trustIdentity !== binding.trustIdentity ||
|
|
78
|
+
selected.cubeId !== binding.cubeId || selected.representativeDroneId !== binding.representativeDroneId))
|
|
79
|
+
lost = true;
|
|
80
|
+
if (closed || lost)
|
|
81
|
+
return refuse(binding);
|
|
82
|
+
if (lease) {
|
|
83
|
+
await refresh();
|
|
84
|
+
if (lost)
|
|
85
|
+
return refuse(binding);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
deps = { ...representativeOwnerDeps(binding), processNonce };
|
|
89
|
+
lease = await privateStorage(() => acquireStreamLease(binding.cubeId, binding.representativeDroneId, STREAM_OWNER_STALE_MS, deps));
|
|
90
|
+
if (!lease)
|
|
91
|
+
return refuse(binding);
|
|
92
|
+
selected = binding;
|
|
93
|
+
timer = setInterval(() => { void serial(refresh); }, heartbeatIntervalMs);
|
|
94
|
+
timer.unref();
|
|
95
|
+
}),
|
|
96
|
+
close: () => serial(async () => {
|
|
97
|
+
closed = true;
|
|
98
|
+
if (timer) {
|
|
99
|
+
clearInterval(timer);
|
|
100
|
+
timer = undefined;
|
|
101
|
+
}
|
|
102
|
+
await lease?.release();
|
|
103
|
+
lease = null;
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=representative-owner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"representative-owner.js","sourceRoot":"","sources":["../src/representative-owner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private on-disk state for the human representative connection.
|
|
3
|
+
*
|
|
4
|
+
* One 0600 file under the Borg config root holds, per representative worktree:
|
|
5
|
+
* - the BINDING: the one explicit cube + Coordinator drone this worktree's
|
|
6
|
+
* dedicated seat may talk to. Changing it requires an explicit operator rebind.
|
|
7
|
+
* - the REQUEST LEDGER: one record per sent request id, so a retry never becomes
|
|
8
|
+
* a second message and an ambiguous send survives a reconnect.
|
|
9
|
+
*
|
|
10
|
+
* The file never holds a bearer (the seat store owns credentials) and never
|
|
11
|
+
* holds message text (only a payload digest).
|
|
12
|
+
*/
|
|
13
|
+
export declare function isRepresentativeUuid(value: unknown): value is string;
|
|
14
|
+
/**
|
|
15
|
+
* Host fence for one binding generation: hex SHA-256 of the canonical JSON array
|
|
16
|
+
* [origin, trustIdentity, cubeId, representativeDroneId, coordinatorDroneId,
|
|
17
|
+
* boundAt]. It changes on every rebind (boundAt) and trust change, and carries
|
|
18
|
+
* no path or credential.
|
|
19
|
+
*/
|
|
20
|
+
export declare function bindingFingerprint(binding: RepresentativeBinding): string;
|
|
21
|
+
export interface RepresentativeBinding {
|
|
22
|
+
/** Canonical worktree holding the dedicated representative seat. */
|
|
23
|
+
worktree: string;
|
|
24
|
+
origin: string;
|
|
25
|
+
trustIdentity: string;
|
|
26
|
+
cubeId: string;
|
|
27
|
+
cubeName: string;
|
|
28
|
+
representativeDroneId: string;
|
|
29
|
+
representativeLabel: string;
|
|
30
|
+
representativeRoleName: string;
|
|
31
|
+
coordinatorDroneId: string;
|
|
32
|
+
coordinatorLabel: string;
|
|
33
|
+
coordinatorRoleName: string;
|
|
34
|
+
repositoryOrigin?: string;
|
|
35
|
+
boundAt: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function representativeRecoveryCommand(binding: RepresentativeBinding): string;
|
|
38
|
+
export type RepresentativeRequestState = 'pending' | 'ambiguous' | 'sent' | 'rejected';
|
|
39
|
+
export interface RepresentativeRequestRecord {
|
|
40
|
+
/** Stable request identity; also the protocol `post_id` idempotency key. */
|
|
41
|
+
requestId: string;
|
|
42
|
+
/** sha256 of kind, authorization, message, cube and Coordinator — never the text. */
|
|
43
|
+
payloadDigest: string;
|
|
44
|
+
kind: string;
|
|
45
|
+
authorization: string;
|
|
46
|
+
state: RepresentativeRequestState;
|
|
47
|
+
entryId?: string;
|
|
48
|
+
/** An attempt under this id may have reached the log; a later refusal must not clear that. */
|
|
49
|
+
maybeStored?: boolean;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
updatedAt: string;
|
|
52
|
+
}
|
|
53
|
+
export declare class RepresentativeStoreError extends Error {
|
|
54
|
+
readonly code: 'BINDING_CONFLICT';
|
|
55
|
+
constructor(code: 'BINDING_CONFLICT', message: string);
|
|
56
|
+
}
|
|
57
|
+
export interface RepresentativeStore {
|
|
58
|
+
getBinding(worktree: string): Promise<RepresentativeBinding | null>;
|
|
59
|
+
readRequests(worktree: string): Promise<RepresentativeRequestRecord[]>;
|
|
60
|
+
saveBinding(binding: RepresentativeBinding, options: {
|
|
61
|
+
rebind: boolean;
|
|
62
|
+
}): Promise<'created' | 'unchanged' | 'rebound'>;
|
|
63
|
+
/** Read-compare-write over one worktree's ledger under the store lock. */
|
|
64
|
+
transactRequests<T>(worktree: string, op: (records: RepresentativeRequestRecord[]) => T): Promise<T>;
|
|
65
|
+
}
|
|
66
|
+
export declare function representativeStorePath(): string;
|
|
67
|
+
export declare function createRepresentativeStore(storePath?: string): RepresentativeStore;
|
|
68
|
+
//# sourceMappingURL=representative-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"representative-store.d.ts","sourceRoot":"","sources":["../src/representative-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEpE;AAGD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAKzE;AAED,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAEpF;AAED,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,GAAG,UAAU,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC1C,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,0BAA0B,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8FAA8F;IAC9F,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAQD,qBAAa,wBAAyB,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,kBAAkB;gBAAxB,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM;CAI/D;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IACpE,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,EAAE,CAAC,CAAC;IACvE,WAAW,CACT,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAC3B,OAAO,CAAC,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC,CAAC;IAChD,0EAA0E;IAC1E,gBAAgB,CAAC,CAAC,EAChB,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,CAAC,OAAO,EAAE,2BAA2B,EAAE,KAAK,CAAC,GAChD,OAAO,CAAC,CAAC,CAAC,CAAC;CACf;AAoED,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD;AAED,wBAAgB,yBAAyB,CAAC,SAAS,GAAE,MAAkC,GAAG,mBAAmB,CAkD5G"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private on-disk state for the human representative connection.
|
|
3
|
+
*
|
|
4
|
+
* One 0600 file under the Borg config root holds, per representative worktree:
|
|
5
|
+
* - the BINDING: the one explicit cube + Coordinator drone this worktree's
|
|
6
|
+
* dedicated seat may talk to. Changing it requires an explicit operator rebind.
|
|
7
|
+
* - the REQUEST LEDGER: one record per sent request id, so a retry never becomes
|
|
8
|
+
* a second message and an ambiguous send survives a reconnect.
|
|
9
|
+
*
|
|
10
|
+
* The file never holds a bearer (the seat store owns credentials) and never
|
|
11
|
+
* holds message text (only a payload digest).
|
|
12
|
+
*/
|
|
13
|
+
import { decodeUuid } from 'borgmcp-shared/protocol';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { borgConfigRoot } from './private-root.js';
|
|
17
|
+
import { shellEscape } from './shell-escape.js';
|
|
18
|
+
import { readStoreFile, withStore } from './seat-store.js';
|
|
19
|
+
export function isRepresentativeUuid(value) {
|
|
20
|
+
try {
|
|
21
|
+
decodeUuid(value);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const SETTLED_REQUEST_LIMIT = 200;
|
|
29
|
+
/**
|
|
30
|
+
* Host fence for one binding generation: hex SHA-256 of the canonical JSON array
|
|
31
|
+
* [origin, trustIdentity, cubeId, representativeDroneId, coordinatorDroneId,
|
|
32
|
+
* boundAt]. It changes on every rebind (boundAt) and trust change, and carries
|
|
33
|
+
* no path or credential.
|
|
34
|
+
*/
|
|
35
|
+
export function bindingFingerprint(binding) {
|
|
36
|
+
return createHash('sha256').update(JSON.stringify([
|
|
37
|
+
binding.origin, binding.trustIdentity, binding.cubeId,
|
|
38
|
+
binding.representativeDroneId, binding.coordinatorDroneId, binding.boundAt,
|
|
39
|
+
])).digest('hex');
|
|
40
|
+
}
|
|
41
|
+
export function representativeRecoveryCommand(binding) {
|
|
42
|
+
return `cd ${shellEscape(binding.worktree)} && borg representative prepare --coordinator ${shellEscape(binding.coordinatorLabel)} --role ${shellEscape(binding.representativeRoleName)} --rebind`;
|
|
43
|
+
}
|
|
44
|
+
export class RepresentativeStoreError extends Error {
|
|
45
|
+
code;
|
|
46
|
+
constructor(code, message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.code = code;
|
|
49
|
+
this.name = 'RepresentativeStoreError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const BINDING_STRING_FIELDS = [
|
|
53
|
+
'worktree', 'origin', 'trustIdentity', 'cubeId', 'cubeName',
|
|
54
|
+
'representativeDroneId', 'representativeLabel', 'representativeRoleName',
|
|
55
|
+
'coordinatorDroneId', 'coordinatorLabel', 'coordinatorRoleName', 'boundAt',
|
|
56
|
+
];
|
|
57
|
+
function validBinding(value, key) {
|
|
58
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
59
|
+
return false;
|
|
60
|
+
const binding = value;
|
|
61
|
+
return BINDING_STRING_FIELDS.every((field) => typeof binding[field] === 'string' && binding[field] !== '') &&
|
|
62
|
+
binding.worktree === key &&
|
|
63
|
+
isRepresentativeUuid(binding.cubeId) &&
|
|
64
|
+
isRepresentativeUuid(binding.representativeDroneId) &&
|
|
65
|
+
isRepresentativeUuid(binding.coordinatorDroneId) &&
|
|
66
|
+
binding.representativeDroneId !== binding.coordinatorDroneId &&
|
|
67
|
+
(binding.repositoryOrigin === undefined || typeof binding.repositoryOrigin === 'string');
|
|
68
|
+
}
|
|
69
|
+
function validRequest(value) {
|
|
70
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
71
|
+
return false;
|
|
72
|
+
const record = value;
|
|
73
|
+
return isRepresentativeUuid(String(record.requestId ?? '')) &&
|
|
74
|
+
typeof record.payloadDigest === 'string' &&
|
|
75
|
+
typeof record.kind === 'string' &&
|
|
76
|
+
typeof record.authorization === 'string' &&
|
|
77
|
+
['pending', 'ambiguous', 'sent', 'rejected'].includes(record.state) &&
|
|
78
|
+
(record.entryId === undefined || typeof record.entryId === 'string') &&
|
|
79
|
+
(record.maybeStored === undefined || typeof record.maybeStored === 'boolean') &&
|
|
80
|
+
typeof record.createdAt === 'string' &&
|
|
81
|
+
typeof record.updatedAt === 'string';
|
|
82
|
+
}
|
|
83
|
+
function parseFile(raw) {
|
|
84
|
+
const parsed = JSON.parse(raw);
|
|
85
|
+
if (parsed === null || typeof parsed !== 'object' || parsed.version !== 1 ||
|
|
86
|
+
parsed.bindings === null || typeof parsed.bindings !== 'object' || Array.isArray(parsed.bindings) ||
|
|
87
|
+
parsed.requests === null || typeof parsed.requests !== 'object' || Array.isArray(parsed.requests))
|
|
88
|
+
return null;
|
|
89
|
+
for (const [key, binding] of Object.entries(parsed.bindings)) {
|
|
90
|
+
if (!validBinding(binding, key))
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
for (const records of Object.values(parsed.requests)) {
|
|
94
|
+
if (!Array.isArray(records) || !records.every(validRequest))
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return parsed;
|
|
98
|
+
}
|
|
99
|
+
const emptyFile = () => ({ version: 1, bindings: {}, requests: {} });
|
|
100
|
+
function sameSelection(a, b) {
|
|
101
|
+
return a.origin === b.origin &&
|
|
102
|
+
a.trustIdentity === b.trustIdentity &&
|
|
103
|
+
a.cubeId === b.cubeId &&
|
|
104
|
+
a.representativeDroneId === b.representativeDroneId &&
|
|
105
|
+
a.coordinatorDroneId === b.coordinatorDroneId;
|
|
106
|
+
}
|
|
107
|
+
/** Settled history is bounded; unresolved (pending/ambiguous) records are never dropped. */
|
|
108
|
+
function pruneSettled(records) {
|
|
109
|
+
const settled = records.filter((record) => record.state === 'sent' || record.state === 'rejected');
|
|
110
|
+
if (settled.length <= SETTLED_REQUEST_LIMIT)
|
|
111
|
+
return records;
|
|
112
|
+
const drop = new Set(settled.slice(0, settled.length - SETTLED_REQUEST_LIMIT).map((record) => record.requestId));
|
|
113
|
+
return records.filter((record) => !drop.has(record.requestId));
|
|
114
|
+
}
|
|
115
|
+
export function representativeStorePath() {
|
|
116
|
+
return join(borgConfigRoot(), 'representative.json');
|
|
117
|
+
}
|
|
118
|
+
export function createRepresentativeStore(storePath = representativeStorePath()) {
|
|
119
|
+
// Atomic writer renames make a single read a complete snapshot, without a
|
|
120
|
+
// store-lock write or pruning settled history during read-only status.
|
|
121
|
+
const read = async () => {
|
|
122
|
+
const raw = await readStoreFile(storePath);
|
|
123
|
+
if (raw === null)
|
|
124
|
+
return emptyFile();
|
|
125
|
+
let data;
|
|
126
|
+
try {
|
|
127
|
+
data = parseFile(raw);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
data = null;
|
|
131
|
+
}
|
|
132
|
+
if (!data)
|
|
133
|
+
throw new Error('Borg representative store is malformed or unsupported; refusing to read it');
|
|
134
|
+
return data;
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
getBinding: async (worktree) => (await read()).bindings[worktree] ?? null,
|
|
138
|
+
readRequests: async (worktree) => (await read()).requests[worktree] ?? [],
|
|
139
|
+
saveBinding: (binding, options) => withStore(storePath, emptyFile, parseFile, async (txn) => {
|
|
140
|
+
if (!validBinding(binding, binding.worktree)) {
|
|
141
|
+
throw new Error('Refusing to save an invalid representative binding');
|
|
142
|
+
}
|
|
143
|
+
const existing = txn.data.bindings[binding.worktree];
|
|
144
|
+
// An explicit rebind always starts a new generation (new boundAt, so a new
|
|
145
|
+
// binding_fingerprint), even for the same selection.
|
|
146
|
+
if (existing && sameSelection(existing, binding) && !options.rebind)
|
|
147
|
+
return 'unchanged';
|
|
148
|
+
if (existing && !options.rebind) {
|
|
149
|
+
throw new RepresentativeStoreError('BINDING_CONFLICT', `This worktree is already bound to Coordinator ${existing.coordinatorLabel} in cube ${existing.cubeName}. ` +
|
|
150
|
+
`To confirm the new selection, run \`${representativeRecoveryCommand(binding)}\`.`);
|
|
151
|
+
}
|
|
152
|
+
txn.data.bindings[binding.worktree] = binding;
|
|
153
|
+
// A different selection invalidates the old ledger: its post ids belong to
|
|
154
|
+
// another cube/Coordinator conversation.
|
|
155
|
+
if (existing && !sameSelection(existing, binding))
|
|
156
|
+
delete txn.data.requests[binding.worktree];
|
|
157
|
+
await txn.commit();
|
|
158
|
+
return existing ? 'rebound' : 'created';
|
|
159
|
+
}),
|
|
160
|
+
transactRequests: (worktree, op) => withStore(storePath, emptyFile, parseFile, async (txn) => {
|
|
161
|
+
const records = txn.data.requests[worktree] ?? [];
|
|
162
|
+
const before = JSON.stringify(records);
|
|
163
|
+
const result = op(records);
|
|
164
|
+
const next = pruneSettled(records);
|
|
165
|
+
if (JSON.stringify(next) !== before) {
|
|
166
|
+
txn.data.requests[worktree] = next;
|
|
167
|
+
await txn.commit();
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=representative-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"representative-store.js","sourceRoot":"","sources":["../src/representative-store.ts"],"names":[],"mappings":"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"}
|
package/dist/seat-probe.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { ActiveCube } from './cubes.js';
|
|
|
24
24
|
* delete or a seat reset
|
|
25
25
|
*/
|
|
26
26
|
export type SeatStatus = 'evicted' | 'revoked' | 'rejected' | 'live' | 'credential-rejected' | 'trust-mismatch' | 'unreachable' | 'endpoint-mismatch' | 'server-failure' | 'indeterminate';
|
|
27
|
+
export declare function isTransportFailure(err: unknown): boolean;
|
|
27
28
|
/**
|
|
28
29
|
* Default seat probe: a lightweight drone-authed `whoami` with the seat's OWN
|
|
29
30
|
* saved token. authedFetch throws TYPED errors on the authoritative outcomes
|
package/dist/seat-probe.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seat-probe.d.ts","sourceRoot":"","sources":["../src/seat-probe.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAS7C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,UAAU,GAClB,SAAS,GACT,SAAS,GACT,UAAU,GACV,MAAM,GACN,qBAAqB,GACrB,gBAAgB,GAChB,aAAa,GACb,mBAAmB,GACnB,gBAAgB,GAChB,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"seat-probe.d.ts","sourceRoot":"","sources":["../src/seat-probe.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAS7C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,UAAU,GAClB,SAAS,GACT,SAAS,GACT,UAAU,GACV,MAAM,GACN,qBAAqB,GACrB,gBAAgB,GAChB,aAAa,GACb,mBAAmB,GACnB,gBAAgB,GAChB,eAAe,CAAC;AAmBpB,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMxD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,UAAU,CAAC,CAwBrB"}
|
package/dist/seat-probe.js
CHANGED
package/dist/seat-probe.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seat-probe.js","sourceRoot":"","sources":["../src/seat-probe.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,EAAE;AACF,iFAAiF;AACjF,gFAAgF;AAChF,gFAAgF;AAChF,0EAA0E;AAE1E,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAsC5B,gFAAgF;AAChF,8EAA8E;AAC9E,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,cAAc;IACd,YAAY;IACZ,WAAW;IACX,cAAc;IACd,aAAa;IACb,WAAW;IACX,WAAW;IACX,OAAO;IACP,yBAAyB;IACzB,yBAAyB;IACzB,gBAAgB;IAChB,WAAW;CACZ,CAAC,CAAC;AAEH,
|
|
1
|
+
{"version":3,"file":"seat-probe.js","sourceRoot":"","sources":["../src/seat-probe.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,EAAE;AACF,iFAAiF;AACjF,gFAAgF;AAChF,gFAAgF;AAChF,0EAA0E;AAE1E,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAsC5B,gFAAgF;AAChF,8EAA8E;AAC9E,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,cAAc;IACd,YAAY;IACZ,WAAW;IACX,cAAc;IACd,aAAa;IACb,WAAW;IACX,WAAW;IACX,OAAO;IACP,yBAAyB;IACzB,yBAAyB;IACzB,gBAAgB;IAChB,WAAW;CACZ,CAAC,CAAC;AAEH,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,IAAI,GAAG,YAAY,0BAA0B;QAAE,OAAO,IAAI,CAAC;IAC3D,MAAM,CAAC,GAAG,GAAkE,CAAC;IAC7E,IAAI,CAAC,EAAE,IAAI,KAAK,YAAY;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,IAAI,GAAG,CAAC,EAAE,IAAI,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC;IACvC,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAgB;IAEhB,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;QACnB,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,iBAAiB;YAAE,OAAO,SAAS,CAAC;QACvD,gFAAgF;QAChF,mEAAmE;QACnE,IAAI,GAAG,YAAY,oBAAoB;YAAE,OAAO,gBAAgB,CAAC;QACjE,IAAI,GAAG,YAAY,eAAe,EAAE,CAAC;YACnC,IAAI,GAAG,CAAC,IAAI,KAAK,iBAAiB;gBAAE,OAAO,SAAS,CAAC;YACrD,IAAI,GAAG,CAAC,IAAI,KAAK,kBAAkB;gBAAE,OAAO,UAAU,CAAC;YACvD,sEAAsE;YACtE,oEAAoE;YACpE,IAAI,GAAG,CAAC,IAAI,KAAK,qBAAqB;gBAAE,OAAO,qBAAqB,CAAC;QACvE,CAAC;QACD,IAAI,GAAG,YAAY,mBAAmB,EAAE,CAAC;YACvC,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;gBAAE,OAAO,mBAAmB,CAAC;YACnD,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG;gBAAE,OAAO,gBAAgB,CAAC;YACpE,OAAO,eAAe,CAAC;QACzB,CAAC;QACD,IAAI,kBAAkB,CAAC,GAAG,CAAC;YAAE,OAAO,aAAa,CAAC;QAClD,OAAO,eAAe,CAAC;IACzB,CAAC;AACH,CAAC"}
|
package/dist/seat-store.d.ts
CHANGED
|
@@ -33,7 +33,15 @@
|
|
|
33
33
|
export interface SecureStoreOptions {
|
|
34
34
|
secureRoot?: string;
|
|
35
35
|
rootMode?: 'private' | 'owner-controlled';
|
|
36
|
+
/** Harden leaf publication for private lease records; default store behavior is unchanged. */
|
|
37
|
+
verifyLeafIdentity?: boolean;
|
|
38
|
+
/** Publish without replacing an existing leaf (lease takeover marker). */
|
|
39
|
+
exclusive?: boolean;
|
|
40
|
+
/** Read-only lease inspection must not recreate a directory moved by its owner. */
|
|
41
|
+
createRoot?: boolean;
|
|
36
42
|
}
|
|
43
|
+
/** Shared directory policy; create=false validates without mutating and returns false if absent. */
|
|
44
|
+
export declare function assertSecureRoot(root: string, rootMode?: SecureStoreOptions['rootMode'], create?: boolean): Promise<boolean>;
|
|
37
45
|
/**
|
|
38
46
|
* Atomic 0600 write (checklist #1 + #2): write to a same-dir temp opened
|
|
39
47
|
* O_CREAT|O_EXCL at mode 0600 (never write-then-chmod), fsync the data, then
|
|
@@ -41,6 +49,11 @@ export interface SecureStoreOptions {
|
|
|
41
49
|
* cleans up the temp so no leftover file ever holds the secret.
|
|
42
50
|
*/
|
|
43
51
|
export declare function atomicWrite0600(filePath: string, data: string, options?: SecureStoreOptions): Promise<void>;
|
|
52
|
+
/** Identity drift is retryable only when inspecting an acquisition lock. */
|
|
53
|
+
export declare class StoreFileIdentityChangedError extends Error {
|
|
54
|
+
readonly code = "STORE_FILE_IDENTITY_CHANGED";
|
|
55
|
+
constructor();
|
|
56
|
+
}
|
|
44
57
|
/**
|
|
45
58
|
* Read the store file, or null when it does not exist (ONLY the missing-file
|
|
46
59
|
* no-op path initializes empty). When the file exists, the 0600-store + 0700-parent
|
package/dist/seat-store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seat-store.d.ts","sourceRoot":"","sources":["../src/seat-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAiBH,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,SAAS,GAAG,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"seat-store.d.ts","sourceRoot":"","sources":["../src/seat-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAiBH,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,SAAS,GAAG,kBAAkB,CAAC;IAC1C,8FAA8F;IAC9F,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,0EAA0E;IAC1E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,mFAAmF;IACnF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAMD,oGAAoG;AACpG,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,kBAAkB,CAAC,UAAU,CAAa,EACpD,MAAM,UAAO,GACZ,OAAO,CAAC,OAAO,CAAC,CA+BlB;AA2HD;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAwDf;AAkCD,4EAA4E;AAC5E,qBAAa,6BAA8B,SAAQ,KAAK;IACtD,QAAQ,CAAC,IAAI,iCAAiC;;CAK/C;AAOD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmDxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,kBAAuB,GACrE,OAAO,CAAC,CAAC,CAAC,CAkFZ;AAED,2DAA2D;AAC3D,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,gFAAgF;IAChF,IAAI,EAAE,CAAC,CAAC;IACR,6EAA6E;IAC7E,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAAE,CAAC,EAClC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,CAAC,EACnB,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,GAAG,IAAI,EAChC,EAAE,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EACpC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,CAAC,CAAC,CAgCZ"}
|
package/dist/seat-store.js
CHANGED
|
@@ -40,7 +40,8 @@ const LOCK_ATTEMPTS = 500;
|
|
|
40
40
|
function expectedUid() {
|
|
41
41
|
return typeof process.getuid === 'function' ? process.getuid() : null;
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
/** Shared directory policy; create=false validates without mutating and returns false if absent. */
|
|
44
|
+
export async function assertSecureRoot(root, rootMode = 'private', create = true) {
|
|
44
45
|
if (!isCanonicalPath(root)) {
|
|
45
46
|
throw new Error(`Borg credential store path ${root} is not canonical`);
|
|
46
47
|
}
|
|
@@ -51,6 +52,8 @@ async function assertSecureRoot(root, rootMode = 'private') {
|
|
|
51
52
|
catch (error) {
|
|
52
53
|
if (error.code !== 'ENOENT')
|
|
53
54
|
throw error;
|
|
55
|
+
if (!create)
|
|
56
|
+
return false;
|
|
54
57
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
55
58
|
metadata = await lstat(root);
|
|
56
59
|
}
|
|
@@ -69,6 +72,7 @@ async function assertSecureRoot(root, rootMode = 'private') {
|
|
|
69
72
|
if (!isCanonicalPath(root)) {
|
|
70
73
|
throw new Error(`Borg credential store root ${root} is not canonical or contains a symlink`);
|
|
71
74
|
}
|
|
75
|
+
return true;
|
|
72
76
|
}
|
|
73
77
|
async function assertSecureFile(filePath) {
|
|
74
78
|
let metadata;
|
|
@@ -97,7 +101,9 @@ async function assertSecurePath(filePath, options) {
|
|
|
97
101
|
if (!isAbsolute(filePath) || resolve(filePath) !== filePath || dirname(filePath) !== secureRoot) {
|
|
98
102
|
throw new Error(`Borg credential store file path ${filePath} is not canonical`);
|
|
99
103
|
}
|
|
100
|
-
await assertSecureRoot(secureRoot, options.rootMode)
|
|
104
|
+
if (!await assertSecureRoot(secureRoot, options.rootMode, options.createRoot !== false)) {
|
|
105
|
+
throw Object.assign(new Error('Borg private store directory disappeared'), { code: 'ENOENT' });
|
|
106
|
+
}
|
|
101
107
|
}
|
|
102
108
|
/**
|
|
103
109
|
* Liveness check for the alive/dead branch (RULED option b). `process.kill(pid, 0)`
|
|
@@ -199,14 +205,30 @@ export async function atomicWrite0600(filePath, data, options = {}) {
|
|
|
199
205
|
if (options.secureRoot)
|
|
200
206
|
await assertSecureFile(filePath);
|
|
201
207
|
const tmp = `${filePath}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
202
|
-
const handle = await open(tmp,
|
|
208
|
+
const handle = await open(tmp, options.verifyLeafIdentity
|
|
209
|
+
? constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW
|
|
210
|
+
: 'wx', 0o600);
|
|
211
|
+
let opened = null;
|
|
212
|
+
const cleanup = async () => {
|
|
213
|
+
if (options.verifyLeafIdentity) {
|
|
214
|
+
const current = await lstat(tmp).catch(() => null);
|
|
215
|
+
if (!opened || !current || current.dev !== opened.dev || current.ino !== opened.ino)
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
await unlink(tmp).catch(() => { });
|
|
219
|
+
};
|
|
203
220
|
try {
|
|
221
|
+
opened = options.verifyLeafIdentity ? await handle.stat() : null;
|
|
222
|
+
if (opened && (!opened.isFile() || (opened.mode & 0o777) !== 0o600 ||
|
|
223
|
+
(expectedUid() !== null && opened.uid !== expectedUid()))) {
|
|
224
|
+
throw new Error('Borg private store temporary file is not private');
|
|
225
|
+
}
|
|
204
226
|
await handle.writeFile(data);
|
|
205
227
|
await handle.sync();
|
|
206
228
|
}
|
|
207
229
|
catch (err) {
|
|
208
230
|
await handle.close().catch(() => { });
|
|
209
|
-
await
|
|
231
|
+
await cleanup();
|
|
210
232
|
throw err;
|
|
211
233
|
}
|
|
212
234
|
await handle.close();
|
|
@@ -215,7 +237,19 @@ export async function atomicWrite0600(filePath, data, options = {}) {
|
|
|
215
237
|
await assertSecurePath(filePath, options);
|
|
216
238
|
await assertSecureFile(filePath);
|
|
217
239
|
}
|
|
218
|
-
|
|
240
|
+
if (opened) {
|
|
241
|
+
const current = await lstat(tmp);
|
|
242
|
+
if (!current.isFile() || current.dev !== opened.dev || current.ino !== opened.ino ||
|
|
243
|
+
current.uid !== opened.uid || (current.mode & 0o777) !== 0o600) {
|
|
244
|
+
throw new Error('Borg private store temporary file identity changed');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (options.exclusive) {
|
|
248
|
+
await link(tmp, filePath);
|
|
249
|
+
await unlink(tmp);
|
|
250
|
+
}
|
|
251
|
+
else
|
|
252
|
+
await rename(tmp, filePath);
|
|
219
253
|
const parent = await open(dirname(filePath), 'r');
|
|
220
254
|
try {
|
|
221
255
|
await parent.sync();
|
|
@@ -225,7 +259,7 @@ export async function atomicWrite0600(filePath, data, options = {}) {
|
|
|
225
259
|
}
|
|
226
260
|
}
|
|
227
261
|
catch (err) {
|
|
228
|
-
await
|
|
262
|
+
await cleanup();
|
|
229
263
|
throw err;
|
|
230
264
|
}
|
|
231
265
|
}
|
|
@@ -255,6 +289,17 @@ async function assertSecureStorePerms(filePath, fileMode) {
|
|
|
255
289
|
`(0${(dirStat.mode & 0o777).toString(8)}, expected 0700); refusing to read a credential under it`);
|
|
256
290
|
}
|
|
257
291
|
}
|
|
292
|
+
/** Identity drift is retryable only when inspecting an acquisition lock. */
|
|
293
|
+
export class StoreFileIdentityChangedError extends Error {
|
|
294
|
+
code = 'STORE_FILE_IDENTITY_CHANGED';
|
|
295
|
+
constructor() {
|
|
296
|
+
super('Borg credential store file changed while it was being opened');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function isLockTurnover(error) {
|
|
300
|
+
const code = error.code;
|
|
301
|
+
return code === 'ENOENT' || code === 'STORE_FILE_IDENTITY_CHANGED';
|
|
302
|
+
}
|
|
258
303
|
/**
|
|
259
304
|
* Read the store file, or null when it does not exist (ONLY the missing-file
|
|
260
305
|
* no-op path initializes empty). When the file exists, the 0600-store + 0700-parent
|
|
@@ -283,7 +328,7 @@ export async function readStoreFile(filePath, options = {}) {
|
|
|
283
328
|
try {
|
|
284
329
|
const opened = await handle.stat();
|
|
285
330
|
if (opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
286
|
-
throw new
|
|
331
|
+
throw new StoreFileIdentityChangedError();
|
|
287
332
|
}
|
|
288
333
|
const raw = await handle.readFile('utf8');
|
|
289
334
|
const after = await handle.stat();
|
|
@@ -376,8 +421,8 @@ export async function withStoreLock(lockPath, op, opts = {}) {
|
|
|
376
421
|
raw = stored;
|
|
377
422
|
}
|
|
378
423
|
catch (readErr) {
|
|
379
|
-
if (readErr
|
|
380
|
-
continue; // released — retry
|
|
424
|
+
if (isLockTurnover(readErr))
|
|
425
|
+
continue; // released or replaced — retry
|
|
381
426
|
throw readErr;
|
|
382
427
|
}
|
|
383
428
|
const held = parseLockPayload(raw);
|
|
@@ -398,7 +443,7 @@ export async function withStoreLock(lockPath, op, opts = {}) {
|
|
|
398
443
|
: {});
|
|
399
444
|
}
|
|
400
445
|
catch (readErr) {
|
|
401
|
-
if (readErr
|
|
446
|
+
if (isLockTurnover(readErr))
|
|
402
447
|
continue;
|
|
403
448
|
throw readErr;
|
|
404
449
|
}
|