@rightkit/release 0.2.89 → 0.2.91
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/direct-bootstrap.mjs +51 -16
- package/package.json +1 -1
- package/rightkit-versions.json +4 -3
package/direct-bootstrap.mjs
CHANGED
|
@@ -205,9 +205,11 @@ export function renderPowerShellBootstrap({
|
|
|
205
205
|
expectedStatus = "complete",
|
|
206
206
|
healthAssertions = null,
|
|
207
207
|
preflightArgs = [],
|
|
208
|
+
allowLocalReleaseRoot = false,
|
|
208
209
|
}) {
|
|
209
210
|
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) throw new Error("invalid GitHub repository");
|
|
210
211
|
if (!VERSION.test(bootstrapVersion)) throw new Error("invalid bootstrap version");
|
|
212
|
+
if (typeof allowLocalReleaseRoot !== "boolean") throw new Error("allowLocalReleaseRoot must be boolean");
|
|
211
213
|
assertRelativeFile(executablePath, "executable path");
|
|
212
214
|
if (!Array.isArray(acceptedManifestSigners) || !acceptedManifestSigners.length) throw new Error("accepted manifest signers are required");
|
|
213
215
|
const signers = {};
|
|
@@ -246,6 +248,7 @@ export function renderPowerShellBootstrap({
|
|
|
246
248
|
statusArgs,
|
|
247
249
|
healthAssertions: assertions,
|
|
248
250
|
preflightArgs,
|
|
251
|
+
...(allowLocalReleaseRoot ? { allowLocalReleaseRoot: true } : {}),
|
|
249
252
|
};
|
|
250
253
|
// Bind generated contract metadata so a caller cannot pass a marker plus an
|
|
251
254
|
// arbitrary base64 payload and have it treated as a RightRelease bootstrap.
|
|
@@ -256,7 +259,9 @@ export function renderPowerShellBootstrap({
|
|
|
256
259
|
const lines = [
|
|
257
260
|
"# rightkit-direct-bootstrap:v1",
|
|
258
261
|
"# generated contract: generator=rightkit-direct-bootstrap contractSha256-bound",
|
|
259
|
-
|
|
262
|
+
allowLocalReleaseRoot
|
|
263
|
+
? "[CmdletBinding()] param([string]$Version, [switch]$AllowDowngrade, [string]$ReleaseRoot)"
|
|
264
|
+
: "[CmdletBinding()] param([string]$Version, [switch]$AllowDowngrade)",
|
|
260
265
|
"$ErrorActionPreference = 'Stop'",
|
|
261
266
|
"$ProgressPreference = 'SilentlyContinue'",
|
|
262
267
|
"$Contract = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(" + psQuote(contractB64) + ")) | ConvertFrom-Json",
|
|
@@ -270,18 +275,32 @@ export function renderPowerShellBootstrap({
|
|
|
270
275
|
"function Expand-BootstrapArgs($Values, $ActiveVersionRoot, $ActiveVersion) { foreach ($Value in $Values) { ([string]$Value).Replace('{current}', $Current).Replace('{installRoot}', $InstallRoot).Replace('{versionRoot}', $ActiveVersionRoot).Replace('{version}', $ActiveVersion) } }",
|
|
271
276
|
"function Resolve-HealthPath($Value, [string]$JsonPath) { $CurrentValue = $Value; foreach ($Segment in $JsonPath.Split('.')) { if ($Segment -match '^\\d+$') { $Index = [int]$Segment; if ($null -eq $CurrentValue -or $Index -ge @($CurrentValue).Count) { throw ('Health path is missing: ' + $JsonPath) }; $CurrentValue = @($CurrentValue)[$Index] } else { $Property = $CurrentValue.PSObject.Properties[$Segment]; if ($null -eq $Property) { throw ('Health path is missing: ' + $JsonPath) }; $CurrentValue = $Property.Value } }; return $CurrentValue }",
|
|
272
277
|
"function Assert-Health($Health, [string]$ActiveVersion) { foreach ($Assertion in $Contract.healthAssertions) { $Actual = Resolve-HealthPath $Health ([string]$Assertion.path); if ($Assertion.PSObject.Properties.Name -contains 'equals') { $Expected = $Assertion.equals; if ($Expected -is [string]) { $Expected = $Expected.Replace('{version}', $ActiveVersion).Replace('{current}', $Current).Replace('{installRoot}', $InstallRoot) }; if ($Actual -ne $Expected) { throw ('Health assertion failed: ' + $Assertion.path) } } elseif ($Assertion.nonempty -eq $true) { if ($null -eq $Actual -or ([string]$Actual).Trim().Length -eq 0) { throw ('Health assertion failed: ' + $Assertion.path) } } elseif (@($Actual).Count -lt [int]$Assertion.minCount) { throw ('Health assertion failed: ' + $Assertion.path) } } }",
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
278
|
+
...(allowLocalReleaseRoot ? [
|
|
279
|
+
"New-Item -ItemType Directory -Force -Path $VersionsRoot | Out-Null",
|
|
280
|
+
"$LocalReleaseRoot = $null; if ($ReleaseRoot) { $LocalReleaseRoot = [IO.Path]::GetFullPath($ReleaseRoot); if (-not (Test-Path -LiteralPath $LocalReleaseRoot -PathType Container)) { throw 'Local release root is missing' } }",
|
|
281
|
+
"if (-not $Version -and -not $LocalReleaseRoot) { $Version = ((Invoke-RestMethod " + psQuote("https://api.github.com/repos/" + repository + "/releases/latest") + ").tag_name -replace '^v','') }",
|
|
282
|
+
"if ($Version -and $Version -notmatch '^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$') { throw 'Invalid stable release version' }",
|
|
283
|
+
"$ReleaseBase = if ($Version) { " + psQuote("https://github.com/" + repository + "/releases/download/v") + " + $Version } else { $null }",
|
|
284
|
+
"$Work = Join-Path ([IO.Path]::GetTempPath()) (" + psQuote(product + "-install-") + " + [guid]::NewGuid())",
|
|
285
|
+
"New-Item -ItemType Directory -Force -Path $Work | Out-Null",
|
|
286
|
+
"$ManifestPath = Join-Path $Work 'release-manifest.json'; $SignaturePath = Join-Path $Work 'release-manifest.cat'; $ChecksumsPath = Join-Path $Work 'checksums.json'",
|
|
287
|
+
"function Receive-ReleaseFile([string]$Name, [string]$Destination) { if ($LocalReleaseRoot) { $RootPrefix = $LocalReleaseRoot.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar; $Source = [IO.Path]::GetFullPath((Join-Path $LocalReleaseRoot $Name)); if (-not $Source.StartsWith($RootPrefix, [StringComparison]::OrdinalIgnoreCase) -or -not (Test-Path -LiteralPath $Source -PathType Leaf)) { throw ('Local release asset is missing or escapes root: ' + $Name) }; Copy-Item -LiteralPath $Source -Destination $Destination } else { Invoke-WebRequest ($ReleaseBase + '/' + $Name) -OutFile $Destination } }",
|
|
288
|
+
"Receive-ReleaseFile 'release-manifest.json' $ManifestPath",
|
|
289
|
+
"Receive-ReleaseFile 'release-manifest.cat' $SignaturePath",
|
|
290
|
+
] : [
|
|
291
|
+
"New-Item -ItemType Directory -Force -Path $VersionsRoot | Out-Null",
|
|
292
|
+
"if (-not $Version) { $Version = ((Invoke-RestMethod " + psQuote("https://api.github.com/repos/" + repository + "/releases/latest") + ").tag_name -replace '^v','') }",
|
|
293
|
+
"if ($Version -notmatch '^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$') { throw 'Invalid stable release version' }",
|
|
294
|
+
"$PriorTarget = $null; if (Test-Path $Current) { $PriorTarget = (Get-Item $Current).Target }",
|
|
295
|
+
"$PriorVersion = if ($PriorTarget) { ((Split-Path ([string]$PriorTarget) -Leaf) -split '-')[0] } else { $null }",
|
|
296
|
+
"if ($PriorVersion -and -not $AllowDowngrade -and ([version]$Version -lt [version]$PriorVersion)) { throw 'Downgrade requires -AllowDowngrade' }",
|
|
297
|
+
"$ReleaseBase = " + psQuote("https://github.com/" + repository + "/releases/download/v") + " + $Version",
|
|
298
|
+
"$Work = Join-Path ([IO.Path]::GetTempPath()) (" + psQuote(product + "-install-") + " + [guid]::NewGuid())",
|
|
299
|
+
"New-Item -ItemType Directory -Force -Path $Work | Out-Null",
|
|
300
|
+
"$ManifestPath = Join-Path $Work 'release-manifest.json'; $SignaturePath = Join-Path $Work 'release-manifest.cat'; $ChecksumsPath = Join-Path $Work 'checksums.json'",
|
|
301
|
+
"Invoke-WebRequest ($ReleaseBase + '/release-manifest.json') -OutFile $ManifestPath",
|
|
302
|
+
"Invoke-WebRequest ($ReleaseBase + '/release-manifest.cat') -OutFile $SignaturePath",
|
|
303
|
+
]),
|
|
285
304
|
"$ManifestBytes = [IO.File]::ReadAllBytes($ManifestPath); Import-Module Microsoft.PowerShell.Security -ErrorAction Stop",
|
|
286
305
|
"$Catalog = Test-FileCatalog -CatalogFilePath $SignaturePath -Path $ManifestPath -Detailed; if ([string]$Catalog.Status -ne 'Valid') { throw 'Release manifest catalog binding is invalid' }",
|
|
287
306
|
"$CatalogSignature = Get-AuthenticodeSignature -FilePath $SignaturePath; if ([string]$CatalogSignature.Status -ne 'Valid' -or -not $CatalogSignature.SignerCertificate) { throw 'Release manifest signature is invalid' }; $SignerCertificate = $CatalogSignature.SignerCertificate",
|
|
@@ -290,20 +309,34 @@ export function renderPowerShellBootstrap({
|
|
|
290
309
|
"$VerifiedKeyId = $null; foreach ($KeyId in $AcceptedSigners.Keys) { $Accepted = $AcceptedSigners[$KeyId]; if ($SignerSubject -eq [string]$Accepted.subject -and $SignerFingerprint -eq [string]$Accepted.certificateSha256) { $VerifiedKeyId = $KeyId; break } }",
|
|
291
310
|
"if (-not $VerifiedKeyId) { throw 'Release manifest signer is not accepted' }",
|
|
292
311
|
"$Manifest = [Text.Encoding]::UTF8.GetString($ManifestBytes) | ConvertFrom-Json",
|
|
312
|
+
...(allowLocalReleaseRoot ? [
|
|
313
|
+
"if (-not $Version) { $Version = [string]$Manifest.version }",
|
|
314
|
+
"if ($Version -notmatch '^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$') { throw 'Invalid stable release version' }",
|
|
315
|
+
"$ReleaseBase = " + psQuote("https://github.com/" + repository + "/releases/download/v") + " + $Version",
|
|
316
|
+
"$PriorTarget = $null; if (Test-Path $Current) { $PriorTarget = (Get-Item $Current).Target }",
|
|
317
|
+
"$PriorVersion = if ($PriorTarget) { ((Split-Path ([string]$PriorTarget) -Leaf) -split '-')[0] } else { $null }",
|
|
318
|
+
"if ($PriorVersion -and -not $AllowDowngrade -and ([version]$Version -lt [version]$PriorVersion)) { throw 'Downgrade requires -AllowDowngrade' }",
|
|
319
|
+
] : []),
|
|
293
320
|
"if ($Manifest.signingKeyId -ne $VerifiedKeyId -or $Manifest.signatureAlgorithm -ne 'authenticode-catalog-sha256' -or $Manifest.signatureProvider -ne 'windows-authenticode-catalog' -or $Manifest.signatureProviderVersion -ne 1) { throw 'Release manifest signer identity mismatch' }",
|
|
294
321
|
"if ($Manifest.product -ne " + psQuote(product) + " -or $Manifest.version -ne $Version -or $Manifest.tag -ne ('v' + $Version)) { throw 'Release identity mismatch' }",
|
|
295
322
|
"if ([version]$Contract.bootstrapVersion -lt [version]$Manifest.minimumBootstrapVersion) { throw 'Bootstrap update required' }",
|
|
296
323
|
"$Machine = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString(); $Target = if ($Machine -eq 'Arm64') { 'windows-arm64' } elseif ($Machine -eq 'X64') { 'windows-x86_64' } else { throw ('Unsupported architecture: ' + $Machine) }",
|
|
297
324
|
"$Matches = @($Manifest.assets | Where-Object { $_.target -eq $Target }); if ($Matches.Count -ne 1) { throw 'Release has no unique exact target' }; $Asset = $Matches[0]",
|
|
298
325
|
"if ($Asset.url -ne ($ReleaseBase + '/' + $Asset.name)) { throw 'Asset URL is not exact immutable GitHub release URL' }",
|
|
299
|
-
|
|
326
|
+
allowLocalReleaseRoot
|
|
327
|
+
? "Receive-ReleaseFile 'checksums.json' $ChecksumsPath"
|
|
328
|
+
: "Invoke-WebRequest ($ReleaseBase + '/checksums.json') -OutFile $ChecksumsPath",
|
|
300
329
|
"$ChecksumHash = (Get-FileHash -Algorithm SHA256 $ChecksumsPath).Hash.ToLowerInvariant(); if ($ChecksumHash -ne $Manifest.checksumsSha256) { throw 'checksums.json is not manifest-bound' }",
|
|
301
330
|
"$Checksums = Get-Content -Raw $ChecksumsPath | ConvertFrom-Json; if ($Checksums.assets.($Asset.name) -ne $Asset.sha256) { throw 'Asset checksum differs from bound checksums' }",
|
|
302
|
-
|
|
331
|
+
allowLocalReleaseRoot
|
|
332
|
+
? "$Archive = Join-Path $Work $Asset.name; Receive-ReleaseFile ([string]$Asset.name) $Archive"
|
|
333
|
+
: "$Archive = Join-Path $Work $Asset.name; Invoke-WebRequest $Asset.url -OutFile $Archive",
|
|
303
334
|
"if ((Get-FileHash -Algorithm SHA256 $Archive).Hash.ToLowerInvariant() -ne $Asset.sha256) { throw 'Archive checksum mismatch' }",
|
|
304
335
|
"if ($Checksums.assets.($Asset.provenanceName) -ne $Asset.provenanceSha256 -or $Checksums.assets.($Asset.sbomName) -ne $Asset.sbomSha256) { throw 'Bound provenance/SBOM checksums are missing' }",
|
|
305
336
|
"$ProvenancePath = Join-Path $Work $Asset.provenanceName; $SbomPath = Join-Path $Work $Asset.sbomName",
|
|
306
|
-
|
|
337
|
+
allowLocalReleaseRoot
|
|
338
|
+
? "Receive-ReleaseFile ([string]$Asset.provenanceName) $ProvenancePath; Receive-ReleaseFile ([string]$Asset.sbomName) $SbomPath"
|
|
339
|
+
: "Invoke-WebRequest $Asset.provenanceUrl -OutFile $ProvenancePath; Invoke-WebRequest $Asset.sbomUrl -OutFile $SbomPath",
|
|
307
340
|
"if ((Get-FileHash -Algorithm SHA256 $ProvenancePath).Hash.ToLowerInvariant() -ne $Asset.provenanceSha256) { throw 'Provenance checksum mismatch' }",
|
|
308
341
|
"if ((Get-FileHash -Algorithm SHA256 $SbomPath).Hash.ToLowerInvariant() -ne $Asset.sbomSha256) { throw 'SBOM checksum mismatch' }",
|
|
309
342
|
"$Stage = Join-Path $Work 'stage'; New-Item -ItemType Directory -Force -Path $Stage | Out-Null",
|
|
@@ -383,6 +416,7 @@ export function validatePowerShellBootstrap(script, expected = {}) {
|
|
|
383
416
|
statusArgs: contract.statusArgs,
|
|
384
417
|
healthAssertions: contract.healthAssertions,
|
|
385
418
|
preflightArgs: contract.preflightArgs,
|
|
419
|
+
allowLocalReleaseRoot: contract.allowLocalReleaseRoot === true,
|
|
386
420
|
});
|
|
387
421
|
if (regenerated !== script) errors.push("bootstrap source does not match canonical generated output");
|
|
388
422
|
} catch {
|
|
@@ -392,6 +426,7 @@ export function validatePowerShellBootstrap(script, expected = {}) {
|
|
|
392
426
|
const markerOffset = script.lastIndexOf("# source-sha256:");
|
|
393
427
|
if (!sourceMarker || markerOffset < 0 || createHash("sha256").update(script.slice(0, markerOffset)).digest("hex") !== sourceMarker[1]) errors.push("bootstrap source digest is missing or tampered");
|
|
394
428
|
if (expected.product && contract.product !== expected.product) errors.push("bootstrap product identity missing");
|
|
429
|
+
if (expected.allowLocalReleaseRoot === true && contract.allowLocalReleaseRoot !== true) errors.push("local release root support missing");
|
|
395
430
|
if (expected.acceptedSignerIds) for (const id of expected.acceptedSignerIds) if (!Object.hasOwn(contract.acceptedManifestSigners ?? {}, id)) errors.push("accepted manifest signer missing: " + id);
|
|
396
431
|
return { valid: errors.length === 0, errors };
|
|
397
432
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.91",
|
|
4
4
|
"description": "Portable Right Suite release CLI/SDK: signed native archives, direct bootstrap transactions, immutable GitHub Release upload, R2 bootstrap publication, and add-on adoption.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/rightkit-versions.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"stagedNpm": {
|
|
16
16
|
"@rightkit/ax": "0.2.1",
|
|
17
|
-
"@rightkit/git": "0.2.
|
|
17
|
+
"@rightkit/git": "0.2.16",
|
|
18
18
|
"@rightkit/hooks": "0.1.1",
|
|
19
19
|
"@rightkit/legal": "0.3.1",
|
|
20
20
|
"@rightkit/legal-ui": "0.1.1",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@rightkit/logs": "0.1.4",
|
|
23
23
|
"@rightkit/platform-ui": "0.1.1",
|
|
24
24
|
"@rightkit/qa": "0.2.1",
|
|
25
|
-
"@rightkit/release": "0.2.
|
|
25
|
+
"@rightkit/release": "0.2.91",
|
|
26
26
|
"@rightkit/tauri": "0.1.1",
|
|
27
27
|
"@rightkit/updates": "0.2.4"
|
|
28
28
|
},
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"0.2.10",
|
|
44
44
|
"0.2.11",
|
|
45
45
|
"0.2.12",
|
|
46
|
-
"0.2.13"
|
|
46
|
+
"0.2.13",
|
|
47
|
+
"0.2.15"
|
|
47
48
|
],
|
|
48
49
|
"@rightkit/legal-ui": [
|
|
49
50
|
"0.1.0"
|