weir-harness 1.2.2 → 2.0.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/cordis.patch.yml +7 -18
- package/lib/client.file-lease-badge.js +307 -0
- package/lib/client.js +20 -30
- package/lib/client.settings-page.js +10 -34
- package/package.json +2 -2
- package/src/capabilities/store/checked-contract.d.ts +2 -2
- package/src/capabilities/store/fs-adapter.js +2 -3
- package/src/context-guard/index.js +2 -2
- package/src/delegate/supervision-mount.js +5 -5
- package/src/delegate/tool.js +2 -10
- package/src/file-lease/client.js +1066 -0
- package/src/file-lease/index.js +30 -0
- package/src/file-lease/lineage.js +207 -0
- package/src/file-lease/node-contracts.d.ts +42 -0
- package/src/file-lease/plugin.js +789 -0
- package/src/file-lease/singleton.js +155 -0
- package/src/file-lease/spawn.js +62 -0
- package/src/file-lease/tools.js +459 -0
- package/src/file-lease/view.js +144 -0
- package/src/hashline-edit/index.js +76 -42
- package/src/hashline-edit/write-tool.js +90 -0
- package/src/intent-gate/matcher.js +27 -0
- package/src/lease-broker/entry.js +98 -0
- package/src/lease-broker/node-contracts.d.ts +63 -0
- package/src/lease-broker/protocol.js +236 -0
- package/src/lease-broker/server.js +399 -0
- package/src/lease-broker/state.js +425 -0
- package/src/lsp/admin.js +24 -13
- package/src/lsp/index.js +5 -2
- package/src/lsp/registry.js +83 -17
- package/src/lsp/tools.js +137 -27
- package/src/notify/commands.js +15 -2
- package/src/notify/index.js +48 -10
- package/src/notify/web-channel.js +26 -3
- package/src/process/index.js +11 -0
- package/src/settings/index.js +2 -44
- package/src/settings/sections.js +29 -28
- package/src/shared/audit.js +6 -1
- package/src/todo-driver/index.js +1 -6
- package/src/worktree/cards.js +6 -21
- package/src/worktree/lane-core.js +1 -19
- package/src/worktree/lane-workflows.js +11 -32
- package/lib/client.edit-lock-maintenance.js +0 -592
- package/lib/client.edit-lock-panel.js +0 -405
- package/src/edit-lock/adapter.js +0 -57
- package/src/edit-lock/admin-ledger.js +0 -149
- package/src/edit-lock/admin-recovery.js +0 -151
- package/src/edit-lock/admission.js +0 -82
- package/src/edit-lock/binding-attestation.js +0 -29
- package/src/edit-lock/binding-guidance.js +0 -72
- package/src/edit-lock/binding-machine.js +0 -312
- package/src/edit-lock/call-context.js +0 -65
- package/src/edit-lock/derived-facts.js +0 -67
- package/src/edit-lock/domain.js +0 -165
- package/src/edit-lock/domains.js +0 -170
- package/src/edit-lock/host.js +0 -149
- package/src/edit-lock/incarnations.js +0 -150
- package/src/edit-lock/index.js +0 -1303
- package/src/edit-lock/inspect.js +0 -219
- package/src/edit-lock/lifecycle.js +0 -579
- package/src/edit-lock/limits.js +0 -61
- package/src/edit-lock/maintenance.js +0 -310
- package/src/edit-lock/manager.js +0 -875
- package/src/edit-lock/negotiation.js +0 -132
- package/src/edit-lock/operation-history.js +0 -214
- package/src/edit-lock/peer-client.js +0 -86
- package/src/edit-lock/peer-transport.js +0 -76
- package/src/edit-lock/peer.js +0 -87
- package/src/edit-lock/publisher.js +0 -191
- package/src/edit-lock/read-authority.js +0 -101
- package/src/edit-lock/recovery.js +0 -139
- package/src/edit-lock/remote.js +0 -362
- package/src/edit-lock/request-binding.js +0 -42
- package/src/edit-lock/request-data.js +0 -40
- package/src/edit-lock/reservation.js +0 -301
- package/src/edit-lock/reserved-runtime.js +0 -45
- package/src/edit-lock/resource-identity.js +0 -130
- package/src/edit-lock/runtime.js +0 -145
- package/src/edit-lock/settle.js +0 -144
- package/src/edit-lock/snapshot.js +0 -175
- package/src/edit-lock/stale-sweep.js +0 -120
- package/src/edit-lock/state.js +0 -462
- package/src/edit-lock/store.js +0 -327
- package/src/edit-lock/tool-scope.js +0 -29
- package/src/edit-lock/view.js +0 -170
- package/src/edit-lock/write-scopes.js +0 -48
- package/src/edit-lock/write-tool.js +0 -45
- package/src/shared/delegate-edit-lifecycle.js +0 -36
- package/src/worktree/authority.js +0 -56
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
// Settings-plane maintenance remains reachable while enforcement is disabled.
|
|
2
|
-
// Inspection is read-only; explicit offline ADMIN OVERRIDE uses its own durable
|
|
3
|
-
// ledger and publisher reservation. No model tools or automatic repair.
|
|
4
|
-
import { lstatSync, readdirSync, realpathSync } from 'node:fs'
|
|
5
|
-
import { join } from 'node:path'
|
|
6
|
-
import { AUTHORITY_DIR } from './domains.js'
|
|
7
|
-
import { reservationPathFor } from './reservation.js'
|
|
8
|
-
import { parseSnapshot } from './snapshot.js'
|
|
9
|
-
import { maintenanceState, summarizeAuthorityImage, recoveryProposals } from './inspect.js'
|
|
10
|
-
import { readSnapshotBytes, checkedPath, verifyPins, missing, errorMessage, MAX_SNAPSHOT_BYTES } from './read-authority.js'
|
|
11
|
-
import { recoverAuthority } from './admin-recovery.js'
|
|
12
|
-
import { createAudit, AUDIT_TYPES } from '../shared/audit.js'
|
|
13
|
-
|
|
14
|
-
const STATUS_PATH = '/api/weir-edit-lock/maintenance/status'
|
|
15
|
-
const INSPECT_PATH = '/api/weir-edit-lock/maintenance/inspect'
|
|
16
|
-
|
|
17
|
-
/** @param {unknown} payload @param {number} [status] */
|
|
18
|
-
function reply(payload, status = 200) {
|
|
19
|
-
return new Response(JSON.stringify(payload), { status, headers: { 'content-type': 'application/json' } })
|
|
20
|
-
}
|
|
21
|
-
/** @param {string} message @param {number} [status] @param {string} [code] */
|
|
22
|
-
function failure(message, status = 400, code) {
|
|
23
|
-
return reply({ ok: false, error: { code: code ?? `weir-edit-lock/${status === 400 ? 'invalid' : 'internal'}`, message } }, status)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Host-lifetime mount evidence exposed by the settings row. Each preset mount
|
|
28
|
-
* owns one generation and reports installation, domain opens and failures.
|
|
29
|
-
* Reporting only: nothing here gates authority, admission, tools or settings.
|
|
30
|
-
* No generations, mixed decisions or incomplete lifecycles mean 'unknown'.
|
|
31
|
-
*/
|
|
32
|
-
export function createEditLockEvidence() {
|
|
33
|
-
const mounts = new Map()
|
|
34
|
-
let next = 0
|
|
35
|
-
return {
|
|
36
|
-
// Each handle owns exactly one generation. Late callbacks cannot resurrect it.
|
|
37
|
-
recordMount(info) {
|
|
38
|
-
const id = ++next
|
|
39
|
-
const row = { id, enabled: info.enabled === true, pinned: info.pinned === true,
|
|
40
|
-
fixed: info.fixed === true, phase: info.phase ?? 'installed', at: Date.now(),
|
|
41
|
-
domains: new Map(), failures: new Map(), error: /** @type {string | null} */ (null) }
|
|
42
|
-
mounts.set(id, row)
|
|
43
|
-
return {
|
|
44
|
-
installed() { if (mounts.has(id) && row.phase === 'installing') row.phase = 'installed' },
|
|
45
|
-
disposing() { if (mounts.has(id)) row.phase = 'disposing' },
|
|
46
|
-
failed(error) { if (mounts.has(id)) { row.phase = 'failed'; row.error = String(error) } },
|
|
47
|
-
dispose() { mounts.delete(id) },
|
|
48
|
-
recordDomain(root, info) {
|
|
49
|
-
if (mounts.has(id) && row.phase !== 'disposing') row.domains.set(root, { root, mode: info.mode ?? null, error: info.error ?? null, mountId: id, readBindings: info.bindings })
|
|
50
|
-
},
|
|
51
|
-
clearSessionFailure(sessionId) {
|
|
52
|
-
if (mounts.has(id) && row.phase !== 'disposing') row.failures.delete(sessionId)
|
|
53
|
-
},
|
|
54
|
-
recordSessionFailure(sessionId, reason) {
|
|
55
|
-
if (!mounts.has(id) || row.phase === 'disposing') return
|
|
56
|
-
if (row.failures.size >= 50) row.failures.delete(row.failures.keys().next().value)
|
|
57
|
-
row.failures.set(sessionId, { sessionId, reason: String(reason), at: Date.now(), mountId: id })
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
snapshot() {
|
|
62
|
-
const rows = [...mounts.values()]
|
|
63
|
-
return {
|
|
64
|
-
mounts: rows.map(({ domains, failures, ...row }) => ({ ...row })),
|
|
65
|
-
domains: rows.flatMap(row => [...row.domains.values()].map(({ readBindings, ...domain }) => {
|
|
66
|
-
try { return { ...domain, bindings: readBindings?.() ?? [] } }
|
|
67
|
-
catch { return { ...domain, bindings: null } }
|
|
68
|
-
})),
|
|
69
|
-
sessionFailures: rows.flatMap(row => [...row.failures.values()]),
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// The bounded, pinned, identity-checked snapshot read is shared with the cold
|
|
76
|
-
// status view: read-authority.js. Only the inspection-specific facts stay here.
|
|
77
|
-
function domainFacts(root) {
|
|
78
|
-
const authorityDir = join(root, AUTHORITY_DIR)
|
|
79
|
-
try {
|
|
80
|
-
const pins = checkedPath(authorityDir)
|
|
81
|
-
if (!pins.at(-1).stat.isDirectory()) throw new Error('authority is not a directory')
|
|
82
|
-
let reservation = false
|
|
83
|
-
try {
|
|
84
|
-
const stat = lstatSync(reservationPathFor(authorityDir))
|
|
85
|
-
reservation = !stat.isSymbolicLink() && stat.isDirectory()
|
|
86
|
-
} catch (error) { if (!missing(error)) throw error }
|
|
87
|
-
verifyPins(pins)
|
|
88
|
-
return { root, authorityDir, hasAuthority: true, reservation, error: null }
|
|
89
|
-
} catch (error) {
|
|
90
|
-
return { root, authorityDir, hasAuthority: missing(error) ? false : null, reservation: null,
|
|
91
|
-
error: missing(error) ? null : errorMessage(error) }
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* The trusted domain-root set: only roots the SERVER derived (live sessions'
|
|
97
|
-
* management roots + mount-evidence roots). The client may send a root back,
|
|
98
|
-
* but it is accepted only when it canonically matches a derived entry — an
|
|
99
|
-
* arbitrary client filesystem path is never read.
|
|
100
|
-
* @param {() => string[]} candidateRoots @param {string[]} evidenceRoots
|
|
101
|
-
* @returns {Set<string>}
|
|
102
|
-
*/
|
|
103
|
-
export function trustedRoots(candidateRoots, evidenceRoots) {
|
|
104
|
-
const trusted = new Set()
|
|
105
|
-
for (const root of [...candidateRoots(), ...evidenceRoots]) {
|
|
106
|
-
if (typeof root !== 'string' || root.length === 0) continue
|
|
107
|
-
try { trusted.add(realpathSync.native(root)) } catch { /* an unresolvable candidate is not trusted */ }
|
|
108
|
-
}
|
|
109
|
-
return trusted
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Read-only inspection of one trusted domain root's authority. Presence is a
|
|
114
|
-
* fact pattern, never inferred: 'none' (no authority directory), 'empty'
|
|
115
|
-
* (directory currently empty; history indeterminate), 'no-committed-snapshot' (contents
|
|
116
|
-
* the store would refuse), 'not-a-file', 'valid', 'corrupt' (validation
|
|
117
|
-
* refusal, message preserved), 'unreadable' (IO failure). The bytes are
|
|
118
|
-
* validated in memory only — the file, the reservation and every history are
|
|
119
|
-
* left byte-identical.
|
|
120
|
-
* @param {string} root - canonical (realpath) trusted root
|
|
121
|
-
*/
|
|
122
|
-
export const MAX_INSPECTION_BYTES = MAX_SNAPSHOT_BYTES
|
|
123
|
-
export function inspectAuthority(root) {
|
|
124
|
-
const facts = domainFacts(root)
|
|
125
|
-
const { authorityDir, reservation } = facts
|
|
126
|
-
const base = { root, authorityDir, reservation, endpoint: null }
|
|
127
|
-
if (facts.error) return { ...base, presence: 'unreadable', message: facts.error }
|
|
128
|
-
if (!facts.hasAuthority) return { ...base, presence: 'none' }
|
|
129
|
-
const read = readSnapshotBytes(authorityDir)
|
|
130
|
-
// The authority directory vanished between the facts and the read: the same
|
|
131
|
-
// race the inline read reported as unreadable.
|
|
132
|
-
if (read.presence === 'no-authority') return { ...base, presence: 'unreadable', message: 'authority path changed during inspection' }
|
|
133
|
-
if (read.presence === 'no-snapshot') {
|
|
134
|
-
try {
|
|
135
|
-
const entries = readdirSync(authorityDir)
|
|
136
|
-
return { ...base, presence: entries.length ? 'no-committed-snapshot' : 'empty' }
|
|
137
|
-
} catch (error) {
|
|
138
|
-
return { ...base, presence: 'unreadable', message: errorMessage(error) }
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (read.presence === 'not-a-file') return { ...base, presence: 'not-a-file' }
|
|
142
|
-
if (read.presence !== 'valid') return { ...base, presence: 'unreadable', message: read.message }
|
|
143
|
-
try {
|
|
144
|
-
const parsed = parseSnapshot(read.bytes, root)
|
|
145
|
-
// Both projections derive from the SAME parsed image: the snapshot summary
|
|
146
|
-
// and the server-computed recovery proposals (additive, schemaVersion 1).
|
|
147
|
-
return { ...base, presence: 'valid', snapshot: summarizeAuthorityImage(parsed), recoveryProposals: recoveryProposals(root, parsed) }
|
|
148
|
-
} catch (error) {
|
|
149
|
-
return { ...base, presence: 'corrupt', message: errorMessage(error) }
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* The profile-wide switch status. `saved` comes from the live settings config;
|
|
155
|
-
* `enabledAtMount` from mount evidence (undefined → the state is honestly
|
|
156
|
-
* 'unknown', never inferred from the saved value). Restart semantics: a saved
|
|
157
|
-
* value that differs from the mounted one is a pending request.
|
|
158
|
-
* @param {{ savedEnabled: () => boolean, evidence: ReturnType<typeof createEditLockEvidence>, candidateRoots: () => string[] }} deps
|
|
159
|
-
*/
|
|
160
|
-
export function maintenanceStatus(deps) {
|
|
161
|
-
const saved = deps.savedEnabled() === true
|
|
162
|
-
const evidence = deps.evidence.snapshot()
|
|
163
|
-
const installed = evidence.mounts.filter(row => row.phase === 'installed')
|
|
164
|
-
const enabledAtMount = installed.length > 0 && installed.length === evidence.mounts.length
|
|
165
|
-
&& installed.every(row => row.enabled === installed[0].enabled) ? installed[0].enabled : undefined
|
|
166
|
-
const state = maintenanceState({ saved, enabledAtMount })
|
|
167
|
-
const evidenceRoots = evidence.domains.map(entry => entry.root)
|
|
168
|
-
// Status lists every server-derived root: canonical candidates plus the
|
|
169
|
-
// row-reported evidence roots. An evidence root that no longer resolves
|
|
170
|
-
// (unmounted volume, deleted workspace) stays listed — it is still the
|
|
171
|
-
// row's own report, and vanishing it would hide a blocked domain.
|
|
172
|
-
const rootSet = trustedRoots(deps.candidateRoots, evidenceRoots)
|
|
173
|
-
for (const root of evidenceRoots) if (typeof root === 'string' && root.length > 0) rootSet.add(root)
|
|
174
|
-
const roots = [...rootSet].sort()
|
|
175
|
-
const evidenceByRoot = new Map()
|
|
176
|
-
for (const entry of evidence.domains) {
|
|
177
|
-
try { evidenceByRoot.set(realpathSync.native(entry.root), entry) } catch { evidenceByRoot.set(entry.root, entry) }
|
|
178
|
-
}
|
|
179
|
-
const domains = roots.map(root => {
|
|
180
|
-
const facts = domainFacts(root)
|
|
181
|
-
const seen = evidenceByRoot.get(root)
|
|
182
|
-
const bindings = evidence.domains.filter(entry => {
|
|
183
|
-
try { return realpathSync.native(entry.root) === root } catch { return entry.root === root }
|
|
184
|
-
}).flatMap(entry => entry.bindings ?? [])
|
|
185
|
-
return { root, hasAuthority: facts.hasAuthority, reservation: facts.reservation, mode: seen?.mode ?? null, error: seen?.error ?? facts.error, bindings }
|
|
186
|
-
})
|
|
187
|
-
const blocked = [
|
|
188
|
-
...evidence.mounts.filter(row => row.phase !== 'installed').map(row => ({ kind: 'mount', mountId: row.id, reason: row.error ?? row.phase })),
|
|
189
|
-
...evidence.domains.filter(entry => entry.error).map(entry => ({ kind: 'domain', root: entry.root, reason: entry.error })),
|
|
190
|
-
...evidence.sessionFailures.map(row => ({ kind: 'session', sessionId: row.sessionId, reason: row.reason })),
|
|
191
|
-
]
|
|
192
|
-
return {
|
|
193
|
-
saved,
|
|
194
|
-
mounted: enabledAtMount === undefined ? null : enabledAtMount,
|
|
195
|
-
mounts: evidence.mounts,
|
|
196
|
-
pinned: evidence.mounts.some(row => row.pinned),
|
|
197
|
-
state,
|
|
198
|
-
scope: 'profile',
|
|
199
|
-
restartRequired: state === 'enable-requested' || state === 'disable-requested',
|
|
200
|
-
blocked,
|
|
201
|
-
domains,
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* @typedef {{ path: string, methods: string[], requestBody: 'buffered', fetch: (request: { json(): Promise<unknown> }) => Promise<Response> }} MaintenanceRoute
|
|
207
|
-
* @typedef {{ fetch: { register(route: MaintenanceRoute): () => void } }} MaintenanceConnection
|
|
208
|
-
* @typedef {{ connection?: MaintenanceConnection, get?: (name: 'connection') => MaintenanceConnection | undefined, emit?: (type: string, record: object) => unknown }} MaintenanceScope
|
|
209
|
-
*/
|
|
210
|
-
/**
|
|
211
|
-
* Register inspection and explicit administrator recovery on the authenticated
|
|
212
|
-
* settings-plane connection (S20). Handler errors become structured payloads;
|
|
213
|
-
* recovery errors never expose filesystem paths or claim rollback.
|
|
214
|
-
* @param {MaintenanceScope} scope @param {{ savedEnabled: () => boolean, evidence: ReturnType<typeof createEditLockEvidence>, candidateRoots: () => string[] }} deps
|
|
215
|
-
* @returns {() => void} disposer
|
|
216
|
-
*/
|
|
217
|
-
export function registerEditLockMaintenanceEndpoints(scope, deps) {
|
|
218
|
-
const connection = scope?.connection ?? scope?.get?.('connection')
|
|
219
|
-
if (!connection?.fetch?.register) return () => {}
|
|
220
|
-
const disposers = []
|
|
221
|
-
disposers.push(connection.fetch.register({
|
|
222
|
-
path: STATUS_PATH,
|
|
223
|
-
methods: ['POST'],
|
|
224
|
-
requestBody: 'buffered',
|
|
225
|
-
fetch: async () => {
|
|
226
|
-
try {
|
|
227
|
-
return reply({ ok: true, value: maintenanceStatus(deps) })
|
|
228
|
-
} catch (error) {
|
|
229
|
-
return failure(errorMessage(error), 500)
|
|
230
|
-
}
|
|
231
|
-
},
|
|
232
|
-
}))
|
|
233
|
-
disposers.push(connection.fetch.register({
|
|
234
|
-
path: INSPECT_PATH,
|
|
235
|
-
methods: ['POST'],
|
|
236
|
-
requestBody: 'buffered',
|
|
237
|
-
fetch: async (request) => {
|
|
238
|
-
let body
|
|
239
|
-
try { body = await request.json() } catch { body = null }
|
|
240
|
-
const requested = body !== null && typeof body === 'object' && 'root' in body && typeof body.root === 'string' ? body.root : ''
|
|
241
|
-
if (!requested) return failure('body needs { root } from the status domain list')
|
|
242
|
-
try {
|
|
243
|
-
const evidenceRoots = deps.evidence.snapshot().domains.map(entry => entry.root)
|
|
244
|
-
// Exact membership BEFORE any operation on client data. No realpath oracle.
|
|
245
|
-
if (!trustedRoots(deps.candidateRoots, evidenceRoots).has(requested)) {
|
|
246
|
-
return failure('not an Edit Lock domain root this server derived', 403, 'weir-edit-lock/untrusted-root')
|
|
247
|
-
}
|
|
248
|
-
return reply({ ok: true, value: inspectAuthority(requested) })
|
|
249
|
-
} catch (error) {
|
|
250
|
-
return failure(errorMessage(error), 500)
|
|
251
|
-
}
|
|
252
|
-
},
|
|
253
|
-
}))
|
|
254
|
-
// Host Connection authenticates browser cookies and checks Host/Origin before
|
|
255
|
-
// dispatching exact /api routes. Never register this through a raw web server.
|
|
256
|
-
const audit = createAudit({ emit: (type, record) => scope.emit?.(type, record) })
|
|
257
|
-
disposers.push(connection.fetch.register({
|
|
258
|
-
path: '/api/weir-edit-lock/maintenance/recover',
|
|
259
|
-
methods: ['POST'],
|
|
260
|
-
requestBody: 'buffered',
|
|
261
|
-
fetch: async request => {
|
|
262
|
-
let body
|
|
263
|
-
try { body = await request.json() } catch { return failure('Invalid recovery JSON') }
|
|
264
|
-
const root = body !== null && typeof body === 'object' && 'root' in body && typeof body.root === 'string' ? body.root : ''
|
|
265
|
-
try {
|
|
266
|
-
const evidenceRoots = deps.evidence.snapshot().domains.map(row => row.root)
|
|
267
|
-
if (!trustedRoots(deps.candidateRoots, evidenceRoots).has(root)) return failure('not an Edit Lock domain root this server derived', 403, 'weir-edit-lock/untrusted-root')
|
|
268
|
-
const value = await recoverAuthority(body)
|
|
269
|
-
audit(null, AUDIT_TYPES.editLockMaintenance, { kind: 'admin-override', recoveryId: value.record.recoveryId, revision: value.revision, idempotent: value.idempotent }, { root })
|
|
270
|
-
return reply({ ok: true, value })
|
|
271
|
-
} catch (error) {
|
|
272
|
-
// Do not return raw filesystem paths, credentials, causes or old content.
|
|
273
|
-
const e = /** @type {{code?: string, commitStatus?: string}} */ (error)
|
|
274
|
-
const code = e.code?.startsWith('weir-edit-lock/') ? e.code : 'weir-edit-lock/recovery-refused'
|
|
275
|
-
return reply({ ok: false, error: { code, message: 'Recovery not acknowledged. Inspect authority and retry the same recovery ID after resolving the cause.', commitStatus: e.commitStatus ?? 'not-acknowledged' } }, 409)
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
}))
|
|
279
|
-
let disposed = false
|
|
280
|
-
return () => {
|
|
281
|
-
if (disposed) return
|
|
282
|
-
disposed = true
|
|
283
|
-
for (const dispose of disposers) dispose?.()
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Wire the maintenance endpoints from the profile-level settings row (the row
|
|
289
|
-
* cannot add package subpaths, S19). Service resolution goes through
|
|
290
|
-
* ctx.inject (S20); nothing registers when the connection service is absent.
|
|
291
|
-
* @param {any} ctx @param {{ savedEnabled: () => boolean, evidence: ReturnType<typeof createEditLockEvidence>, candidateRoots: () => string[] }} deps
|
|
292
|
-
* @returns {() => void} idempotent disposer
|
|
293
|
-
*/
|
|
294
|
-
export function wireEditLockMaintenance(ctx, deps) {
|
|
295
|
-
let closed = false
|
|
296
|
-
let off = () => {}
|
|
297
|
-
const injection = ctx.inject?.(['connection'], (/** @type {any} */ scope) => {
|
|
298
|
-
if (closed) return () => {}
|
|
299
|
-
off()
|
|
300
|
-
off = registerEditLockMaintenanceEndpoints(scope, deps)
|
|
301
|
-
return off
|
|
302
|
-
})
|
|
303
|
-
return () => {
|
|
304
|
-
if (closed) return
|
|
305
|
-
closed = true
|
|
306
|
-
off()
|
|
307
|
-
if (typeof injection === 'function') injection()
|
|
308
|
-
else injection?.dispose?.()
|
|
309
|
-
}
|
|
310
|
-
}
|