@visa/cli 4.1.0-rc.99 → 5.0.0-rc.335
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 +282 -156
- package/dist/cli.js +333 -532
- package/dist/managed-runtime/resolve-and-update.mjs +268 -0
- package/dist/managed-runtime/runtime-readiness.mjs +126 -0
- package/dist/managed-runtime/update-and-restart.mjs +1079 -0
- package/dist/mcp-apps/ucp-checkout.html +280 -0
- package/dist/mcp-server/index.js +234 -387
- package/dist/merchant-ucp-mcp/index.js +7 -0
- package/dist/skills/pair-visa-agent/RUNTIMES.md +56 -26
- package/dist/skills/pair-visa-agent/SKILL.md +319 -320
- package/dist/skills/pair-visa-agent/scripts/__tests__/setup.test.mjs +407 -0
- package/dist/skills/pair-visa-agent/scripts/setup.mjs +310 -30
- package/dist/skills/visa-shopify-checkout/SKILL.md +122 -0
- package/dist/skills/visa-shopify-checkout/references/evidence-and-states.md +37 -0
- package/dist/skills/visa-ucp-shopping/SKILL.md +86 -0
- package/install.ps1 +10 -6
- package/install.sh +7 -2
- package/native/bin/darwin-arm64/visa-runtime-signer +0 -0
- package/native/bin/darwin-x64/visa-runtime-signer +0 -0
- package/native/bin/linux-arm64/visa-runtime-signer +0 -0
- package/native/bin/linux-x64/visa-runtime-signer +0 -0
- package/native/bin/win32-arm64/visa-runtime-signer.exe +0 -0
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/native/bin/win32-x64/visa-runtime-signer.exe +0 -0
- package/package.json +23 -31
- package/server.json +3 -3
- package/dist/checkout-engine/adapters/generic.d.ts +0 -23
- package/dist/checkout-engine/adapters/generic.js +0 -216
- package/dist/checkout-engine/adapters/index.d.ts +0 -10
- package/dist/checkout-engine/adapters/index.js +0 -24
- package/dist/checkout-engine/adapters/shopify.d.ts +0 -31
- package/dist/checkout-engine/adapters/shopify.js +0 -423
- package/dist/checkout-engine/adapters/stripe-like.d.ts +0 -10
- package/dist/checkout-engine/adapters/stripe-like.js +0 -21
- package/dist/checkout-engine/amount.d.ts +0 -15
- package/dist/checkout-engine/amount.js +0 -72
- package/dist/checkout-engine/browser-launch.d.ts +0 -46
- package/dist/checkout-engine/browser-launch.js +0 -81
- package/dist/checkout-engine/ceremony.d.ts +0 -64
- package/dist/checkout-engine/ceremony.js +0 -261
- package/dist/checkout-engine/cli-engine.d.ts +0 -227
- package/dist/checkout-engine/cli-engine.js +0 -779
- package/dist/checkout-engine/detect.d.ts +0 -61
- package/dist/checkout-engine/detect.js +0 -398
- package/dist/checkout-engine/evidence.d.ts +0 -25
- package/dist/checkout-engine/evidence.js +0 -104
- package/dist/checkout-engine/executor.d.ts +0 -176
- package/dist/checkout-engine/executor.js +0 -1325
- package/dist/checkout-engine/hosted-approval.d.ts +0 -187
- package/dist/checkout-engine/hosted-approval.js +0 -478
- package/dist/checkout-engine/index.d.ts +0 -6
- package/dist/checkout-engine/index.js +0 -8
- package/dist/checkout-engine/inline-target.d.ts +0 -13
- package/dist/checkout-engine/inline-target.js +0 -37
- package/dist/checkout-engine/instrument.d.ts +0 -61
- package/dist/checkout-engine/instrument.js +0 -87
- package/dist/checkout-engine/live-fill-approval.d.ts +0 -43
- package/dist/checkout-engine/live-fill-approval.js +0 -90
- package/dist/checkout-engine/mandate/card-mandate.d.ts +0 -121
- package/dist/checkout-engine/mandate/card-mandate.js +0 -227
- package/dist/checkout-engine/mandate/mandate-ledger.d.ts +0 -142
- package/dist/checkout-engine/mandate/mandate-ledger.js +0 -338
- package/dist/checkout-engine/mandate.d.ts +0 -25
- package/dist/checkout-engine/mandate.js +0 -100
- package/dist/checkout-engine/outcome.d.ts +0 -30
- package/dist/checkout-engine/outcome.js +0 -225
- package/dist/checkout-engine/owner-only-file.d.ts +0 -19
- package/dist/checkout-engine/owner-only-file.js +0 -41
- package/dist/checkout-engine/package.json +0 -3
- package/dist/checkout-engine/receipt.d.ts +0 -81
- package/dist/checkout-engine/receipt.js +0 -109
- package/dist/checkout-engine/repo-env.d.ts +0 -11
- package/dist/checkout-engine/repo-env.js +0 -23
- package/dist/checkout-engine/trace-handles.d.ts +0 -8
- package/dist/checkout-engine/trace-handles.js +0 -12
- package/dist/checkout-engine/types.d.ts +0 -44
- package/dist/checkout-engine/types.js +0 -2
- package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
- package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +0 -248
- package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +0 -82
- package/dist/checkout-engine/vgs-gateway/server-mint-client.js +0 -180
- package/dist/checkout-engine/vgs-live-instrument.d.ts +0 -179
- package/dist/checkout-engine/vgs-live-instrument.js +0 -296
- package/dist/checkout-engine/vic-confirmation.d.ts +0 -34
- package/dist/checkout-engine/vic-confirmation.js +0 -39
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Supervisor-owned exact-RC deployment transaction for managed runtimes. */
|
|
3
|
+
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
5
|
+
import { execFileSync, execSync } from 'node:child_process'
|
|
6
|
+
import {
|
|
7
|
+
chmodSync,
|
|
8
|
+
closeSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
fsyncSync,
|
|
11
|
+
lstatSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
openSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
readlinkSync,
|
|
17
|
+
realpathSync,
|
|
18
|
+
renameSync,
|
|
19
|
+
rmSync,
|
|
20
|
+
symlinkSync,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from 'node:fs'
|
|
24
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
26
|
+
import {
|
|
27
|
+
assertSameRuntimeBuild,
|
|
28
|
+
assertSameShopifyRuntime,
|
|
29
|
+
commerceSkillArgs,
|
|
30
|
+
parseRuntimeReadiness,
|
|
31
|
+
resolveRuntimeUpdateConfig,
|
|
32
|
+
runtimeReadinessExecOptions,
|
|
33
|
+
} from './runtime-readiness.mjs'
|
|
34
|
+
|
|
35
|
+
const PREFIX = '[managed-runtime-update]'
|
|
36
|
+
const EXACT_RC_RE = /^\d+\.\d+\.\d+-rc\.\d+$/
|
|
37
|
+
const SRI_RE = /^sha512-[A-Za-z0-9+/]+={0,2}$/
|
|
38
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 120_000
|
|
39
|
+
const MAX_ROLLBACK_ATTEMPTS = 2
|
|
40
|
+
const COMMANDS_PER_TRANSACTION_BOUND = 12
|
|
41
|
+
const READINESS_PROBES_PER_TRANSACTION_BOUND = 3
|
|
42
|
+
const RELEASE_INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]+={0,2}$/
|
|
43
|
+
const PENDING_TRANSACTION_FILE = '.pending-update.json'
|
|
44
|
+
|
|
45
|
+
function exactRcParts(version) {
|
|
46
|
+
const match = /^(\d+)\.(\d+)\.(\d+)-rc\.(\d+)$/.exec(version)
|
|
47
|
+
if (!match) return null
|
|
48
|
+
const parts = match.slice(1).map(Number)
|
|
49
|
+
return parts.every(Number.isSafeInteger) ? parts : null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function compareExactRc(left, right) {
|
|
53
|
+
const leftParts = exactRcParts(left)
|
|
54
|
+
const rightParts = exactRcParts(right)
|
|
55
|
+
if (!leftParts || !rightParts) {
|
|
56
|
+
throw new ManagedRuntimeUpdateError(
|
|
57
|
+
'invalid_runtime_state',
|
|
58
|
+
'managed release version is not an exact RC'
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
for (let index = 0; index < leftParts.length; index += 1) {
|
|
62
|
+
if (leftParts[index] !== rightParts[index]) return leftParts[index] - rightParts[index]
|
|
63
|
+
}
|
|
64
|
+
return 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class ManagedRuntimeUpdateError extends Error {
|
|
68
|
+
constructor(code, message, options = {}) {
|
|
69
|
+
super(message, options)
|
|
70
|
+
this.name = 'ManagedRuntimeUpdateError'
|
|
71
|
+
this.code = code
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function structuredLog(event, fields = {}, logger = console) {
|
|
76
|
+
logger.log(`${PREFIX} ${JSON.stringify({ event, ...fields })}`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function safeInteger(value, fallback, name) {
|
|
80
|
+
if (value === undefined || value === '') return fallback
|
|
81
|
+
const parsed = Number(value)
|
|
82
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
83
|
+
throw new ManagedRuntimeUpdateError(
|
|
84
|
+
'invalid_configuration',
|
|
85
|
+
`${name} must be a positive integer`
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
return parsed
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resolveManagedUpdateConfig(env = process.env) {
|
|
92
|
+
const exactVersion = env.VISA_CLI_EXACT_VERSION?.trim() || ''
|
|
93
|
+
if (!EXACT_RC_RE.test(exactVersion) || !exactRcParts(exactVersion)) {
|
|
94
|
+
throw new ManagedRuntimeUpdateError(
|
|
95
|
+
'exact_target_required',
|
|
96
|
+
'VISA_CLI_EXACT_VERSION must be an exact RC such as 4.1.0-rc.253; floating dist-tags are refused'
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
const managedDir = env.VISA_MANAGED_RUNTIME_DIR?.trim() || ''
|
|
100
|
+
if (!managedDir || !isAbsolute(managedDir)) {
|
|
101
|
+
throw new ManagedRuntimeUpdateError(
|
|
102
|
+
'unmanaged_runtime',
|
|
103
|
+
'VISA_MANAGED_RUNTIME_DIR must be an absolute supervisor-owned directory'
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
const runtime = resolveRuntimeUpdateConfig(env)
|
|
107
|
+
if (!runtime.restartCommand || !runtime.readinessCommand) {
|
|
108
|
+
throw new ManagedRuntimeUpdateError(
|
|
109
|
+
'unmanaged_runtime',
|
|
110
|
+
'managed updates require RUNTIME_RESTART_COMMAND and RUNTIME_READINESS_COMMAND'
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
const expectedEnvironment = env.VISA_RUNTIME_EXPECTED_ENVIRONMENT?.trim() || ''
|
|
114
|
+
if (!/^(?:development|staging|production)$/.test(expectedEnvironment)) {
|
|
115
|
+
throw new ManagedRuntimeUpdateError(
|
|
116
|
+
'expected_environment_required',
|
|
117
|
+
'VISA_RUNTIME_EXPECTED_ENVIRONMENT must be development, staging, or production'
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
const resolvedManagedDir = resolve(managedDir)
|
|
121
|
+
if (existsSync(resolvedManagedDir) && realpathSync(resolvedManagedDir) !== resolvedManagedDir) {
|
|
122
|
+
throw new ManagedRuntimeUpdateError(
|
|
123
|
+
'invalid_runtime_state',
|
|
124
|
+
'VISA_MANAGED_RUNTIME_DIR and its parents must not contain symbolic links'
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
const commandTimeoutMs = safeInteger(
|
|
128
|
+
env.VISA_RUNTIME_UPDATE_TIMEOUT_MS,
|
|
129
|
+
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
130
|
+
'VISA_RUNTIME_UPDATE_TIMEOUT_MS'
|
|
131
|
+
)
|
|
132
|
+
return {
|
|
133
|
+
...runtime,
|
|
134
|
+
exactVersion,
|
|
135
|
+
expectedEnvironment,
|
|
136
|
+
managedDir: existsSync(resolvedManagedDir)
|
|
137
|
+
? realpathSync(resolvedManagedDir)
|
|
138
|
+
: resolvedManagedDir,
|
|
139
|
+
commandTimeoutMs,
|
|
140
|
+
lockLeaseMs:
|
|
141
|
+
commandTimeoutMs * COMMANDS_PER_TRANSACTION_BOUND +
|
|
142
|
+
runtime.readinessTimeoutMs * READINESS_PROBES_PER_TRANSACTION_BOUND,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function ensureCanonicalDirectory(path, label) {
|
|
147
|
+
mkdirSync(path, { recursive: true, mode: 0o700 })
|
|
148
|
+
if (lstatSync(path).isSymbolicLink() || realpathSync(path) !== resolve(path)) {
|
|
149
|
+
throw new ManagedRuntimeUpdateError(
|
|
150
|
+
'invalid_runtime_state',
|
|
151
|
+
`${label} and its parents must not contain symbolic links`
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function processIsAlive(pid, processKill = process.kill) {
|
|
157
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false
|
|
158
|
+
try {
|
|
159
|
+
processKill(pid, 0)
|
|
160
|
+
return true
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return error?.code === 'EPERM'
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function acquireUpdateLock(managedDir, correlationId, options = {}) {
|
|
167
|
+
const now = options.now?.() ?? Date.now()
|
|
168
|
+
const leaseMs =
|
|
169
|
+
options.leaseMs ??
|
|
170
|
+
DEFAULT_COMMAND_TIMEOUT_MS * COMMANDS_PER_TRANSACTION_BOUND +
|
|
171
|
+
60_000 * READINESS_PROBES_PER_TRANSACTION_BOUND
|
|
172
|
+
mkdirSync(managedDir, { recursive: true, mode: 0o700 })
|
|
173
|
+
ensureCanonicalDirectory(managedDir, 'VISA_MANAGED_RUNTIME_DIR')
|
|
174
|
+
const lockDir = join(managedDir, '.update-lock')
|
|
175
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
176
|
+
const candidate = join(managedDir, `.update-lock-candidate-${correlationId}-${attempt}`)
|
|
177
|
+
try {
|
|
178
|
+
mkdirSync(candidate, { mode: 0o700 })
|
|
179
|
+
try {
|
|
180
|
+
writeFileSync(
|
|
181
|
+
join(candidate, 'owner.json'),
|
|
182
|
+
JSON.stringify({
|
|
183
|
+
correlationId,
|
|
184
|
+
pid: process.pid,
|
|
185
|
+
createdAtMs: now,
|
|
186
|
+
expiresAtMs: now + leaseMs,
|
|
187
|
+
}),
|
|
188
|
+
{ mode: 0o600, flag: 'wx' }
|
|
189
|
+
)
|
|
190
|
+
fsyncDirectory(candidate)
|
|
191
|
+
renameSync(candidate, lockDir)
|
|
192
|
+
fsyncDirectory(managedDir)
|
|
193
|
+
} catch (error) {
|
|
194
|
+
rmSync(candidate, { recursive: true, force: true })
|
|
195
|
+
throw error
|
|
196
|
+
}
|
|
197
|
+
return () => {
|
|
198
|
+
try {
|
|
199
|
+
const owner = JSON.parse(readFileSync(join(lockDir, 'owner.json'), 'utf8'))
|
|
200
|
+
if (owner.correlationId === correlationId) {
|
|
201
|
+
rmSync(lockDir, { recursive: true, force: true })
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// Never remove a lock whose ownership can no longer be proven.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
rmSync(candidate, { recursive: true, force: true })
|
|
209
|
+
if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY') throw error
|
|
210
|
+
let owner = null
|
|
211
|
+
try {
|
|
212
|
+
owner = JSON.parse(readFileSync(join(lockDir, 'owner.json'), 'utf8'))
|
|
213
|
+
} catch {
|
|
214
|
+
// An incomplete owner record is stale because owner creation is atomic under mkdir.
|
|
215
|
+
}
|
|
216
|
+
const active =
|
|
217
|
+
owner &&
|
|
218
|
+
Number.isSafeInteger(owner.expiresAtMs) &&
|
|
219
|
+
owner.expiresAtMs > now &&
|
|
220
|
+
processIsAlive(owner.pid, options.processKill)
|
|
221
|
+
if (active) {
|
|
222
|
+
throw new ManagedRuntimeUpdateError(
|
|
223
|
+
'update_in_progress',
|
|
224
|
+
'another supervisor-owned runtime update is already in progress'
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
const stale = join(managedDir, `.stale-update-lock-${correlationId}-${attempt}`)
|
|
228
|
+
try {
|
|
229
|
+
renameSync(lockDir, stale)
|
|
230
|
+
rmSync(stale, { recursive: true, force: true })
|
|
231
|
+
} catch (renameError) {
|
|
232
|
+
if (renameError?.code !== 'ENOENT') throw renameError
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
throw new ManagedRuntimeUpdateError('update_in_progress', 'could not acquire runtime update lock')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseJson(raw, label) {
|
|
240
|
+
try {
|
|
241
|
+
return JSON.parse(raw)
|
|
242
|
+
} catch (error) {
|
|
243
|
+
throw new ManagedRuntimeUpdateError(
|
|
244
|
+
'invalid_package_metadata',
|
|
245
|
+
`${label} returned invalid JSON`,
|
|
246
|
+
{
|
|
247
|
+
cause: error,
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function assertManagedChild(managedDir, candidate, label) {
|
|
254
|
+
const rel = relative(managedDir, candidate)
|
|
255
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel)) {
|
|
256
|
+
throw new ManagedRuntimeUpdateError(
|
|
257
|
+
'invalid_runtime_state',
|
|
258
|
+
`${label} escaped the managed directory`
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function releaseTreeIntegrity(releaseDir) {
|
|
264
|
+
const hash = createHash('sha512')
|
|
265
|
+
const visit = (directory, prefix = '') => {
|
|
266
|
+
for (const name of readdirSync(directory).sort()) {
|
|
267
|
+
if (!prefix && name === 'deployment-receipt.json') continue
|
|
268
|
+
const absolute = join(directory, name)
|
|
269
|
+
const entry = lstatSync(absolute)
|
|
270
|
+
const path = prefix ? `${prefix}/${name}` : name
|
|
271
|
+
if (entry.isDirectory()) {
|
|
272
|
+
hash.update(`directory\0${path}\0${entry.mode & 0o777}\0`)
|
|
273
|
+
visit(absolute, path)
|
|
274
|
+
} else if (entry.isFile()) {
|
|
275
|
+
hash.update(`file\0${path}\0${entry.mode & 0o777}\0${entry.size}\0`)
|
|
276
|
+
hash.update(readFileSync(absolute))
|
|
277
|
+
} else if (entry.isSymbolicLink()) {
|
|
278
|
+
const target = readlinkSync(absolute)
|
|
279
|
+
const resolvedTarget = resolve(dirname(absolute), target)
|
|
280
|
+
assertManagedChild(releaseDir, resolvedTarget, `release symlink ${path}`)
|
|
281
|
+
assertManagedChild(releaseDir, realpathSync(absolute), `release symlink ${path}`)
|
|
282
|
+
hash.update(`symlink\0${path}\0${target}\0`)
|
|
283
|
+
} else {
|
|
284
|
+
throw new ManagedRuntimeUpdateError(
|
|
285
|
+
'invalid_runtime_state',
|
|
286
|
+
`managed release contains unsupported filesystem entry ${path}`
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
visit(releaseDir)
|
|
292
|
+
return `sha512-${hash.digest('base64')}`
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function verifyReleaseReceipt(releaseDir, receipt) {
|
|
296
|
+
if (!RELEASE_INTEGRITY_RE.test(receipt.releaseIntegrity || '')) {
|
|
297
|
+
throw new ManagedRuntimeUpdateError(
|
|
298
|
+
'invalid_runtime_state',
|
|
299
|
+
'managed deployment receipt omitted release integrity'
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
if (releaseTreeIntegrity(releaseDir) !== receipt.releaseIntegrity) {
|
|
303
|
+
throw new ManagedRuntimeUpdateError(
|
|
304
|
+
'release_integrity_mismatch',
|
|
305
|
+
'managed release bytes no longer match the known-good deployment receipt'
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function fsyncDirectory(path) {
|
|
311
|
+
const fd = openSync(path, 'r')
|
|
312
|
+
try {
|
|
313
|
+
fsyncSync(fd)
|
|
314
|
+
} finally {
|
|
315
|
+
closeSync(fd)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function atomicWritePrivateJson(path, value, id = randomUUID()) {
|
|
320
|
+
const temp = join(dirname(path), `.${basename(path)}-${id}.tmp`)
|
|
321
|
+
let fd
|
|
322
|
+
try {
|
|
323
|
+
fd = openSync(temp, 'wx', 0o600)
|
|
324
|
+
writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`)
|
|
325
|
+
fsyncSync(fd)
|
|
326
|
+
closeSync(fd)
|
|
327
|
+
fd = undefined
|
|
328
|
+
renameSync(temp, path)
|
|
329
|
+
chmodSync(path, 0o600)
|
|
330
|
+
fsyncDirectory(dirname(path))
|
|
331
|
+
} finally {
|
|
332
|
+
if (fd !== undefined) closeSync(fd)
|
|
333
|
+
rmSync(temp, { force: true })
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function readCurrentRelease(managedDir) {
|
|
338
|
+
const current = join(managedDir, 'current')
|
|
339
|
+
if (!existsSync(current)) return null
|
|
340
|
+
if (!lstatSync(current).isSymbolicLink()) {
|
|
341
|
+
throw new ManagedRuntimeUpdateError(
|
|
342
|
+
'invalid_runtime_state',
|
|
343
|
+
'current runtime pointer is not a symbolic link'
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
const target = realpathSync(resolve(dirname(current), readlinkSync(current)))
|
|
347
|
+
assertManagedChild(managedDir, target, 'current runtime link')
|
|
348
|
+
const receiptPath = join(target, 'deployment-receipt.json')
|
|
349
|
+
if (!existsSync(receiptPath)) {
|
|
350
|
+
throw new ManagedRuntimeUpdateError(
|
|
351
|
+
'invalid_runtime_state',
|
|
352
|
+
'current managed runtime has no deployment receipt and cannot be used as known-good'
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
const receipt = parseJson(readFileSync(receiptPath, 'utf8'), 'known-good deployment receipt')
|
|
356
|
+
const commerceReady = receipt.commerceReady ?? Boolean(receipt.shopifyRuntime)
|
|
357
|
+
if (
|
|
358
|
+
!EXACT_RC_RE.test(receipt.version) ||
|
|
359
|
+
receipt.releaseDir !== target ||
|
|
360
|
+
typeof receipt.targetEnvironment !== 'string' ||
|
|
361
|
+
typeof commerceReady !== 'boolean' ||
|
|
362
|
+
(commerceReady &&
|
|
363
|
+
(typeof receipt.shopifyRuntime !== 'object' || receipt.shopifyRuntime === null))
|
|
364
|
+
) {
|
|
365
|
+
throw new ManagedRuntimeUpdateError(
|
|
366
|
+
'invalid_runtime_state',
|
|
367
|
+
'known-good deployment receipt is invalid'
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
verifyReleaseReceipt(target, receipt)
|
|
371
|
+
return {
|
|
372
|
+
releaseDir: target,
|
|
373
|
+
receipt: {
|
|
374
|
+
...receipt,
|
|
375
|
+
commerceReady,
|
|
376
|
+
commerceActionRequired: receipt.commerceActionRequired ?? null,
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function visaBinary(releaseDir) {
|
|
382
|
+
return join(releaseDir, 'node_modules', '.bin', 'visa')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function sha512Integrity(filePath) {
|
|
386
|
+
return `sha512-${createHash('sha512').update(readFileSync(filePath)).digest('base64')}`
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function stageExactRelease(config, dependencies = {}) {
|
|
390
|
+
const execFile = dependencies.execFileSync ?? execFileSync
|
|
391
|
+
const id = dependencies.randomUUID?.() ?? randomUUID()
|
|
392
|
+
const correlationId = config.correlationId ?? 'standalone-stage'
|
|
393
|
+
const observedNpm = (operation, args, options) => {
|
|
394
|
+
const startedAt = Date.now()
|
|
395
|
+
structuredLog(
|
|
396
|
+
'external_request',
|
|
397
|
+
{ correlationId, service: 'npm_registry', operation, targetVersion: config.exactVersion },
|
|
398
|
+
dependencies.logger
|
|
399
|
+
)
|
|
400
|
+
try {
|
|
401
|
+
const result = execFile('npm', args, options)
|
|
402
|
+
structuredLog(
|
|
403
|
+
'external_response',
|
|
404
|
+
{
|
|
405
|
+
correlationId,
|
|
406
|
+
service: 'npm_registry',
|
|
407
|
+
operation,
|
|
408
|
+
ok: true,
|
|
409
|
+
durationMs: Date.now() - startedAt,
|
|
410
|
+
},
|
|
411
|
+
dependencies.logger
|
|
412
|
+
)
|
|
413
|
+
return result
|
|
414
|
+
} catch (error) {
|
|
415
|
+
structuredLog(
|
|
416
|
+
'external_response',
|
|
417
|
+
{
|
|
418
|
+
correlationId,
|
|
419
|
+
service: 'npm_registry',
|
|
420
|
+
operation,
|
|
421
|
+
ok: false,
|
|
422
|
+
durationMs: Date.now() - startedAt,
|
|
423
|
+
},
|
|
424
|
+
dependencies.logger
|
|
425
|
+
)
|
|
426
|
+
throw error
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const releasesDir = join(config.managedDir, 'releases')
|
|
430
|
+
const releaseDir = join(releasesDir, config.exactVersion)
|
|
431
|
+
ensureCanonicalDirectory(releasesDir, 'managed releases directory')
|
|
432
|
+
|
|
433
|
+
if (existsSync(releaseDir)) {
|
|
434
|
+
if (lstatSync(releaseDir).isSymbolicLink()) {
|
|
435
|
+
throw new ManagedRuntimeUpdateError(
|
|
436
|
+
'invalid_runtime_state',
|
|
437
|
+
'managed release directory must not be a symbolic link'
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
const receiptPath = join(releaseDir, 'deployment-receipt.json')
|
|
441
|
+
if (existsSync(receiptPath) && existsSync(visaBinary(releaseDir))) {
|
|
442
|
+
const installed = parseJson(
|
|
443
|
+
readFileSync(join(releaseDir, 'node_modules', '@visa', 'cli', 'package.json'), 'utf8'),
|
|
444
|
+
'installed package manifest'
|
|
445
|
+
)
|
|
446
|
+
const receipt = parseJson(readFileSync(receiptPath, 'utf8'), 'deployment receipt')
|
|
447
|
+
if (
|
|
448
|
+
installed.version !== config.exactVersion ||
|
|
449
|
+
receipt.version !== config.exactVersion ||
|
|
450
|
+
receipt.releaseDir !== releaseDir
|
|
451
|
+
) {
|
|
452
|
+
throw new ManagedRuntimeUpdateError(
|
|
453
|
+
'invalid_runtime_state',
|
|
454
|
+
'existing managed release does not match its deployment receipt'
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
verifyReleaseReceipt(releaseDir, receipt)
|
|
458
|
+
return releaseDir
|
|
459
|
+
}
|
|
460
|
+
// A prior attempt that never passed loaded-process health is not known-good.
|
|
461
|
+
const quarantine = join(releasesDir, `.quarantine-${config.exactVersion}-${id}`)
|
|
462
|
+
renameSync(releaseDir, quarantine)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const stagingDir = join(releasesDir, `.staging-${config.exactVersion}-${id}`)
|
|
466
|
+
mkdirSync(stagingDir, { recursive: true, mode: 0o700 })
|
|
467
|
+
try {
|
|
468
|
+
const packageSpec = `@visa/cli@${config.exactVersion}`
|
|
469
|
+
const metadata = parseJson(
|
|
470
|
+
observedNpm(
|
|
471
|
+
'view_exact_metadata',
|
|
472
|
+
['view', packageSpec, 'version', 'dist.integrity', '--json'],
|
|
473
|
+
{
|
|
474
|
+
encoding: 'utf8',
|
|
475
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
476
|
+
timeout: config.commandTimeoutMs,
|
|
477
|
+
}
|
|
478
|
+
),
|
|
479
|
+
'npm view'
|
|
480
|
+
)
|
|
481
|
+
const expectedIntegrity = metadata?.dist?.integrity ?? metadata?.['dist.integrity'] ?? ''
|
|
482
|
+
if (metadata.version !== config.exactVersion || !SRI_RE.test(expectedIntegrity)) {
|
|
483
|
+
throw new ManagedRuntimeUpdateError(
|
|
484
|
+
'target_not_authorized',
|
|
485
|
+
'registry metadata did not attest the exact requested RC and sha512 integrity'
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const pack = parseJson(
|
|
490
|
+
observedNpm(
|
|
491
|
+
'download_exact_tarball',
|
|
492
|
+
['pack', packageSpec, '--json', '--pack-destination', stagingDir],
|
|
493
|
+
{
|
|
494
|
+
encoding: 'utf8',
|
|
495
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
496
|
+
timeout: config.commandTimeoutMs,
|
|
497
|
+
}
|
|
498
|
+
),
|
|
499
|
+
'npm pack'
|
|
500
|
+
)
|
|
501
|
+
const packed = Array.isArray(pack) ? pack[0] : null
|
|
502
|
+
if (
|
|
503
|
+
!packed ||
|
|
504
|
+
packed.version !== config.exactVersion ||
|
|
505
|
+
packed.integrity !== expectedIntegrity ||
|
|
506
|
+
basename(packed.filename) !== packed.filename
|
|
507
|
+
) {
|
|
508
|
+
throw new ManagedRuntimeUpdateError(
|
|
509
|
+
'package_integrity_mismatch',
|
|
510
|
+
'packed artifact metadata did not match the exact authorized RC'
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
const tarball = join(stagingDir, packed.filename)
|
|
514
|
+
if (sha512Integrity(tarball) !== expectedIntegrity) {
|
|
515
|
+
throw new ManagedRuntimeUpdateError(
|
|
516
|
+
'package_integrity_mismatch',
|
|
517
|
+
'packed artifact bytes did not match the registry sha512 integrity'
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
observedNpm(
|
|
523
|
+
'install_staged_tarball',
|
|
524
|
+
['install', '--prefix', stagingDir, '--omit=dev', tarball],
|
|
525
|
+
{
|
|
526
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
527
|
+
timeout: config.commandTimeoutMs,
|
|
528
|
+
}
|
|
529
|
+
)
|
|
530
|
+
} catch (error) {
|
|
531
|
+
throw new ManagedRuntimeUpdateError(
|
|
532
|
+
'package_install_failed',
|
|
533
|
+
'the exact verified package could not be installed into the staged release',
|
|
534
|
+
{ cause: error }
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
const installed = parseJson(
|
|
538
|
+
readFileSync(join(stagingDir, 'node_modules', '@visa', 'cli', 'package.json'), 'utf8'),
|
|
539
|
+
'installed package manifest'
|
|
540
|
+
)
|
|
541
|
+
if (installed.version !== config.exactVersion || !existsSync(visaBinary(stagingDir))) {
|
|
542
|
+
throw new ManagedRuntimeUpdateError(
|
|
543
|
+
'installed_version_mismatch',
|
|
544
|
+
'staged CLI did not expose the exact requested version'
|
|
545
|
+
)
|
|
546
|
+
}
|
|
547
|
+
rmSync(tarball, { force: true })
|
|
548
|
+
renameSync(stagingDir, releaseDir)
|
|
549
|
+
return releaseDir
|
|
550
|
+
} catch (error) {
|
|
551
|
+
rmSync(stagingDir, { recursive: true, force: true })
|
|
552
|
+
throw error
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function activateRelease(managedDir, releaseDir, id = randomUUID()) {
|
|
557
|
+
assertManagedChild(managedDir, releaseDir, 'release')
|
|
558
|
+
const current = join(managedDir, 'current')
|
|
559
|
+
const next = join(managedDir, `.current-${id}`)
|
|
560
|
+
symlinkSync(relative(managedDir, releaseDir), next, 'dir')
|
|
561
|
+
renameSync(next, current)
|
|
562
|
+
fsyncDirectory(managedDir)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function removeCurrentIfTarget(managedDir, releaseDir) {
|
|
566
|
+
const current = join(managedDir, 'current')
|
|
567
|
+
if (!existsSync(current) && !lstatExists(current)) return
|
|
568
|
+
if (!lstatSync(current).isSymbolicLink()) return
|
|
569
|
+
if (resolve(dirname(current), readlinkSync(current)) === releaseDir) {
|
|
570
|
+
unlinkSync(current)
|
|
571
|
+
fsyncDirectory(managedDir)
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function lstatExists(path) {
|
|
576
|
+
try {
|
|
577
|
+
lstatSync(path)
|
|
578
|
+
return true
|
|
579
|
+
} catch (error) {
|
|
580
|
+
if (error?.code === 'ENOENT') return false
|
|
581
|
+
throw error
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function pendingTransactionPath(managedDir) {
|
|
586
|
+
return join(managedDir, PENDING_TRANSACTION_FILE)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function writePendingTransaction(config, targetRelease, previous, id) {
|
|
590
|
+
atomicWritePrivateJson(
|
|
591
|
+
pendingTransactionPath(config.managedDir),
|
|
592
|
+
{
|
|
593
|
+
correlationId: config.correlationId,
|
|
594
|
+
targetRelease,
|
|
595
|
+
previousRelease: previous?.releaseDir ?? null,
|
|
596
|
+
createdAt: new Date().toISOString(),
|
|
597
|
+
},
|
|
598
|
+
id
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function clearPendingTransaction(managedDir) {
|
|
603
|
+
const path = pendingTransactionPath(managedDir)
|
|
604
|
+
if (!lstatExists(path)) return
|
|
605
|
+
unlinkSync(path)
|
|
606
|
+
fsyncDirectory(managedDir)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function recoverInterruptedActivation(managedDir, logger = console) {
|
|
610
|
+
const path = pendingTransactionPath(managedDir)
|
|
611
|
+
if (!lstatExists(path)) return false
|
|
612
|
+
const pending = parseJson(readFileSync(path, 'utf8'), 'pending managed update')
|
|
613
|
+
if (typeof pending.targetRelease !== 'string') {
|
|
614
|
+
throw new ManagedRuntimeUpdateError(
|
|
615
|
+
'invalid_runtime_state',
|
|
616
|
+
'pending managed update is invalid'
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
assertManagedChild(managedDir, pending.targetRelease, 'pending target release')
|
|
620
|
+
if (pending.previousRelease !== null && typeof pending.previousRelease !== 'string') {
|
|
621
|
+
throw new ManagedRuntimeUpdateError(
|
|
622
|
+
'invalid_runtime_state',
|
|
623
|
+
'pending previous release is invalid'
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
if (pending.previousRelease) {
|
|
627
|
+
assertManagedChild(managedDir, pending.previousRelease, 'pending previous release')
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const targetReceipt = join(pending.targetRelease, 'deployment-receipt.json')
|
|
631
|
+
if (existsSync(targetReceipt)) {
|
|
632
|
+
clearPendingTransaction(managedDir)
|
|
633
|
+
return false
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (pending.previousRelease) {
|
|
637
|
+
const previousReceiptPath = join(pending.previousRelease, 'deployment-receipt.json')
|
|
638
|
+
const receipt = parseJson(
|
|
639
|
+
readFileSync(previousReceiptPath, 'utf8'),
|
|
640
|
+
'previous deployment receipt'
|
|
641
|
+
)
|
|
642
|
+
verifyReleaseReceipt(pending.previousRelease, receipt)
|
|
643
|
+
activateRelease(managedDir, pending.previousRelease)
|
|
644
|
+
} else {
|
|
645
|
+
removeCurrentIfTarget(managedDir, pending.targetRelease)
|
|
646
|
+
}
|
|
647
|
+
clearPendingTransaction(managedDir)
|
|
648
|
+
structuredLog(
|
|
649
|
+
'interrupted_activation_recovered',
|
|
650
|
+
{
|
|
651
|
+
correlationId: pending.correlationId || null,
|
|
652
|
+
restoredPrevious: Boolean(pending.previousRelease),
|
|
653
|
+
},
|
|
654
|
+
logger
|
|
655
|
+
)
|
|
656
|
+
return true
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function runCommerceInstall(releaseDir, env, execFile, timeout) {
|
|
660
|
+
try {
|
|
661
|
+
execFile(visaBinary(releaseDir), commerceSkillArgs(env), {
|
|
662
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
663
|
+
timeout,
|
|
664
|
+
})
|
|
665
|
+
} catch (error) {
|
|
666
|
+
throw new ManagedRuntimeUpdateError(
|
|
667
|
+
'rebuild_or_remount_required',
|
|
668
|
+
'the managed runtime could not refresh its installer-owned commerce mount; rebuild or remount it',
|
|
669
|
+
{ cause: error }
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function certifyLocalBinary(releaseDir, execFile, timeout) {
|
|
675
|
+
const rawCapabilities = execFile(visaBinary(releaseDir), ['capabilities', '--format', 'json'], {
|
|
676
|
+
encoding: 'utf8',
|
|
677
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
678
|
+
timeout,
|
|
679
|
+
})
|
|
680
|
+
const capabilities = parseJson(rawCapabilities, 'visa capabilities')
|
|
681
|
+
const build = capabilities?.data?.build ?? capabilities?.build
|
|
682
|
+
if (typeof build?.version !== 'string' || typeof build?.targetEnvironment !== 'string') {
|
|
683
|
+
throw new ManagedRuntimeUpdateError(
|
|
684
|
+
'local_readiness_failed',
|
|
685
|
+
'local capabilities omitted build identity'
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
version: build.version,
|
|
691
|
+
targetEnvironment: build.targetEnvironment,
|
|
692
|
+
minCompatibleVersion: build.minCompatibleVersion || null,
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function certifyLocalCommerce(releaseDir, env, execFile, timeout) {
|
|
697
|
+
let rawCommerce
|
|
698
|
+
try {
|
|
699
|
+
rawCommerce = execFile(visaBinary(releaseDir), commerceSkillArgs(env, true), {
|
|
700
|
+
encoding: 'utf8',
|
|
701
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
702
|
+
timeout,
|
|
703
|
+
})
|
|
704
|
+
} catch (error) {
|
|
705
|
+
throw new ManagedRuntimeUpdateError(
|
|
706
|
+
'commerce_runtime_not_ready',
|
|
707
|
+
'local Shopify UCP runtime readiness command failed',
|
|
708
|
+
{ cause: error }
|
|
709
|
+
)
|
|
710
|
+
}
|
|
711
|
+
const commerce = parseJson(rawCommerce, 'commerce readiness')
|
|
712
|
+
if (
|
|
713
|
+
commerce?.localBundleReady !== true ||
|
|
714
|
+
commerce?.shopifyRuntime?.status !== 'ready' ||
|
|
715
|
+
typeof commerce?.shopifyRuntime?.toolSchemaFingerprint !== 'string'
|
|
716
|
+
) {
|
|
717
|
+
throw new ManagedRuntimeUpdateError(
|
|
718
|
+
'commerce_runtime_not_ready',
|
|
719
|
+
'local Shopify UCP runtime did not certify its required tools'
|
|
720
|
+
)
|
|
721
|
+
}
|
|
722
|
+
return commerce.shopifyRuntime
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function assessLocalCommerce(releaseDir, env, execFile, timeout) {
|
|
726
|
+
try {
|
|
727
|
+
runCommerceInstall(releaseDir, env, execFile, timeout)
|
|
728
|
+
return {
|
|
729
|
+
commerceReady: true,
|
|
730
|
+
commerceActionRequired: null,
|
|
731
|
+
shopifyRuntime: certifyLocalCommerce(releaseDir, env, execFile, timeout),
|
|
732
|
+
}
|
|
733
|
+
} catch (error) {
|
|
734
|
+
const code =
|
|
735
|
+
error instanceof ManagedRuntimeUpdateError && error.code === 'rebuild_or_remount_required'
|
|
736
|
+
? 'rebuild_or_remount_required'
|
|
737
|
+
: 'commerce_runtime_not_ready'
|
|
738
|
+
return {
|
|
739
|
+
commerceReady: false,
|
|
740
|
+
commerceActionRequired: code,
|
|
741
|
+
shopifyRuntime: null,
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function assessLoadedCommerce(loaded, localCommerce) {
|
|
747
|
+
if (!localCommerce.commerceReady) return localCommerce
|
|
748
|
+
try {
|
|
749
|
+
assertSameShopifyRuntime(loaded.shopifyRuntime, localCommerce.shopifyRuntime)
|
|
750
|
+
return localCommerce
|
|
751
|
+
} catch {
|
|
752
|
+
return {
|
|
753
|
+
commerceReady: false,
|
|
754
|
+
commerceActionRequired: 'commerce_runtime_not_ready',
|
|
755
|
+
shopifyRuntime: null,
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function restartSupervisor(config, dependencies, operation = 'restart') {
|
|
761
|
+
const execShell = dependencies.execSync ?? execSync
|
|
762
|
+
const startedAt = Date.now()
|
|
763
|
+
structuredLog(
|
|
764
|
+
'supervisor_request',
|
|
765
|
+
{ correlationId: config.correlationId, operation },
|
|
766
|
+
dependencies.logger
|
|
767
|
+
)
|
|
768
|
+
try {
|
|
769
|
+
execShell(config.restartCommand, {
|
|
770
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
771
|
+
timeout: config.commandTimeoutMs,
|
|
772
|
+
})
|
|
773
|
+
} catch (error) {
|
|
774
|
+
structuredLog(
|
|
775
|
+
'supervisor_response',
|
|
776
|
+
{
|
|
777
|
+
correlationId: config.correlationId,
|
|
778
|
+
operation,
|
|
779
|
+
ok: false,
|
|
780
|
+
durationMs: Date.now() - startedAt,
|
|
781
|
+
},
|
|
782
|
+
dependencies.logger
|
|
783
|
+
)
|
|
784
|
+
throw new ManagedRuntimeUpdateError('restart_failed', 'runtime supervisor restart failed', {
|
|
785
|
+
cause: error,
|
|
786
|
+
})
|
|
787
|
+
}
|
|
788
|
+
structuredLog(
|
|
789
|
+
'supervisor_response',
|
|
790
|
+
{
|
|
791
|
+
correlationId: config.correlationId,
|
|
792
|
+
operation,
|
|
793
|
+
ok: true,
|
|
794
|
+
durationMs: Date.now() - startedAt,
|
|
795
|
+
},
|
|
796
|
+
dependencies.logger
|
|
797
|
+
)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function restartAndReadLoaded(config, dependencies) {
|
|
801
|
+
const execShell = dependencies.execSync ?? execSync
|
|
802
|
+
restartSupervisor(config, dependencies)
|
|
803
|
+
let raw
|
|
804
|
+
const startedAt = Date.now()
|
|
805
|
+
structuredLog(
|
|
806
|
+
'supervisor_request',
|
|
807
|
+
{ correlationId: config.correlationId, operation: 'loaded_readiness' },
|
|
808
|
+
dependencies.logger
|
|
809
|
+
)
|
|
810
|
+
try {
|
|
811
|
+
raw = execShell(config.readinessCommand, runtimeReadinessExecOptions(config.readinessTimeoutMs))
|
|
812
|
+
} catch (error) {
|
|
813
|
+
structuredLog(
|
|
814
|
+
'supervisor_response',
|
|
815
|
+
{
|
|
816
|
+
correlationId: config.correlationId,
|
|
817
|
+
operation: 'loaded_readiness',
|
|
818
|
+
ok: false,
|
|
819
|
+
durationMs: Date.now() - startedAt,
|
|
820
|
+
},
|
|
821
|
+
dependencies.logger
|
|
822
|
+
)
|
|
823
|
+
throw new ManagedRuntimeUpdateError(
|
|
824
|
+
'loaded_readiness_failed',
|
|
825
|
+
'loaded runtime readiness command failed or timed out',
|
|
826
|
+
{ cause: error }
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
structuredLog(
|
|
830
|
+
'supervisor_response',
|
|
831
|
+
{
|
|
832
|
+
correlationId: config.correlationId,
|
|
833
|
+
operation: 'loaded_readiness',
|
|
834
|
+
ok: true,
|
|
835
|
+
durationMs: Date.now() - startedAt,
|
|
836
|
+
},
|
|
837
|
+
dependencies.logger
|
|
838
|
+
)
|
|
839
|
+
return parseRuntimeReadiness(raw)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export function writeReceipt(releaseDir, receipt, id) {
|
|
843
|
+
const path = join(releaseDir, 'deployment-receipt.json')
|
|
844
|
+
atomicWritePrivateJson(path, receipt, id)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function rollbackKnownGood(config, previous, dependencies, correlationId) {
|
|
848
|
+
if (!previous) {
|
|
849
|
+
throw new ManagedRuntimeUpdateError(
|
|
850
|
+
'rollback_unavailable',
|
|
851
|
+
'fresh managed runtime had no known-good release to restore'
|
|
852
|
+
)
|
|
853
|
+
}
|
|
854
|
+
let lastError
|
|
855
|
+
for (let attempt = 1; attempt <= MAX_ROLLBACK_ATTEMPTS; attempt += 1) {
|
|
856
|
+
try {
|
|
857
|
+
verifyReleaseReceipt(previous.releaseDir, previous.receipt)
|
|
858
|
+
activateRelease(config.managedDir, previous.releaseDir)
|
|
859
|
+
const loaded = restartAndReadLoaded(config, dependencies)
|
|
860
|
+
assertSameRuntimeBuild(loaded, {
|
|
861
|
+
version: previous.receipt.version,
|
|
862
|
+
targetEnvironment: previous.receipt.targetEnvironment,
|
|
863
|
+
})
|
|
864
|
+
structuredLog(
|
|
865
|
+
'rollback_succeeded',
|
|
866
|
+
{ correlationId, attempt, version: loaded.version },
|
|
867
|
+
dependencies.logger
|
|
868
|
+
)
|
|
869
|
+
return loaded
|
|
870
|
+
} catch (error) {
|
|
871
|
+
lastError = error
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
throw new ManagedRuntimeUpdateError(
|
|
875
|
+
'rollback_failed',
|
|
876
|
+
`known-good runtime could not be restored after ${MAX_ROLLBACK_ATTEMPTS} attempts`,
|
|
877
|
+
{ cause: lastError }
|
|
878
|
+
)
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export async function runManagedRuntimeUpdate(env = process.env, dependencies = {}) {
|
|
882
|
+
const baseConfig = resolveManagedUpdateConfig(env)
|
|
883
|
+
// Prefer a caller-supplied id so a convergence run's discovery logs and this
|
|
884
|
+
// transaction's logs join on one correlation id.
|
|
885
|
+
const correlationId = dependencies.correlationId ?? dependencies.randomUUID?.() ?? randomUUID()
|
|
886
|
+
const config = { ...baseConfig, correlationId }
|
|
887
|
+
const releaseLock = acquireUpdateLock(config.managedDir, correlationId, {
|
|
888
|
+
leaseMs: config.lockLeaseMs,
|
|
889
|
+
})
|
|
890
|
+
const startedAt = Date.now()
|
|
891
|
+
let previous
|
|
892
|
+
let activated = false
|
|
893
|
+
let targetRelease
|
|
894
|
+
try {
|
|
895
|
+
recoverInterruptedActivation(config.managedDir, dependencies.logger)
|
|
896
|
+
previous = readCurrentRelease(config.managedDir)
|
|
897
|
+
if (previous && previous.receipt.targetEnvironment !== config.expectedEnvironment) {
|
|
898
|
+
throw new ManagedRuntimeUpdateError(
|
|
899
|
+
'target_environment_mismatch',
|
|
900
|
+
`known-good runtime targets ${previous.receipt.targetEnvironment}, expected ${config.expectedEnvironment}`
|
|
901
|
+
)
|
|
902
|
+
}
|
|
903
|
+
if (previous && compareExactRc(config.exactVersion, previous.receipt.version) < 0) {
|
|
904
|
+
throw new ManagedRuntimeUpdateError(
|
|
905
|
+
'target_rollback_refused',
|
|
906
|
+
`exact target ${config.exactVersion} is older than known-good ${previous.receipt.version}`
|
|
907
|
+
)
|
|
908
|
+
}
|
|
909
|
+
structuredLog(
|
|
910
|
+
'update_started',
|
|
911
|
+
{
|
|
912
|
+
correlationId,
|
|
913
|
+
targetVersion: config.exactVersion,
|
|
914
|
+
previousVersion: previous?.receipt.version ?? null,
|
|
915
|
+
},
|
|
916
|
+
dependencies.logger
|
|
917
|
+
)
|
|
918
|
+
targetRelease = stageExactRelease(config, dependencies)
|
|
919
|
+
const execFile = dependencies.execFileSync ?? execFileSync
|
|
920
|
+
const local = certifyLocalBinary(targetRelease, execFile, config.commandTimeoutMs)
|
|
921
|
+
if (local.version !== config.exactVersion) {
|
|
922
|
+
throw new ManagedRuntimeUpdateError(
|
|
923
|
+
'installed_version_mismatch',
|
|
924
|
+
`staged CLI reported ${local.version} instead of ${config.exactVersion}`
|
|
925
|
+
)
|
|
926
|
+
}
|
|
927
|
+
if (local.targetEnvironment !== config.expectedEnvironment) {
|
|
928
|
+
throw new ManagedRuntimeUpdateError(
|
|
929
|
+
'target_environment_mismatch',
|
|
930
|
+
`staged CLI targets ${local.targetEnvironment}, expected ${config.expectedEnvironment}`
|
|
931
|
+
)
|
|
932
|
+
}
|
|
933
|
+
const localCommerce = assessLocalCommerce(targetRelease, env, execFile, config.commandTimeoutMs)
|
|
934
|
+
|
|
935
|
+
const releaseIntegrity = releaseTreeIntegrity(targetRelease)
|
|
936
|
+
writePendingTransaction(config, targetRelease, previous, correlationId)
|
|
937
|
+
activateRelease(config.managedDir, targetRelease)
|
|
938
|
+
activated = true
|
|
939
|
+
const loaded = restartAndReadLoaded(config, dependencies)
|
|
940
|
+
assertSameRuntimeBuild(loaded, local)
|
|
941
|
+
const commerce = assessLoadedCommerce(loaded, localCommerce)
|
|
942
|
+
if (!commerce.commerceReady) {
|
|
943
|
+
structuredLog(
|
|
944
|
+
'commerce_action_required',
|
|
945
|
+
{
|
|
946
|
+
correlationId,
|
|
947
|
+
version: local.version,
|
|
948
|
+
code: commerce.commerceActionRequired,
|
|
949
|
+
checkoutBlocked: true,
|
|
950
|
+
},
|
|
951
|
+
dependencies.logger
|
|
952
|
+
)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const receipt = {
|
|
956
|
+
ok: true,
|
|
957
|
+
binaryReady: true,
|
|
958
|
+
correlationId,
|
|
959
|
+
version: local.version,
|
|
960
|
+
targetEnvironment: local.targetEnvironment,
|
|
961
|
+
minCompatibleVersion: local.minCompatibleVersion,
|
|
962
|
+
releaseDir: targetRelease,
|
|
963
|
+
previousVersion: previous?.receipt.version ?? null,
|
|
964
|
+
releaseIntegrity,
|
|
965
|
+
commerceReady: commerce.commerceReady,
|
|
966
|
+
checkoutBlocked: !commerce.commerceReady,
|
|
967
|
+
commerceActionRequired: commerce.commerceActionRequired,
|
|
968
|
+
shopifyRuntime: commerce.commerceReady
|
|
969
|
+
? {
|
|
970
|
+
serverName: 'shopify-ucp',
|
|
971
|
+
installedVersion: commerce.shopifyRuntime.installedVersion,
|
|
972
|
+
toolSchemaFingerprint: commerce.shopifyRuntime.toolSchemaFingerprint,
|
|
973
|
+
command: commerce.shopifyRuntime.command,
|
|
974
|
+
args: commerce.shopifyRuntime.args,
|
|
975
|
+
blockedTools: commerce.shopifyRuntime.blockedTools,
|
|
976
|
+
}
|
|
977
|
+
: null,
|
|
978
|
+
durationMs: Date.now() - startedAt,
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
;(dependencies.writeReceipt ?? writeReceipt)(targetRelease, receipt, correlationId)
|
|
982
|
+
} catch (error) {
|
|
983
|
+
throw new ManagedRuntimeUpdateError(
|
|
984
|
+
'receipt_write_failed',
|
|
985
|
+
'loaded runtime passed health but its durable deployment receipt could not be written',
|
|
986
|
+
{ cause: error }
|
|
987
|
+
)
|
|
988
|
+
}
|
|
989
|
+
clearPendingTransaction(config.managedDir)
|
|
990
|
+
structuredLog(
|
|
991
|
+
'update_succeeded',
|
|
992
|
+
{
|
|
993
|
+
correlationId,
|
|
994
|
+
version: receipt.version,
|
|
995
|
+
binaryReady: receipt.binaryReady,
|
|
996
|
+
targetEnvironment: receipt.targetEnvironment,
|
|
997
|
+
releaseIntegrity: receipt.releaseIntegrity,
|
|
998
|
+
commerceReady: receipt.commerceReady,
|
|
999
|
+
checkoutBlocked: receipt.checkoutBlocked,
|
|
1000
|
+
commerceActionRequired: receipt.commerceActionRequired,
|
|
1001
|
+
durationMs: receipt.durationMs,
|
|
1002
|
+
},
|
|
1003
|
+
dependencies.logger
|
|
1004
|
+
)
|
|
1005
|
+
return {
|
|
1006
|
+
ok: true,
|
|
1007
|
+
binaryReady: true,
|
|
1008
|
+
correlationId,
|
|
1009
|
+
version: receipt.version,
|
|
1010
|
+
targetEnvironment: receipt.targetEnvironment,
|
|
1011
|
+
minCompatibleVersion: receipt.minCompatibleVersion,
|
|
1012
|
+
previousVersion: receipt.previousVersion,
|
|
1013
|
+
releaseIntegrity: receipt.releaseIntegrity,
|
|
1014
|
+
commerceReady: receipt.commerceReady,
|
|
1015
|
+
checkoutBlocked: receipt.checkoutBlocked,
|
|
1016
|
+
commerceActionRequired: receipt.commerceActionRequired,
|
|
1017
|
+
durationMs: receipt.durationMs,
|
|
1018
|
+
shopifyRuntime: receipt.shopifyRuntime
|
|
1019
|
+
? {
|
|
1020
|
+
serverName: receipt.shopifyRuntime.serverName,
|
|
1021
|
+
installedVersion: receipt.shopifyRuntime.installedVersion,
|
|
1022
|
+
toolSchemaFingerprint: receipt.shopifyRuntime.toolSchemaFingerprint,
|
|
1023
|
+
blockedTools: receipt.shopifyRuntime.blockedTools,
|
|
1024
|
+
}
|
|
1025
|
+
: null,
|
|
1026
|
+
}
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
const updateError =
|
|
1029
|
+
error instanceof ManagedRuntimeUpdateError
|
|
1030
|
+
? error
|
|
1031
|
+
: new ManagedRuntimeUpdateError('loaded_readiness_mismatch', error.message, {
|
|
1032
|
+
cause: error,
|
|
1033
|
+
})
|
|
1034
|
+
structuredLog(
|
|
1035
|
+
'update_failed',
|
|
1036
|
+
{ correlationId, code: updateError.code, activated },
|
|
1037
|
+
dependencies.logger
|
|
1038
|
+
)
|
|
1039
|
+
if (activated) {
|
|
1040
|
+
try {
|
|
1041
|
+
if (previous) {
|
|
1042
|
+
rollbackKnownGood(config, previous, dependencies, correlationId)
|
|
1043
|
+
} else if (targetRelease) {
|
|
1044
|
+
removeCurrentIfTarget(config.managedDir, targetRelease)
|
|
1045
|
+
// A successful first activation has no managed known-good release to
|
|
1046
|
+
// restore. Restart after removing its pointer so the supervisor
|
|
1047
|
+
// cannot keep serving the now-uncommitted target process while
|
|
1048
|
+
// `current` says that no managed release is active.
|
|
1049
|
+
restartSupervisor(config, dependencies, 'deactivation_restart')
|
|
1050
|
+
}
|
|
1051
|
+
clearPendingTransaction(config.managedDir)
|
|
1052
|
+
} catch (rollbackError) {
|
|
1053
|
+
throw new ManagedRuntimeUpdateError(
|
|
1054
|
+
'rollback_failed',
|
|
1055
|
+
`update failed with ${updateError.code}; the prior runtime could not be restored`,
|
|
1056
|
+
{ cause: new AggregateError([updateError, rollbackError]) }
|
|
1057
|
+
)
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
throw updateError
|
|
1061
|
+
} finally {
|
|
1062
|
+
releaseLock()
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async function main() {
|
|
1067
|
+
try {
|
|
1068
|
+
const result = await runManagedRuntimeUpdate(process.env)
|
|
1069
|
+
process.stdout.write(`${JSON.stringify(result)}\n`)
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
const code = error instanceof ManagedRuntimeUpdateError ? error.code : 'update_failed'
|
|
1072
|
+
process.stderr.write(`${JSON.stringify({ ok: false, code, message: error.message })}\n`)
|
|
1073
|
+
process.exitCode = 1
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
1078
|
+
void main()
|
|
1079
|
+
}
|