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.
@@ -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
+ }