@rightkit/release 0.2.137 → 0.2.138
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 +1 -0
- package/cli/right-release.mjs +4 -1
- package/committed-source.mjs +1 -1
- package/development.mjs +2 -2
- package/inventory-check.mjs +1 -1
- package/managed-cargo.mjs +9 -2
- package/msix-build.mjs +128 -0
- package/ownership-admission.mjs +125 -0
- package/ownership-approval.mjs +3 -0
- package/ownership-internal-owners.json +0 -35
- package/ownership-key-command.mjs +13 -4
- package/ownership-key-elevation.mjs +124 -0
- package/ownership-repositories.json +3 -1
- package/ownership.mjs +2 -2
- package/package.json +3 -3
- package/pe-imports.mjs +59 -0
- package/rightkit-ownership.json +0 -35
- package/rightkit-sync.mjs +2 -5
- package/rightkit-versions.json +13 -8
- package/swift-mac-app.mjs +146 -0
- package/swift-package-command.mjs +85 -0
- package/swift-windows-stage.mjs +103 -0
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Signing and publication rules are in the workspace file `docs/rules/release-sign
|
|
|
18
18
|
| Group | Subcommands |
|
|
19
19
|
|---|---|
|
|
20
20
|
| Build | `release`, `build`, `package` |
|
|
21
|
+
| 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}`) |
|
|
21
22
|
| Install | `install-app` |
|
|
22
23
|
| Publish | `publish`, `upload`, `publish-update`, `github`, `mirror-root-artifact` |
|
|
23
24
|
| Signing | `sign-windows`, `sign-updater`, `signing-agent`, `create-mac-updater` |
|
package/cli/right-release.mjs
CHANGED
|
@@ -26,6 +26,7 @@ const commands = new Map([
|
|
|
26
26
|
["ownership", "ownership-command.mjs"],
|
|
27
27
|
["ownership-approval", "ownership-approval-command.mjs"],
|
|
28
28
|
["ownership-report", "ownership-report-command.mjs"],
|
|
29
|
+
["swift-package", "swift-package-command.mjs"],
|
|
29
30
|
]);
|
|
30
31
|
|
|
31
32
|
const args = process.argv.slice(2);
|
|
@@ -36,7 +37,7 @@ if (first === "dev") {
|
|
|
36
37
|
process.exitCode = 1;
|
|
37
38
|
});
|
|
38
39
|
} else {
|
|
39
|
-
const githubBuildCommands = new Set(["release", "build", "package", "sign-windows", "sign-updater", "create-mac-updater", "cargo-target"]);
|
|
40
|
+
const githubBuildCommands = new Set(["release", "build", "package", "swift-package", "sign-windows", "sign-updater", "create-mac-updater", "cargo-target"]);
|
|
40
41
|
|
|
41
42
|
try {
|
|
42
43
|
if (githubBuildCommands.has(first)
|
|
@@ -178,6 +179,8 @@ Commands:
|
|
|
178
179
|
Resume macOS at prepackage, sign, package, post-sign, or harden
|
|
179
180
|
dev --platform win|mac [--config <path>] [--dry-run] [--build-only]
|
|
180
181
|
Incremental unsigned build & optional local install
|
|
182
|
+
swift-package <windows-stage|msix|mac-app> --config <swift-package.json> [--dry-run] [--no-sign] [--sign] [--notarize] [--profile <name>]
|
|
183
|
+
Stage SwiftPM app; build+sign MSIX (Windows) or assemble+sign+notarize .app (macOS); never uploads
|
|
181
184
|
upload --release <id> --platform mac|win --tier patch|update
|
|
182
185
|
Upload one sealed release; never builds
|
|
183
186
|
publish-update --release <id> --platform mac|win --tier patch|update
|
package/committed-source.mjs
CHANGED
|
@@ -132,7 +132,7 @@ function acquire(root, { expectedCommit, expectedTree, requiredPaths = [], runne
|
|
|
132
132
|
const rows = parseTree(runGit(root, ["ls-tree", "-r", "-z", "--full-tree", tree], { encoding: "buffer", maxBuffer: MAX_TREE_BYTES, timeout: COMMAND_TIMEOUT_MS, runner, env }), objectFormat);
|
|
133
133
|
const wanted = new Set(requiredPaths.filter(validatePath));
|
|
134
134
|
const shouldRead = (row) => row.type === "blob" && row.mode !== "120000" && (
|
|
135
|
-
wanted.has(row.rel) || ["Cargo.toml", "package.json", "Package.swift", "ownership-exceptions.json", "Cargo.lock", "pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock"].includes(path.posix.basename(row.rel)) ||
|
|
135
|
+
wanted.has(row.rel) || ["Cargo.toml", "package.json", "Package.swift", "Package.resolved", "ownership-exceptions.json", "Cargo.lock", "pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock"].includes(path.posix.basename(row.rel)) ||
|
|
136
136
|
/\.(?:rs|ts|tsx|js|mjs|cjs|jsx|swift|m|mm|c|cc|cpp|h|py|go|kt|java|cs)$/i.test(row.rel)
|
|
137
137
|
);
|
|
138
138
|
const candidates = rows.filter(shouldRead);
|
package/development.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
|
|
8
8
|
import { assertReleaseExecutionAllowed } from "./execution-policy.mjs";
|
|
9
|
-
import { acquireManagedRunLease, managedRuntimeClient, runCargoSync } from "./managed-cargo.mjs";
|
|
9
|
+
import { acquireManagedRunLease, managedRuntimeClient, managedRuntimeCommand, runCargoSync } from "./managed-cargo.mjs";
|
|
10
10
|
import { BUNDLE_MARKERS, bundleMarkerBuffer, readTauriBundleMarkers } from "./tauri-bundle-marker.mjs";
|
|
11
11
|
import { installPortableDevelopment } from "./portable-development.mjs";
|
|
12
12
|
import { signMacosDevelopmentBundle } from "./development-signing.mjs";
|
|
@@ -213,7 +213,7 @@ export function resolveCommand(command, args, { root, env = process.env } = {})
|
|
|
213
213
|
if (!client) throw new Error("installed RightKit runtime client is unavailable for development command");
|
|
214
214
|
const socket = discoverBrokerSocket(env);
|
|
215
215
|
const childEnv = !env.RIGHTKIT_BUILD_BROKER_SOCKET && existsSync(socket) ? { ...env, RIGHTKIT_BUILD_BROKER_SOCKET: socket } : undefined;
|
|
216
|
-
return {
|
|
216
|
+
return { ...managedRuntimeCommand(client, args), ...(childEnv ? { env: childEnv } : {}) };
|
|
217
217
|
}
|
|
218
218
|
return { command, args };
|
|
219
219
|
}
|
package/inventory-check.mjs
CHANGED
|
@@ -24,7 +24,7 @@ const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
|
24
24
|
const CONCURRENCY = 8;
|
|
25
25
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
26
26
|
const CRATES_API_INTERVAL_MS = 1000;
|
|
27
|
-
const USER_AGENT = "rightkit-inventory-check/1 (+https://github.com/bogusyogi/
|
|
27
|
+
const USER_AGENT = "rightkit-inventory-check/1 (+https://github.com/bogusyogi/rightkit)";
|
|
28
28
|
const EXCLUDED_PREFIX = "build-control-";
|
|
29
29
|
const OWNER_PROBE_CRATE = "rightkit-fs";
|
|
30
30
|
const EXTRA_ACCOUNT_CRATES = Object.freeze(["agent-room-core"]);
|
package/managed-cargo.mjs
CHANGED
|
@@ -7,13 +7,19 @@ import { spawnSync } from "./background-process.mjs";
|
|
|
7
7
|
export function managedRuntimeClient(env = process.env, platform = process.platform, exists = existsSync) {
|
|
8
8
|
const roots = managedControlCandidates({ env, platform });
|
|
9
9
|
const paths = platform === 'win32' ? path.win32 : path.posix;
|
|
10
|
+
// runtime-current ships the native `rustright` client.
|
|
10
11
|
for (const root of roots) {
|
|
11
|
-
const client = paths.join(root, "runtime-current",
|
|
12
|
+
const client = paths.join(root, "runtime-current", platform === 'win32' ? "rustright.exe" : "rustright");
|
|
12
13
|
if (exists(client)) return client;
|
|
13
14
|
}
|
|
14
15
|
return undefined;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/** Command line that runs the managed client: the native rustright client runs directly. */
|
|
19
|
+
export function managedRuntimeCommand(client, args) {
|
|
20
|
+
return { command: client, args };
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
/** Hold a client-run lease that blocks broker runtime cutover until release() (see
|
|
18
24
|
* proveDevelopmentRunsDrained). No managed runtime → no lease; lease I/O failure warns and proceeds. */
|
|
19
25
|
export function acquireManagedRunLease({ command, env = process.env, platform = process.platform, exists = existsSync, pid = process.pid } = {}) {
|
|
@@ -43,7 +49,8 @@ export function runCargoSync(args, options = {}, { platform = process.platform,
|
|
|
43
49
|
const socket = discoverBrokerSocket(env, { platform, exists });
|
|
44
50
|
if (platform === 'darwin' && (!socket || !exists(socket))) return unavailable('RightKit broker socket is unavailable');
|
|
45
51
|
const childEnv = socket ? { ...env, RIGHTKIT_BUILD_BROKER_SOCKET: socket } : env;
|
|
46
|
-
const
|
|
52
|
+
const invocation = managedRuntimeCommand(client, ["cargo", ...args]);
|
|
53
|
+
const result = spawn(invocation.command, invocation.args, { ...options, env: childEnv });
|
|
47
54
|
if (platform === 'darwin' && result.status !== 0 && /RK_BROKER_UNAVAILABLE|RightKit broker (?:unavailable|endpoint is not configured)/i.test(String(result.stderr ?? ''))) {
|
|
48
55
|
return { ...result, error: unavailable(String(result.stderr).trim()).error };
|
|
49
56
|
}
|
package/msix-build.mjs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Windows MSIX packaging for staged desktop apps (SwiftPM exe + app-local DLLs).
|
|
2
|
+
// Order mirrors the Windows rule in docs/rules/release-signing.md: sign raw PE
|
|
3
|
+
// files first, pack the exact signed bytes, then sign the package. All signing
|
|
4
|
+
// goes through sign-windows.mjs (provisioned Azure Artifact Signing); this module
|
|
5
|
+
// never touches credentials and never authenticates anything itself.
|
|
6
|
+
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { spawnSync } from "./background-process.mjs";
|
|
11
|
+
import { findWindowsSdkTool } from "./preflight.mjs";
|
|
12
|
+
|
|
13
|
+
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
14
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
export const MSIX_SIGNER_SUBJECT = "CN=Damned Ventures LLC";
|
|
16
|
+
|
|
17
|
+
const xml = (value) => String(value).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
18
|
+
const PNG = Buffer.from("89504e470d0a1a0a", "hex");
|
|
19
|
+
|
|
20
|
+
export function validateMsixIdentity(o) {
|
|
21
|
+
const errors = [];
|
|
22
|
+
if (!/^[-.A-Za-z0-9]{3,50}$/.test(o.name ?? "")) errors.push("name must be 3-50 chars of letters, digits, '-' or '.'");
|
|
23
|
+
if (!/^\d+\.\d+\.\d+\.\d+$/.test(o.version ?? "") || o.version.split(".").some((n) => Number(n) > 65535)) errors.push("version must be four numeric parts <=65535");
|
|
24
|
+
if (!["x64", "arm64"].includes(o.architecture)) errors.push("architecture must be x64 or arm64");
|
|
25
|
+
if (!/\bCN=/.test(o.publisher ?? "")) errors.push("publisher must be a distinguished name containing CN=");
|
|
26
|
+
else if (!o.publisher.includes(MSIX_SIGNER_SUBJECT)) errors.push(`publisher must contain ${MSIX_SIGNER_SUBJECT} (the provisioned signing subject); confirm the full DN natively`);
|
|
27
|
+
if (!o.displayName || !o.publisherDisplayName) errors.push("displayName and publisherDisplayName are required");
|
|
28
|
+
if (!/\.exe$/i.test(o.executable ?? "") || /[\\/]/.test(o.executable ?? "")) errors.push("executable must be a flat .exe name inside the package");
|
|
29
|
+
if (errors.length) throw fail("MSIX_INVALID_IDENTITY", errors.join("; "));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function generateAppxManifest(o) {
|
|
33
|
+
validateMsixIdentity(o);
|
|
34
|
+
const deps = (o.dependencies ?? []).filter((d) => d.minVersion).map((d) =>
|
|
35
|
+
` <PackageDependency Name="${xml(d.name)}" MinVersion="${xml(d.minVersion)}" Publisher="${xml(d.publisher ?? "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US")}" />`);
|
|
36
|
+
const a = o.assets ?? {};
|
|
37
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
38
|
+
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
|
39
|
+
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
|
40
|
+
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
|
41
|
+
IgnorableNamespaces="uap rescap">
|
|
42
|
+
<Identity Name="${xml(o.name)}" Publisher="${xml(o.publisher)}" Version="${xml(o.version)}" ProcessorArchitecture="${o.architecture}" />
|
|
43
|
+
<Properties>
|
|
44
|
+
<DisplayName>${xml(o.displayName)}</DisplayName>
|
|
45
|
+
<PublisherDisplayName>${xml(o.publisherDisplayName)}</PublisherDisplayName>
|
|
46
|
+
<Logo>Assets\\StoreLogo.png</Logo>
|
|
47
|
+
</Properties>
|
|
48
|
+
<Dependencies>
|
|
49
|
+
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="${xml(o.minWindowsVersion ?? "10.0.19041.0")}" MaxVersionTested="${xml(o.maxWindowsVersionTested ?? "10.0.26100.0")}" />
|
|
50
|
+
${deps.join("\n")}${deps.length ? "\n" : ""} </Dependencies>
|
|
51
|
+
<Resources><Resource Language="${xml(o.language ?? "en-us")}" /></Resources>
|
|
52
|
+
<Applications>
|
|
53
|
+
<Application Id="App" Executable="${xml(o.executable)}" EntryPoint="Windows.FullTrustApplication">
|
|
54
|
+
<uap:VisualElements DisplayName="${xml(o.displayName)}" Description="${xml(o.description ?? o.displayName)}" BackgroundColor="transparent"
|
|
55
|
+
Square150x150Logo="Assets\\Square150x150Logo.png" Square44x44Logo="Assets\\Square44x44Logo.png" />
|
|
56
|
+
</Application>
|
|
57
|
+
</Applications>
|
|
58
|
+
<Capabilities>
|
|
59
|
+
<rescap:Capability Name="runFullTrust" />${(o.capabilities ?? []).map((c) => `\n <Capability Name="${xml(c)}" />`).join("")}
|
|
60
|
+
</Capabilities>
|
|
61
|
+
</Package>
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ASSETS = { storeLogo: "StoreLogo.png", square150: "Square150x150Logo.png", square44: "Square44x44Logo.png" };
|
|
66
|
+
|
|
67
|
+
/** Copy a staged directory plus assets and manifest into a package root. */
|
|
68
|
+
export function assembleMsixRoot({ stageDir, root, manifest, assets }) {
|
|
69
|
+
stageDir = path.resolve(stageDir); root = path.resolve(root);
|
|
70
|
+
if (!existsSync(path.join(stageDir, manifest.executable))) throw fail("MSIX_MISSING_EXECUTABLE", `executable not in staged directory: ${manifest.executable}`);
|
|
71
|
+
rmSync(root, { recursive: true, force: true });
|
|
72
|
+
mkdirSync(path.join(root, "Assets"), { recursive: true });
|
|
73
|
+
for (const entry of readdirSync(stageDir, { withFileTypes: true })) {
|
|
74
|
+
if (entry.isFile()) copyFileSync(path.join(stageDir, entry.name), path.join(root, entry.name));
|
|
75
|
+
else throw fail("MSIX_NESTED_STAGE", `staged directory must be flat; found ${entry.name}/ (use include with flat files)`);
|
|
76
|
+
}
|
|
77
|
+
for (const [key, target] of Object.entries(ASSETS)) {
|
|
78
|
+
const source = assets?.[key] && path.resolve(assets[key]);
|
|
79
|
+
if (!source || !existsSync(source)) throw fail("MSIX_MISSING_ASSET", `assets.${key} (PNG) is required: ${assets?.[key] ?? "unset"}`);
|
|
80
|
+
if (!readFileSync(source).subarray(0, 8).equals(PNG)) throw fail("MSIX_MISSING_ASSET", `assets.${key} is not a PNG: ${source}`);
|
|
81
|
+
copyFileSync(source, path.join(root, "Assets", target));
|
|
82
|
+
}
|
|
83
|
+
writeFileSync(path.join(root, "AppxManifest.xml"), generateAppxManifest(manifest));
|
|
84
|
+
return root;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** PE files the package ships; these get Authenticode before packing. */
|
|
88
|
+
export function peFilesIn(root) {
|
|
89
|
+
return readdirSync(root).filter((name) => /\.(exe|dll)$/i.test(name)).sort().map((name) => path.join(root, name));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Pack + sign. `run` is injectable for tests. Steps:
|
|
94
|
+
* 1 sign every PE file in the root (raw bytes), 2 makeappx pack, 3 sign the .msix, 4 verify.
|
|
95
|
+
* Windows-only unless dryRun.
|
|
96
|
+
*/
|
|
97
|
+
export function buildMsix({ stageDir, out, workDir, manifest, assets, sign = true, dryRun = false, receiptDir },
|
|
98
|
+
{ platform = process.platform, run = spawnSync, findTool = findWindowsSdkTool, env = process.env, log = (e) => console.error(JSON.stringify(e)) } = {}) {
|
|
99
|
+
out = path.resolve(out);
|
|
100
|
+
if (!out.endsWith(".msix")) throw fail("MSIX_INVALID_OPTIONS", "out must end with .msix");
|
|
101
|
+
validateMsixIdentity(manifest);
|
|
102
|
+
workDir = path.resolve(workDir ?? `${out}.root`);
|
|
103
|
+
const receipts = path.resolve(receiptDir ?? path.dirname(out));
|
|
104
|
+
const steps = ["assemble", ...(sign ? ["sign-pe"] : []), "pack", ...(sign ? ["sign-msix", "verify-msix"] : [])];
|
|
105
|
+
if (dryRun) { log({ event: "msix-dry-run", out, steps }); return { out, steps, dryRun: true }; }
|
|
106
|
+
if (platform !== "win32") throw fail("MSIX_NATIVE_HOST_REQUIRED", "MSIX packing and signing must run on Windows");
|
|
107
|
+
const makeappx = env.MAKEAPPX_PATH || findTool("makeappx.exe")?.toolPath;
|
|
108
|
+
if (!makeappx) throw fail("MSIX_TOOL_MISSING", "makeappx.exe not found; install the Windows SDK or set MAKEAPPX_PATH");
|
|
109
|
+
const checked = (cmd, args) => {
|
|
110
|
+
const r = run(cmd, args, { encoding: "utf8", windowsHide: true, timeout: 600000 });
|
|
111
|
+
if (r.error || r.status !== 0) throw fail("MSIX_COMMAND_FAILED", `${path.basename(cmd)} ${args[0] ?? ""} failed: ${r.error?.message ?? r.stderr ?? r.stdout ?? r.status}`);
|
|
112
|
+
return r;
|
|
113
|
+
};
|
|
114
|
+
const signer = (files, receipt) => checked(process.execPath, [path.join(here, "sign-windows.mjs"), "--receipt", receipt, ...files]);
|
|
115
|
+
const root = assembleMsixRoot({ stageDir, root: workDir, manifest, assets });
|
|
116
|
+
mkdirSync(receipts, { recursive: true });
|
|
117
|
+
if (sign) signer(peFilesIn(root), path.join(receipts, "msix-pe-sign.receipt.json"));
|
|
118
|
+
mkdirSync(path.dirname(out), { recursive: true });
|
|
119
|
+
rmSync(out, { force: true });
|
|
120
|
+
checked(makeappx, ["pack", "/d", root, "/p", out, "/o"]);
|
|
121
|
+
if (!existsSync(out) || statSync(out).size === 0) throw fail("MSIX_PACK_EMPTY", `makeappx produced no package: ${out}`);
|
|
122
|
+
if (sign) {
|
|
123
|
+
signer([out], path.join(receipts, "msix-sign.receipt.json"));
|
|
124
|
+
checked(process.execPath, [path.join(here, "sign-windows.mjs"), "--verify-only", out]);
|
|
125
|
+
}
|
|
126
|
+
log({ event: "msix-built", out, signed: sign });
|
|
127
|
+
return { out, steps, root, signed: sign };
|
|
128
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from './background-process.mjs';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
/** Protected ownership evidence: bounded worker scans & digest-bound verdict reuse. */
|
|
7
|
+
export const OWNERSHIP_MODES = Object.freeze(['off', 'report', 'enforce']);
|
|
8
|
+
const MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
/** The scanner lives beside this module (reference implementation for the RustRight admission contract). */
|
|
12
|
+
export function defaultScannerRoot() {
|
|
13
|
+
for (const candidate of [here]) {
|
|
14
|
+
if (existsSync(join(candidate, 'ownership-scan-worker.mjs')) && existsSync(join(candidate, 'rightkit-ownership.json'))) return candidate;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const shared = defaultScannerRoot();
|
|
19
|
+
const { ownershipVerdict, ownershipCacheKey } = shared
|
|
20
|
+
? await import(pathToFileURL(join(shared, 'ownership-verdict.mjs')).href)
|
|
21
|
+
: { ownershipVerdict: () => ({ admit: false, code: 'RK_OWNERSHIP_UNVERIFIED', problems: ['shared ownership verdict is unavailable'] }), ownershipCacheKey: () => { throw new Error('shared ownership verdict is unavailable'); } };
|
|
22
|
+
|
|
23
|
+
/** Always scan repository coverage; owner authority comes from protected registry. */
|
|
24
|
+
export function ownershipScanRoot({ repositoryRoot, workspaceRoot }) {
|
|
25
|
+
const repository = repositoryRoot ?? workspaceRoot;
|
|
26
|
+
return typeof repository === 'string' && repository.length ? { root: resolve(repository) } : { error: 'repository root is missing or malformed' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function runScanWorker({ scannerRoot, scanRoot, timeoutMs, inputsOnly = false, spawn = nodeSpawn, execPath = process.execPath }) {
|
|
30
|
+
return new Promise((resolveScan) => {
|
|
31
|
+
let child, timer;
|
|
32
|
+
try {
|
|
33
|
+
child = spawn(execPath, [join(scannerRoot, 'ownership-scan-worker.mjs'), scanRoot, join(scannerRoot, 'rightkit-ownership.json'), ...(inputsOnly ? ['--inputs'] : [])], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
34
|
+
} catch (error) { resolveScan({ error: 'scanner failed to start: ' + error.message }); return; }
|
|
35
|
+
const out = []; const err = []; let bytes = 0; let settled = false;
|
|
36
|
+
const settleScan = (value) => { if (settled) return; settled = true; clearTimeout(timer); resolveScan(value); };
|
|
37
|
+
const collect = (sink) => (chunk) => {
|
|
38
|
+
bytes += chunk.length;
|
|
39
|
+
if (bytes > MAX_OUTPUT_BYTES) { child.kill(); settleScan({ error: 'scanner output exceeded ' + MAX_OUTPUT_BYTES + ' bytes' }); return; }
|
|
40
|
+
sink.push(chunk);
|
|
41
|
+
};
|
|
42
|
+
child.stdout.on('data', collect(out)); child.stderr.on('data', collect(err));
|
|
43
|
+
child.once('error', (error) => settleScan({ error: 'scanner failed to start: ' + error.message }));
|
|
44
|
+
child.once('close', (code) => {
|
|
45
|
+
if (code !== 0) return settleScan({ error: Buffer.concat(err).toString('utf8').trim().slice(0, 2000) || 'scanner exited ' + code });
|
|
46
|
+
try { settleScan(JSON.parse(Buffer.concat(out).toString('utf8'))); }
|
|
47
|
+
catch (error) { settleScan({ error: 'scanner returned invalid JSON: ' + error.message }); }
|
|
48
|
+
});
|
|
49
|
+
timer = setTimeout(() => { child.kill(); settleScan({ error: 'scan exceeded ' + timeoutMs + ' ms' }); }, timeoutMs);
|
|
50
|
+
timer.unref?.();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const outcomes = Object.freeze({
|
|
55
|
+
RK_OWNERSHIP_CLEAN: 'clean', RK_OWNERSHIP_VIOLATION: 'violations', RK_OWNERSHIP_PENDING_ENROLLMENT: 'pending-enrollment',
|
|
56
|
+
RK_OWNERSHIP_MALFORMED_REPORT: 'malformed-report', RK_OWNERSHIP_INCOMPLETE_COVERAGE: 'incomplete-coverage',
|
|
57
|
+
RK_OWNERSHIP_INVALID_REGISTRATION: 'invalid-registration', RK_OWNERSHIP_POLICY_DOWNGRADE: 'policy-downgrade',
|
|
58
|
+
RK_OWNERSHIP_REPORT_FAILED: 'report-failed', RK_OWNERSHIP_UNVERIFIED: 'problems',
|
|
59
|
+
});
|
|
60
|
+
/** Classification delegates every acceptance decision to shared verdict contract. */
|
|
61
|
+
export function classifyOwnershipReport(report, trust) {
|
|
62
|
+
const verdict = ownershipVerdict(report, trust);
|
|
63
|
+
const violations = Array.isArray(report?.violations) ? report.violations.filter((v) => v && typeof v === 'object').map((v) => ({ capability: v.capability ?? null, owner: v.package ?? null, fingerprint: v.fingerprint ?? null, file: v.file ?? null, line: v.line ?? null })) : [];
|
|
64
|
+
return { ...verdict, outcome: outcomes[verdict.code] ?? 'unscanned', violations };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class OwnershipAdmission {
|
|
68
|
+
#mode; #timeoutMs; #scannerRoot; #now; #run; #inputs; #cache = new Map(); #inflight = new Map();
|
|
69
|
+
|
|
70
|
+
constructor({ mode = 'report', timeoutMs = 30_000, scannerRoot = defaultScannerRoot(), now = Date.now, runScan = runScanWorker, runInputs = runScanWorker } = {}) {
|
|
71
|
+
if (!OWNERSHIP_MODES.includes(mode)) throw new TypeError('ownership mode must be one of ' + OWNERSHIP_MODES.join(', '));
|
|
72
|
+
this.#mode = mode; this.#timeoutMs = timeoutMs; this.#scannerRoot = scannerRoot; this.#now = now; this.#run = runScan; this.#inputs = runInputs;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get mode() { return this.#mode; }
|
|
76
|
+
|
|
77
|
+
#failure(root, error, code = 'RK_OWNERSHIP_UNVERIFIED') {
|
|
78
|
+
return Object.freeze({ mode: this.#mode, admit: false, code, outcome: code === 'RK_OWNERSHIP_UNVERIFIED' ? 'unscanned' : outcomes[code] ?? 'unscanned', scanRoot: root, error, problems: [error], violations: [], scanMs: 0, cached: false });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Re-authenticate & hash inputs on every admission, including cache hits. */
|
|
82
|
+
async evaluate({ repositoryRoot, workspaceRoot } = {}) {
|
|
83
|
+
if (this.#mode === 'off') return Object.freeze({ mode: 'off', outcome: 'off' });
|
|
84
|
+
const target = ownershipScanRoot({ repositoryRoot, workspaceRoot });
|
|
85
|
+
if (target.error) return this.#failure(null, target.error, 'RK_OWNERSHIP_INVALID_REGISTRATION');
|
|
86
|
+
if (!this.#scannerRoot) return this.#failure(target.root, 'ownership scanner is not staged in this runtime');
|
|
87
|
+
try {
|
|
88
|
+
const observed = await this.#inputs({ scannerRoot: this.#scannerRoot, scanRoot: target.root, timeoutMs: this.#timeoutMs, inputsOnly: true });
|
|
89
|
+
if (observed?.error) return this.#failure(target.root, observed.error, observed.code);
|
|
90
|
+
if (typeof observed?.complete !== 'boolean') return this.#failure(target.root, 'source inventory completeness is missing or malformed', 'RK_OWNERSHIP_INVALID_INPUTS');
|
|
91
|
+
const key = ownershipCacheKey(observed?.inputs);
|
|
92
|
+
if (observed.complete === false) return this.#failure(target.root, 'source content inventory has incomplete coverage', 'RK_OWNERSHIP_INCOMPLETE_COVERAGE');
|
|
93
|
+
const cached = this.#cache.get(key);
|
|
94
|
+
if (cached?.scanRoot === target.root) return Object.freeze({ ...cached, cached: true });
|
|
95
|
+
// Root keeps distinct repository provenance separate; digests determine freshness.
|
|
96
|
+
const flight = target.root + ':' + key;
|
|
97
|
+
let pending = this.#inflight.get(flight);
|
|
98
|
+
if (!pending) {
|
|
99
|
+
pending = this.#scan(target.root, key, observed).finally(() => this.#inflight.delete(flight));
|
|
100
|
+
this.#inflight.set(flight, pending);
|
|
101
|
+
}
|
|
102
|
+
return await pending;
|
|
103
|
+
} catch (error) { return this.#failure(target.root, error.message, error.code?.startsWith('RK_OWNERSHIP_') ? error.code : 'RK_OWNERSHIP_UNVERIFIED'); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Enforce refuses every non-admitted result; report records identical verdicts. */
|
|
107
|
+
assertAdmitted(result) {
|
|
108
|
+
if (this.#mode !== 'enforce') return;
|
|
109
|
+
if (result?.admit === true && result.code === 'RK_OWNERSHIP_CLEAN') return;
|
|
110
|
+
throw Object.assign(new Error('RightKit ownership refuses this build: ' + (result?.problems?.slice(0, 5).join('; ') || result?.error || 'ownership verdict is missing or malformed')), { code: result?.code ?? 'RK_OWNERSHIP_UNVERIFIED' });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async #scan(root, key, observed) {
|
|
114
|
+
const started = this.#now();
|
|
115
|
+
const scanned = await this.#run({ scannerRoot: this.#scannerRoot, scanRoot: root, timeoutMs: this.#timeoutMs });
|
|
116
|
+
if (scanned?.error) return this.#failure(root, scanned.error, scanned.code);
|
|
117
|
+
if (ownershipCacheKey(scanned?.inputs) !== key) return this.#failure(root, 'ownership inputs changed between admission & protected scan', 'RK_OWNERSHIP_INPUTS_CHANGED');
|
|
118
|
+
const result = Object.freeze({ mode: this.#mode, ...classifyOwnershipReport(scanned?.report, observed.trust), scanRoot: root, scanMs: this.#now() - started, cached: false });
|
|
119
|
+
if (!['RK_OWNERSHIP_MALFORMED_REPORT', 'RK_OWNERSHIP_UNVERIFIED'].includes(result.code)) {
|
|
120
|
+
this.#cache.set(key, result);
|
|
121
|
+
if (this.#cache.size > 128) this.#cache.delete(this.#cache.keys().next().value);
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
}
|
package/ownership-approval.mjs
CHANGED
|
@@ -327,6 +327,9 @@ function protectedLocation(file, platform, { runner = spawnSync, inspect = lstat
|
|
|
327
327
|
}
|
|
328
328
|
export function loadTrustedExternal() {
|
|
329
329
|
const platform = process.platform;
|
|
330
|
+
// Hosts without a protected trust location (Linux CI admission runners) can never hold
|
|
331
|
+
// enrolled trust: report the same pending, trust-absent state as an unenrolled host.
|
|
332
|
+
if (!DEFAULT_TRUST_PATH[platform]) return { enrolled: false, pending: true, protection: { ok: false, reason: "external trust file is absent" }, trust: null, path: null };
|
|
330
333
|
const file = trustedPath();
|
|
331
334
|
if (!existsSync(file)) return { enrolled: false, pending: true, protection: { ok: false, reason: "external trust file is absent" }, trust: null, path: file };
|
|
332
335
|
if (lstatSync(file).isSymbolicLink()) throw new Error("ownership trust file may not be a symlink");
|
|
@@ -1,44 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"owners": [
|
|
3
|
-
{
|
|
4
|
-
"package": "@rightkit/build-control-broker",
|
|
5
|
-
"path": "packages/build-control-broker/package.json",
|
|
6
|
-
"reason": "Private build-control infrastructure; not an application capability"
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
"package": "@rightkit/build-control-clients",
|
|
10
|
-
"path": "packages/build-control-clients/package.json",
|
|
11
|
-
"reason": "Private build-control infrastructure; not an application capability"
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"package": "@rightkit/build-control-core",
|
|
15
|
-
"path": "packages/build-control-core/package.json",
|
|
16
|
-
"reason": "Private build-control infrastructure; not an application capability"
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"package": "@rightkit/build-control-phase0",
|
|
20
|
-
"path": "packages/build-control-phase0/package.json",
|
|
21
|
-
"reason": "Private build-control infrastructure; not an application capability"
|
|
22
|
-
},
|
|
23
3
|
{
|
|
24
4
|
"package": "@rightkit/build-control-policy",
|
|
25
5
|
"path": "packages/build-control-policy/package.json",
|
|
26
6
|
"reason": "Private build-control infrastructure; not an application capability"
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"package": "@rightkit/build-control-staging",
|
|
30
|
-
"path": "packages/build-control-staging/package.json",
|
|
31
|
-
"reason": "Private build-control infrastructure; not an application capability"
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"package": "@rightkit/build-control-phase0-macos",
|
|
35
|
-
"path": "packages/build-control-phase0/macos/package.json",
|
|
36
|
-
"reason": "Private native build-control feasibility fixture"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"package": "@rightkit/build-control-phase0-windows",
|
|
40
|
-
"path": "packages/build-control-phase0/windows/package.json",
|
|
41
|
-
"reason": "Private native build-control feasibility fixture"
|
|
42
7
|
}
|
|
43
8
|
]
|
|
44
9
|
}
|
|
@@ -7,6 +7,7 @@ import { spawnSync, spawnSyncInteractive, windowsPowerShellEnvironment } from ".
|
|
|
7
7
|
import { createKeyEvent, keyFingerprint, verifyKeyLog, verifyKeyRequest } from "./ownership-key-log.mjs";
|
|
8
8
|
import { canonicalRepositoryIdentity, DEFAULT_TRUST_PATH, loadTrustedExternal, resolveKeyLogTrust, __testOnly as approvalTestOnly } from "./ownership-approval.mjs";
|
|
9
9
|
import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
|
|
10
|
+
import { buildMacInvocation, delegateToDesktop, hasTerminal, needsDesktopDelegation } from "./ownership-key-elevation.mjs";
|
|
10
11
|
|
|
11
12
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const LOG = ".rightkit/ownership-keys.json";
|
|
@@ -258,7 +259,7 @@ function validateNative(out, expectedHost = platform) {
|
|
|
258
259
|
if (out.keyId !== `${expectedHost === "windows" ? "win" : "mac"}-${fingerprint}`) throw new Error("native helper key id does not match its public key");
|
|
259
260
|
return { keyId: fingerprint, publicKeyPem: out.publicKeyPem, platform: expectedHost };
|
|
260
261
|
}
|
|
261
|
-
function elevate(run, requestPath, digest, { modulePath = fileURLToPath(import.meta.url), nodeExe = process.execPath, hostPlatform = process.platform, audit = (target) => approvalTestOnly.protectedLocation(target, hostPlatform) } = {}) {
|
|
262
|
+
function elevate(run, requestPath, digest, { modulePath = fileURLToPath(import.meta.url), nodeExe = process.execPath, hostPlatform = process.platform, audit = (target) => approvalTestOnly.protectedLocation(target, hostPlatform), label, tty = hasTerminal() } = {}) {
|
|
262
263
|
let child, wrapper, registry, currentNode;
|
|
263
264
|
const refuse = (message) => { throw Object.assign(new Error(`RK_OWNERSHIP_ELEVATION_UNPROTECTED: ${message}`), { code: "RK_OWNERSHIP_ELEVATION_UNPROTECTED", type: "RK_OWNERSHIP_ELEVATION_UNPROTECTED" }); };
|
|
264
265
|
try {
|
|
@@ -302,9 +303,10 @@ function elevate(run, requestPath, digest, { modulePath = fileURLToPath(import.m
|
|
|
302
303
|
const r = run(invocation.parentExecutable, invocation.parentArgs, { env: windowsPowerShellEnvironment() });
|
|
303
304
|
if (r.status !== 0) throw new Error("elevated trust update failed or was cancelled");
|
|
304
305
|
} else {
|
|
306
|
+
const i = buildMacInvocation({ tty, nodeExe: currentNode, args, label });
|
|
305
307
|
let r;
|
|
306
|
-
try { r = run(
|
|
307
|
-
finally { try { run("/usr/bin/sudo", ["-K"], { stdio: "inherit" }); } catch {} }
|
|
308
|
+
try { r = run(i.command, i.args, i.options); }
|
|
309
|
+
finally { if (tty) { try { run("/usr/bin/sudo", ["-K"], { stdio: "inherit" }); } catch {} } }
|
|
308
310
|
if (r.status !== 0) throw new Error("elevated trust update failed or was cancelled");
|
|
309
311
|
}
|
|
310
312
|
}
|
|
@@ -483,6 +485,13 @@ async function resumeRootEnrollment(log, d) {
|
|
|
483
485
|
}
|
|
484
486
|
/** Advance this host's protected checkpoint to the committed key log (after another host approved or revoked a key). */
|
|
485
487
|
async function trustSync(_args, d) {
|
|
488
|
+
if (needsDesktopDelegation({ hostPlatform: d.hostPlatform, env: d.env, run: d.run, elevatedCheck: d.elevatedCheck })) {
|
|
489
|
+
const result = delegateToDesktop({ run: d.run, command: d.desktopCommand ?? [process.execPath, ...process.argv.slice(1)], ...(d.desktopOptions ?? {}) });
|
|
490
|
+
if (result.output) d.print(result.output.trimEnd());
|
|
491
|
+
if (result.status !== 0) throw new Error(`desktop-session trust-sync failed (exit ${result.status}); see output above`);
|
|
492
|
+
return 0;
|
|
493
|
+
}
|
|
494
|
+
d = { ...d, label: "RightKit ownership trust-sync" };
|
|
486
495
|
const log = readLog(d.root);
|
|
487
496
|
const pin = log.events[0]?.payload?.rootFingerprint;
|
|
488
497
|
const state = verifyKeyLog(log, { rootFingerprint: pin });
|
|
@@ -498,7 +507,7 @@ async function trustSync(_args, d) {
|
|
|
498
507
|
}
|
|
499
508
|
async function installCheckpoint(log, rootFingerprint, baseline, d) {
|
|
500
509
|
const request = makeTrustRequest(log, rootFingerprint, baseline, d.authority);
|
|
501
|
-
try { await (d.elevate ?? elevate)(d.run, request.file, request.digest); }
|
|
510
|
+
try { await (d.elevate ?? elevate)(d.run, request.file, request.digest, d.label ? { label: d.label } : undefined); }
|
|
502
511
|
finally { await import("node:fs").then(({ unlinkSync }) => unlinkSync(request.file)).catch(() => {}); }
|
|
503
512
|
}
|
|
504
513
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Launch helpers for the protected ownership trust update. These only change HOW the
|
|
2
|
+
// privileged step is authorized or started; the verified trust child itself is unchanged.
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { windowsPowerShellEnvironment } from "./background-process.mjs";
|
|
8
|
+
|
|
9
|
+
const POWERSHELL = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
10
|
+
const SCHTASKS = "C:\\Windows\\System32\\schtasks.exe";
|
|
11
|
+
export const DESKTOP_DELEGATED_ENV = "RK_OWNERSHIP_DESKTOP_DELEGATED";
|
|
12
|
+
export const DEFAULT_ELEVATION_LABEL = "RightKit ownership trust update";
|
|
13
|
+
|
|
14
|
+
// ---- macOS: system Authorization Services prompt via osascript -----------------------
|
|
15
|
+
// `do shell script ... with administrator privileges` shows the native auth dialog; the
|
|
16
|
+
// password goes to Authorization Services only and never transits this process or a script.
|
|
17
|
+
export function hasTerminal(stdin = process.stdin, stdout = process.stdout) {
|
|
18
|
+
return stdin?.isTTY === true && stdout?.isTTY === true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Strict POSIX single-quote escaping: the result is one shell word that equals `value` exactly. */
|
|
22
|
+
export function shellQuote(value) {
|
|
23
|
+
const text = String(value);
|
|
24
|
+
if (text.includes("\0")) throw new Error("shell argument contains a NUL byte");
|
|
25
|
+
return `'${text.replaceAll("'", "'\\''")}'`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Escape text for an AppleScript double-quoted string literal. */
|
|
29
|
+
export function appleScriptString(value) {
|
|
30
|
+
const text = String(value);
|
|
31
|
+
if (/[\r\n\0]/.test(text)) throw new Error("AppleScript string contains a line break or NUL byte");
|
|
32
|
+
return `"${text.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildMacShellCommand(nodeExe, args) {
|
|
36
|
+
return ["/usr/bin/env", "-u", "NODE_OPTIONS", "-u", "NODE_PATH", nodeExe, ...args].map(shellQuote).join(" ");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Choose the macOS launch. TTY keeps the unchanged sudo prompt; otherwise the native admin dialog via osascript. */
|
|
40
|
+
export function buildMacInvocation({ tty, nodeExe, args, label = DEFAULT_ELEVATION_LABEL, env = process.env }) {
|
|
41
|
+
const baseEnv = { ...env, NODE_OPTIONS: "", NODE_PATH: "" };
|
|
42
|
+
if (tty) return { mode: "tty", command: "/usr/bin/sudo", args: ["-k", nodeExe, ...args], options: { stdio: "inherit", env: baseEnv } };
|
|
43
|
+
const script = `do shell script ${appleScriptString(buildMacShellCommand(nodeExe, args))} with administrator privileges with prompt ${appleScriptString(label)}`;
|
|
44
|
+
return { mode: "dialog", command: "/usr/bin/osascript", args: ["-e", script], options: { stdio: ["ignore", "inherit", "inherit"], env: baseEnv } };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---- Windows: hand a non-interactive/elevated run to the logged-on desktop session -
|
|
48
|
+
export function isSshSession(env = process.env) {
|
|
49
|
+
return Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function windowsProcessIsElevated(run) {
|
|
53
|
+
const r = run(POWERSHELL, ["-NoProfile", "-NonInteractive", "-Command", "$p=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); if (!$p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 9 }"], { env: windowsPowerShellEnvironment() });
|
|
54
|
+
return r.status === 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function needsDesktopDelegation({ hostPlatform = process.platform, env = process.env, run, elevatedCheck = windowsProcessIsElevated }) {
|
|
58
|
+
if (hostPlatform !== "win32" || env[DESKTOP_DELEGATED_ENV] === "1") return false;
|
|
59
|
+
return isSshSession(env) || elevatedCheck(run) === true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const psq = (value) => `'${String(value).replaceAll("'", "''")}'`;
|
|
63
|
+
|
|
64
|
+
export function buildDesktopWrapper(command, logPath, codePath) {
|
|
65
|
+
const invocation = command.map(psq).join(" ");
|
|
66
|
+
return [
|
|
67
|
+
"$ErrorActionPreference='Continue'",
|
|
68
|
+
`$env:${DESKTOP_DELEGATED_ENV}='1'`,
|
|
69
|
+
"$c=1",
|
|
70
|
+
`try { & ${invocation} *>&1 | Tee-Object -FilePath ${psq(logPath)}; $c=$LASTEXITCODE } catch { $_ | Out-File -Append -FilePath ${psq(logPath)}; $c=1 }`,
|
|
71
|
+
`Set-Content -LiteralPath ${psq(codePath)} -Value $c`,
|
|
72
|
+
"exit $c",
|
|
73
|
+
"",
|
|
74
|
+
].join("\r\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildSchtasksCreate(taskName, wrapperPath, user) {
|
|
78
|
+
return [SCHTASKS, ["/Create", "/TN", taskName, "/TR", `"${POWERSHELL}" -NoProfile -ExecutionPolicy Bypass -File "${wrapperPath}"`, "/SC", "ONCE", "/ST", "00:00", "/RU", user, "/IT", "/RL", "LIMITED", "/F"]];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const USER_RE = /^[^\\/:*?"<>|\s][^\\/:*?"<>|]*\\[^\\/:*?"<>|]+$/;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run `command` once in the logged-on user's interactive, non-elevated desktop session via a
|
|
85
|
+
* one-shot scheduled task, wait for it, then clean up. Returns { status, output }.
|
|
86
|
+
*/
|
|
87
|
+
export function delegateToDesktop({ run, command, tmp = os.tmpdir(), sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms), now = Date.now, timeoutMs = 10 * 60 * 1000, pollMs = 1000, startGraceMs = 15000, suffix = randomBytes(6).toString("hex") }) {
|
|
88
|
+
const env = windowsPowerShellEnvironment();
|
|
89
|
+
const who = run(POWERSHELL, ["-NoProfile", "-NonInteractive", "-Command", "(Get-CimInstance -ClassName Win32_ComputerSystem).UserName"], { env, encoding: "utf8" });
|
|
90
|
+
const user = String(who.stdout ?? "").trim();
|
|
91
|
+
if (who.status !== 0 || !user) throw new Error("no interactive user is logged on to this Windows machine; sign in at the desktop and rerun trust-sync");
|
|
92
|
+
if (!USER_RE.test(user)) throw new Error("the logged-on desktop user name is not in a recognized DOMAIN\\user form");
|
|
93
|
+
const dir = mkdtempSync(path.join(tmp, "rk-desktop-"));
|
|
94
|
+
const logPath = path.join(dir, "output.log");
|
|
95
|
+
const codePath = path.join(dir, "exit-code.txt");
|
|
96
|
+
const wrapperPath = path.join(dir, "run.ps1");
|
|
97
|
+
const taskName = `RightKitOwnershipTrustSync-${suffix}`;
|
|
98
|
+
let created = false;
|
|
99
|
+
try {
|
|
100
|
+
writeFileSync(wrapperPath, buildDesktopWrapper(command, logPath, codePath), { flag: "wx", mode: 0o600 });
|
|
101
|
+
const [createCmd, createArgs] = buildSchtasksCreate(taskName, wrapperPath, user);
|
|
102
|
+
const c = run(createCmd, createArgs, { env, encoding: "utf8" });
|
|
103
|
+
if (c.status !== 0) throw new Error(`could not create the desktop-session task: ${String(c.stderr || c.stdout || "").trim()}`);
|
|
104
|
+
created = true;
|
|
105
|
+
const s = run(SCHTASKS, ["/Run", "/TN", taskName], { env, encoding: "utf8" });
|
|
106
|
+
if (s.status !== 0) throw new Error(`could not start the desktop-session task: ${String(s.stderr || s.stdout || "").trim()}`);
|
|
107
|
+
const started = now();
|
|
108
|
+
for (;;) {
|
|
109
|
+
if (existsSync(codePath)) break;
|
|
110
|
+
const elapsed = now() - started;
|
|
111
|
+
if (elapsed > timeoutMs) throw new Error("timed out waiting for the desktop-session trust-sync (UAC prompt not answered?)");
|
|
112
|
+
const q = run(SCHTASKS, ["/Query", "/TN", taskName, "/FO", "LIST", "/V"], { env, encoding: "utf8" });
|
|
113
|
+
const running = /^\s*Status:\s*Running\s*$/im.test(String(q.stdout ?? ""));
|
|
114
|
+
if (!running && elapsed > startGraceMs && !existsSync(codePath)) throw new Error("the desktop-session task ended without reporting a result; check that the logged-on user is the same account");
|
|
115
|
+
sleep(pollMs);
|
|
116
|
+
}
|
|
117
|
+
const status = Number.parseInt(readFileSync(codePath, "utf8").replace(/^\uFEFF/, "").trim(), 10);
|
|
118
|
+
const output = existsSync(logPath) ? readFileSync(logPath, "utf8") : "";
|
|
119
|
+
return { status: Number.isInteger(status) ? status : 1, output, user };
|
|
120
|
+
} finally {
|
|
121
|
+
if (created) { try { run(SCHTASKS, ["/Delete", "/TN", taskName, "/F"], { env, encoding: "utf8" }); } catch {} }
|
|
122
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
{ "repositoryIdentity": "https://github.com/orthic-labs/cockpit", "roots": ["."], "visibility": "public" },
|
|
15
15
|
{ "repositoryIdentity": "https://github.com/orthic-labs/legion", "roots": ["."], "visibility": "public" },
|
|
16
16
|
{ "repositoryIdentity": "https://github.com/bogusyogi/workright", "roots": ["."], "visibility": "private" },
|
|
17
|
-
{ "repositoryIdentity": "https://github.com/orthic-labs/sellright", "roots": ["."], "visibility": "public" }
|
|
17
|
+
{ "repositoryIdentity": "https://github.com/orthic-labs/sellright", "roots": ["."], "visibility": "public" },
|
|
18
|
+
{ "repositoryIdentity": "https://github.com/orthic-labs/pulse", "roots": ["."], "visibility": "public" },
|
|
19
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/swiftright", "roots": ["."], "visibility": "private" }
|
|
18
20
|
]
|
|
19
21
|
}
|
package/ownership.mjs
CHANGED
|
@@ -192,7 +192,7 @@ function fileInventory(appRoot, trust, pathPrefix = "", repositoryRoot = appRoot
|
|
|
192
192
|
const inventory = gitListedInventory(appRoot, {
|
|
193
193
|
repositoryRoot,
|
|
194
194
|
protectedPaths: [...protectedPaths],
|
|
195
|
-
relevant: (rel) => ["Cargo.toml", "package.json", "Package.swift", ...OWNERSHIP_LOCKFILES].includes(path.posix.basename(rel)) || CODE_EXT.has(path.posix.extname(rel).toLowerCase()),
|
|
195
|
+
relevant: (rel) => ["Cargo.toml", "package.json", "Package.swift", "Package.resolved", ...OWNERSHIP_LOCKFILES].includes(path.posix.basename(rel)) || CODE_EXT.has(path.posix.extname(rel).toLowerCase()),
|
|
196
196
|
maxFileBytes: (rel) => CODE_EXT.has(path.posix.extname(rel).toLowerCase()) || protectedPaths.has(rel) ? 2 * 1024 * 1024 : 64 * 1024 * 1024,
|
|
197
197
|
});
|
|
198
198
|
for (const item of inventory.coverage) if (item.reason === "file-over-size-limit") item.reason = protectedPaths.has(item.path) ? "protected-provenance-file-over-2MiB" : CODE_EXT.has(path.posix.extname(item.path).toLowerCase()) ? "source-file-over-2MiB" : "manifest-file-over-64MiB";
|
|
@@ -212,7 +212,7 @@ function committedFileInventory(appRoot, trust, pathPrefix, committed) {
|
|
|
212
212
|
if (entry.mode === "120000") { coverage.push({ path: rel, status: "unscanned", reason: "committed-symlink" }); continue; }
|
|
213
213
|
if (entry.mode === "160000" || entry.type === "commit") { coverage.push({ path: rel, status: "unscanned", reason: "committed-gitlink" }); continue; }
|
|
214
214
|
if (entry.type !== "blob") { coverage.push({ path: rel, status: "unscanned", reason: "committed-unsupported-entry" }); continue; }
|
|
215
|
-
const relevant = protectedPaths.has(entry.rel) || ["Cargo.toml", "package.json", "Package.swift", "ownership-exceptions.json", ...OWNERSHIP_LOCKFILES].includes(path.posix.basename(rel)) || CODE_EXT.has(path.posix.extname(rel).toLowerCase());
|
|
215
|
+
const relevant = protectedPaths.has(entry.rel) || ["Cargo.toml", "package.json", "Package.swift", "Package.resolved", "ownership-exceptions.json", ...OWNERSHIP_LOCKFILES].includes(path.posix.basename(rel)) || CODE_EXT.has(path.posix.extname(rel).toLowerCase());
|
|
216
216
|
const bytes = relevant ? entry.bytes : undefined;
|
|
217
217
|
if (relevant && !bytes) coverage.push({ path: rel, status: "unscanned", reason: "committed-blob-unavailable" });
|
|
218
218
|
const isExceptionFile = path.posix.basename(rel) === "ownership-exceptions.json";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.138",
|
|
4
4
|
"description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/bogusyogi/
|
|
27
|
-
"directory": "
|
|
26
|
+
"url": "git+https://github.com/bogusyogi/rightkit.git",
|
|
27
|
+
"directory": "packages/release"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"smol-toml": "1.7.0"
|
package/pe-imports.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Minimal PE import reader: names of DLLs a Windows binary loads (regular + delay-load).
|
|
2
|
+
// Pure Buffer parsing so dependency closure checks run on any host.
|
|
3
|
+
|
|
4
|
+
export const PE_MACHINES = Object.freeze({ 0x8664: "x64", 0xaa64: "arm64", 0x014c: "x86" });
|
|
5
|
+
|
|
6
|
+
function fail(message) { throw Object.assign(new Error(message), { code: "PE_INVALID" }); }
|
|
7
|
+
|
|
8
|
+
export function readPeImports(bytes) {
|
|
9
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
10
|
+
if (buf.length < 0x40 || buf.readUInt16LE(0) !== 0x5a4d) fail("not a PE file (missing MZ)");
|
|
11
|
+
const pe = buf.readUInt32LE(0x3c);
|
|
12
|
+
if (pe + 24 > buf.length || buf.readUInt32LE(pe) !== 0x00004550) fail("not a PE file (missing PE signature)");
|
|
13
|
+
const machine = buf.readUInt16LE(pe + 4);
|
|
14
|
+
const sectionCount = buf.readUInt16LE(pe + 6);
|
|
15
|
+
const optionalSize = buf.readUInt16LE(pe + 20);
|
|
16
|
+
const optional = pe + 24;
|
|
17
|
+
const magic = buf.readUInt16LE(optional);
|
|
18
|
+
if (magic !== 0x20b && magic !== 0x10b) fail("unsupported PE optional header");
|
|
19
|
+
const directories = optional + (magic === 0x20b ? 112 : 96);
|
|
20
|
+
const sectionsAt = optional + optionalSize;
|
|
21
|
+
const sections = [];
|
|
22
|
+
for (let i = 0; i < sectionCount; i++) {
|
|
23
|
+
const at = sectionsAt + i * 40;
|
|
24
|
+
if (at + 40 > buf.length) fail("truncated PE section table");
|
|
25
|
+
sections.push({ virtualSize: buf.readUInt32LE(at + 8), rva: buf.readUInt32LE(at + 12), rawSize: buf.readUInt32LE(at + 16), rawPtr: buf.readUInt32LE(at + 20) });
|
|
26
|
+
}
|
|
27
|
+
const toOffset = (rva) => {
|
|
28
|
+
for (const s of sections) if (rva >= s.rva && rva < s.rva + Math.max(s.virtualSize, s.rawSize)) return s.rawPtr + (rva - s.rva);
|
|
29
|
+
fail(`RVA 0x${rva.toString(16)} is outside every section`);
|
|
30
|
+
};
|
|
31
|
+
const cstring = (rva) => {
|
|
32
|
+
const start = toOffset(rva);
|
|
33
|
+
const end = buf.indexOf(0, start);
|
|
34
|
+
if (end < 0) fail("unterminated import name");
|
|
35
|
+
return buf.toString("latin1", start, end);
|
|
36
|
+
};
|
|
37
|
+
const directory = (index) => {
|
|
38
|
+
const at = directories + index * 8;
|
|
39
|
+
return at + 8 <= buf.length ? { rva: buf.readUInt32LE(at), size: buf.readUInt32LE(at + 4) } : { rva: 0, size: 0 };
|
|
40
|
+
};
|
|
41
|
+
const imports = new Set(), delayImports = new Set();
|
|
42
|
+
const table = directory(1);
|
|
43
|
+
if (table.rva) {
|
|
44
|
+
for (let at = toOffset(table.rva); at + 20 <= buf.length; at += 20) {
|
|
45
|
+
const name = buf.readUInt32LE(at + 12);
|
|
46
|
+
if (!name && !buf.readUInt32LE(at) && !buf.readUInt32LE(at + 16)) break;
|
|
47
|
+
imports.add(cstring(name));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const delay = directory(13);
|
|
51
|
+
if (delay.rva) {
|
|
52
|
+
for (let at = toOffset(delay.rva); at + 32 <= buf.length; at += 32) {
|
|
53
|
+
const attrs = buf.readUInt32LE(at), name = buf.readUInt32LE(at + 4);
|
|
54
|
+
if (!attrs && !name) break;
|
|
55
|
+
delayImports.add(cstring(name));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { machine, architecture: PE_MACHINES[machine] ?? `0x${machine.toString(16)}`, imports: [...imports], delayImports: [...delayImports] };
|
|
59
|
+
}
|
package/rightkit-ownership.json
CHANGED
|
@@ -2348,45 +2348,10 @@
|
|
|
2348
2348
|
}
|
|
2349
2349
|
],
|
|
2350
2350
|
"internalOwners": [
|
|
2351
|
-
{
|
|
2352
|
-
"package": "@rightkit/build-control-broker",
|
|
2353
|
-
"path": "packages/build-control-broker/package.json",
|
|
2354
|
-
"reason": "Private build-control infrastructure; not an application capability"
|
|
2355
|
-
},
|
|
2356
|
-
{
|
|
2357
|
-
"package": "@rightkit/build-control-clients",
|
|
2358
|
-
"path": "packages/build-control-clients/package.json",
|
|
2359
|
-
"reason": "Private build-control infrastructure; not an application capability"
|
|
2360
|
-
},
|
|
2361
|
-
{
|
|
2362
|
-
"package": "@rightkit/build-control-core",
|
|
2363
|
-
"path": "packages/build-control-core/package.json",
|
|
2364
|
-
"reason": "Private build-control infrastructure; not an application capability"
|
|
2365
|
-
},
|
|
2366
|
-
{
|
|
2367
|
-
"package": "@rightkit/build-control-phase0",
|
|
2368
|
-
"path": "packages/build-control-phase0/package.json",
|
|
2369
|
-
"reason": "Private build-control infrastructure; not an application capability"
|
|
2370
|
-
},
|
|
2371
2351
|
{
|
|
2372
2352
|
"package": "@rightkit/build-control-policy",
|
|
2373
2353
|
"path": "packages/build-control-policy/package.json",
|
|
2374
2354
|
"reason": "Private build-control infrastructure; not an application capability"
|
|
2375
|
-
},
|
|
2376
|
-
{
|
|
2377
|
-
"package": "@rightkit/build-control-staging",
|
|
2378
|
-
"path": "packages/build-control-staging/package.json",
|
|
2379
|
-
"reason": "Private build-control infrastructure; not an application capability"
|
|
2380
|
-
},
|
|
2381
|
-
{
|
|
2382
|
-
"package": "@rightkit/build-control-phase0-macos",
|
|
2383
|
-
"path": "packages/build-control-phase0/macos/package.json",
|
|
2384
|
-
"reason": "Private native build-control feasibility fixture"
|
|
2385
|
-
},
|
|
2386
|
-
{
|
|
2387
|
-
"package": "@rightkit/build-control-phase0-windows",
|
|
2388
|
-
"path": "packages/build-control-phase0/windows/package.json",
|
|
2389
|
-
"reason": "Private native build-control feasibility fixture"
|
|
2390
2355
|
}
|
|
2391
2356
|
]
|
|
2392
2357
|
}
|
package/rightkit-sync.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const rightkitRoot = path.resolve(packageRoot, "..", "..");
|
|
12
12
|
const VERSIONS_FILE = path.join(packageRoot, "rightkit-versions.json");
|
|
13
|
-
const USER_AGENT = "rightkit-sync/1 (https://github.com/bogusyogi/
|
|
13
|
+
const USER_AGENT = "rightkit-sync/1 (https://github.com/bogusyogi/rightkit)";
|
|
14
14
|
|
|
15
15
|
const field = (toml, key) => toml.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"))?.[1] ?? null;
|
|
16
16
|
|
|
@@ -178,12 +178,10 @@ export function recordPublished({ registry, name, version }, { file = VERSIONS_F
|
|
|
178
178
|
} catch (error) { log(`[right-release] release feed not updated (${error.message}); run node rightkit-sync.mjs`); }
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
/** The
|
|
181
|
+
/** The session hook carries byte copies of the catalog and versions; keep them exact. */
|
|
182
182
|
export function syncConsumerCopies(root = rightkitRoot) {
|
|
183
183
|
const release = path.join(root, "packages", "release");
|
|
184
184
|
const targets = [
|
|
185
|
-
["rightkit-ownership.json", path.join(root, "packages", "build-control-clients", "src", "ownership-catalog.json")],
|
|
186
|
-
["rightkit-versions.json", path.join(root, "packages", "build-control-clients", "src", "rightkit-versions.json")],
|
|
187
185
|
["rightkit-ownership.json", path.join(root, "packages", "build-control-policy", "src", "ownership-session-catalog.json")],
|
|
188
186
|
["rightkit-ownership.json", path.join(root, "packages", "build-control-policy", "src", "ownership-fingerprints.json")],
|
|
189
187
|
["rightkit-versions.json", path.join(root, "packages", "build-control-policy", "src", "ownership-session-versions.json")],
|
|
@@ -191,7 +189,6 @@ export function syncConsumerCopies(root = rightkitRoot) {
|
|
|
191
189
|
["background-process.mjs", path.join(root, "packages", "build-control-policy", "src", "background-process.mjs")],
|
|
192
190
|
["ownership-source.mjs", path.join(root, "packages", "build-control-policy", "src", "ownership-source.mjs")],
|
|
193
191
|
["ownership-availability.mjs", path.join(root, "packages", "build-control-policy", "src", "ownership-availability.mjs")],
|
|
194
|
-
["ownership-availability.mjs", path.join(root, "packages", "build-control-clients", "src", "ownership-availability.mjs")],
|
|
195
192
|
];
|
|
196
193
|
const updated = [];
|
|
197
194
|
for (const [source, target] of targets) {
|
package/rightkit-versions.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"@rightkit/app-shell": "0.3.0",
|
|
17
17
|
"@rightkit/ax": "0.2.4",
|
|
18
18
|
"@rightkit/doc-render": "0.1.4",
|
|
19
|
-
"@rightkit/git": "0.2.
|
|
19
|
+
"@rightkit/git": "0.2.31",
|
|
20
20
|
"@rightkit/hooks": "0.1.2",
|
|
21
21
|
"@rightkit/i18n": "0.1.0",
|
|
22
22
|
"@rightkit/legal": "0.3.1",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@rightkit/platform-ui": "0.2.0",
|
|
30
30
|
"@rightkit/qa": "0.4.0",
|
|
31
31
|
"@rightkit/qa-chrome": "0.1.3",
|
|
32
|
-
"@rightkit/release": "0.2.
|
|
32
|
+
"@rightkit/release": "0.2.138",
|
|
33
33
|
"@rightkit/settings-ui": "0.3.0",
|
|
34
34
|
"@rightkit/shell": "0.1.0",
|
|
35
35
|
"@rightkit/tauri": "0.2.0",
|
|
@@ -71,7 +71,8 @@
|
|
|
71
71
|
"0.2.26",
|
|
72
72
|
"0.2.27",
|
|
73
73
|
"0.2.28",
|
|
74
|
-
"0.2.29"
|
|
74
|
+
"0.2.29",
|
|
75
|
+
"0.2.30"
|
|
75
76
|
],
|
|
76
77
|
"@rightkit/legal-ui": [
|
|
77
78
|
"0.1.0",
|
|
@@ -166,7 +167,8 @@
|
|
|
166
167
|
"0.2.133",
|
|
167
168
|
"0.2.134",
|
|
168
169
|
"0.2.135",
|
|
169
|
-
"0.2.136"
|
|
170
|
+
"0.2.136",
|
|
171
|
+
"0.2.137"
|
|
170
172
|
],
|
|
171
173
|
"@rightkit/qa": [
|
|
172
174
|
"0.1.0",
|
|
@@ -283,7 +285,7 @@
|
|
|
283
285
|
"rightkit-oauth": "0.3.0",
|
|
284
286
|
"rightkit-ocr": "0.1.2",
|
|
285
287
|
"rightkit-ort": "0.2.2",
|
|
286
|
-
"rightkit-ownership": "0.2.
|
|
288
|
+
"rightkit-ownership": "0.2.1",
|
|
287
289
|
"rightkit-pdf": "0.2.0",
|
|
288
290
|
"rightkit-playback": "0.1.1",
|
|
289
291
|
"rightkit-process": "0.3.3",
|
|
@@ -340,7 +342,7 @@
|
|
|
340
342
|
"rightkit-oauth": "0.3.0",
|
|
341
343
|
"rightkit-ocr": "0.1.2",
|
|
342
344
|
"rightkit-ort": "0.2.2",
|
|
343
|
-
"rightkit-ownership": "0.2.
|
|
345
|
+
"rightkit-ownership": "0.2.1",
|
|
344
346
|
"rightkit-pdf": "0.2.0",
|
|
345
347
|
"rightkit-playback": "0.1.1",
|
|
346
348
|
"rightkit-process": "0.3.3",
|
|
@@ -365,7 +367,7 @@
|
|
|
365
367
|
"@rightkit/app-shell": "0.3.0",
|
|
366
368
|
"@rightkit/ax": "0.2.4",
|
|
367
369
|
"@rightkit/doc-render": "0.1.4",
|
|
368
|
-
"@rightkit/git": "0.2.
|
|
370
|
+
"@rightkit/git": "0.2.31",
|
|
369
371
|
"@rightkit/hooks": "0.1.2",
|
|
370
372
|
"@rightkit/i18n": "0.1.0",
|
|
371
373
|
"@rightkit/legal": "0.3.1",
|
|
@@ -378,7 +380,7 @@
|
|
|
378
380
|
"@rightkit/platform-ui": "0.2.0",
|
|
379
381
|
"@rightkit/qa": "0.4.0",
|
|
380
382
|
"@rightkit/qa-chrome": "0.1.3",
|
|
381
|
-
"@rightkit/release": "0.2.
|
|
383
|
+
"@rightkit/release": "0.2.138",
|
|
382
384
|
"@rightkit/settings-ui": "0.3.0",
|
|
383
385
|
"@rightkit/shell": "0.1.0",
|
|
384
386
|
"@rightkit/tauri": "0.2.0",
|
|
@@ -387,6 +389,9 @@
|
|
|
387
389
|
}
|
|
388
390
|
},
|
|
389
391
|
"legacyCargo": {
|
|
392
|
+
"rightkit-ownership": [
|
|
393
|
+
"0.2.0"
|
|
394
|
+
],
|
|
390
395
|
"rightkit-browser": [
|
|
391
396
|
"0.2.0",
|
|
392
397
|
"0.2.1",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Assemble a macOS .app bundle from a SwiftPM executable, then sign (and
|
|
2
|
+
// optionally notarize) it through the existing RightKit signers. Signing is
|
|
3
|
+
// inside-out: nested code first (no entitlements), then the bundle. This module
|
|
4
|
+
// owns bundle assembly only; sign-macos.mjs and notarize-macos.mjs own signing.
|
|
5
|
+
|
|
6
|
+
import { cpSync, existsSync, mkdirSync, openSync, readSync, closeSync, readdirSync, rmSync, statSync, writeFileSync, lstatSync, chmodSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { spawnSync } from "./background-process.mjs";
|
|
10
|
+
|
|
11
|
+
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
12
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const xml = (v) => String(v).replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c]);
|
|
14
|
+
|
|
15
|
+
function plistValue(value, indent) {
|
|
16
|
+
const pad = "\t".repeat(indent);
|
|
17
|
+
if (typeof value === "string") return `${pad}<string>${xml(value)}</string>`;
|
|
18
|
+
if (typeof value === "boolean") return `${pad}<${value}/>`;
|
|
19
|
+
if (Number.isInteger(value)) return `${pad}<integer>${value}</integer>`;
|
|
20
|
+
if (typeof value === "number") return `${pad}<real>${value}</real>`;
|
|
21
|
+
if (Array.isArray(value)) return `${pad}<array>\n${value.map((v) => plistValue(v, indent + 1)).join("\n")}\n${pad}</array>`;
|
|
22
|
+
if (value && typeof value === "object") {
|
|
23
|
+
const rows = Object.keys(value).sort().map((k) => `${pad}\t<key>${xml(k)}</key>\n${plistValue(value[k], indent + 1)}`);
|
|
24
|
+
return `${pad}<dict>\n${rows.join("\n")}\n${pad}</dict>`;
|
|
25
|
+
}
|
|
26
|
+
throw fail("MAC_APP_PLIST", `unsupported plist value: ${value}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function generateInfoPlist(o) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(o.name ?? "")) errors.push("name must be a simple bundle name");
|
|
32
|
+
if (!/^[A-Za-z0-9.-]+\.[A-Za-z0-9.-]+$/.test(o.bundleId ?? "")) errors.push("bundleId must be reverse-DNS");
|
|
33
|
+
if (!/^\d+(\.\d+){0,2}$/.test(o.version ?? "")) errors.push("version must be 1-3 numeric parts");
|
|
34
|
+
if (!/^\d+(\.\d+){0,2}$/.test(o.build ?? o.version ?? "")) errors.push("build must be 1-3 numeric parts");
|
|
35
|
+
if (!o.executable || /[\\/]/.test(o.executable)) errors.push("executable must be a flat file name");
|
|
36
|
+
if (errors.length) throw fail("MAC_APP_INVALID", errors.join("; "));
|
|
37
|
+
const info = {
|
|
38
|
+
CFBundleDevelopmentRegion: "en", CFBundleExecutable: o.executable, CFBundleIdentifier: o.bundleId,
|
|
39
|
+
CFBundleInfoDictionaryVersion: "6.0", CFBundleName: o.name, CFBundleDisplayName: o.displayName ?? o.name,
|
|
40
|
+
CFBundlePackageType: "APPL", CFBundleShortVersionString: o.version, CFBundleVersion: o.build ?? o.version,
|
|
41
|
+
LSMinimumSystemVersion: o.minimumSystemVersion ?? "14.0", NSHighResolutionCapable: true, NSPrincipalClass: "NSApplication",
|
|
42
|
+
...(o.iconFile ? { CFBundleIconFile: path.basename(o.iconFile) } : {}),
|
|
43
|
+
...(o.infoPlistExtra ?? {}),
|
|
44
|
+
};
|
|
45
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n${plistValue(info, 0)}\n</plist>\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const MACHO = new Set([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe, 0xbebafeca]);
|
|
49
|
+
function isMachO(file) {
|
|
50
|
+
const fd = openSync(file, "r"); const b = Buffer.alloc(4);
|
|
51
|
+
try { return readSync(fd, b, 0, 4, 0) === 4 && MACHO.has(b.readUInt32BE(0)); } finally { closeSync(fd); }
|
|
52
|
+
}
|
|
53
|
+
const BUNDLE_DIR = /\.(framework|app|xpc|bundle|appex)$/;
|
|
54
|
+
|
|
55
|
+
/** Nested code to sign before the app: Mach-O files and code bundles, deepest first. Main executable excluded. */
|
|
56
|
+
export function discoverNestedCode(app, mainName) {
|
|
57
|
+
const main = path.join(app, "Contents", "MacOS");
|
|
58
|
+
const found = [];
|
|
59
|
+
const walk = (dir) => {
|
|
60
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
61
|
+
const full = path.join(dir, entry.name);
|
|
62
|
+
if (entry.isSymbolicLink()) continue;
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
walk(full);
|
|
65
|
+
if (/\.framework$/.test(entry.name) || (BUNDLE_DIR.test(entry.name) && existsSync(path.join(full, "Contents")))) found.push(full);
|
|
66
|
+
} else if (entry.isFile() && full !== main && !full.startsWith(main + path.sep) && isMachO(full)) found.push(full);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
for (const sub of ["Contents/Frameworks", "Contents/Resources", "Contents/PlugIns", "Contents/XPCServices", "Contents/Helpers", "Contents/Library"]) {
|
|
70
|
+
const dir = path.join(app, sub);
|
|
71
|
+
if (existsSync(dir)) walk(dir);
|
|
72
|
+
}
|
|
73
|
+
const mainDir = path.join(app, "Contents", "MacOS");
|
|
74
|
+
for (const entry of readdirSync(mainDir)) { /* helpers beside main: sign them too */
|
|
75
|
+
const full = path.join(mainDir, entry);
|
|
76
|
+
if (entry !== mainName && lstatSync(full).isFile() && isMachO(full)) found.push(full);
|
|
77
|
+
}
|
|
78
|
+
const depth = (p) => p.split(path.sep).length;
|
|
79
|
+
return [...new Set(found)].sort((a, b) => depth(b) - depth(a) || a.localeCompare(b));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Assemble `<out>/<name>.app`. options: { executable (SwiftPM binary), name, bundleId, version, build,
|
|
84
|
+
* resources[] (files/dirs, e.g. SwiftPM *.bundle, copied into Contents/Resources), frameworks[] (dylibs/.frameworks
|
|
85
|
+
* copied into Contents/Frameworks), iconFile, infoPlistExtra, out }.
|
|
86
|
+
*/
|
|
87
|
+
export function assembleMacApp(o, { run = spawnSync, platform = process.platform } = {}) {
|
|
88
|
+
const exe = path.resolve(o.executable ?? "");
|
|
89
|
+
if (!existsSync(exe) || !statSync(exe).isFile()) throw fail("MAC_APP_INVALID", `executable not found: ${exe}`);
|
|
90
|
+
const execName = o.executableName ?? path.basename(exe);
|
|
91
|
+
const app = path.join(path.resolve(o.out), `${o.name}.app`);
|
|
92
|
+
const plist = generateInfoPlist({ ...o, executable: execName });
|
|
93
|
+
rmSync(app, { recursive: true, force: true });
|
|
94
|
+
const contents = path.join(app, "Contents");
|
|
95
|
+
for (const dir of ["MacOS", "Resources", ...(o.frameworks?.length ? ["Frameworks"] : [])]) mkdirSync(path.join(contents, dir), { recursive: true });
|
|
96
|
+
cpSync(exe, path.join(contents, "MacOS", execName)); chmodSync(path.join(contents, "MacOS", execName), 0o755);
|
|
97
|
+
const copyInto = (list, sub) => {
|
|
98
|
+
for (const item of list ?? []) {
|
|
99
|
+
const source = path.resolve(item);
|
|
100
|
+
if (!existsSync(source)) throw fail("MAC_APP_INVALID", `missing input: ${source}`);
|
|
101
|
+
cpSync(source, path.join(contents, sub, path.basename(source)), { recursive: true, verbatimSymlinks: true });
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
copyInto(o.resources, "Resources"); copyInto(o.frameworks, "Frameworks");
|
|
105
|
+
if (o.iconFile) copyInto([o.iconFile], "Resources");
|
|
106
|
+
writeFileSync(path.join(contents, "Info.plist"), plist);
|
|
107
|
+
writeFileSync(path.join(contents, "PkgInfo"), "APPL????");
|
|
108
|
+
if (o.frameworks?.length && platform === "darwin") {
|
|
109
|
+
// SwiftPM binaries do not search Contents/Frameworks by default.
|
|
110
|
+
const probe = run("otool", ["-l", path.join(contents, "MacOS", execName)], { encoding: "utf8" });
|
|
111
|
+
if (!probe.error && !/@executable_path\/\.\.\/Frameworks/.test(probe.stdout ?? "")) {
|
|
112
|
+
const r = run("install_name_tool", ["-add_rpath", "@executable_path/../Frameworks", path.join(contents, "MacOS", execName)], { encoding: "utf8" });
|
|
113
|
+
if (r.error || r.status !== 0) throw fail("MAC_APP_RPATH", `install_name_tool failed: ${r.error?.message ?? r.stderr}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { app, nested: discoverNestedCode(app, execName), executable: execName };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Sign inside-out through sign-macos.mjs, then optionally notarize via notarize-macos.mjs. */
|
|
120
|
+
export function signAndNotarizeMacApp({ app, nested, entitlements, receiptDir, notarize = false, profile, dryRun = false },
|
|
121
|
+
{ run = spawnSync, platform = process.platform, log = (e) => console.error(JSON.stringify(e)) } = {}) {
|
|
122
|
+
const plan = [];
|
|
123
|
+
const node = (script, args) => { plan.push([script, ...args]); return args; };
|
|
124
|
+
const receipts = path.resolve(receiptDir);
|
|
125
|
+
const dry = dryRun ? ["--dry-run"] : [];
|
|
126
|
+
if (!dryRun && platform !== "darwin") throw fail("MAC_APP_NATIVE_HOST_REQUIRED", "macOS signing and notarization must run on macOS");
|
|
127
|
+
const calls = [];
|
|
128
|
+
if (nested.length) calls.push(["sign-macos.mjs", [...dry, "--receipt", path.join(receipts, "app-nested-sign.receipt.json"), ...nested]]);
|
|
129
|
+
calls.push(["sign-macos.mjs", [...dry, "--receipt", path.join(receipts, "app-sign.receipt.json"), ...(entitlements ? ["--entitlements", path.resolve(entitlements)] : []), app]]);
|
|
130
|
+
let zip;
|
|
131
|
+
if (notarize) {
|
|
132
|
+
zip = `${app}.zip`;
|
|
133
|
+
calls.push(["__ditto__", ["-c", "-k", "--keepParent", app, zip]]);
|
|
134
|
+
calls.push(["notarize-macos.mjs", [...dry, "--receipt", path.join(receipts, "app-notarize.receipt.json"), ...(profile ? ["--profile", profile] : []), "--app", app, zip]]);
|
|
135
|
+
}
|
|
136
|
+
if (!dryRun) mkdirSync(receipts, { recursive: true });
|
|
137
|
+
for (const [script, args] of calls) {
|
|
138
|
+
node(script, args);
|
|
139
|
+
if (dryRun && script === "__ditto__") continue;
|
|
140
|
+
const r = script === "__ditto__" ? run("/usr/bin/ditto", args, { encoding: "utf8" })
|
|
141
|
+
: run(process.execPath, [path.join(here, script), ...args], { encoding: "utf8" });
|
|
142
|
+
if (r.error || r.status !== 0) throw fail("MAC_APP_COMMAND_FAILED", `${script} failed: ${r.error?.message ?? r.stderr ?? r.stdout ?? r.status}`);
|
|
143
|
+
log({ event: "mac-app-step", script, ok: true });
|
|
144
|
+
}
|
|
145
|
+
return { plan, zip };
|
|
146
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// right-release swift-package <windows-stage|msix|mac-app|doctor> --config <swift-package.json> [options]
|
|
3
|
+
//
|
|
4
|
+
// One product config (schema 1) drives all three steps; paths resolve against the config file.
|
|
5
|
+
// Build-only: nothing here uploads or publishes.
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { assembleMacApp, signAndNotarizeMacApp } from "./swift-mac-app.mjs";
|
|
10
|
+
import { buildMsix } from "./msix-build.mjs";
|
|
11
|
+
import { stageSwiftWindows } from "./swift-windows-stage.mjs";
|
|
12
|
+
|
|
13
|
+
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
14
|
+
|
|
15
|
+
export function loadSwiftPackageConfig(file) {
|
|
16
|
+
const configPath = path.resolve(file ?? "");
|
|
17
|
+
if (!existsSync(configPath)) throw fail("SWIFT_PACKAGE_CONFIG", `config not found: ${configPath}`);
|
|
18
|
+
let config;
|
|
19
|
+
try { config = JSON.parse(readFileSync(configPath, "utf8")); } catch (e) { throw fail("SWIFT_PACKAGE_CONFIG", `invalid JSON: ${e.message}`); }
|
|
20
|
+
if (config.schema !== 1) throw fail("SWIFT_PACKAGE_CONFIG", "config.schema must be 1");
|
|
21
|
+
if (!config.name || !config.version) throw fail("SWIFT_PACKAGE_CONFIG", "config.name and config.version are required");
|
|
22
|
+
return { config, base: path.dirname(configPath) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const rel = (base, value) => (value == null ? value : path.resolve(base, value));
|
|
26
|
+
const relList = (base, list) => (list ?? []).map((v) => rel(base, v));
|
|
27
|
+
|
|
28
|
+
export function windowsStageOptions({ config, base }) {
|
|
29
|
+
const w = config.windows;
|
|
30
|
+
if (!w?.exe || !w?.out) throw fail("SWIFT_PACKAGE_CONFIG", "windows.exe and windows.out are required");
|
|
31
|
+
return { exe: rel(base, w.exe), out: rel(base, w.stageDir ?? `${w.out}.stage`), searchDirs: relList(base, w.searchDirs),
|
|
32
|
+
include: relList(base, w.include), systemDlls: w.systemDlls, dependencies: w.dependencies, architecture: w.architecture };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function msixOptions({ config, base }, staged) {
|
|
36
|
+
const w = config.windows;
|
|
37
|
+
const manifest = { name: w.packageName ?? config.name, version: w.msixVersion ?? `${config.version.replace(/^(\d+\.\d+\.\d+).*$/, "$1")}.0`,
|
|
38
|
+
architecture: staged.architecture, publisher: w.publisher, publisherDisplayName: w.publisherDisplayName,
|
|
39
|
+
displayName: config.displayName ?? config.name, description: config.description, executable: staged.exe,
|
|
40
|
+
dependencies: w.dependencies, capabilities: w.capabilities, minWindowsVersion: w.minWindowsVersion };
|
|
41
|
+
const assets = Object.fromEntries(Object.entries(w.assets ?? {}).map(([k, v]) => [k, rel(base, v)]));
|
|
42
|
+
return { manifest, assets, out: rel(base, w.out), stageDir: staged.out };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function macOptions({ config, base }) {
|
|
46
|
+
const m = config.mac;
|
|
47
|
+
if (!m?.executable || !m?.bundleId || !m?.out) throw fail("SWIFT_PACKAGE_CONFIG", "mac.executable, mac.bundleId and mac.out are required");
|
|
48
|
+
return { executable: rel(base, m.executable), name: m.appName ?? config.displayName ?? config.name, displayName: config.displayName, bundleId: m.bundleId,
|
|
49
|
+
version: config.version, build: m.build, minimumSystemVersion: m.minimumSystemVersion, resources: relList(base, m.resources),
|
|
50
|
+
frameworks: relList(base, m.frameworks), iconFile: rel(base, m.iconFile), infoPlistExtra: m.infoPlist, out: rel(base, m.out) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function main(argv, deps = {}) {
|
|
54
|
+
const [command, ...rest] = argv;
|
|
55
|
+
const flag = (name) => { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : undefined; };
|
|
56
|
+
const has = (name) => rest.includes(name);
|
|
57
|
+
const log = deps.log ?? ((v) => console.log(JSON.stringify(v, null, 2)));
|
|
58
|
+
if (!["windows-stage", "msix", "mac-app"].includes(command)) {
|
|
59
|
+
throw fail("SWIFT_PACKAGE_USAGE", "usage: swift-package <windows-stage|msix|mac-app> --config <file> [--dry-run] [--no-sign] [--notarize]");
|
|
60
|
+
}
|
|
61
|
+
const loaded = loadSwiftPackageConfig(flag("--config"));
|
|
62
|
+
if (command === "windows-stage") return log(stageSwiftWindows(windowsStageOptions(loaded)));
|
|
63
|
+
if (command === "msix") {
|
|
64
|
+
const staged = stageSwiftWindows(windowsStageOptions(loaded));
|
|
65
|
+
const options = msixOptions(loaded, staged);
|
|
66
|
+
return log(buildMsix({ ...options, sign: !has("--no-sign"), dryRun: has("--dry-run") }, deps.msix));
|
|
67
|
+
}
|
|
68
|
+
const options = macOptions(loaded);
|
|
69
|
+
const assembled = assembleMacApp(options, deps.mac);
|
|
70
|
+
const out = { app: assembled.app, nested: assembled.nested };
|
|
71
|
+
if (has("--sign") || has("--notarize")) {
|
|
72
|
+
out.signing = signAndNotarizeMacApp({ app: assembled.app, nested: assembled.nested,
|
|
73
|
+
entitlements: rel(loaded.base, loaded.config.mac.entitlements), receiptDir: path.join(options.out, "receipts"),
|
|
74
|
+
notarize: has("--notarize"), profile: flag("--profile"), dryRun: has("--dry-run") }, deps.mac);
|
|
75
|
+
}
|
|
76
|
+
return log(out);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const self = (file) => { try { return path.resolve(file) === fileURLToPath(import.meta.url); } catch { return false; } };
|
|
80
|
+
if (process.argv[1] && self(process.argv[1])) {
|
|
81
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
82
|
+
console.error(`right-release swift-package: [${error.code ?? "SWIFT_PACKAGE_FAILED"}] ${error.message}`);
|
|
83
|
+
process.exitCode = error.code === "SWIFT_PACKAGE_USAGE" ? 2 : 1;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Stage a SwiftPM-built Windows app for packaging: copy the executable and every
|
|
2
|
+
// app-local DLL it needs (transitively) into one flat directory, and fail closed
|
|
3
|
+
// on any dependency that is neither staged, a Windows system DLL, nor provided by
|
|
4
|
+
// a declared package dependency. Runs on any host (pure file + PE parsing).
|
|
5
|
+
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { readPeImports } from "./pe-imports.mjs";
|
|
10
|
+
|
|
11
|
+
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
12
|
+
|
|
13
|
+
// Windows-owned DLLs present on every supported install. Anything else must ship.
|
|
14
|
+
export const WINDOWS_SYSTEM_DLLS = Object.freeze([
|
|
15
|
+
"kernel32.dll", "kernelbase.dll", "ntdll.dll", "user32.dll", "gdi32.dll", "gdi32full.dll", "advapi32.dll", "shell32.dll",
|
|
16
|
+
"shlwapi.dll", "ole32.dll", "oleaut32.dll", "combase.dll", "rpcrt4.dll", "sechost.dll", "ws2_32.dll", "wsock32.dll",
|
|
17
|
+
"mswsock.dll", "iphlpapi.dll", "bcrypt.dll", "bcryptprimitives.dll", "ncrypt.dll", "crypt32.dll", "secur32.dll", "wintrust.dll",
|
|
18
|
+
"userenv.dll", "version.dll", "winmm.dll", "comctl32.dll", "comdlg32.dll", "imm32.dll", "uxtheme.dll", "dwmapi.dll",
|
|
19
|
+
"d2d1.dll", "d3d11.dll", "d3d12.dll", "dxgi.dll", "dwrite.dll", "dcomp.dll", "uiautomationcore.dll", "propsys.dll",
|
|
20
|
+
"msctf.dll", "setupapi.dll", "cfgmgr32.dll", "shcore.dll", "powrprof.dll", "psapi.dll", "dbghelp.dll", "wldap32.dll",
|
|
21
|
+
"normaliz.dll", "netapi32.dll", "winhttp.dll", "wininet.dll", "ucrtbase.dll", "msvcrt.dll", "oleacc.dll", "avrt.dll",
|
|
22
|
+
"mf.dll", "mfplat.dll", "dnsapi.dll", "wtsapi32.dll", "winspool.drv", "ntmarta.dll", "dxcore.dll", "windows.ui.dll", "cabinet.dll",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const isApiSet = (name) => /^(api|ext)-ms-win-[a-z0-9-]+-l\d+-\d+-\d+\.dll$/.test(name);
|
|
26
|
+
const lower = (value) => String(value).toLowerCase();
|
|
27
|
+
const isPe = (file) => /\.(exe|dll)$/i.test(file);
|
|
28
|
+
const sha256 = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
29
|
+
|
|
30
|
+
function findInDirs(name, dirs) {
|
|
31
|
+
for (const dir of dirs) {
|
|
32
|
+
if (!existsSync(dir)) continue;
|
|
33
|
+
const match = readdirSync(dir).find((entry) => lower(entry) === lower(name));
|
|
34
|
+
if (match) return path.join(dir, match);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* options: { exe, out, searchDirs[], include[] (extra files copied verbatim),
|
|
41
|
+
* systemDlls[] (extra allowed), dependencies[{ name, provides[] }], architecture }
|
|
42
|
+
* Returns a manifest; throws SWIFT_STAGE_UNRESOLVED listing every missing DLL.
|
|
43
|
+
*/
|
|
44
|
+
export function stageSwiftWindows({ exe, out, searchDirs = [], include = [], systemDlls = [], dependencies = [], architecture, clean = true } = {}) {
|
|
45
|
+
if (!exe || !out) throw fail("SWIFT_STAGE_OPTIONS", "exe and out are required");
|
|
46
|
+
exe = path.resolve(exe); out = path.resolve(out);
|
|
47
|
+
if (!/\.exe$/i.test(exe) || !existsSync(exe) || !statSync(exe).isFile()) throw fail("SWIFT_STAGE_OPTIONS", `exe must be an existing .exe: ${exe}`);
|
|
48
|
+
const dirs = [path.dirname(exe), ...searchDirs.map((dir) => path.resolve(dir))];
|
|
49
|
+
const system = new Set([...WINDOWS_SYSTEM_DLLS, ...systemDlls].map(lower));
|
|
50
|
+
const provided = new Map();
|
|
51
|
+
for (const dep of dependencies) for (const dll of dep.provides ?? []) provided.set(lower(dll), dep.name);
|
|
52
|
+
if (clean) rmSync(out, { recursive: true, force: true });
|
|
53
|
+
mkdirSync(out, { recursive: true });
|
|
54
|
+
|
|
55
|
+
const staged = new Map(); // lower name -> { file(source), name }
|
|
56
|
+
const queue = [{ name: path.basename(exe), source: exe, from: "<root>" }];
|
|
57
|
+
const missing = new Map(); // lower name -> importers
|
|
58
|
+
const seenSystem = new Set(), seenProvided = new Map();
|
|
59
|
+
let arch = architecture;
|
|
60
|
+
const extra = include.map((file) => path.resolve(file));
|
|
61
|
+
for (const file of extra) {
|
|
62
|
+
if (!existsSync(file) || !statSync(file).isFile()) throw fail("SWIFT_STAGE_OPTIONS", `include file missing: ${file}`);
|
|
63
|
+
queue.push({ name: path.basename(file), source: file, from: "<include>" });
|
|
64
|
+
}
|
|
65
|
+
while (queue.length) {
|
|
66
|
+
const item = queue.shift();
|
|
67
|
+
const key = lower(item.name);
|
|
68
|
+
if (staged.has(key)) continue;
|
|
69
|
+
staged.set(key, item);
|
|
70
|
+
if (!isPe(item.name)) continue;
|
|
71
|
+
const info = readPeImports(readFileSync(item.source));
|
|
72
|
+
if (!["x64", "arm64"].includes(info.architecture)) throw fail("SWIFT_STAGE_ARCH", `${item.source} is ${info.architecture}; only x64 and arm64 are supported`);
|
|
73
|
+
arch ??= info.architecture;
|
|
74
|
+
if (info.architecture !== arch) throw fail("SWIFT_STAGE_ARCH", `${item.source} is ${info.architecture} but package architecture is ${arch}`);
|
|
75
|
+
for (const dep of [...info.imports, ...info.delayImports]) {
|
|
76
|
+
const dk = lower(dep);
|
|
77
|
+
if (staged.has(dk)) continue;
|
|
78
|
+
const found = findInDirs(dep, dirs);
|
|
79
|
+
if (found) { queue.push({ name: path.basename(found), source: found, from: item.name }); continue; }
|
|
80
|
+
if (isApiSet(dk) || system.has(dk)) { seenSystem.add(dk); continue; }
|
|
81
|
+
if (provided.has(dk)) { seenProvided.set(dk, provided.get(dk)); continue; }
|
|
82
|
+
if (!missing.has(dk)) missing.set(dk, new Set());
|
|
83
|
+
missing.get(dk).add(item.name);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (missing.size) {
|
|
87
|
+
const detail = [...missing].map(([dll, by]) => `${dll} (needed by ${[...by].join(", ")})`).join("; ");
|
|
88
|
+
throw fail("SWIFT_STAGE_UNRESOLVED", `unresolved DLL dependencies: ${detail}. Add the directory to searchDirs, stage it via include, or declare a package dependency that provides it`);
|
|
89
|
+
}
|
|
90
|
+
const files = [];
|
|
91
|
+
for (const [, item] of [...staged].sort(([a], [b]) => a.localeCompare(b))) {
|
|
92
|
+
const target = path.join(out, item.name);
|
|
93
|
+
copyFileSync(item.source, target);
|
|
94
|
+
files.push({ name: item.name, source: item.source, sha256: sha256(target), sizeBytes: statSync(target).size, requiredBy: item.from, pe: isPe(item.name) });
|
|
95
|
+
}
|
|
96
|
+
const manifest = {
|
|
97
|
+
schema: 1, architecture: arch, exe: path.basename(exe), out, files,
|
|
98
|
+
systemDlls: [...seenSystem].sort(),
|
|
99
|
+
providedByDependencies: Object.fromEntries([...seenProvided].sort()),
|
|
100
|
+
};
|
|
101
|
+
writeFileSync(`${out}.stage.json`, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
102
|
+
return manifest;
|
|
103
|
+
}
|