@rightkit/release 0.2.90 → 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.
@@ -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
- "[CmdletBinding()] param([string]$Version, [switch]$AllowDowngrade)",
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
- "New-Item -ItemType Directory -Force -Path $VersionsRoot | Out-Null",
274
- "if (-not $Version) { $Version = ((Invoke-RestMethod " + psQuote("https://api.github.com/repos/" + repository + "/releases/latest") + ").tag_name -replace '^v','') }",
275
- "if ($Version -notmatch '^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$') { throw 'Invalid stable release version' }",
276
- "$PriorTarget = $null; if (Test-Path $Current) { $PriorTarget = (Get-Item $Current).Target }",
277
- "$PriorVersion = if ($PriorTarget) { ((Split-Path ([string]$PriorTarget) -Leaf) -split '-')[0] } else { $null }",
278
- "if ($PriorVersion -and -not $AllowDowngrade -and ([version]$Version -lt [version]$PriorVersion)) { throw 'Downgrade requires -AllowDowngrade' }",
279
- "$ReleaseBase = " + psQuote("https://github.com/" + repository + "/releases/download/v") + " + $Version",
280
- "$Work = Join-Path ([IO.Path]::GetTempPath()) (" + psQuote(product + "-install-") + " + [guid]::NewGuid())",
281
- "New-Item -ItemType Directory -Force -Path $Work | Out-Null",
282
- "$ManifestPath = Join-Path $Work 'release-manifest.json'; $SignaturePath = Join-Path $Work 'release-manifest.cat'; $ChecksumsPath = Join-Path $Work 'checksums.json'",
283
- "Invoke-WebRequest ($ReleaseBase + '/release-manifest.json') -OutFile $ManifestPath",
284
- "Invoke-WebRequest ($ReleaseBase + '/release-manifest.cat') -OutFile $SignaturePath",
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
- "Invoke-WebRequest ($ReleaseBase + '/checksums.json') -OutFile $ChecksumsPath",
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
- "$Archive = Join-Path $Work $Asset.name; Invoke-WebRequest $Asset.url -OutFile $Archive",
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
- "Invoke-WebRequest $Asset.provenanceUrl -OutFile $ProvenancePath; Invoke-WebRequest $Asset.sbomUrl -OutFile $SbomPath",
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.90",
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",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "stagedNpm": {
16
16
  "@rightkit/ax": "0.2.1",
17
- "@rightkit/git": "0.2.15",
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.90",
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"