@rightkit/release 0.2.174 → 0.4.0
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 +200 -2
- package/addon-command.mjs +3 -4
- package/android-play-upload.mjs +2 -1
- package/apple-profile-renew.mjs +11 -3
- package/artifact-storage.mjs +35 -2
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/build-release.mjs +138 -47
- package/cli/right-release.mjs +40 -5
- package/codesign-environment.mjs +2 -2
- package/command-spec.mjs +80 -0
- package/content-digest.mjs +209 -0
- package/deps-cargo.mjs +55 -0
- package/deps.mjs +7 -0
- package/development.mjs +17 -6
- package/direct-bootstrap.mjs +5 -6
- package/disk-command.mjs +1 -0
- package/disk-policy.json +6 -2
- package/disk-stable-root.mjs +13 -9
- package/dist-scan.mjs +102 -0
- package/dmg-build.mjs +53 -4
- package/github-updater-release.mjs +70 -7
- package/icon-pack.mjs +117 -0
- package/in-place-install.mjs +17 -14
- package/init-command.mjs +116 -0
- package/install-app-windows.mjs +24 -7
- package/install-app.mjs +56 -42
- package/ios-asc-credentials.mjs +163 -0
- package/ios-build-context.mjs +129 -0
- package/ios-build-upload.mjs +282 -0
- package/ios-credentials-command.mjs +70 -0
- package/ios-dev.mjs +248 -0
- package/ios-host.mjs +76 -0
- package/ios-lock.mjs +75 -0
- package/ios-package-recipe.mjs +707 -0
- package/ios-testflight.mjs +266 -0
- package/ios-upload.mjs +313 -0
- package/local-publish.mjs +390 -47
- package/macho-approval-hash.mjs +181 -0
- package/managed-cargo.mjs +57 -1
- package/media-sidecar.mjs +20 -4
- package/mirror-root-artifact.mjs +74 -22
- package/native-cargo-layout.mjs +47 -4
- package/npm-license-notices.mjs +159 -0
- package/ownership-command.mjs +2 -1
- package/ownership-dependency-allowlist.json +2 -1
- package/ownership-evidence.mjs +30 -18
- package/ownership-key-command.mjs +69 -2
- package/ownership-mandatory-set.mjs +66 -16
- package/ownership-repositories.json +1 -1
- package/ownership-scope.mjs +94 -0
- package/ownership.mjs +86 -11
- package/package-recipe-targets.mjs +56 -2
- package/package.json +1 -1
- package/pe-content.mjs +133 -0
- package/preflight.mjs +14 -7
- package/project-type.mjs +3 -1
- package/protected-credentials.mjs +28 -1
- package/publication-artifacts.mjs +30 -5
- package/publication-gate.mjs +83 -5
- package/publish-cargo.mjs +124 -18
- package/publish-update.mjs +15 -2
- package/qa-build-features.mjs +16 -13
- package/qa-contract.mjs +30 -2
- package/qa-control-scan.mjs +4 -0
- package/release-dependencies-command.mjs +45 -0
- package/release-dependencies.mjs +60 -0
- package/release-invocation.mjs +118 -8
- package/release-switch-registry.json +126 -27
- package/release-switch-scan.mjs +62 -24
- package/release.mjs +144 -21
- package/rightkit-ownership.json +46 -2
- package/rightkit-versions.json +380 -189
- package/runtime-budgets.mjs +134 -23
- package/sign-updater.mjs +56 -59
- package/source-gate.mjs +8 -1
- package/stage-sidecar.mjs +634 -69
- package/target-distribution.mjs +5 -0
- package/test-command.mjs +190 -0
- package/toolchain-swift.mjs +239 -0
- package/upload-release.mjs +7 -2
- package/web-build.mjs +67 -0
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Every Right app has absolutely NO access to DevTools; the single exception is Vi
|
|
|
56
56
|
|
|
57
57
|
- `name` (required, unique): environment variable, argument flag, settings key or `VITE_*` name.
|
|
58
58
|
- `class` (required): `S1`..`S6`. `channel` (required): `env`, `argv`, `settings`, `preference` or `frontend`.
|
|
59
|
-
- S1-S4 need `compiledOut`: `feature:<name>`, `cfg:test` or `
|
|
59
|
+
- S1-S4 need `compiledOut`: `feature:<name>`, `cfg:test`, `cfg:debug_assertions` or, for Swift, `swift:DEBUG` (the read sits inside `#if DEBUG`; the read exists only there). The source walk skips test directories, including Swift bundles named `Tests`, `UITests`, `<App>Tests` and `<App>UITests`. S5 needs `userGesture: true`. S6 needs `valueType` of `bool`, `int` or `enum`.
|
|
60
60
|
- `anchor` and `note` are informational. Fixtures: `fixtures/release-switches/` (`app/` is a clean example).
|
|
61
61
|
|
|
62
62
|
## Commands
|
|
@@ -65,7 +65,7 @@ Every Right app has absolutely NO access to DevTools; the single exception is Vi
|
|
|
65
65
|
|
|
66
66
|
| Group | Subcommands |
|
|
67
67
|
|---|---|
|
|
68
|
-
| Build | `release`, `build`, `package` |
|
|
68
|
+
| Build | `release`, `build`, `package`, `build --web` / `web-build` (web frontend only) |
|
|
69
69
|
| Build (SwiftPM apps) | `swift-package` (`windows-stage`, `msix`, `mac-app`) (config schema 1: `windows.{exe,out,searchDirs,include,dependencies,publisher,assets}`, `mac.{executable,bundleId,out,frameworks,resources,entitlements}`) |
|
|
70
70
|
| Install | `install-app` |
|
|
71
71
|
| Publish | `publish`, `upload`, `publish-update`, `github`, `mirror-root-artifact` |
|
|
@@ -75,8 +75,86 @@ Every Right app has absolutely NO access to DevTools; the single exception is Vi
|
|
|
75
75
|
| Dev artifact signing | `sign-dev-artifact --artifact <dir> --manifest <sign.json> [--metadata <payload.json>] [--evidence <path>] [--dry-run]`: Developer ID signs an existing CI dev artifact per file with fixed identifiers (macOS). It verifies the recorded hashes in the payload metadata (default `payload.json`, `{ schema: 1, files: [{ path, sha256 }] }`) first, signs each sign-manifest entry (`file` relative to the artifact, `identifier` required, nested code before its bundle) through the dev-signing identity (missing Developer ID: `DEV_MAC_SIGNING_DEVELOPER_ID_REQUIRED`, no ad hoc fallback), verifies with `codesign --verify --strict`, reads back Identifier and TeamIdentifier, refreshes the metadata hashes, and writes evidence JSON. Never builds, notarizes or publishes. The payload metadata must sit outside every signed entry (`SIGN_DEV_ARTIFACT_METADATA_IN_SIGNED_ENTRY`). An optional declarative `refresh` in the sign manifest recomputes digests that other JSON files record about signed bytes: `{ "refreshRoot": "<dir inside the artifact>", "refresh": [{ "file", "pointer" (JSON pointer to an existing string), "compute": "sha256-file" \| "sha256-tree-v1" \| "verify-sha256-file", "of", "format"?, "currentMatches"? }] }` (`file` and `of` relative to `refreshRoot`; `sha256-tree-v1` = sha256 over the sorted relative paths, each as path, NUL, 8-byte big-endian length, bytes; `verify-sha256-file` writes nothing and refuses a digest that changed; `format` embeds the digest in a locator string through one `{value}`; `currentMatches` refuses to overwrite a value of another kind, such as a signed attestation locator). Order is enforced: operations run after the entries they read are signed and before the first entry that contains a refreshed file, so that bundle (the wrapper) signs last over the final bytes; an operation may not read what a later one writes. JSON is rewritten in place with key order kept, and a file the writer would have to reformat is refused. Evidence gains a `refresh` list |
|
|
76
76
|
| Checks | `deps`, `hardening`, `harden`, `hardeningscan`, `cargo-target` |
|
|
77
77
|
| Ownership | `ownership`, `ownership-approval`, `ownership-report` |
|
|
78
|
+
| Test | `test` (the app's declared suites) |
|
|
79
|
+
| Legal | `license-notices npm` |
|
|
78
80
|
| Other | `dev`, `doctor`, `suite-doctor`, `qa`, `dmg`, `cache`, `apple`, `inventory`, `model`, `addon`, `hooks`, `lsclean`, `generate-dmg-background`, `publish cargo`, `publish npm`, `publish swift` |
|
|
79
81
|
|
|
82
|
+
## Declared test suites, web build, npm notices and artifact mirroring
|
|
83
|
+
|
|
84
|
+
These replace per-app wrapper scripts. Each is declared in `right-release.config.mjs` and runs without a shell from the app
|
|
85
|
+
root (the config file's directory). A step is `{ cmd | node, args?, cwd?, env?, nodeFlags?, nodeOptions? }`: `node` is an
|
|
86
|
+
app-relative script run by the Node that runs `right-release`, `cmd: "node"` means the same Node, `nodeFlags` go before the
|
|
87
|
+
script, `nodeOptions` are merged once into `NODE_OPTIONS`. `cwd` and `node` must stay inside the app; unknown keys are refused.
|
|
88
|
+
|
|
89
|
+
### `right-release test`
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
tests: {
|
|
93
|
+
eventPrefix: "e2e", // optional, default "test": events are <prefix>_suite_started / _completed
|
|
94
|
+
runners: { // optional named bases
|
|
95
|
+
vitest: { node: "node_modules/vitest/vitest.mjs", nodeFlags: ["--no-experimental-webstorage"], nodeOptions: ["--no-experimental-webstorage"] },
|
|
96
|
+
},
|
|
97
|
+
suites: [
|
|
98
|
+
{ name: "tooling", runner: "vitest", args: ["run", "scripts/tooling.test.mjs", "--no-file-parallelism", "--maxWorkers=1"] },
|
|
99
|
+
{ name: "runtime-config", node: "scripts/runtime-config.test.mjs", nodeFlags: ["--test"] },
|
|
100
|
+
// Expands to journeys-1, journeys-2, ...: the names in the JSON list run `chunk` at a time, one process per chunk.
|
|
101
|
+
{ name: "journeys", node: "scripts/qa-browser-functional.mjs", args: ["--keep-going"],
|
|
102
|
+
journeys: { file: "scripts/qa-e2e-journeys.json", key: "journeys", chunk: 12, flag: "--journey" } },
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`right-release test` runs every suite in order and stops at the first failure, exiting with that suite's code; each suite prints
|
|
108
|
+
a JSON line before and after (`{"event":"e2e_suite_completed","suite":"tooling","exitCode":0,"durationMs":1234}`). `--suite a,b`
|
|
109
|
+
(repeatable) selects suites; a group name such as `journeys` selects all its chunks. `--runner vitest -- run --project unit`
|
|
110
|
+
runs a runner once with the arguments after `--` (watch mode, one project); `-- <args>` after `--suite` is allowed when exactly
|
|
111
|
+
one suite is selected. `--list [--json]` prints the resolved commands and runs nothing. Wrap it as the repository needs, for
|
|
112
|
+
example `rightkit storage run --app <app> -- right-release test`.
|
|
113
|
+
|
|
114
|
+
### `right-release build --web` (or `web-build`)
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
web: { build: [
|
|
118
|
+
{ node: "node_modules/typescript/bin/tsc", nodeFlags: ["--disable-warning=DEP0205"] },
|
|
119
|
+
{ node: "node_modules/vite/bin/vite.js", args: ["build"], nodeFlags: ["--disable-warning=DEP0205"] },
|
|
120
|
+
] }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Runs only the `web.build` steps: no Tauri, no signing, no packaging. `--dry-run` prints the commands. It is guarded by the same
|
|
124
|
+
GitHub-Actions-only execution policy as the other build commands.
|
|
125
|
+
|
|
126
|
+
### `right-release license-notices npm`
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
legal: { npmNotices: { output: "legal/THIRD_PARTY_NPM_LICENSES.html", productName: "ViewRight", exclude: ["@rightkit/"] } }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Writes the third-party npm license appendix for the production closure of a pnpm app (`pnpm licenses list --prod --json`),
|
|
133
|
+
with each package's own LICENSE / COPYING / NOTICE text. A package with no license file gets the standard published text for
|
|
134
|
+
its declared MIT or ISC license, labelled as a fallback; any other license without a file fails the run. The page records the
|
|
135
|
+
app version, the `pnpm-lock.yaml` sha256 and `generated-by=right-release license-notices npm`, so equal inputs give equal bytes.
|
|
136
|
+
`--check` writes nothing and exits 1 when the file differs. `--output` and `--product-name` override the config.
|
|
137
|
+
|
|
138
|
+
### Mirroring an artifact (`mirror-root-artifact`)
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
targets: { win: {
|
|
142
|
+
mirror: [{ file: "src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/App_1.0.0_x64-setup.exe", pruneSuffix: "-setup.exe" }],
|
|
143
|
+
postSign: { cmd: "right-release", args: ["mirror-root-artifact", "--config", "right-release.config.mjs", "--platform", "win"] },
|
|
144
|
+
} }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Copies each `targets.<platform>.mirror` entry (an app-relative path, or `{ file, name?, pruneSuffix? }`) atomically into
|
|
148
|
+
`RIGHT_RELEASE_ARTIFACT_DIR`. The directory must be outside the repository and the command refuses to run without it, so it
|
|
149
|
+
never writes into the repository root. `pruneSuffix` then removes the other files with that suffix from that directory, so it
|
|
150
|
+
holds only the current installer. The one-file form (`--file <path> --package-root <dir> [--name <name>]`) is unchanged.
|
|
151
|
+
|
|
152
|
+
### macOS updater tarball from `macPackage`
|
|
153
|
+
|
|
154
|
+
`targets.mac.macPackage.updater: true` (or an app-relative `.tar.gz` path) adds a `create-mac-updater` command at the front of
|
|
155
|
+
`postSign`: it packs the signed, stapled `.app` and signs the tarball. The default output is `<App>.app.tar.gz`, named after the
|
|
156
|
+
app in `notarize.app`. Declare the file in `updater.artifacts` as before. See `docs/MAC-PACKAGE-RECIPE.md`.
|
|
157
|
+
|
|
80
158
|
## QA receipts (warn-and-record)
|
|
81
159
|
|
|
82
160
|
`right-release qa --stage source|installed --platform mac|win` still runs `rightkit-qa` and writes a signed receipt, and receipt verification is unchanged. A missing or invalid receipt no longer blocks a release. `build`, `upload`, `publish-update` and `github` print `[right-release] QA warning: ...` to stderr, continue, and record the outcome:
|
|
@@ -86,6 +164,10 @@ Every Right app has absolutely NO access to DevTools; the single exception is Vi
|
|
|
86
164
|
|
|
87
165
|
Audit ad hoc or on a weekly schedule with `right-release qa audit [--app <dir>] [--json] [--since <YYYY-MM-DD>]`. It reads the app repo's sealed manifests and state and lists source and installed QA status per release and platform (passed, missing, rejected, not-uploaded). It exits 0 when every uploaded build has passing QA and 1 when any uploaded build lacks it. An installed receipt can only chain to a build sealed with a passing source receipt, so a build sealed with `qa.status: "missing"` needs source QA and a rebuild before installed QA can pass.
|
|
88
166
|
|
|
167
|
+
## Local publish (`publish npm|cargo ... --local`, Mac or Windows)
|
|
168
|
+
|
|
169
|
+
`right-release publish cargo --crate <name> --local` (and the batch form `--crates a,b,c | --crates-file <file>`) publishes from the Mac **or from Windows** under the committed local-build grant (`.rightkit-local-build.json`, `allow: "build-test-publish"`, naming this repository); `publish npm --package @rightkit/<name> --local` stays Mac only (the npm token comes from the macOS Keychain and `@rightkit/release` ships a signed helper), and `--local` refuses any other host (Linux) and any host without the grant. The gate is the same on both hosts: the unit's own `ci/gate.config.json` commands run with zero failures and no skipped evidence, the artifact sha256 is recorded, and the existing `enforcePublicationGate` and publisher run, including the registry readback. The evidence carries one receipt for the publishing host: `hostPolicy: "darwin-only"` with a `darwin` run on the Mac, `hostPolicy: "win32-only"` (plus a `hostPolicyReason`) with a `win32` run on Windows; the gate requires that host's receipt and `isDarwinOnlyPolicy`, `isWin32OnlyPolicy` and `singleHostOfPolicy` in `publication-gate.mjs` are the one definition. A crate whose `ci/gate.config.json` `os` list lacks the publishing host (`macos` or `windows`) is refused. Windows publishes with the credential Cargo stores itself (`~/.cargo/credentials.toml`): no `--token` is passed, read or logged. A runtime-budget crate needs the measurement of the publishing host only, under the same evidence policy that the budget file may not contradict.
|
|
170
|
+
|
|
89
171
|
## Local app install (macOS)
|
|
90
172
|
|
|
91
173
|
Agents must not run `osascript`, `open`, `screencapture`, or `notifyutil -p` directly. Install a signed build through:
|
|
@@ -203,6 +285,10 @@ sign: {
|
|
|
203
285
|
|
|
204
286
|
Nested code that the map does not name (frameworks, other helpers) is signed first with the defaults; the appex's own `Contents/MacOS` executable is signed before the appex, and the appex signature then carries the extension entitlements. The Finder extension's entitlements never reach the app and the app's Apple Events entitlement never reaches the extension.
|
|
205
287
|
|
|
288
|
+
### Signing the built DMG (`dmg build --sign`)
|
|
289
|
+
|
|
290
|
+
`right-release dmg build ... --sign [--identifier <id>] [--receipt <path>]` Developer ID signs the finished image with the shared Developer ID Application identity (secure timestamp, no hardened runtime, as for any disk image; optional stable codesign `--identifier`; strict verification and identity/timestamp readback), so an app whose `package` step builds its own DMG needs no direct `sign-macos.mjs` call before `notarize`. `--receipt` writes the same signing receipt `sign-macos.mjs` writes. A signing failure removes the unsigned image (`DMG_SIGN_FAILED`). It never notarizes or uploads; there is no ad hoc or Apple Development fallback. `buildDmg({ ..., sign: true, identifier, signReceipt })` is the SDK form. `targets.mac.macPackage` and SwiftPM apps already sign the DMG themselves.
|
|
291
|
+
|
|
206
292
|
### DMG from an appdmg-style spec
|
|
207
293
|
|
|
208
294
|
`right-release dmg build --app <.app> --out <dmg> --spec <appdmg.json>` (and `targets.mac.dmg.spec` for SwiftPM apps that right-release packs itself) renders an appdmg-style JSON file through the same headless hdiutil pipeline; explicit flags and `targets.mac.dmg` options win over the spec, and the spec's `title` is the volume name (on the command line, `--volname` is required when the spec has no `title`). Relative `background` and `icon` paths resolve against the spec file's directory. Pulse's `release/appdmg.json` works unchanged:
|
|
@@ -262,6 +348,23 @@ The Android lane is native Kotlin with Gradle plus Rust `.so` libraries per ABI
|
|
|
262
348
|
- Secrets: the keystore passwords and the Play service-account JSON live in the OS credential store (macOS Keychain `rightkit-android-<name>`; a protected token file on Windows and Linux, so store the JSON as one line) or in a 0600 file outside the repository, selected by `{ credential: "<name>" }` or `{ file: "<path>" }`. They never appear in the environment, on argv, in config, or in a log; key material found in the environment refuses the command.
|
|
263
349
|
- Dev: a debug build installed by serial (never "the only device") with a package ending in `.dev`, `.qa`, `.test` or `.debug`; release tasks and signing properties are refused.
|
|
264
350
|
|
|
351
|
+
## iOS (archive, export, TestFlight, simulator and device install)
|
|
352
|
+
|
|
353
|
+
One declarative recipe for every iOS app (bogusyogi/rightkit#349); full reference and example config in `docs/IOS-PACKAGE-RECIPE.md`. Same split as Android: `build` never uploads, `upload` never builds, and the upload is a dry run unless `--apply`.
|
|
354
|
+
|
|
355
|
+
| Step | Command | Config |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| Build | `right-release build --platform ios` | `targets.ios`: `iosPackage` (`project`, `projectSpec?`, `scheme`, `teamId`, `bundleId`, `archive`, `ipa`, `extraBuildSettings?`, `prepare?`, `cleanup?`, `checks?`) and `artifacts` listing the IPA |
|
|
358
|
+
| Upload | `right-release upload --platform ios --release <sealed-id> [--apply]` | top-level `distribution: { provider: "app-store-connect", appId, bundleId, track: "testflight", groups (required), submitExternalBetaReview? (default false), credentials? }` |
|
|
359
|
+
| Credentials, once | `right-release ios-credentials store --key-id <KEYID> --issuer <uuid>` (`.p8` on stdin), `ios-credentials check` | Keychain service names, optional |
|
|
360
|
+
| Dev | `right-release dev --platform ios [--serial <device id>]` | `development.targets.ios`: `project`, `projectSpec?`, `scheme`, `teamId?`, `install: { kind: "simulator" \| "device", device, bundleId, ... }` |
|
|
361
|
+
|
|
362
|
+
- Build: `xcodegen generate` (required when `projectSpec` is named; a committed xcodeproj is never a fallback), `xcodebuild archive` with automatic signing, `-allowProvisioningUpdates`, the configured team and a date build number `YYYYMMDDNN` (the marketing version stays as the app sets it), then the archive checks (one version across the app, appex and Watch plists, QA/release-switch scan, forbidden strings), `xcodebuild -exportArchive`, the IPA checks (entitlements, distribution profile, code signature) and a receipt over the final IPA bytes. A failure deletes the archive, the IPA and the receipt. macOS only, in the GUI signing session; on Windows and Linux only config validation and `--dry-run` run.
|
|
363
|
+
- Credentials (D4): the App Store Connect `.p8`, key id and issuer id are read from the macOS login Keychain (`security find-generic-password -s <service> -a <account> -w`; defaults `rightkit-asc-api-key`, `rightkit-asc-key-id`, `rightkit-asc-issuer-id`). A key path, key id, issuer or key in config, and key material in the environment, are refused; the ASC environment variables are refused on managed hosts. Key material never reaches a log.
|
|
364
|
+
- Upload: the sealed IPA is uploaded in process through the App Store Connect build-upload resources (`buildUploads`, `buildUploadFiles`, pre-signed part uploads, commit, poll to `COMPLETE`), then the exact build is assigned to the TestFlight beta groups (in-process ES256 JWT). Idempotent calls are retried with bounded backoff (the create POSTs never), an interrupted upload for the same version and build is resumed or finished, pre-signed URLs are limited to `*.blobstore.apple.com` and may not carry credential headers, and the IPA is streamed in ranges. A per-release lock, a GUI-session check before the Keychain read and marker identity checks guard a rerun. No altool and no file ever holds the key. No App Store submission. Apps must git-ignore `.right-release/` (see `docs/IOS-PACKAGE-RECIPE.md`).
|
|
365
|
+
- Dev: simulator (`simctl boot/install/launch`) or physical device (`devicectl install` and `process launch`), chosen by id, built Debug with generated commands.
|
|
366
|
+
|
|
367
|
+
|
|
265
368
|
## Bundled FFmpeg decode sidecar (Ember and other media apps)
|
|
266
369
|
|
|
267
370
|
An app that decodes video through `rightkit-ffmpeg` ships the verified LGPL `ffmpeg` and `ffprobe` programs as a Tauri `externalBin` sidecar. right-release adds no new bundling mechanism: Tauri bundles the files (mac `Contents/MacOS`, Windows next to the app EXE), and right-release plans them from the catalog the app compiled against, ships the LGPL text and source offer, signs the binaries with the app, and seals their hashes. Set `targets.mac.mediaSidecar` and `targets.win.mediaSidecar`:
|
|
@@ -292,6 +395,22 @@ Linux has catalog and planner support only: the planner maps `linux` to the `*-u
|
|
|
292
395
|
|
|
293
396
|
Any app that ships more than the ffmpeg sidecar (MailRight: `mailright-engine`, `mailright-core`) sets `targets.mac.externalBin` / `targets.win.externalBin`: `{ names: [...], arch?, stageDir? ("src-tauri/binaries"), bundleDir? (mac: "Contents/MacOS" | "Contents/Resources"), stage?: { cmd, args? } }`. It is the generic form of the ffmpeg staging above and reuses its target-triple naming (`<name>-<triple>[.exe]`), staged-file checks and signing evidence checks (`external-bin.mjs` over `media-sidecar.mjs`); it has no catalog, notices or manifest seal. `stage` is the app's own build of the files. Windows signs the staged files in the raw-EXE step; macOS signs `<bundleDir>/<name>` as nested code (hardened runtime) and checks the nested receipt. `external-bin.json` (state root) records pre-sign and signed digests. Names that `mediaSidecar` stages (`ffmpeg`, `ffprobe`) are refused here. Tests: `external-bin.test.mjs`.
|
|
294
397
|
|
|
398
|
+
## Sidecar staging: source builds, prebuilt checks, archives, mixed variants, universal macOS
|
|
399
|
+
|
|
400
|
+
`stage-sidecar` (the `sidecars` list in `right-release.config.mjs`, receipt `stage-sidecar.json`) covers these cases without an app script. Every download is https and digest-pinned, nothing runs through a shell, and a failed check stages nothing.
|
|
401
|
+
|
|
402
|
+
- **`cmake` (Orthic-Labs/rightkit#489):** `{ kind: "cmake", name: "whisper-cli", repo: "https://github.com/ggml-org/whisper.cpp", commit: "<40 hex>", target?: "whisper-cli", output?: "bin/whisper-cli", cmakeArgs?: ["-DBUILD_SHARED_LIBS=OFF"], platformArgs?: { mac: ["-DGGML_METAL=ON"], win: [...] }, generator?, smoke?: ["--help"], sha256?, dest?, clean?, platforms? }`. It fetches exactly that commit into the fixed `.right-release/cmake/<name>/src` (`git init`, `fetch --depth 1 origin <commit>`, detach; a checkout already at the commit is reused, one with local edits or at another commit is refused with `STAGE_SIDECAR_SOURCE_MISMATCH`), configures and builds `target` in `.right-release/cmake/<name>/build` (`CMAKE_OSX_ARCHITECTURES` is set from the target; `universal-apple-darwin` builds `arm64;x86_64` and checks `lipo -archs`), finds the executable (`output`, then `Release/` layouts, `.exe` on Windows), optionally pins its sha256, stages it as `<name>-<triple>[.exe]` and smoke-runs it when the target can run on this host. `cmakeArgs` and `platformArgs` accept only `-DNAME=value` definitions. Toolchain controls (#691, all optional, defaults unchanged): `cmakeBin: "<name or path>"` runs that cmake (a bare name is looked up on PATH, a path with separators is resolved against the app root); `generator` is per entry and `platformGenerators: { win: "Visual Studio 17 2022" }` overrides it for one platform (`-G`); `architecture: "x64"` (any `-A` platform: `x64`, `Win32`, `ARM64`) adds `-A <arch>` after `-G`. With no `cmakeBin`, a Windows host uses `cmake` from PATH (`where cmake`) when there is one, else the Visual Studio-bundled cmake (`vswhere.exe` under `%ProgramFiles(x86)%\Microsoft Visual Studio\Installer`, `-latest -products * -requires Microsoft.VisualStudio.Component.VC.CMake.Project -property installationPath`, then `Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe`), else plain `cmake`. The receipt row carries `cmakeBin` (when not plain `cmake`) and `generator`. There is no environment-variable override: the staging recipe has none for any other tool.
|
|
403
|
+
- **`prebuilt` (#490):** verifies a host-built binary already staged at `<stageDir>/<name>-<triple>` (or copied from `from`) against an app manifest, never building or downloading: `manifest: { file, entry: "binaries.{platform}", sha256?: "sha256", approvalSha256?: "approval_sha256", approvalAlgorithm?: "approval_hash_algorithm" }` (`{platform}` is `macos-aarch64`, `windows-x86_64`, ...). The raw sha256 must match; when an approval hash is named it must match too, with `raw-sha256` or `macho-codesign-strip-linkedit-v1` (`macho-approval-hash.mjs`: the sha256 of a thin Mach-O with its code signature removed and `__LINKEDIT` normalised, so a signed and an unsigned copy of one build agree; macOS only, `codesign --remove-signature` on a private copy). A missing file, entry or digest fails closed. With `from: { aarch64: "<path>", x86_64: "<path>" }` the entry also works on `universal-apple-darwin` (#692): each arch is verified against its own manifest entry (`macos-aarch64`, `macos-x86_64`; the approval algorithm default is the macOS one), then both are lipo-merged by the same helper `ffmpeg`, `onnxruntime` and `cmake` use into `<name>-universal-apple-darwin` (both archs required, `STAGE_SIDECAR_NOT_UNIVERSAL` otherwise; nothing is merged after a failed slice). With `from` omitted the inputs are the staged `<name>-aarch64-apple-darwin` and `<name>-x86_64-apple-darwin`; a plain string `from` is refused for universal. On a thin target an object `from` picks the entry for that arch. The row carries `platform: "macos-universal"` and per-arch `slices`.
|
|
404
|
+
- **`remote` archives (#491):** `extract: "<member file name>"` (with `memberSha256?`, `executable?`) unpacks one member of a zip or tgz after the archive digest passes, so a Windows zip needs no `command` entry.
|
|
405
|
+
- **`ffmpeg` mixed variants, fetch and probes (#491):** `variants: { ffmpeg: "export-ass" }` lets one entry take the export-ass ffmpeg with the decode ffprobe (a tool not named uses `variant`). `sources: { ffmpeg: { url, sha256, member }, ffprobe: {...} }` downloads the bytes the catalog only pins: the archive is digest-checked, the member is unpacked and its sha256 must equal the APPROVED catalog row's content digest, so a swapped archive can never stage. A source may instead be a local path (#692): `sources: { ffmpeg: { path: "../vendor/ffmpeg-lgpl", member?: "ffmpeg" } }` (also per arch: `{ aarch64: { path }, x86_64: { path } }`). `path` is relative to the app root and may step out of it; it names the binary itself or a directory holding `<tool>[.exe]` or `bin/<tool>[.exe]` (`member` renames the file). Nothing is downloaded and there is no archive digest, but the file must still equal the APPROVED catalog row content digest (`STAGE_SIDECAR_HASH_MISMATCH` otherwise); notices, probe and universal lipo are unchanged; a missing file is `STAGE_SIDECAR_BINARY_NOT_FOUND`. `probe: true` runs `-version` and `-encoders` on a staged binary this host can run: the configuration must say `--disable-gpl` and `--disable-nonfree` and must not enable GPL, nonfree, libx264, libx265, libxvid or libfdk-aac, and ffmpeg may not list those encoders (`STAGE_SIDECAR_FFMPEG_PROBE_FAILED`; another architecture or OS is recorded as `skipped-cross-target`). The catalog has no Windows `export-ass` row yet (only `export-ass-zimg`); that row belongs to the rightkit-ffmpeg crate.
|
|
406
|
+
- **`universal-apple-darwin` for `ffmpeg` and `onnxruntime` (#492):** each arch row is staged thin under `.right-release/slices/<triple>/` and the files are lipo-merged into `<stageDir>/<tool>-universal-apple-darwin` (onnxruntime: into `resourceDir`), requiring both archs and every row approved (onnxruntime also accepts `pinned`). For `ffmpeg`, `{arch}`, `{triple}` and `{stageDir}` in the `stage` command's arguments name the slice (it runs once per slice), `sources` need the per-arch form `{ ffmpeg: { aarch64: {...}, x86_64: {...} } }`, one source offer lists both slices, and the receipt rows carry `slices`. A merged file identical to the staged one is not rewritten.
|
|
407
|
+
|
|
408
|
+
## Declared Rust audit, dist-scan and the Windows Swift toolchain
|
|
409
|
+
|
|
410
|
+
- `right-release deps --audit` also runs `cargo audit` in every folder named by `deps.cargoWorkdirs` (app-relative, each must hold a `Cargo.lock`), through the managed cargo client (the RightKit broker on managed hosts; there is no bare-cargo fallback on a managed Mac), and fails when any workdir fails or cannot start. An app with no `deps.cargoWorkdirs` audits no Rust. Orthic-Labs/rightkit#674.
|
|
411
|
+
- `right-release dist-scan --dist <dir> --forbid <string>... [--forbid-file <path>] [--forbid-name <regex>]... [--ext js,css,...]` fails a built web `dist/` that carries a forbidden marker string in a text file, or a chunk named `devSource-*`, `mockDataSource-*`, `fixtures-*` or matching `--forbid-name` (exit 1; exit 2 on a usage error, a missing `dist/` or a run with no markers). It replaces per-app copies of a release-bundle checker. Orthic-Labs/rightkit#484.
|
|
412
|
+
- `right-release toolchain swift --version 6.4.0 --sha256 <pin> [--github-env] [--json]` (Windows) downloads the swift.org installer, refuses unless its sha256 equals the pin (before anything runs; a bad download is deleted), skips the install when that Swift version is already on PATH, otherwise runs it silently (`/quiet /norestart`, exit 0 and 3010 pass), checks `swift --version`, finds the runtime bin (`Runtimes\<ver>\usr\bin` with `swiftCore.dll`) and, with `--github-env`, appends only the variables the installer added or changed (names printed, values never) to `GITHUB_ENV`, and the new PATH entries, the swift directory and the runtime bin to `GITHUB_PATH`. Orthic-Labs/rightkit#436 (the `@rightkit/ci` `swift-toolchain` step calls this and lives in the ci package).
|
|
413
|
+
|
|
295
414
|
## Keeping agent writes off the internal drive (`disk agent-env`, `disk probe`)
|
|
296
415
|
|
|
297
416
|
No agent may write to the internal drive (C: on Windows, the internal disk on macOS); only installed apps go there.
|
|
@@ -326,3 +445,82 @@ No agent may write to the internal drive (C: on Windows, the internal disk on ma
|
|
|
326
445
|
`target.postSign` accepts either one command object or an array of command objects. An array runs in order, for example `create-mac-updater` followed by `lsclean`, and stops at the first failing command, so the release fails closed. Every command in the array is scanned for forbidden build features and release switches, exactly like a single command. A single object behaves as before.
|
|
327
446
|
|
|
328
447
|
Each command may set `timeoutMs`. When it expires the release kills the whole process tree (`taskkill /T /F` on Windows, the process group elsewhere) and exits non-zero, so a hung child such as Tauri leaves no orphan.
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
## App-specific forbidden release crates (G14)
|
|
451
|
+
|
|
452
|
+
Declare exact Cargo package names in `right-release.config.mjs`:
|
|
453
|
+
|
|
454
|
+
```js
|
|
455
|
+
export default {
|
|
456
|
+
// Existing app, version & distribution fields...
|
|
457
|
+
releaseDependencies: {
|
|
458
|
+
manifest: "src-tauri/Cargo.toml",
|
|
459
|
+
forbiddenCrates: [
|
|
460
|
+
"rightkit-record", "rightkit-automation", "rightkit-hotkeys",
|
|
461
|
+
"rightkit-overlay", "rightkit-agent-tools", "rightkit-qa", "rightkit-qa-driver",
|
|
462
|
+
],
|
|
463
|
+
// Optional extra features for build flags hidden inside opaque scripts:
|
|
464
|
+
features: [],
|
|
465
|
+
// Set true only when every release command disables default features:
|
|
466
|
+
noDefaultFeatures: false,
|
|
467
|
+
},
|
|
468
|
+
targets: {
|
|
469
|
+
mac: { cargoTarget: "aarch64-apple-darwin", /* existing packaging config */ },
|
|
470
|
+
win: { cargoTarget: "x86_64-pc-windows-msvc", /* existing packaging config */ },
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
`right-release build` enforces this policy before packaging. `--skip-checks` skips only app scripts;
|
|
476
|
+
this gate also blocks dry runs when Cargo cannot resolve dependencies. Development builds do not run it.
|
|
477
|
+
Without `releaseDependencies`, existing apps retain their current behavior. An explicit empty
|
|
478
|
+
`forbiddenCrates: []` still verifies that Cargo can resolve its graph.
|
|
479
|
+
|
|
480
|
+
Standalone checks, run from app root:
|
|
481
|
+
|
|
482
|
+
```sh
|
|
483
|
+
pnpm exec right-release check-release-dependencies
|
|
484
|
+
pnpm exec right-release check-release-dependencies --platform win
|
|
485
|
+
pnpm exec right-release check-release-dependencies --all-targets
|
|
486
|
+
pnpm exec right-release check-release-dependencies --config path/to/right-release.config.mjs
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Default selection uses host platform's release commands & host architecture's triple. `--all-targets` checks every configured
|
|
490
|
+
release target; non-host targets need `cargoTarget`, `targetTriple` or `rustTarget`. Each target must be
|
|
491
|
+
registered with managed Cargo. Unknown hosts fail unless an explicit platform or all targets is selected.
|
|
492
|
+
|
|
493
|
+
Graphs use `cargo tree --locked -e normal --prefix none --target <triple>` through normal Cargo dispatch.
|
|
494
|
+
Normal direct & transitive dependencies count; build-only & dev-only edges do not. Names match exactly,
|
|
495
|
+
including renamed dependencies' actual package names; versions, sources & duplicate tree entries do not
|
|
496
|
+
change matching. Checks never read Cargo.lock as evidence of reachability.
|
|
497
|
+
|
|
498
|
+
Each distinct command feature set is resolved separately. Commands, recipe steps & package scripts
|
|
499
|
+
(up to three nested script calls) contribute `--features`, `-F`, `-f`, `--all-features` &
|
|
500
|
+
`--no-default-features`; `releaseSwitches.features` & `releaseDependencies.features` add explicit features.
|
|
501
|
+
Declare flags hidden inside shell/Node script files in config. Manifest precedence is
|
|
502
|
+
`releaseDependencies.manifest`, target `cargoManifest`, `releaseSwitches.manifest`, native assembly
|
|
503
|
+
manifest, then `src-tauri/Cargo.toml` or `Cargo.toml`. Declared missing manifests fail.
|
|
504
|
+
|
|
505
|
+
SDK usage:
|
|
506
|
+
|
|
507
|
+
```js
|
|
508
|
+
import { assertReleaseDependencies, forbiddenCrateFindings } from "@rightkit/release/release-dependencies.mjs";
|
|
509
|
+
|
|
510
|
+
const evidence = assertReleaseDependencies({ root: appRoot, config, target: config.targets.mac });
|
|
511
|
+
// { manifest, target, trees, forbiddenCrates }; throws with crate & target on refusal.
|
|
512
|
+
const names = forbiddenCrateFindings(treeText, config.releaseDependencies.forbiddenCrates);
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
`run(command, args, options)` can inject Cargo execution for tests. Standalone CLI rejects missing policy,
|
|
516
|
+
invalid arguments, failed resolution & forbidden dependencies with nonzero exit status. Demo capability
|
|
517
|
+
files, base Tauri config content & app-specific default-feature restrictions remain separate guards.
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
## Template rename, PE digest, icon packing
|
|
521
|
+
|
|
522
|
+
`right-release init --name <Product Name> --id <reverse.dns.bundle.id> [--slug <lowercase>] [--dir <root>] [--dry-run]` renames the desktop app template identity in place (see `init-command.mjs`); it runs no gate or build.
|
|
523
|
+
|
|
524
|
+
`right-release pe digest <file> [--algorithm sha256|sha384|sha512] [--json]` prints the signature-independent PE content digest (Authenticode image hash: CheckSum, certificate table entry and certificate data excluded, so signing, re-signing and timestamping do not change it). `right-release pe imports <file> [--allow dll,...] [--allow-file <path>] [--json]` lists regular and delay-load imports and exits 1 when any is outside the allow list (case-insensitive; a trailing `*` allows a prefix such as `api-ms-win-*`; no allow list refuses every import). SDK: `peContentDigest(bytes)` and `verifyImportClosure(bytes, allowedDlls)` in `pe-content.mjs`.
|
|
525
|
+
|
|
526
|
+
`right-release icons ico|icns --out <file> --frame <PNG>... [--dir <folder>]` packs per-size PNG frames, hand-tuned small ones included, into an ICO (sizes 1..256, PNG-compressed entries) or an ICNS (16, 32, 64, 128, 256, 512, 1024). Frame sizes come from each PNG's IHDR; non-square, duplicate or unsupported sizes are refused. Rendering the PNG frames from a design master stays with the app's design source; the DMG background generator is `generate-dmg-background.py`.
|
package/addon-command.mjs
CHANGED
|
@@ -172,11 +172,10 @@ async function adopt(options) {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
copyFileSync(path.join(source, "addon-manifest.json"), path.join(stage, "addon-manifest.json"));
|
|
175
|
-
// Directory rename is atomic on one filesystem.
|
|
176
|
-
//
|
|
177
|
-
// replacement fails.
|
|
175
|
+
// Directory rename is atomic on one filesystem. No `<destination>.previous` is retained (D01): the
|
|
176
|
+
// verified staged copy replaces the fixed path and any stale `.previous` is removed.
|
|
178
177
|
mkdirSync(output, { recursive: true });
|
|
179
|
-
swapInPlace({ staging: stage, destination });
|
|
178
|
+
swapInPlace({ staging: stage, destination, previous: null });
|
|
180
179
|
if (temporary) rmSync(temporary, { recursive: true, force: true });
|
|
181
180
|
const repoRoot = git(path.dirname(lockPath), ["rev-parse", "--show-toplevel"]);
|
|
182
181
|
const receipt = path.join(repoRoot, ".right-release", "addons", "adoptions", `${manifest.addon}-${manifest.platform}.json`);
|
package/android-play-upload.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import { pathToFileURL } from "node:url";
|
|
|
27
27
|
import { spawnSync } from "./background-process.mjs";
|
|
28
28
|
import { describeAabProblems, scanAabBuffer } from "./android-aab-scan.mjs";
|
|
29
29
|
import { sealedManifestAllowsContentDevtools } from "./devtools-policy.mjs";
|
|
30
|
+
import { targetDistribution } from "./target-distribution.mjs";
|
|
30
31
|
import { AndroidReleaseError, createAndroidCredentialStore, resolveAndroidSecret, validateSecretSource } from "./android-release.mjs";
|
|
31
32
|
import { assertArtifactUnchanged, digestBytes, verifyPublicationRelease } from "./publication-artifacts.mjs";
|
|
32
33
|
import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
|
|
@@ -199,7 +200,7 @@ export async function authorizePlayUpload({ releaseId, configName = "right-relea
|
|
|
199
200
|
if (!existsSync(configPath)) refuse("PLAY_CONFIG_INVALID", `release config is required: ${configName}`);
|
|
200
201
|
// The config is a module that runs on import: loaded only after the tree is proven clean.
|
|
201
202
|
const config = (await import(`${pathToFileURL(configPath).href}?upload=${Date.now()}`)).default;
|
|
202
|
-
const distribution = validatePlayDistribution(config
|
|
203
|
+
const distribution = validatePlayDistribution(targetDistribution(config, "android"));
|
|
203
204
|
const sealedDir = path.join(repoRoot, ".right-release", "sealed", releaseId, "android");
|
|
204
205
|
const sealed = verifyPublicationRelease({ sealedDir, repoRoot, appRoot: path.dirname(configPath), commit, app: config.app, version: config.version, platform: "android", configPath, releaseId });
|
|
205
206
|
const entries = sealed.manifest.files.filter((entry) => entry.role === "aab");
|
package/apple-profile-renew.mjs
CHANGED
|
@@ -43,12 +43,20 @@ export function appleProfileAuth(env = process.env, { config = {}, root = proces
|
|
|
43
43
|
// Only this in-process signer reads key material. Never send it to a subprocess,
|
|
44
44
|
// include it in diagnostics, or expose it in returned renewal plans.
|
|
45
45
|
export function signAppleApiJwt({ keyPath, keyId, issuer }, now = new Date()) {
|
|
46
|
+
let key;
|
|
47
|
+
try { key = createPrivateKey(readFileSync(keyPath)); } catch { fail("APPLE_PROFILE_KEY_INVALID", "Unable to sign API JWT with configured .p8 key."); }
|
|
48
|
+
return signAppleApiJwtWithKey(key, { keyId, issuer }, now);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The same ES256 App Store Connect JWT from a key that is already in memory (a KeyObject, for example one read from the
|
|
52
|
+
// macOS Keychain by ios-asc-credentials.mjs). No file is read. lifetimeSeconds is at most Apple's 1200 second limit.
|
|
53
|
+
export function signAppleApiJwtWithKey(key, { keyId, issuer }, now = new Date(), { lifetimeSeconds = 600 } = {}) {
|
|
46
54
|
try {
|
|
47
|
-
|
|
48
|
-
if (key
|
|
55
|
+
if (!Number.isInteger(lifetimeSeconds) || lifetimeSeconds < 1 || lifetimeSeconds > 1200) throw new Error();
|
|
56
|
+
if (key?.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") throw new Error();
|
|
49
57
|
const iat = Math.floor(now.getTime() / 1000);
|
|
50
58
|
const encoded = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
51
|
-
const payload = `${encoded({ alg: "ES256", kid: keyId, typ: "JWT" })}.${encoded({ iss: issuer, iat, exp: iat +
|
|
59
|
+
const payload = `${encoded({ alg: "ES256", kid: keyId, typ: "JWT" })}.${encoded({ iss: issuer, iat, exp: iat + lifetimeSeconds, aud: "appstoreconnect-v1" })}`;
|
|
52
60
|
return `${payload}.${sign("sha256", Buffer.from(payload), { key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
53
61
|
} catch { fail("APPLE_PROFILE_KEY_INVALID", "Unable to sign API JWT with configured .p8 key."); }
|
|
54
62
|
}
|
package/artifact-storage.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
mkdirSync,
|
|
7
7
|
readFileSync,
|
|
8
8
|
readdirSync,
|
|
9
|
+
realpathSync,
|
|
9
10
|
renameSync,
|
|
10
11
|
rmSync,
|
|
11
12
|
statSync,
|
|
@@ -20,6 +21,21 @@ export const DEFAULT_MANAGED_ROOT = resolveManagedRoots().dataRoot;
|
|
|
20
21
|
|
|
21
22
|
const SAFE_COMPONENT = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/;
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The configured root, canonicalised once: an owner may move the managed root to another volume and leave a symlink at the
|
|
26
|
+
* configured path (2026-10-18: /Volumes/D/.rightkit-managed -> /Volumes/E/.rightkit-managed). Every path below it is still checked
|
|
27
|
+
* for symlinks by its consumer (ensurePrivateDirectory refuses symlink ancestors); only the root itself is resolved here.
|
|
28
|
+
* Used only where a strict ancestor walk needs it (codesign-environment.mjs): resolveManagedRoot stays lexical, because the
|
|
29
|
+
* Mac signing agent's allowed roots are recorded in that lexical spelling.
|
|
30
|
+
*/
|
|
31
|
+
export function canonicalManagedRoot(root, { realpath = realpathSync.native } = {}) {
|
|
32
|
+
const resolved = path.resolve(root);
|
|
33
|
+
try { return realpath(resolved); } catch (error) {
|
|
34
|
+
if (error?.code === "ENOENT") return resolved;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
export function resolveManagedRoot({ env = process.env, exists = existsSync } = {}) {
|
|
24
40
|
if (env.RIGHTKIT_DATA_ROOT || env.RIGHTKIT_MANAGED_ROOT || env.RIGHTKIT_EXTERNAL_ROOT) return path.resolve(resolveManagedRoots({ env }).dataRoot);
|
|
25
41
|
// Workstation default only when its drive exists; hosted CI runners fall back to RUNNER_TEMP, then the system temp dir.
|
|
@@ -310,15 +326,32 @@ function ensureDirectory(directory) {
|
|
|
310
326
|
mkdirSync(directory, { recursive: true });
|
|
311
327
|
}
|
|
312
328
|
|
|
313
|
-
|
|
329
|
+
/**
|
|
330
|
+
* The managed roots an owner configures (env overrides and the workstation default), as spelled. Only these exact paths may
|
|
331
|
+
* themselves be symlinks (2026-10-18: /Volumes/D/.rightkit-managed -> /Volumes/E/.rightkit-managed); every path below them,
|
|
332
|
+
* and every other ancestor, is still refused when it is a symlink.
|
|
333
|
+
*/
|
|
334
|
+
export function configuredManagedRootSpellings({ env = process.env } = {}) {
|
|
335
|
+
const spellings = [DEFAULT_MANAGED_ROOT, env.RIGHTKIT_DATA_ROOT, env.RIGHTKIT_MANAGED_ROOT, env.RIGHTKIT_EXTERNAL_ROOT]
|
|
336
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
337
|
+
.map((value) => path.resolve(value));
|
|
338
|
+
return new Set(process.platform === "win32" ? spellings.map((value) => value.toLowerCase()) : spellings);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isConfiguredManagedRoot(candidate, roots) {
|
|
342
|
+
return roots.has(process.platform === "win32" ? path.resolve(candidate).toLowerCase() : path.resolve(candidate));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function assertSafeMutation(target, { env = process.env } = {}) {
|
|
314
346
|
const absolute = path.resolve(target);
|
|
315
347
|
const root = path.parse(absolute).root;
|
|
316
348
|
const relative = path.relative(root, absolute);
|
|
349
|
+
const managedRoots = configuredManagedRootSpellings({ env });
|
|
317
350
|
let current = root;
|
|
318
351
|
for (const segment of relative ? relative.split(path.sep) : []) {
|
|
319
352
|
current = path.join(current, segment);
|
|
320
353
|
try {
|
|
321
|
-
if (lstatSync(current).isSymbolicLink()) throw new Error(`refusing filesystem mutation through symlink: ${current}`);
|
|
354
|
+
if (lstatSync(current).isSymbolicLink() && !isConfiguredManagedRoot(current, managedRoots)) throw new Error(`refusing filesystem mutation through symlink: ${current}`);
|
|
322
355
|
} catch (error) {
|
|
323
356
|
if (error.code === "ENOENT") continue;
|
|
324
357
|
throw error;
|
package/bin/darwin/rightkit-swap
CHANGED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
|
-
"sha256": "
|
|
3
|
+
"sha256": "a41ce13fd3bac31db0230d70bed20db0ed9bf554bdae2255bdf2064589b48f29",
|
|
4
4
|
"identity": "Developer ID Application: Adrian D'souza (6KLGD3LLKF)",
|
|
5
5
|
"codesign": "strict-valid"
|
|
6
6
|
}
|