@rightkit/release 0.6.7 → 0.6.10

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.
Files changed (53) hide show
  1. package/README.md +35 -10
  2. package/admission-files.mjs +1 -1
  3. package/app-check.mjs +94 -20
  4. package/assemble-app-command.mjs +151 -0
  5. package/assembled-app.mjs +445 -0
  6. package/bin/darwin/rightkit-swap +0 -0
  7. package/bin/darwin/rightkit-swap.sign.json +6 -0
  8. package/bin/rightkit-gate.mjs +957 -83
  9. package/build-launch.mjs +88 -0
  10. package/build-ownership.mjs +1231 -0
  11. package/build-release.mjs +2 -1
  12. package/cargo-contract.mjs +3 -1
  13. package/cargo-target-shim.mjs +166 -0
  14. package/class-a-launch-guard.mjs +126 -0
  15. package/cli/right-release.mjs +3 -1
  16. package/codesign-shim.mjs +11 -1
  17. package/deterministic-scans.mjs +807 -56
  18. package/development-signing.mjs +4 -1
  19. package/development.mjs +110 -20
  20. package/door-policy.mjs +61 -6
  21. package/door.mjs +23 -0
  22. package/gate-executor.mjs +14 -2
  23. package/gate-stages.mjs +15 -5
  24. package/heavy-command.mjs +3 -2
  25. package/install-app.mjs +11 -2
  26. package/install-assembled-app.mjs +423 -0
  27. package/ios-dev.mjs +70 -3
  28. package/linux-artifacts.mjs +93 -0
  29. package/local-publish.mjs +161 -26
  30. package/managed-cargo.mjs +47 -5
  31. package/managed-root-guard.mjs +34 -0
  32. package/native-helpers.mjs +355 -0
  33. package/package-recipe-targets.mjs +52 -7
  34. package/package.json +5 -3
  35. package/pipeline-receipts.mjs +302 -302
  36. package/project-type.mjs +2 -2
  37. package/provenance-verify.mjs +115 -0
  38. package/publish-cargo.mjs +8 -6
  39. package/qa-admission-policy.json +10 -1
  40. package/recipe-build.mjs +73 -5
  41. package/release-config.mjs +1 -1
  42. package/release.mjs +15 -5
  43. package/rightkit-versions.json +13 -7
  44. package/snapshot-check.mjs +306 -0
  45. package/snapshot-git.mjs +166 -0
  46. package/snapshot-request.mjs +161 -0
  47. package/snapshot-root-config.mjs +76 -0
  48. package/snapshot.mjs +856 -0
  49. package/stage-commands.mjs +60 -0
  50. package/stage-sidecar.mjs +57 -7
  51. package/test-command.mjs +17 -4
  52. package/unit-gate.mjs +259 -0
  53. package/web-build.mjs +38 -6
@@ -0,0 +1,355 @@
1
+ // Declared native Swift helpers embedded in a macOS .app (shared by the dev lane and the release lane).
2
+ //
3
+ // nativeHelpers: [{
4
+ // name: "menubar", // unique label (logs, receipts)
5
+ // package: "macos-native/Pkg" // app-relative SwiftPM package dir ...
6
+ // // sharedPackage: "cloudkit", // ... or a RightKit shared Swift package key (exactly one of the two)
7
+ // product: "MenuBar", // SwiftPM executable product
8
+ // kind: "app" | "executable",
9
+ // embed: "Contents/Library/LoginItems/MenuBar.app", // path inside the .app
10
+ // universal: true, // arm64 + x86_64 (required, explicit)
11
+ // entitlements: "src-tauri/entitlements-x.plist", // optional, app-relative
12
+ // // kind "app" only (wrap the SwiftPM executable as a helper .app):
13
+ // executable: "MenuBar", // CFBundleExecutable / file in Contents/MacOS (default: product)
14
+ // info: { bundleId, name?, displayName?, uiElement?, minimumSystemVersion? }, // Info.plist fields
15
+ // resources: [{ from: "src-tauri/icons/tray", to: "tray" }], // copied into Contents/Resources
16
+ // appIntents: { moduleName, deploymentTarget? }, // appintentsmetadataprocessor step
17
+ // provisioningProfile: "src-tauri/provisioning/X.provisionprofile", // -> Contents/embedded.provisionprofile
18
+ // }]
19
+ //
20
+ // Build: `swift build -c <release|debug> --package-path <pkg> --product <product> --arch <a> [--arch <b>]` (SwiftPM, the same
21
+ // tool swift-package / assemble-app use; one explicit --arch per slice so the output lands in SwiftPM's xcbuild layout for
22
+ // both thin and universal builds), then `--show-bin-path` to find the product. Embedded BEFORE the app is signed. Signing is
23
+ // inside-out: release.mjs signs the helpers through sign.nestedCode (macPackage expansion derives it), the dev signer signs
24
+ // nested bundles itself, and signNativeHelpers below does the same for any other caller. Every external effect is injectable.
25
+ import { createHash } from "node:crypto";
26
+ import { buildLaunch } from "./build-launch.mjs";
27
+ import * as nodeFs from "node:fs";
28
+ import os from "node:os";
29
+ import path from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+ import { spawnSync as nodeSpawnSync } from "./background-process.mjs";
32
+ import { signMacosEntries } from "./mac-sign-core.mjs";
33
+ import { discoverNestedCode } from "./swift-mac-app.mjs";
34
+
35
+ export class NativeHelpersError extends Error {
36
+ constructor(code, message, details = {}) { super(message); this.name = "NativeHelpersError"; this.code = code; this.details = details; }
37
+ }
38
+ const fail = (code, message, details) => { throw new NativeHelpersError(code, message, details); };
39
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
40
+
41
+ export const NATIVE_HELPER_KEYS = Object.freeze(["name", "package", "sharedPackage", "product", "kind", "embed", "universal", "entitlements", "executable", "info", "resources", "appIntents", "provisioningProfile"]);
42
+ const INFO_KEYS = ["bundleId", "name", "displayName", "uiElement", "minimumSystemVersion"];
43
+ const APP_INTENTS_KEYS = ["moduleName", "deploymentTarget"];
44
+ const RESOURCE_KEYS = ["from", "to"];
45
+ /** RightKit shared Swift packages, keyed, relative to the RightKit checkout root (env RIGHTKIT_SWIFT_ROOT overrides the root). */
46
+ export const SHARED_SWIFT_PACKAGES = Object.freeze({ cloudkit: "swift/RightKitCloudKitHelper" });
47
+ /** Where a helper may be embedded: only roots the inside-out signing discovery walks. */
48
+ const EMBED_ROOTS = ["Contents/Library/", "Contents/Helpers/", "Contents/XPCServices/", "Contents/PlugIns/", "Contents/MacOS/"];
49
+ const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
50
+ const PRODUCT = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
51
+ const FLAT = /^[A-Za-z0-9_][A-Za-z0-9_. -]{0,127}$/;
52
+ const VERSION = /^\d+(\.\d+){0,2}$/;
53
+ const BUNDLE_ID = /^[A-Za-z0-9.-]+\.[A-Za-z0-9.-]+$/;
54
+ const ARCHS = ["arm64", "x86_64"];
55
+
56
+ function relative(value, label, errors, { ext } = {}) {
57
+ if (typeof value !== "string" || !value.trim()) { errors.push(`${label} must be a non-empty relative path`); return undefined; }
58
+ if (value.includes("\0") || value.includes("\\") || path.isAbsolute(value) || /^[A-Za-z]:/.test(value)) { errors.push(`${label} must be a relative path with forward slashes: ${JSON.stringify(value)}`); return undefined; }
59
+ const segments = value.split("/");
60
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) { errors.push(`${label} must not contain empty, '.' or '..' segments or escape its root: ${JSON.stringify(value)}`); return undefined; }
61
+ if (ext && !ext.test(value)) { errors.push(`${label} must match ${ext}: ${JSON.stringify(value)}`); return undefined; }
62
+ return value;
63
+ }
64
+ const unknown = (object, allowed, label, errors) => { for (const key of Object.keys(object)) if (!allowed.includes(key)) errors.push(`${label}: unknown key ${JSON.stringify(key)} (allowed: ${allowed.join(", ")})`); };
65
+
66
+ /** Strictly validate and normalize a `nativeHelpers` list. Every problem is reported at once (NATIVE_HELPERS_INVALID). */
67
+ export function validateNativeHelpers(list, { label = "nativeHelpers" } = {}) {
68
+ const errors = [];
69
+ if (!Array.isArray(list) || !list.length) fail("NATIVE_HELPERS_INVALID", `${label} must be a non-empty array`);
70
+ const out = [];
71
+ list.forEach((raw, index) => {
72
+ const at = `${label}[${index}]`;
73
+ if (!isObject(raw)) { errors.push(`${at} must be an object`); return; }
74
+ unknown(raw, NATIVE_HELPER_KEYS, at, errors);
75
+ const helper = {};
76
+ if (typeof raw.name !== "string" || !NAME.test(raw.name)) errors.push(`${at}.name must be 1-64 characters of letters, digits, '.', '_' and '-'`); else helper.name = raw.name;
77
+ if ((raw.package === undefined) === (raw.sharedPackage === undefined)) errors.push(`${at} needs exactly one of package or sharedPackage`);
78
+ else if (raw.package !== undefined) { const value = relative(raw.package, `${at}.package`, errors); if (value) helper.package = value; }
79
+ else if (typeof raw.sharedPackage !== "string" || !Object.hasOwn(SHARED_SWIFT_PACKAGES, raw.sharedPackage)) errors.push(`${at}.sharedPackage must be one of ${Object.keys(SHARED_SWIFT_PACKAGES).join(", ")}`);
80
+ else helper.sharedPackage = raw.sharedPackage;
81
+ if (typeof raw.product !== "string" || !PRODUCT.test(raw.product)) errors.push(`${at}.product must be a SwiftPM product name`); else helper.product = raw.product;
82
+ if (raw.kind !== "app" && raw.kind !== "executable") errors.push(`${at}.kind must be "app" or "executable"`); else helper.kind = raw.kind;
83
+ if (typeof raw.universal !== "boolean") errors.push(`${at}.universal is required and must be true or false`); else helper.universal = raw.universal;
84
+ const embed = relative(raw.embed, `${at}.embed`, errors);
85
+ if (embed) {
86
+ if (!EMBED_ROOTS.some((root) => embed.startsWith(root)) || embed.split("/").length < 3) errors.push(`${at}.embed must be inside the .app under ${EMBED_ROOTS.join(" | ")}: ${embed}`);
87
+ else if (helper.kind === "app" && (!embed.endsWith(".app") || embed.startsWith("Contents/MacOS/"))) errors.push(`${at}.embed for kind "app" must be a <Name>.app below Contents/Library, Helpers, XPCServices or PlugIns: ${embed}`);
88
+ else if (helper.kind === "executable" && (embed.endsWith(".app") || (embed.startsWith("Contents/MacOS/") && embed.split("/").length !== 3))) errors.push(`${at}.embed for kind "executable" must be a file (Contents/MacOS/<name> or a path below another allowed root): ${embed}`);
89
+ else helper.embed = embed;
90
+ }
91
+ if (raw.entitlements !== undefined) { const value = relative(raw.entitlements, `${at}.entitlements`, errors, { ext: /\.plist$/i }); if (value) helper.entitlements = value; }
92
+ if (raw.executable !== undefined) {
93
+ if (raw.kind !== "app") errors.push(`${at}.executable applies only to kind "app"`);
94
+ else if (typeof raw.executable !== "string" || !FLAT.test(raw.executable) || raw.executable.includes("/")) errors.push(`${at}.executable must be a flat file name`);
95
+ else helper.executable = raw.executable;
96
+ }
97
+ if (raw.kind === "app") {
98
+ if (!isObject(raw.info)) errors.push(`${at}.info is required for kind "app" ({ bundleId, name?, displayName?, uiElement?, minimumSystemVersion? })`);
99
+ else {
100
+ unknown(raw.info, INFO_KEYS, `${at}.info`, errors);
101
+ const info = {};
102
+ if (typeof raw.info.bundleId !== "string" || !BUNDLE_ID.test(raw.info.bundleId)) errors.push(`${at}.info.bundleId must be reverse-DNS`); else info.bundleId = raw.info.bundleId;
103
+ for (const key of ["name", "displayName"]) if (raw.info[key] !== undefined) { if (typeof raw.info[key] !== "string" || !FLAT.test(raw.info[key])) errors.push(`${at}.info.${key} must be a simple name`); else info[key] = raw.info[key]; }
104
+ if (raw.info.uiElement !== undefined) { if (typeof raw.info.uiElement !== "boolean") errors.push(`${at}.info.uiElement must be a boolean`); else info.uiElement = raw.info.uiElement; }
105
+ if (raw.info.minimumSystemVersion !== undefined) { if (typeof raw.info.minimumSystemVersion !== "string" || !VERSION.test(raw.info.minimumSystemVersion)) errors.push(`${at}.info.minimumSystemVersion must be 1-3 numeric parts`); else info.minimumSystemVersion = raw.info.minimumSystemVersion; }
106
+ helper.info = info;
107
+ }
108
+ if (raw.resources !== undefined) {
109
+ if (!Array.isArray(raw.resources)) errors.push(`${at}.resources must be an array of { from, to }`);
110
+ else helper.resources = raw.resources.map((item, i) => {
111
+ const where = `${at}.resources[${i}]`;
112
+ if (!isObject(item)) { errors.push(`${where} must be an object`); return undefined; }
113
+ unknown(item, RESOURCE_KEYS, where, errors);
114
+ const from = relative(item.from, `${where}.from`, errors);
115
+ const to = relative(item.to, `${where}.to`, errors);
116
+ return from && to ? { from, to } : undefined;
117
+ }).filter(Boolean);
118
+ }
119
+ if (raw.appIntents !== undefined) {
120
+ if (!isObject(raw.appIntents)) errors.push(`${at}.appIntents must be { moduleName, deploymentTarget? }`);
121
+ else {
122
+ unknown(raw.appIntents, APP_INTENTS_KEYS, `${at}.appIntents`, errors);
123
+ if (typeof raw.appIntents.moduleName !== "string" || !PRODUCT.test(raw.appIntents.moduleName)) errors.push(`${at}.appIntents.moduleName must be a Swift module name`);
124
+ if (raw.appIntents.deploymentTarget !== undefined && (typeof raw.appIntents.deploymentTarget !== "string" || !VERSION.test(raw.appIntents.deploymentTarget))) errors.push(`${at}.appIntents.deploymentTarget must be 1-3 numeric parts`);
125
+ helper.appIntents = { moduleName: raw.appIntents.moduleName, deploymentTarget: raw.appIntents.deploymentTarget ?? "14.0" };
126
+ }
127
+ }
128
+ if (raw.provisioningProfile !== undefined) { const value = relative(raw.provisioningProfile, `${at}.provisioningProfile`, errors, { ext: /\.(provisionprofile|mobileprovision)$/ }); if (value) helper.provisioningProfile = value; }
129
+ } else if (raw.kind === "executable") {
130
+ for (const key of ["info", "resources", "appIntents", "provisioningProfile"]) if (raw[key] !== undefined) errors.push(`${at}.${key} applies only to kind "app"`);
131
+ }
132
+ out.push(helper);
133
+ });
134
+ const names = new Set();
135
+ const embeds = [];
136
+ for (const helper of out) {
137
+ if (helper.name) { if (names.has(helper.name)) errors.push(`${label}: duplicate name ${helper.name}`); names.add(helper.name); }
138
+ if (helper.embed) {
139
+ const clash = embeds.find((other) => other === helper.embed || other.startsWith(`${helper.embed}/`) || helper.embed.startsWith(`${other}/`));
140
+ if (clash) errors.push(`${label}: embed paths must be unique and not nested in each other (${clash} vs ${helper.embed})`);
141
+ embeds.push(helper.embed);
142
+ }
143
+ }
144
+ if (errors.length) fail("NATIVE_HELPERS_INVALID", `invalid ${label}:\n${errors.map((line) => `- ${line}`).join("\n")}`, { errors });
145
+ return out;
146
+ }
147
+
148
+ /** Release signing config derived from the list: `sign.nestedCode[embed]` per helper (hardened runtime is implicit, entitlements per helper). */
149
+ export function nativeHelperNestedCode(helpers) {
150
+ return Object.fromEntries(helpers.map((helper) => [helper.embed, helper.entitlements ? { entitlements: helper.entitlements } : {}]));
151
+ }
152
+ /** Dev signer map `{ <bundle-relative embed>: <entitlements plist> }` for helpers that declare entitlements (merge into install.entitlements). */
153
+ export function nativeHelperEntitlementMap(helpers) {
154
+ return Object.fromEntries(helpers.filter((helper) => helper.entitlements).map((helper) => [helper.embed, helper.entitlements]));
155
+ }
156
+ /** The shared list from a release config: `targets.mac.macPackage.nativeHelpers` (undefined when none is declared). */
157
+ export function nativeHelpersFromConfig(config) {
158
+ const raw = config?.targets?.mac?.macPackage?.nativeHelpers;
159
+ return raw === undefined ? undefined : validateNativeHelpers(raw, { label: "targets.mac.macPackage.nativeHelpers" });
160
+ }
161
+
162
+ const esc = (value) => String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
163
+ /** Info.plist for a wrapped helper .app (same fields the helper bundles always carried). */
164
+ export function helperInfoPlist({ helper, executable, version }) {
165
+ const info = helper.info;
166
+ const rows = [
167
+ ["CFBundleExecutable", `<string>${esc(executable)}</string>`],
168
+ ["CFBundleIdentifier", `<string>${esc(info.bundleId)}</string>`],
169
+ ["CFBundleName", `<string>${esc(info.name ?? helper.product)}</string>`],
170
+ ...(info.displayName ? [["CFBundleDisplayName", `<string>${esc(info.displayName)}</string>`]] : []),
171
+ ["CFBundlePackageType", "<string>APPL</string>"],
172
+ ["CFBundleShortVersionString", `<string>${esc(version)}</string>`],
173
+ ["CFBundleVersion", `<string>${esc(version)}</string>`],
174
+ ...(info.uiElement ? [["LSUIElement", "<true/>"]] : []),
175
+ ["LSMinimumSystemVersion", `<string>${esc(info.minimumSystemVersion ?? "14.0")}</string>`],
176
+ ];
177
+ 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"><dict>\n${rows.map(([key, value]) => ` <key>${key}</key>${value}`).join("\n")}\n</dict></plist>\n`;
178
+ }
179
+
180
+ const sha256 = (fs, file) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
181
+ const tail = (text, lines = 15) => String(text ?? "").split(/\r?\n/).slice(-lines).join("\n");
182
+ const archSet = (text) => String(text ?? "").trim().split(/\s+/).filter(Boolean).sort();
183
+
184
+ function findFile(fs, root, predicate) {
185
+ let entries;
186
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return null; }
187
+ for (const entry of entries) {
188
+ const candidate = path.join(root, entry.name);
189
+ if (entry.isDirectory()) { const found = findFile(fs, candidate, predicate); if (found) return found; }
190
+ else if (predicate(candidate.split(path.sep).join("/"))) return candidate;
191
+ }
192
+ return null;
193
+ }
194
+
195
+ /**
196
+ * Build every helper with SwiftPM and embed it into `app` (before signing). Strictly in declared order; the first failure throws.
197
+ * options: appRoot (absolute), app (absolute .app), helpers (validated or raw), version (required with a kind "app" helper),
198
+ * forceArch ("arm64"|"x86_64": build only that slice for every helper, e.g. a dev build or a thin release triple),
199
+ * configuration ("release"|"debug"), env, spawn, fs, log, replaceExisting (default false: an existing embed path is an error),
200
+ * sharedRoot, hostArch, tmpdir. Without forceArch a universal helper gets both slices and a thin one the host arch.
201
+ * Returns { helpers: [{ name, embed, path, source, archs, sha256 }], events: [...] }.
202
+ */
203
+ export function buildAndEmbedNativeHelpers({ appRoot, app, helpers: rawHelpers, version, forceArch, configuration = "release", env = process.env, spawn = nodeSpawnSync, fs = nodeFs, log = () => {}, replaceExisting = false, sharedRoot, hostArch = process.arch === "arm64" ? "arm64" : "x86_64", tmpdir = os.tmpdir(), packageOwner = (pkg) => { const result = nodeSpawnSync("git", ["-C", pkg, "rev-parse", "--show-toplevel"], { encoding: "utf8", windowsHide: true }); return result.status === 0 ? String(result.stdout).trim() : null; } } = {}) {
204
+ if (typeof appRoot !== "string" || !path.isAbsolute(appRoot)) fail("NATIVE_HELPERS_INVALID", "appRoot must be an absolute path");
205
+ if (typeof app !== "string" || !path.isAbsolute(app) || !app.replace(/\/+$/, "").endsWith(".app")) fail("NATIVE_HELPERS_INVALID", "app must be the absolute path of the .app bundle");
206
+ if (!["release", "debug"].includes(configuration)) fail("NATIVE_HELPERS_INVALID", "configuration must be release or debug");
207
+ if (forceArch !== undefined && !ARCHS.includes(forceArch)) fail("NATIVE_HELPERS_INVALID", `forceArch must be one of ${ARCHS.join(", ")}`);
208
+ const helpers = validateNativeHelpers(rawHelpers);
209
+ if (helpers.some((helper) => helper.kind === "app") && (typeof version !== "string" || !VERSION.test(version))) fail("NATIVE_HELPERS_INVALID", "a kind \"app\" helper needs the app version (1-3 numeric parts) for its Info.plist");
210
+ if (!fs.existsSync(path.join(app, "Contents", "Info.plist"))) fail("NATIVE_HELPERS_INPUT_MISSING", `${app} is not a built .app (no Contents/Info.plist); helpers are embedded after the app build`);
211
+ const root = sharedRoot ?? env.RIGHTKIT_SWIFT_ROOT ?? fileURLToPath(new URL("../../", import.meta.url));
212
+ const events = [];
213
+ const results = [];
214
+ const run = (what, cmd, args, { capture = false, cwd = appRoot } = {}) => {
215
+ log(`native-helpers: ${what}: ${cmd} ${args.join(" ")}`);
216
+ const launch = cmd === "swift" ? buildLaunch(cmd, args, { cwd, env, exists: fs.existsSync }) : { command: cmd, args };
217
+ const result = spawn(launch.command, launch.args, { cwd, env, ...(capture ? { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] } : { stdio: "inherit" }), windowsHide: true });
218
+ if (!result || result.error || result.status !== 0) fail("NATIVE_HELPERS_STEP_FAILED", `native helper ${what} failed (${result?.error?.message ?? `exit ${result?.status ?? result?.signal ?? "unknown"}`})${capture ? `: ${tail(result?.stderr)}` : ""}`, { cmd, status: result?.status ?? null });
219
+ return result;
220
+ };
221
+ for (const helper of helpers) {
222
+ const pkgDir = helper.package ? path.resolve(appRoot, helper.package) : path.resolve(root, SHARED_SWIFT_PACKAGES[helper.sharedPackage]);
223
+ if (helper.package && path.relative(appRoot, pkgDir).startsWith("..")) fail("NATIVE_HELPERS_INVALID", `${helper.name}: package escapes the app root`);
224
+ if (!fs.existsSync(path.join(pkgDir, "Package.swift"))) fail("NATIVE_HELPERS_INPUT_MISSING", `${helper.name}: no Package.swift in ${pkgDir}${helper.sharedPackage ? " (set RIGHTKIT_SWIFT_ROOT to the RightKit checkout)" : ""}`);
225
+ const entitlements = helper.entitlements ? path.resolve(appRoot, helper.entitlements) : undefined;
226
+ if (entitlements && !fs.existsSync(entitlements)) fail("NATIVE_HELPERS_INPUT_MISSING", `${helper.name}: entitlements file not found: ${entitlements}`);
227
+ const archs = forceArch ? [forceArch] : helper.universal ? [...ARCHS] : [hostArch];
228
+ // Each Swift job is admitted against the checkout that owns its package, including shared RightKit helpers.
229
+ const packageRoot = packageOwner(pkgDir) || appRoot;
230
+ const relativePackage = path.relative(packageRoot, pkgDir);
231
+ const packageArg = relativePackage === "" ? "." : relativePackage;
232
+ const swiftArgs = ["build", "-c", configuration, "--package-path", packageArg, "--product", helper.product, ...archs.flatMap((arch) => ["--arch", arch])];
233
+ run(`build ${helper.name}`, "swift", swiftArgs, { cwd: packageRoot });
234
+ events.push(`build:${helper.name}`);
235
+ const binDir = String(run(`bin path ${helper.name}`, "swift", [...swiftArgs, "--show-bin-path"], { capture: true, cwd: packageRoot }).stdout ?? "").trim().split(/\r?\n/).pop();
236
+ if (!binDir || !path.isAbsolute(binDir)) fail("NATIVE_HELPERS_STEP_FAILED", `${helper.name}: swift build --show-bin-path returned no absolute path`);
237
+ const source = path.join(binDir, helper.product);
238
+ if (!fs.existsSync(source)) fail("NATIVE_HELPERS_INPUT_MISSING", `${helper.name}: the build did not produce ${source}`);
239
+ const found = archSet(run(`lipo ${helper.name}`, "lipo", ["-archs", source], { capture: true }).stdout);
240
+ if (found.join(" ") !== [...archs].sort().join(" ")) fail("NATIVE_HELPERS_ARCH_MISMATCH", `${helper.name}: ${source} holds [${found.join(" ")}], expected exactly [${[...archs].sort().join(" ")}]`, { found, expected: archs });
241
+ const dest = path.join(app, ...helper.embed.split("/"));
242
+ if (fs.existsSync(dest)) {
243
+ if (!replaceExisting) fail("NATIVE_HELPERS_EMBED_EXISTS", `${helper.name}: ${helper.embed} already exists in ${app}; something else wrote it (dev rebuilds pass replaceExisting)`);
244
+ fs.rmSync(dest, { recursive: true, force: true });
245
+ }
246
+ let binary;
247
+ if (helper.kind === "executable") {
248
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
249
+ fs.copyFileSync(source, dest);
250
+ fs.chmodSync(dest, 0o755);
251
+ binary = dest;
252
+ } else {
253
+ const executable = helper.executable ?? helper.product;
254
+ binary = path.join(dest, "Contents", "MacOS", executable);
255
+ fs.mkdirSync(path.dirname(binary), { recursive: true });
256
+ fs.writeFileSync(path.join(dest, "Contents", "Info.plist"), helperInfoPlist({ helper, executable, version }));
257
+ fs.copyFileSync(source, binary);
258
+ fs.chmodSync(binary, 0o755);
259
+ for (const resource of helper.resources ?? []) {
260
+ const from = path.resolve(appRoot, resource.from);
261
+ if (!fs.existsSync(from)) fail("NATIVE_HELPERS_INPUT_MISSING", `${helper.name}: resource not found: ${from}`);
262
+ const to = path.join(dest, "Contents", "Resources", ...resource.to.split("/"));
263
+ fs.mkdirSync(path.dirname(to), { recursive: true });
264
+ fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true });
265
+ }
266
+ if (helper.appIntents) embedAppIntents({ helper, run, fs, tmpdir, pkgDir, binDir, dest, archs, configuration });
267
+ if (helper.provisioningProfile) {
268
+ const profile = path.resolve(appRoot, helper.provisioningProfile);
269
+ if (!fs.existsSync(profile)) fail("NATIVE_HELPERS_INPUT_MISSING", `${helper.name}: provisioning profile not found: ${profile}`);
270
+ fs.copyFileSync(profile, path.join(dest, "Contents", "embedded.provisionprofile"));
271
+ }
272
+ }
273
+ events.push(`embed:${helper.name}`);
274
+ results.push({ name: helper.name, embed: helper.embed, path: dest, source, archs: found, sha256: sha256(fs, binary) });
275
+ }
276
+ return { helpers: results, events };
277
+ }
278
+
279
+ /** The appintentsmetadataprocessor step: SwiftPM leaves the const-value inputs under Intermediates.noindex (xcbuild layout). */
280
+ function embedAppIntents({ helper, run, fs, tmpdir, pkgDir, binDir, dest, archs, configuration }) {
281
+ const { moduleName, deploymentTarget } = helper.appIntents;
282
+ const cfg = configuration === "debug" ? "Debug" : "Release";
283
+ const arch = archs.includes("arm64") ? "arm64" : archs[0];
284
+ const intermediates = path.join(path.resolve(binDir, "../.."), "Intermediates.noindex");
285
+ const sourceList = findFile(fs, intermediates, (file) => file.endsWith(`/${moduleName}.SwiftFileList`) && file.includes(`/${cfg}/`) && file.includes(`/${arch}/`));
286
+ const constVals = sourceList
287
+ ? fs.readdirSync(path.dirname(sourceList), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".swiftconstvalues")).map((entry) => path.join(path.dirname(sourceList), entry.name)).sort()
288
+ : [];
289
+ if (!sourceList || !constVals.length) fail("NATIVE_HELPERS_APPINTENTS_FAILED", `${helper.name}: SwiftPM did not emit App Intents metadata inputs for ${moduleName} under ${intermediates}`);
290
+ const out = (what, cmd, args) => String(run(what, cmd, args, { capture: true }).stdout ?? "").trim();
291
+ const swiftc = out("xcrun swiftc", "xcrun", ["--find", "swiftc"]);
292
+ const sdkRoot = out("sdk path", "xcrun", ["--sdk", "macosx", "--show-sdk-path"]);
293
+ const xcodeVersion = /^Xcode (.+)$/m.exec(out("xcodebuild version", "xcodebuild", ["-version"]))?.[1];
294
+ if (!swiftc || !sdkRoot || !xcodeVersion) fail("NATIVE_HELPERS_APPINTENTS_FAILED", `${helper.name}: cannot determine swiftc, the macOS SDK and the Xcode version for App Intents metadata`);
295
+ const scratch = fs.mkdtempSync(path.join(tmpdir, "rightkit-appintents-"));
296
+ const list = path.join(scratch, "swift-const-values.list");
297
+ const output = path.join(dest, "Contents", "Resources");
298
+ try {
299
+ fs.writeFileSync(list, `${constVals.join("\n")}\n`);
300
+ run(`appintents metadata ${helper.name}`, path.join(path.dirname(swiftc), "appintentsmetadataprocessor"), [
301
+ "--output", output, "--toolchain-dir", path.dirname(path.dirname(path.dirname(swiftc))), "--module-name", moduleName, "--sdk-root", sdkRoot,
302
+ "--xcode-version", xcodeVersion, "--platform-family", "macOS", "--deployment-target", deploymentTarget, "--target-triple", `${arch}-apple-macosx${deploymentTarget}`,
303
+ "--source-file-list", sourceList, "--swift-const-vals-list", list, "--force"], { cwd: pkgDir });
304
+ } finally { fs.rmSync(scratch, { recursive: true, force: true }); }
305
+ if (!fs.existsSync(path.join(output, "Metadata.appintents", "extract.actionsdata"))) fail("NATIVE_HELPERS_APPINTENTS_FAILED", `${helper.name}: App Intents metadata generation produced no Metadata.appintents/extract.actionsdata`);
306
+ }
307
+
308
+ const depth = (file) => file.split(/[\\/]/).length;
309
+
310
+ /**
311
+ * Inside-out signing plan for embedded helpers: for every helper the code nested inside it first (a kind "app" helper's
312
+ * own frameworks and plug-ins; its main executable is sealed with its bundle), then the helper itself with its own
313
+ * entitlements and the hardened runtime; deepest path first overall. The app itself is NOT in the plan (it is signed last).
314
+ */
315
+ export function planNativeHelperSigning({ app, appRoot, helpers: rawHelpers, discover = discoverNestedCode, readExecutable }) {
316
+ const helpers = validateNativeHelpers(rawHelpers);
317
+ const entries = [];
318
+ for (const helper of helpers) {
319
+ const file = path.join(app, ...helper.embed.split("/"));
320
+ if (helper.kind === "app") {
321
+ const executable = helper.executable ?? helper.product;
322
+ for (const nested of discover(file, readExecutable ? readExecutable(file) : executable)) entries.push({ file: nested, hardenedRuntime: true, options: [] });
323
+ }
324
+ entries.push({ file, hardenedRuntime: true, options: [], ...(helper.entitlements ? { entitlements: path.resolve(appRoot, helper.entitlements) } : {}) });
325
+ }
326
+ return entries.sort((a, b) => depth(b.file) - depth(a.file) || a.file.localeCompare(b.file));
327
+ }
328
+
329
+ /** Sign the helpers inside-out (see planNativeHelperSigning). deps: identity, signOne(codesignArgs), inspect(cmd, args) -> text. */
330
+ export function signNativeHelpers({ app, appRoot, helpers, identity, signOne, inspect, discover, evidence, hashFile }) {
331
+ if (typeof identity !== "string" || !identity.trim()) fail("NATIVE_HELPERS_SIGN_FAILED", "a signing identity is required");
332
+ const entries = planNativeHelperSigning({ app, appRoot, helpers, ...(discover ? { discover } : {}) });
333
+ return signMacosEntries(entries, { identity, signOne, inspect, ...(evidence ? { evidence } : {}), ...(hashFile ? { hashFile } : {}) });
334
+ }
335
+
336
+ /**
337
+ * Whole sequence for a caller that owns signing itself: build helpers, embed, sign helpers inside-out, sign the app
338
+ * (`signApp()` supplied by the caller), verify (`codesign --verify --strict --deep`). Returns { build, signing, events }.
339
+ */
340
+ export function buildEmbedSignNativeHelpers({ signApp, verify = true, ...options }) {
341
+ if (typeof signApp !== "function") fail("NATIVE_HELPERS_INVALID", "signApp is required: the app is signed after its helpers");
342
+ const built = buildAndEmbedNativeHelpers(options);
343
+ const events = [...built.events];
344
+ const signing = signNativeHelpers({ app: options.app, appRoot: options.appRoot, helpers: options.helpers, ...options.signing });
345
+ for (const record of signing) events.push(`sign:${path.relative(options.app, record.file).split(path.sep).join("/")}`);
346
+ signApp();
347
+ events.push("sign:app");
348
+ if (verify) {
349
+ const spawn = options.spawn ?? nodeSpawnSync;
350
+ const checked = spawn("codesign", ["--verify", "--strict", "--deep", options.app], { cwd: options.appRoot, env: options.env ?? process.env, encoding: "utf8", stdio: "pipe", windowsHide: true });
351
+ if (!checked || checked.error || checked.status !== 0) fail("NATIVE_HELPERS_VERIFY_FAILED", `codesign --verify --strict --deep failed for ${options.app}: ${tail(checked?.stderr)}`);
352
+ events.push("verify");
353
+ }
354
+ return { build: built, signing, events };
355
+ }
@@ -9,6 +9,7 @@ import path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { resolveTargetRoot as cargoResolveTargetRoot } from "./cargo-target.mjs";
11
11
  import { defaultManagedRoots, isUnderManagedRoot } from "./managed-target-stage.mjs";
12
+ import { NativeHelpersError, nativeHelperNestedCode, validateNativeHelpers } from "./native-helpers.mjs";
12
13
  import { validateWinPackage, WinPackageConfigError } from "./win-package-recipe.mjs";
13
14
  import { IOS_SIGNING_CONTRACT, IosPackageError, validateIosPackageConfig } from "./ios-package-recipe.mjs";
14
15
  import { IosUploadError, validateIosDistribution } from "./ios-upload.mjs";
@@ -78,13 +79,13 @@ const configArgs = (extras, appRoot, cwd) => (extras.tauriConfig ? ["--config",
78
79
  * release.mjs runs ONE prePackage command: node recipe-build.mjs runs the built-in cleanup, prepare, the Tauri build,
79
80
  * the artifact stability poll and postBuild in order. `cleanup` and `stability` are recipe-build plan sections.
80
81
  */
81
- function recipeBuildCommand({ extras, build, env, cleanup, stability, stage, timeoutMs }) {
82
+ function recipeBuildCommand({ extras, build, env, cleanup, stability, stage, timeoutMs, verifySidecars, nativeHelpers }) {
82
83
  const steps = [
83
84
  ...extras.prepare.map((step) => ({ phase: "prepare", ...step })),
84
85
  { phase: "build", cmd: build.cmd, args: build.args, cwd: build.cwd },
85
86
  ...extras.postBuild.map((step) => ({ phase: "postBuild", ...step })),
86
87
  ];
87
- const plan = { version: 1, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage && (stage.pre?.length || stage.post?.length) ? { stage } : {}) };
88
+ const plan = { version: 1, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage && (stage.pre?.length || stage.post?.length) ? { stage } : {}), ...(verifySidecars ? { verifySidecars } : {}), ...(nativeHelpers ? { nativeHelpers } : {}) };
88
89
  return { cmd: process.execPath, args: [RECIPE_BUILD, "--plan", JSON.stringify(plan)], env, ...(timeoutMs ? { timeoutMs } : {}) };
89
90
  }
90
91
 
@@ -199,7 +200,9 @@ export function expandWinPackageTarget(target, { appRoot, config = {}, env = pro
199
200
  };
200
201
  }
201
202
 
202
- const MAC_KEYS = new Set(["universal", "triple", "tauriCwd", "features", "profile", "tauriConfig", "prepare", "postBuild", "cleanup", "cargoTargetRoot", "updater"]);
203
+ const MAC_KEYS = new Set(["universal", "triple", "tauriCwd", "features", "profile", "tauriConfig", "prepare", "postBuild", "cleanup", "cargoTargetRoot", "updater", "stageSidecars", "nativeHelpers"]);
204
+ const STAGE_SIDECARS_KEYS = new Set(["config", "receipt"]);
205
+ const DEFAULT_SIDECAR_RECEIPT = "src-tauri/binaries/stage-sidecar.json";
203
206
  const MAC_TRIPLES = ["aarch64-apple-darwin", "x86_64-apple-darwin"];
204
207
  const MAC_CLEANUP_KEYS = new Set(["staleApps", "staleDmgVolumes", "lsclean"]);
205
208
 
@@ -221,7 +224,26 @@ function macCleanupSpec(value) {
221
224
  * `updater: true | "<app-relative .tar.gz>"` emits the Tauri updater tarball and its signature from the signed, stapled .app:
222
225
  * it prepends a `create-mac-updater` command to `postSign` (default output `<App>.app.tar.gz` beside the app's working root).
223
226
  */
224
- export function expandMacPackageTarget(target, { appRoot, env = process.env, resolveTargetRoot = cargoResolveTargetRoot, managedRoots }) {
227
+ /**
228
+ * `stageSidecars: true | { config?, receipt? }`: stage the release config's declarative `sidecars` for the build's triple through this
229
+ * package's own `right-release stage-sidecar --target <triple>` as the first prepare step. For `universal-apple-darwin` stage-sidecar
230
+ * builds each slice (aarch64 and x86_64), `lipo -create`s them into `<name>-universal-apple-darwin` (the name Tauri's universal
231
+ * build expects) and refuses a result without both archs; recipe-build then re-checks every externalBin in the receipt
232
+ * (`receipt`, default src-tauri/binaries/stage-sidecar.json) with `lipo -archs` (exactly arm64 + x86_64) before the Tauri build.
233
+ */
234
+ function stageSidecarsSpec(value) {
235
+ if (value === undefined || value === false) return undefined;
236
+ if (value !== true && !isObject(value)) fail("targets.mac.macPackage.stageSidecars must be true or { config?, receipt? }");
237
+ const spec = value === true ? {} : value;
238
+ const unknownKeys = Object.keys(spec).filter((key) => !STAGE_SIDECARS_KEYS.has(key));
239
+ if (unknownKeys.length) fail(`targets.mac.macPackage.stageSidecars has unknown key(s): ${unknownKeys.join(", ")} (allowed: ${[...STAGE_SIDECARS_KEYS].join(", ")})`);
240
+ return {
241
+ ...(spec.config === undefined ? {} : { config: relativeInside(spec.config, "targets.mac.macPackage.stageSidecars.config") }),
242
+ receipt: spec.receipt === undefined ? DEFAULT_SIDECAR_RECEIPT : relativeInside(spec.receipt, "targets.mac.macPackage.stageSidecars.receipt"),
243
+ };
244
+ }
245
+
246
+ export function expandMacPackageTarget(target, { appRoot, config = {}, env = process.env, resolveTargetRoot = cargoResolveTargetRoot, managedRoots }) {
225
247
  if (!target?.macPackage) return withoutRecipe(target);
226
248
  const spec = target.macPackage;
227
249
  if (!isObject(spec)) fail("targets.mac.macPackage must be an object");
@@ -247,6 +269,25 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
247
269
  }
248
270
  const override = { bundle: { createUpdaterArtifacts: false, macOS: { signingIdentity: null } } };
249
271
  const extras = buildExtras(spec, "targets.mac.macPackage", { postBuild: true });
272
+ const sidecars = stageSidecarsSpec(spec.stageSidecars);
273
+ if (sidecars) {
274
+ // First prepare step: stage the declared sidecars for this triple (universal: both slices, lipo, <name>-universal-apple-darwin).
275
+ extras.prepare = [{ cmd: "right-release", args: ["stage-sidecar", "--target", triple, ...(sidecars.config ? ["--config", sidecars.config] : [])] }, ...extras.prepare];
276
+ }
277
+ let helpers;
278
+ if (spec.nativeHelpers !== undefined) {
279
+ try { helpers = validateNativeHelpers(spec.nativeHelpers, { label: "targets.mac.macPackage.nativeHelpers" }); } catch (error) {
280
+ if (error instanceof NativeHelpersError) fail(error.message);
281
+ throw error;
282
+ }
283
+ // A universal app may not carry a thin helper: it would compile, sign and notarize and then fail on half the Macs.
284
+ const thin = triple === "universal-apple-darwin" ? helpers.filter((helper) => !helper.universal) : [];
285
+ if (thin.length) fail(`targets.mac.macPackage.nativeHelpers: ${thin.map((helper) => helper.name).join(", ")} must declare universal: true (this build's triple is universal-apple-darwin)`);
286
+ if (helpers.some((helper) => helper.kind === "app") && (typeof config?.version !== "string" || !/^\d+(\.\d+){0,2}$/.test(config.version))) fail("targets.mac.macPackage.nativeHelpers: a kind \"app\" helper needs the release config version (1-3 numeric parts) for its Info.plist");
287
+ const collisions = Object.keys(nativeHelperNestedCode(helpers)).filter((key) => isObject(target.sign?.nestedCode) && Object.hasOwn(target.sign.nestedCode, key));
288
+ if (collisions.length) fail(`targets.mac.macPackage.nativeHelpers: sign.nestedCode must not redeclare ${collisions.join(", ")}; declare entitlements on the helper entry`);
289
+ if (isObject(target.sign?.entitlements) ? helpers.some((helper) => Object.hasOwn(target.sign.entitlements, helper.embed)) : false) fail("targets.mac.macPackage.nativeHelpers: sign.entitlements must not redeclare a helper's embed path; declare entitlements on the helper entry");
290
+ }
250
291
  const args = ["exec", "tauri", "build", "--target", triple, "--bundles", "app", "--no-sign", ...configArgs(extras, appRoot, cwd), "--config", JSON.stringify(override)];
251
292
  if (spec.features?.length) args.push("--features", spec.features.join(","));
252
293
  args.push(...profileArgs(extras));
@@ -270,7 +311,10 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
270
311
  // An entry inside another staged entry (a file within the .app) is covered by copying the outer one.
271
312
  const macEntries = [...mapPath.staged.values()].filter((entry) => ![...mapPath.staged.values()].some((outer) => outer !== entry && entry.to.startsWith(`${outer.to}/`)));
272
313
  const macStage = macEntries.length ? { post: macEntries.map((entry) => ({ ...entry, fresh: true })), resolve: mapPath.resolve() } : undefined;
273
- const wrapped = extras.prepare.length || extras.postBuild.length || cleanup || macStage;
314
+ const wrapped = extras.prepare.length || extras.postBuild.length || cleanup || macStage || helpers;
315
+ const forceArch = helpers && triple !== "universal-apple-darwin" ? (triple === "aarch64-apple-darwin" ? "arm64" : "x86_64") : undefined;
316
+ const nativeHelpersPlan = helpers ? { app, helpers, ...(config?.version ? { version: config.version } : {}), ...(forceArch ? { forceArch } : {}), configuration: "release" } : undefined;
317
+ const verifySidecars = sidecars && triple === "universal-apple-darwin" ? { receipt: sidecars.receipt, target: triple } : undefined;
274
318
  const { macPackage, recipe: _ignoredMac, ...rest } = target;
275
319
  // The updater tarball is made from the signed .app, so it runs in postSign, ahead of any postSign the app declares.
276
320
  const updaterOutput = spec.updater === true ? `${productName}.app.tar.gz` : typeof spec.updater === "string" ? relativeInside(spec.updater, "targets.mac.macPackage.updater") : undefined;
@@ -283,8 +327,9 @@ export function expandMacPackageTarget(target, { appRoot, env = process.env, res
283
327
  return {
284
328
  ...rest,
285
329
  ...postSign,
286
- prePackage: wrapped ? recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stage: macStage }) : { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
287
- sign: { ...(target.sign ?? {}), prePackageFiles },
330
+ prePackage: wrapped ? recipeBuildCommand({ extras, build: { cmd: "pnpm", args, cwd }, env: QUIET_TAURI_ENV, cleanup, stage: macStage, verifySidecars, nativeHelpers: nativeHelpersPlan }) : { cmd: "pnpm", args, cwd, env: QUIET_TAURI_ENV },
331
+ // Helpers are signed by release.mjs inside-out through sign.nestedCode (deepest path first, hardened runtime, each helper's own entitlements), then the app.
332
+ sign: { ...(target.sign ?? {}), ...(helpers ? { nestedCode: { ...nativeHelperNestedCode(helpers), ...(target.sign?.nestedCode ?? {}) } } : {}), prePackageFiles },
288
333
  notarize: { ...target.notarize, app, appBeforePackage: true },
289
334
  dmg: isObject(target.dmg) ? target.dmg : {},
290
335
  recipe: { kind: "macPackage", universal: triple === "universal-apple-darwin" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.6.7",
3
+ "version": "0.6.10",
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",
@@ -19,6 +19,7 @@
19
19
  "./direct-bootstrap.mjs": "./direct-bootstrap.mjs",
20
20
  "./disk-policy.mjs": "./disk-policy.mjs",
21
21
  "./dmg-build.mjs": "./dmg-build.mjs",
22
+ "./door.mjs": "./door.mjs",
22
23
  "./engine-locator.mjs": "./engine-locator.mjs",
23
24
  "./github-release.mjs": "./github-release.mjs",
24
25
  "./legal-contract.mjs": "./legal-contract.mjs",
@@ -77,7 +78,8 @@
77
78
  "!*.test.mjs",
78
79
  "!cli/*.test.mjs",
79
80
  "!standalone-clone-evidence.json",
80
- "!refusals.json"
81
+ "!refusals.json",
82
+ "!provenance-real-receipt.fixture.json"
81
83
  ],
82
84
  "sideEffects": false,
83
85
  "publishConfig": {
@@ -90,7 +92,7 @@
90
92
  "directory": "packages/release"
91
93
  },
92
94
  "dependencies": {
93
- "@rightkit/outdated": "0.2.0",
95
+ "@rightkit/outdated": "0.3.0",
94
96
  "smol-toml": "1.9.1"
95
97
  },
96
98
  "scripts": {