dsh-creator-mode-plus 0.3.8
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/AGENTS.md +40 -0
- package/CHANGELOG.md +104 -0
- package/LICENSE +21 -0
- package/README.en.md +83 -0
- package/README.md +82 -0
- package/docs/bridge-contract.md +431 -0
- package/docs/dshx-v0.7-alignment.md +108 -0
- package/docs/screenshots/already-installed.png +0 -0
- package/docs/screenshots/install.png +0 -0
- package/docs/screenshots/mode-picker.gif +0 -0
- package/docs/screenshots/mode-picker.png +0 -0
- package/docs/screenshots/mode-selected.png +0 -0
- package/dshx.yml +16 -0
- package/package.json +67 -0
- package/preset/preset.yml +2 -0
- package/preset/skills/creator-mode-plus/SKILL.md +101 -0
- package/preset/skills/creator-mode-plus/existing-plugin-trials.md +60 -0
- package/preset/skills/creator-mode-plus/maintenance.md +48 -0
- package/preset/skills/creator-mode-plus/verification-recovery.md +54 -0
- package/scripts/install.mjs +292 -0
- package/scripts/verify-dshx.mjs +77 -0
- package/scripts/verify-harness-install.mjs +101 -0
- package/src/auth.js +26 -0
- package/src/client.js +112 -0
- package/src/compatibility.js +261 -0
- package/src/delivery.js +331 -0
- package/src/index.js +377 -0
- package/src/preset-015.js +28 -0
- package/src/runner.js +464 -0
- package/src/safety.js +79 -0
- package/src/takeover.js +157 -0
package/src/delivery.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
function receiptPath(root, sessionId) {
|
|
6
|
+
return join(root, '.dshx', 'creator-plus', 'deliveries', `${createHash('sha256').update(sessionId).digest('hex')}.json`)
|
|
7
|
+
}
|
|
8
|
+
export function readDelivery(root, sessionId) {
|
|
9
|
+
const file = receiptPath(root, sessionId)
|
|
10
|
+
if (!existsSync(file)) return undefined
|
|
11
|
+
const row = JSON.parse(readFileSync(file, 'utf8'))
|
|
12
|
+
if (row.version !== 1 || row.sessionId !== sessionId || !/^[a-z][a-z0-9-]*$/.test(row.pluginId)) throw new Error('Invalid Creator+ delivery receipt')
|
|
13
|
+
return row
|
|
14
|
+
}
|
|
15
|
+
function save(root, row) {
|
|
16
|
+
const file = receiptPath(root, row.sessionId)
|
|
17
|
+
mkdirSync(join(root, '.dshx', 'creator-plus', 'deliveries'), { recursive: true })
|
|
18
|
+
const temporary = `${file}.${randomUUID()}.tmp`
|
|
19
|
+
writeFileSync(temporary, JSON.stringify(row), { mode: 0o600, flag: 'wx' })
|
|
20
|
+
renameSync(temporary, file)
|
|
21
|
+
return row
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Conservative package identity: content, not build timestamps. Never follow
|
|
25
|
+
// dependency or external symlinks. An unchanged re-check keeps its boot baseline.
|
|
26
|
+
export function deliveryFingerprint(directory) {
|
|
27
|
+
const hash = createHash('sha256')
|
|
28
|
+
function visit(relative = '') {
|
|
29
|
+
for (const entry of readdirSync(join(directory, relative), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
30
|
+
if (entry.name.startsWith('.') || ['node_modules', 'coverage'].includes(entry.name)) continue
|
|
31
|
+
const name = join(relative, entry.name)
|
|
32
|
+
if (entry.isDirectory()) visit(name)
|
|
33
|
+
else {
|
|
34
|
+
hash.update(JSON.stringify(name))
|
|
35
|
+
hash.update(entry.isSymbolicLink() ? readlinkSync(join(directory, name)) : readFileSync(join(directory, name)))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
visit()
|
|
40
|
+
return hash.digest('hex')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function object(value) {
|
|
44
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function boundedGenerationIds(value, label) {
|
|
48
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 128
|
|
49
|
+
|| value.some(id => typeof id !== 'string' || !/^generation-[1-9][0-9]*$/.test(id))
|
|
50
|
+
|| new Set(value).size !== value.length) {
|
|
51
|
+
throw new Error(`DSHX hot-reload ${label} is invalid`)
|
|
52
|
+
}
|
|
53
|
+
return value
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isoTimestamp(value, label) {
|
|
57
|
+
if (typeof value !== 'string' || !Number.isFinite(Date.parse(value))) {
|
|
58
|
+
throw new Error(`DSHX hot-reload ${label} is invalid`)
|
|
59
|
+
}
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validatedHotReload(result, pluginId, expectedPid, expectedPort) {
|
|
64
|
+
const report = object(JSON.parse(result.stdout))
|
|
65
|
+
const data = object(report?.data)
|
|
66
|
+
const value = object(data?.result)
|
|
67
|
+
const proof = object(value?.proof)
|
|
68
|
+
const ready = object(proof?.ready)
|
|
69
|
+
const reloadedReport = object(proof?.moduleReloaded)
|
|
70
|
+
const reloaded = object(reloadedReport?.moduleReloaded)
|
|
71
|
+
const hmrDisposedReport = object(proof?.hmrDisposed)
|
|
72
|
+
const hmrDisposed = object(hmrDisposedReport?.hmrDisposed)
|
|
73
|
+
const observerDisposedReport = object(proof?.observerDisposed)
|
|
74
|
+
const observerDisposed = object(observerDisposedReport?.observerDisposed)
|
|
75
|
+
const journal = object(value?.journal)
|
|
76
|
+
const evidence = Array.isArray(data?.evidence) ? data.evidence : []
|
|
77
|
+
const hash = proof?.hashBefore
|
|
78
|
+
const artifacts = proof?.artifactHashes
|
|
79
|
+
if (value?.targetScope !== 'root' || !Array.isArray(artifacts) || artifacts.length < 1 || artifacts.length > 32
|
|
80
|
+
|| artifacts.some(item => {
|
|
81
|
+
const artifact = object(item)
|
|
82
|
+
return typeof artifact?.path !== 'string' || artifact.path.startsWith('/')
|
|
83
|
+
|| artifact.path.includes('\\') || artifact.path.split('/').some(part => !part || part === '.' || part === '..' || part === 'node_modules')
|
|
84
|
+
|| typeof artifact.before !== 'string' || !/^[0-9a-f]{64}$/.test(artifact.before)
|
|
85
|
+
|| artifact.after !== artifact.before
|
|
86
|
+
})
|
|
87
|
+
|| new Set(artifacts.map(item => item.path)).size !== artifacts.length
|
|
88
|
+
|| JSON.stringify(value.watchRoots) !== JSON.stringify(artifacts.map(item => item.path))) {
|
|
89
|
+
throw new Error('DSHX hot-reload artifact-set or root-scope evidence is invalid')
|
|
90
|
+
}
|
|
91
|
+
if (report?.command !== 'hot-reload' || report?.ok !== true
|
|
92
|
+
|| !evidence.includes('HOST_MODULE_RELOADED') || data?.hostRestart !== false || data?.behaviorVerified !== false
|
|
93
|
+
|| value?.pluginId !== pluginId || value?.profile !== 'web'
|
|
94
|
+
|| value?.hostPid !== expectedPid || value?.hostPort !== expectedPort || value?.hostRestart !== false
|
|
95
|
+
|| typeof value?.transactionId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value.transactionId)
|
|
96
|
+
|| typeof value?.hmrEntryId !== 'string' || !/^dshx-hot-reload-hmr-[a-z0-9-]+$/.test(value.hmrEntryId)
|
|
97
|
+
|| proof?.sameHost !== true || proof?.bytesUnchanged !== true
|
|
98
|
+
|| typeof hash !== 'string' || !/^[0-9a-f]{64}$/.test(hash) || proof?.hashAfter !== hash
|
|
99
|
+
|| journal?.status !== 'succeeded' || journal?.automaticRecovery !== false || journal?.cleanupProved !== true) {
|
|
100
|
+
throw new Error('DSHX hot-reload did not return complete same-PID module evidence')
|
|
101
|
+
}
|
|
102
|
+
const identity = reportValue => reportValue?.transactionId === value.transactionId
|
|
103
|
+
&& reportValue?.pluginId === pluginId
|
|
104
|
+
&& reportValue?.hmrEntryId === value.hmrEntryId
|
|
105
|
+
&& reportValue?.expectedPid === expectedPid
|
|
106
|
+
&& reportValue?.pid === expectedPid
|
|
107
|
+
&& reportValue?.samePid === true
|
|
108
|
+
if (!identity(ready) || ready.phase !== 'READY'
|
|
109
|
+
|| !identity(reloadedReport) || reloadedReport.phase !== 'MODULE_RELOADED'
|
|
110
|
+
|| !identity(hmrDisposedReport) || hmrDisposedReport.phase !== 'HMR_DISPOSED'
|
|
111
|
+
|| !identity(observerDisposedReport) || observerDisposedReport.phase !== 'OBSERVER_DISPOSED'
|
|
112
|
+
|| reloaded?.samePid !== true) {
|
|
113
|
+
throw new Error('DSHX hot-reload observer identity or phase evidence is invalid')
|
|
114
|
+
}
|
|
115
|
+
const oldGenerationIds = boundedGenerationIds(reloaded.oldGenerationIds, 'oldGenerationIds')
|
|
116
|
+
const targetGenerationIds = boundedGenerationIds(ready.targetGenerationIds, 'targetGenerationIds')
|
|
117
|
+
const newGenerationIds = boundedGenerationIds(reloaded.newGenerationIds, 'newGenerationIds')
|
|
118
|
+
const matchedGenerationIds = boundedGenerationIds(reloaded.hmrEventMatchedGenerationIds, 'hmrEventMatchedGenerationIds')
|
|
119
|
+
if (!Array.isArray(ready.targetGenerationStates)
|
|
120
|
+
|| ready.targetGenerationStates.length !== targetGenerationIds.length
|
|
121
|
+
|| ready.targetGenerationStates.some((state, index) => {
|
|
122
|
+
const target = object(state)
|
|
123
|
+
return target?.generationId !== targetGenerationIds[index]
|
|
124
|
+
|| (target.state !== 'ACTIVE' && target.state !== 'FAILED')
|
|
125
|
+
})) {
|
|
126
|
+
throw new Error('DSHX hot-reload original generation states are invalid')
|
|
127
|
+
}
|
|
128
|
+
if (JSON.stringify(oldGenerationIds) !== JSON.stringify(targetGenerationIds)
|
|
129
|
+
|| JSON.stringify(matchedGenerationIds) !== JSON.stringify(targetGenerationIds)
|
|
130
|
+
|| newGenerationIds.length !== targetGenerationIds.length
|
|
131
|
+
|| newGenerationIds.some(id => targetGenerationIds.includes(id))
|
|
132
|
+
|| typeof ready.hmrGenerationId !== 'string'
|
|
133
|
+
|| hmrDisposed.generationId !== ready.hmrGenerationId
|
|
134
|
+
|| !Number.isFinite(proof.mtimeBeforeMs) || !Number.isFinite(proof.mtimeAfterMs)
|
|
135
|
+
|| proof.mtimeAfterMs <= proof.mtimeBeforeMs) {
|
|
136
|
+
throw new Error('DSHX hot-reload generation evidence is inconsistent')
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
transactionId: value.transactionId,
|
|
140
|
+
hostPid: expectedPid,
|
|
141
|
+
hostPort: expectedPort,
|
|
142
|
+
samePid: true,
|
|
143
|
+
bytesUnchanged: true,
|
|
144
|
+
entryHash: hash,
|
|
145
|
+
artifactHashes: artifacts,
|
|
146
|
+
targetScope: 'root',
|
|
147
|
+
targetGenerationIds,
|
|
148
|
+
targetGenerationStates: ready.targetGenerationStates,
|
|
149
|
+
newGenerationIds,
|
|
150
|
+
hmrEventAt: isoTimestamp(reloaded.hmrEventAt, 'hmrEventAt'),
|
|
151
|
+
hmrDisposedAt: isoTimestamp(hmrDisposed.at, 'hmrDisposedAt'),
|
|
152
|
+
observerDisposedAt: isoTimestamp(observerDisposed.at, 'observerDisposedAt'),
|
|
153
|
+
cleanupProved: true,
|
|
154
|
+
behavior: 'UNVERIFIED',
|
|
155
|
+
recordedAt: Date.now(),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Session-local journal only; this never activates a plugin or controls the Host. */
|
|
160
|
+
export function recordDelivery(root, args, result, sessionId, hostPid = process.pid) {
|
|
161
|
+
if (!['activation-plan', 'check', 'hot-reload'].includes(args[0])) return undefined
|
|
162
|
+
const previous = readDelivery(root, sessionId)
|
|
163
|
+
const row = previous?.pluginId === args[1] ? previous : { version: 1, sessionId, pluginId: args[1], sourceBuilt: false }
|
|
164
|
+
if (args[0] === 'hot-reload') {
|
|
165
|
+
delete row.hotReload
|
|
166
|
+
const port = Number(args[5])
|
|
167
|
+
if (result.exitCode !== 0) {
|
|
168
|
+
if (previous?.pluginId !== args[1]) return undefined
|
|
169
|
+
row.hotReloadFailed = { at: Date.now(), hostPid, hostPort: port }
|
|
170
|
+
return save(root, row)
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
row.hotReload = validatedHotReload(result, args[1], hostPid, port)
|
|
174
|
+
delete row.hotReloadFailed
|
|
175
|
+
} catch (error) {
|
|
176
|
+
row.hotReloadFailed = { at: Date.now(), hostPid, hostPort: port }
|
|
177
|
+
save(root, row)
|
|
178
|
+
throw error
|
|
179
|
+
}
|
|
180
|
+
return save(root, row)
|
|
181
|
+
}
|
|
182
|
+
if (args[0] === 'activation-plan') {
|
|
183
|
+
delete row.hotReload
|
|
184
|
+
delete row.hotReloadFailed
|
|
185
|
+
const report = JSON.parse(result.stdout)
|
|
186
|
+
const data = report.data
|
|
187
|
+
if (data?.facts && data?.decision) {
|
|
188
|
+
row.plan = { change: data.change, packageDir: data.facts.packageDir, hasClient: data.facts.hasClient, handoff: data.facts.handoff, hostRestart: data.decision.hostRestart }
|
|
189
|
+
row.plan.boundedHotReloadEligible = data.change === 'server'
|
|
190
|
+
&& data.decision.hostRestart === 'not-decided'
|
|
191
|
+
&& (result.exitCode === 0 || (Array.isArray(report.findings)
|
|
192
|
+
&& report.findings.some(item => item.level === 'error' && item.code === 'activation-blocker')
|
|
193
|
+
&& report.findings.filter(item => item.level === 'error').every(item => item.code === 'activation-blocker')))
|
|
194
|
+
if (result.exitCode === 0) delete row.planFailed
|
|
195
|
+
else row.planFailed = true
|
|
196
|
+
} else if (result.exitCode !== 0) {
|
|
197
|
+
delete row.plan
|
|
198
|
+
row.planFailed = true
|
|
199
|
+
} else {
|
|
200
|
+
throw new Error('DSHX activation plan has no structured delivery facts')
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
const fingerprint = result.exitCode === 0 && row.plan?.packageDir ? deliveryFingerprint(row.plan.packageDir) : undefined
|
|
204
|
+
const unchanged = row.sourceBuilt && fingerprint !== undefined && fingerprint === row.fingerprint
|
|
205
|
+
row.sourceBuilt = result.exitCode === 0
|
|
206
|
+
if (!unchanged) {
|
|
207
|
+
delete row.hotReload
|
|
208
|
+
delete row.hotReloadFailed
|
|
209
|
+
row.builtAt = Date.now()
|
|
210
|
+
row.buildHostPid = hostPid
|
|
211
|
+
row.fingerprint = fingerprint
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return save(root, row)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function deliveryStatus(row, { pid = process.pid, startedAt = Date.now() - process.uptime() * 1000, port } = {}) {
|
|
218
|
+
if (!row) return undefined
|
|
219
|
+
const restart = row.plan?.hostRestart === 'required'
|
|
220
|
+
const undecided = row.plan?.hostRestart === 'not-decided'
|
|
221
|
+
const planFailed = row.planFailed === true
|
|
222
|
+
const boundedHotReload = undecided && row.plan?.change === 'server'
|
|
223
|
+
&& (!planFailed || row.plan.boundedHotReloadEligible === true)
|
|
224
|
+
const changed = row.sourceBuilt && pid !== row.buildHostPid && startedAt > row.builtAt
|
|
225
|
+
const expectedPort = row.hotReload?.hostPort ?? row.hotReloadFailed?.hostPort ?? row.plan?.handoff?.port
|
|
226
|
+
const wrongTarget = expectedPort !== undefined && expectedPort !== port
|
|
227
|
+
const currentModuleProof = row.hotReload?.hostPid === pid
|
|
228
|
+
const state = wrongTarget ? 'TARGET_MISMATCH' : row.hotReload ? 'RUNTIME_VERIFICATION_REQUIRED'
|
|
229
|
+
: row.hotReloadFailed ? 'ACTIVATION_DECISION_REQUIRED'
|
|
230
|
+
: !row.sourceBuilt ? 'SOURCE_BUILD_REQUIRED' : !row.plan ? 'ACTIVATION_PLAN_REQUIRED'
|
|
231
|
+
: planFailed && !boundedHotReload ? 'ACTIVATION_PLAN_REQUIRED' : boundedHotReload ? 'HOT_RELOAD_READY' : undecided ? 'ACTIVATION_DECISION_REQUIRED'
|
|
232
|
+
: restart && !changed ? 'AWAITING_LAUNCHER_RESTART' : 'RUNTIME_VERIFICATION_REQUIRED'
|
|
233
|
+
const nextAction = state === 'HOT_RELOAD_READY'
|
|
234
|
+
? { tool: 'dshx_hot_reload', arguments: { name: row.pluginId } }
|
|
235
|
+
: undefined
|
|
236
|
+
return { pluginId: row.pluginId, state, currentPid: pid,
|
|
237
|
+
...nextAction ? { nextAction } : {},
|
|
238
|
+
...(row.plan?.packageDir !== undefined ? { sourcePath: row.plan.packageDir, targetScope: 'This plan and fixed ID refer only to sourcePath. A build in another directory does not check or activate this target. Read the existing-plugin trial workflow before promoting candidate changes.' } : {}),
|
|
239
|
+
...(row.buildHostPid !== undefined ? { previousPid: row.buildHostPid } : {}),
|
|
240
|
+
...(row.plan?.handoff !== undefined ? { handoff: row.plan.handoff } : {}),
|
|
241
|
+
...row.hotReload ? { moduleProof: {
|
|
242
|
+
...row.hotReload,
|
|
243
|
+
evidenceState: currentModuleProof ? 'SAME_PID_AT_RELOAD_AND_CURRENT_PID_MATCHES' : 'HISTORICAL_HOST',
|
|
244
|
+
currentHostPidMatches: currentModuleProof,
|
|
245
|
+
} } : {},
|
|
246
|
+
next: wrongTarget
|
|
247
|
+
? 'Resolve the Host target mismatch before activation or runtime verification. Preserve the source and report the exact target blocker.'
|
|
248
|
+
: row.hotReload && !currentModuleProof
|
|
249
|
+
? 'The previous activation evidence belongs to an earlier runtime. Verify the requested behavior in the current session before marking the plugin complete.'
|
|
250
|
+
: row.hotReload
|
|
251
|
+
? 'The implementation is active. Exercise the requested behavior before marking the plugin complete; activation alone is not functional acceptance.'
|
|
252
|
+
: row.hotReloadFailed
|
|
253
|
+
? 'Activation failed or its evidence was incomplete. Resolve the failure, then obtain a new activation plan or changed-source check before retrying.'
|
|
254
|
+
: !row.sourceBuilt
|
|
255
|
+
? 'Build the claimed sourcePath (resolve the claimed target first if no plan exists), then call dshx_check for this plugin id. A candidate built elsewhere does not satisfy this check. Keep delivery pending.'
|
|
256
|
+
: boundedHotReload
|
|
257
|
+
? 'Call dshx_hot_reload now for this checked plugin, then exercise the changed feature. Browser-adapter readiness is independent.'
|
|
258
|
+
: !row.plan || planFailed
|
|
259
|
+
? 'The activation plan remains pending. Obtain a successful plan before attempting activation or runtime verification.'
|
|
260
|
+
: undecided
|
|
261
|
+
? 'Keep delivery pending: the changed component and activation method are unresolved. Call dshx_activation_plan for the claimed target.'
|
|
262
|
+
: 'Exercise the requested plugin behavior in the current application. Mark it complete only when the requested features work; otherwise report the remaining work.' }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Bridge outcomes describe the next operation; raw CLI exit codes remain auditable. */
|
|
266
|
+
export function withOperationOutcome(args, result, delivery) {
|
|
267
|
+
let report
|
|
268
|
+
try { report = JSON.parse(result.stdout) } catch { /* Text commands have no JSON report. */ }
|
|
269
|
+
const errors = Array.isArray(report?.findings) ? report.findings.filter(item => item.level === 'error') : []
|
|
270
|
+
// DSHX 0.7.6 used exit 1 for an evidence request. Adapt only that exact
|
|
271
|
+
// planning case; authentication, composition and contract errors stay errors.
|
|
272
|
+
const evidenceRequest = args[0] === 'activation-plan' && report?.command === 'activation-plan'
|
|
273
|
+
&& report?.data?.change === 'server' && report?.data?.decision?.hostRestart === 'not-decided'
|
|
274
|
+
&& typeof report?.data?.facts?.packageDir === 'string'
|
|
275
|
+
&& result.exitCode === 1 && errors.length > 0 && errors.every(item => item.code === 'activation-blocker')
|
|
276
|
+
const code = evidenceRequest ? 0 : result.exitCode
|
|
277
|
+
const browser = args[0] === 'browser'
|
|
278
|
+
const errorText = errors.map(item => String(item.message ?? '')).join('\n')
|
|
279
|
+
const browserOnly = browser && /\bBROWSER_[A-Z_]+\b/.test(errorText) && !/\bWEB_[A-Z_]+\b/.test(errorText)
|
|
280
|
+
const scope = browser ? (code !== 0 && !browserOnly ? 'host' : 'browser')
|
|
281
|
+
: args[0] === 'check' ? 'source'
|
|
282
|
+
: ['activation-plan', 'hot-reload', 'activate-new-client'].includes(args[0]) ? 'activation' : 'operation'
|
|
283
|
+
const continueWith = delivery?.nextAction && (code === 0 || browserOnly) ? [delivery.nextAction] : []
|
|
284
|
+
const outcome = {
|
|
285
|
+
status: code !== 0 ? 'BLOCKED' : evidenceRequest || continueWith.length > 0 ? 'ACTION_REQUIRED' : 'SUCCEEDED',
|
|
286
|
+
scope,
|
|
287
|
+
continueWith,
|
|
288
|
+
...browserOnly ? { message: 'This browser adapter is unavailable. Continue independent build/check/activation work. Verify through an authorized, already-authenticated UI or the plugin service/tool when applicable; record only the verification that actually ran.' } : {},
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
outcome,
|
|
292
|
+
...result,
|
|
293
|
+
...evidenceRequest ? { exitCode: 0, commandExitCode: result.exitCode } : {},
|
|
294
|
+
...delivery ? { delivery } : {},
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Read-only, same-origin proof through Connection authentication; no credential leaves this function. */
|
|
299
|
+
export async function verifyDeliveryClient(row, port, startupUrl, request = fetch) {
|
|
300
|
+
if (!row.plan?.hasClient) return { state: 'SERVER_BEHAVIOR_VERIFICATION_REQUIRED' }
|
|
301
|
+
if (!startupUrl) return { state: 'WEB_AUTH_REQUIRED' }
|
|
302
|
+
const origin = `http://127.0.0.1:${port}`
|
|
303
|
+
const start = new URL(startupUrl)
|
|
304
|
+
if (start.origin !== origin) return { state: 'TARGET_MISMATCH' }
|
|
305
|
+
const get = (url, cookie) => request(url, { redirect: 'manual', headers: cookie ? { cookie } : {}, signal: AbortSignal.timeout(5_000) })
|
|
306
|
+
let response = await get(start)
|
|
307
|
+
let cookie = (response.headers.getSetCookie?.() ?? [response.headers.get('set-cookie') ?? '']).map(value => value.split(';')[0]).filter(Boolean).join('; ')
|
|
308
|
+
if (response.status >= 300 && response.status < 400) {
|
|
309
|
+
const location = response.headers.get('location')
|
|
310
|
+
if (!location || !cookie) return { state: 'WEB_AUTH_REQUIRED' }
|
|
311
|
+
const next = new URL(location, start)
|
|
312
|
+
if (next.origin !== origin) return { state: 'TARGET_MISMATCH' }
|
|
313
|
+
response = await get(next, cookie)
|
|
314
|
+
}
|
|
315
|
+
if ([401, 403].includes(response.status)) return { state: 'WEB_AUTH_REQUIRED' }
|
|
316
|
+
if (response.status !== 200) return { state: 'WEB_PAGE_UNAVAILABLE', httpStatus: response.status }
|
|
317
|
+
const html = await response.text()
|
|
318
|
+
const match = /(?:window\s*\.\s*__DSH_BOOT__|globalThis(?:\s*\.\s*__DSH_BOOT__|\s*\[\s*["']__DSH_BOOT__["']\s*\]))\s*=\s*([\s\S]*?)<\/script>/.exec(html)
|
|
319
|
+
if (!match) return { state: 'CLIENT_MANIFEST_MISSING' }
|
|
320
|
+
const boot = JSON.parse(match[1].trim().replace(/;$/, ''))
|
|
321
|
+
const entry = boot.entries?.find(entry => entry.id === row.pluginId)
|
|
322
|
+
if (!entry || typeof entry.url !== 'string') return { state: 'CLIENT_ENTRY_MISSING' }
|
|
323
|
+
const bundleUrl = new URL(entry.url, origin)
|
|
324
|
+
if (bundleUrl.origin !== origin || !bundleUrl.pathname.startsWith('/plugins/')) return { state: 'TARGET_MISMATCH' }
|
|
325
|
+
const bundle = await get(bundleUrl, cookie)
|
|
326
|
+
if ([401, 403].includes(bundle.status)) return { state: 'WEB_AUTH_REQUIRED' }
|
|
327
|
+
if (bundle.status !== 200) return { state: 'CLIENT_BUNDLE_UNAVAILABLE', httpStatus: bundle.status }
|
|
328
|
+
const text = await bundle.text()
|
|
329
|
+
if (!text.includes('__ModuleLoader__')) return { state: 'CLIENT_HANDOFF_INVALID' }
|
|
330
|
+
return { state: 'CLIENT_MANIFEST_PRESENT', bundleServed: true, behavior: 'UNVERIFIED: exercise the changed feature in the authenticated current WebUI' }
|
|
331
|
+
}
|