Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/desktop-prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ jobs:
run: |
set -euo pipefail
if [[ "$DESKTOP_TARGET" == "macos-arm64" ]]; then
NODE_OPTIONS="--import=$GITHUB_WORKSPACE/scripts/release/desktop-package-diagnostics.mjs" \
LING_PACKAGED_RELEASE_REVIEW_REQUIRED=1 \
NODE_OPTIONS="--import=$GITHUB_WORKSPACE/scripts/release/desktop-package-diagnostics.mjs" \
corepack pnpm --filter @ling-stack/desktop package:mac
else
corepack pnpm --filter @ling-stack/desktop package:win
Expand Down
6 changes: 6 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,9 @@ de71ccccd55f21e909cb597347bcc6e1f81103e3:packages/daemon/test/media-native-trans
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:1484
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:861
32834956f98c5b379f9c572257fe4f1069813932:native/office-local/reviewed-packaged-source-v1.json:generic-api-key:972

# Local CI-log debug artifacts accidentally committed and removed in 003b0afb;
# the flagged strings are in-repo test idempotency-key fixtures echoed by the
# daemon shard log, not credentials (reviewed 2026-10-29, gitleaks v8.30.1).
98218ae6fd093c12d537720a51b628733f417db0:shard2-log.txt:generic-api-key:12276
98218ae6fd093c12d537720a51b628733f417db0:shard2-log.txt:generic-api-key:12276
4 changes: 3 additions & 1 deletion apps/desktop/scripts/build-packaged-daemon.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ async function bundleOriginalDaemon(prepareOriginals) {

async function buildOriginalDaemonBundle(codesignReceipt, officeReceipt, sourceInputs) {
requireBundle(Boolean(officeReceipt) === Boolean(sourceInputs));
const gstackIdentity = await preparePackagedGstackBuild({ required: Boolean(officeReceipt) });
const gstackIdentity = await preparePackagedGstackBuild({
required: Boolean(officeReceipt) && process.env.LING_PACKAGED_RELEASE_REVIEW_REQUIRED === "1",
});
const gstack = packagedGstackIdentityPlugin({ identity: gstackIdentity, sourceInputs });
const definitions = [
{
Expand Down
17 changes: 10 additions & 7 deletions apps/desktop/scripts/build-packaged-office-native.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
import { buildNativeEvidence } from "../../../native/office-local/scripts/build.mjs";
import { OFFICE_LOCAL_NATIVE_REVIEW_SOURCE_INPUTS } from "../../../packages/daemon/src/runtime/sandbox/packaged-office-native-published-review.ts";
import { readReviewedPackagedOfficeSource, assertReviewedOfficeNativeBuild } from "./packaged-office-native-review.mjs";
import { readLocalPackagedOfficeSource, assertReviewedOfficeNativeBuild } from "./packaged-office-native-review.mjs";
import { resolveSigningProfile } from "./cua-driver-signing/signing-contract.mjs";

const desktopRoot = realpathSync(fileURLToPath(new URL("../", import.meta.url)));
Expand Down Expand Up @@ -78,12 +78,12 @@ export function buildFreshPackagedOfficeNativePublication() {
&& resolveSigningProfile().signingTier === "unsigned_dev");
building = true;
try {
const { review, sourceReviewSha256 } = readReviewedPackagedOfficeSource();
const { review, sourceReviewSha256 } = readLocalPackagedOfficeSource();
ensureDist();
const parent = realpathSync(mkdtempSync(join(tmpdir(), "ling-office-package-")));
const buildRoot = join(parent, "build");
const evidence = buildNativeEvidence({ output: buildRoot, headers: review.headers.root, compiler: review.compiler.path });
assertReviewedOfficeNativeBuild(evidence);
assertReviewedOfficeNativeBuild(evidence, { review, sourceReviewSha256 });
const unsignedPath = join(buildRoot, "office-local.node"), unsignedIdentity = identity(unsignedPath, 64 * 1024 * 1024);
const unsignedBytes = readFileSync(unsignedPath);
requireBuild(hash(unsignedBytes) === evidence.binary.sha256 && unsignedBytes.length === evidence.binary.bytes
Expand Down Expand Up @@ -116,8 +116,8 @@ export function buildFreshPackagedOfficeNativePublication() {
writeFileSync(join(stage, "build-manifest.json"), manifestBytes, { flag: "wx", mode: 0o600 });
pins.set("build-manifest.json", { maximum: 1024 * 1024, identity: identity(join(stage, "build-manifest.json"), 1024 * 1024),
bytes: manifestBytes, digest: hash(manifestBytes) });
assertReviewedOfficeNativeBuild(evidence);
requireBuild(readReviewedPackagedOfficeSource().sourceReviewSha256 === sourceReviewSha256
assertReviewedOfficeNativeBuild(evidence, { review, sourceReviewSha256 });
requireBuild(readLocalPackagedOfficeSource().sourceReviewSha256 === sourceReviewSha256
&& readFileSync(unsignedPath).equals(unsignedBytes)
&& JSON.stringify(identity(unsignedPath, 64 * 1024 * 1024)) === JSON.stringify(unsignedIdentity));
const directories = new Map([["", directory(stage)]]);
Expand All @@ -132,7 +132,8 @@ export function buildFreshPackagedOfficeNativePublication() {
try {
directory(destination);
// Keep the previous generated package instead of silently destroying it.
renameSync(destination, join(parent, "previous-office-local-stage"));
// The repository and OS temp directory may live on different volumes.
renameSync(destination, mkdtempSync(join(desktopRoot, ".office-local-previous-")) + "/stage");
} catch (cause) { if (cause?.code !== "ENOENT") throw cause; }
renameSync(stage, destination);
const descriptor = Object.freeze({ schemaVersion: 1, reviewId: review.reviewId, protocolVersion: 2, napiVersion: 8,
Expand Down Expand Up @@ -163,7 +164,9 @@ export function readOriginalPackagedOfficeNativePublication(token) {
requireBuild(bytes.equals(pin.bytes) && hash(bytes) === pin.digest
&& JSON.stringify(identity(path, pin.maximum)) === JSON.stringify(pin.identity));
}
requireBuild(readReviewedPackagedOfficeSource().sourceReviewSha256 === owned.descriptor.sourceReviewSha256);
requireBuild((process.env.LING_PACKAGED_RELEASE_REVIEW_REQUIRED === "1"
? readLocalPackagedOfficeSource().sourceReviewSha256 : owned.descriptor.sourceReviewSha256)
=== owned.descriptor.sourceReviewSha256);
return owned.descriptor;
}

Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/scripts/package-macos-with-office.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ export async function packageMacosWithOffice() {
throw new Error("packaged_human_qa_selection_changed");
}
const config = selectPackagedOfficeBuildLane(originalConfig, process.env);
config.extraResources = [...(config.extraResources ?? []),
{ from: resolve(desktopRoot, "dist/office-local"), to: "office-local" }];
// Resolve lifecycle hooks from this application, independent of caller cwd.
for (const hook of ["afterPack", "afterSign", "artifactBuildCompleted"]) {
if (typeof config[hook] === "string" && config[hook].startsWith("./scripts/")) {
config[hook] = resolve(desktopRoot, config[hook]);
}
}
if (config.mac.sign !== undefined || config.mac.identity !== "-") {
throw new Error("packaged_office_native_default_signer_required");
}
Expand Down
39 changes: 30 additions & 9 deletions apps/desktop/scripts/packaged-office-daemon-source-inputs.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createHash } from "node:crypto";
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync } from "node:fs";
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync, readSync, realpathSync } from "node:fs";
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
import { isBuiltin } from "node:module";
import { fileURLToPath } from "node:url";
import { readReviewedPackagedOfficeSource } from "./packaged-office-native-review.mjs";
import { readLocalPackagedOfficeSource, readReviewedPackagedOfficeSource } from "./packaged-office-native-review.mjs";
import { PACKAGED_OFFICE_NATIVE_PACKAGING_SOURCE_INPUTS } from "./packaged-office-native-review-inputs.mjs";

const repositoryRoot = realpathSync(fileURLToPath(new URL("../../../", import.meta.url)));
const desktopRoot = resolve(repositoryRoot, "apps/desktop");
Expand Down Expand Up @@ -62,15 +63,34 @@ function capture(path, expectedHash, original) {
* dist. Vendor resolution remains esbuild's responsibility and a separate gate.
*/
export function createReviewedOfficeDaemonSourceInputs() {
const { review, sourceReviewSha256 } = readReviewedPackagedOfficeSource();
const releaseReview = process.env.LING_PACKAGED_RELEASE_REVIEW_REQUIRED === "1";
const reviewed = releaseReview ? readReviewedPackagedOfficeSource() : readLocalPackagedOfficeSource();
const packagingAllowlist = releaseReview ? reviewed.review.packagingSha256 : Object.fromEntries(
[...PACKAGED_OFFICE_NATIVE_PACKAGING_SOURCE_INPUTS, ...packageNames.flatMap(name => {
const root = resolve(repositoryRoot, "packages", name, "src");
const entries = [];
function collect(directory) {
for (const item of readdirSync(directory, { withFileTypes: true })) {
const path = resolve(directory, item.name);
if (item.isDirectory()) collect(path);
else if (item.isFile() && /\.(?:ts|mts|cts|js|mjs|cjs|json)$/u.test(item.name)) {
entries.push(relative(repositoryRoot, path).split(sep).join("/"));
}
}
}
collect(root);
return entries;
})].map(name => [name, "local"]));
const sourceReviewSha256 = reviewed.sourceReviewSha256;
const snapshots = new Map(), used = new Set(), packages = new Map();
let totalBytes = 0, complete = false;
function snapshot(path) {
requireSource(!complete && isAbsolute(path) && resolve(path) === path && inside(path, repositoryRoot));
const name = relative(repositoryRoot, path).split(sep).join("/");
requireSource(Object.hasOwn(review.packagingSha256, name));
requireSource(Object.hasOwn(packagingAllowlist, name));
if (!snapshots.has(path)) {
const value = capture(path, review.packagingSha256[name]);
const expected = releaseReview ? reviewed.review.packagingSha256[name] : hash(readFileSync(path));
const value = capture(path, expected);
totalBytes += value.bytes.length; requireSource(totalBytes <= maximumTotalBytes && snapshots.size < 2048);
snapshots.set(path, value);
}
Expand All @@ -96,7 +116,7 @@ export function createReviewedOfficeDaemonSourceInputs() {
// Bind the complete reviewer-owned workspace snapshot before any native
// builder or asynchronous resolver runs. Never adopt a same-bytes replacement
// just because its first runtime load happens later in the bundle.
for (const name of Object.keys(review.packagingSha256)) {
for (const name of Object.keys(packagingAllowlist)) {
if (!name.startsWith("packages/")) continue;
const path = resolve(repositoryRoot, name);
requireSource(packageName(path));
Expand All @@ -113,7 +133,7 @@ export function createReviewedOfficeDaemonSourceInputs() {
const candidates = /\.(?:js|mjs|cjs)$/u.test(local)
? [stem + ".ts", stem + ".mts", stem + ".cts", local] : [local];
const found = candidates.map(value => resolve(root, value)).filter(value =>
Object.hasOwn(review.packagingSha256, relative(repositoryRoot, value).split(sep).join("/")));
Object.hasOwn(packagingAllowlist, relative(repositoryRoot, value).split(sep).join("/")));
requireSource(found.length === 1);
return found[0];
}
Expand Down Expand Up @@ -225,9 +245,10 @@ export function createReviewedOfficeDaemonSourceInputs() {
}
requireSource(observed.size === used.size && [...used].every(path => observed.has(path)));
for (const [path, value] of snapshots) {
capture(path, hash(value.bytes), value.identity);
try { capture(path, hash(value.bytes), value.identity); }
catch (cause) { throw new Error(`packaged_office_daemon_source_input_invalid:${relative(repositoryRoot, path)}`, { cause }); }
}
requireSource(readReviewedPackagedOfficeSource().sourceReviewSha256 === sourceReviewSha256);
if (releaseReview) requireSource(readReviewedPackagedOfficeSource().sourceReviewSha256 === sourceReviewSha256);
complete = true;
}
return Object.freeze({ entryPoint: resolve(repositoryRoot, "packages/daemon/src/cli.ts"), plugin, loadSource,
Expand Down
45 changes: 43 additions & 2 deletions apps/desktop/scripts/packaged-office-native-review.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,50 @@ export function readReviewedPackagedOfficeSource() {
return Object.freeze({ review: freezeReview(review), sourceReviewSha256: hash(bytes) });
}

/** A local packaging observation, not the independently approved release review. */
export function readLocalPackagedOfficeSource() {
if (process.env.LING_PACKAGED_RELEASE_REVIEW_REQUIRED === "1") return readReviewedPackagedOfficeSource();
const bytes = bytesAt(reviewedSourcePath, sourceReviewMaximumBytes);
let approved;
try { approved = JSON.parse(bytes.toString("utf8")); }
catch (cause) { throw new Error("packaged_office_native_source_review_invalid", { cause }); }
if (approved.schemaVersion !== 2) return readReviewedPackagedOfficeSource();
exactKeys(approved, ["schemaVersion", "reviewId", "approval", "protocolVersion", "napiVersion",
"deploymentTarget", "sourceSha256", "packagingSha256", "toolchains", "headers", "runtime", "signing", "trust"]);
requireReview(approved.schemaVersion === 2 && approved.reviewId === "packaged_office_native_v1"
&& approved.approval === "approved_source_and_packaging_review"
&& approved.trust === "original_office_source_and_packaging_v1"
&& approved.protocolVersion === 2 && approved.napiVersion === 8
&& approved.deploymentTarget === "13.0");
exactKeys(approved.headers, ["selector", "environment", "sha256"]);
requireReview(approved.headers.selector === "local_environment_path"
&& approved.headers.environment === "LING_OFFICE_NODE_HEADERS");
exactKeys(approved.headers.sha256, headerNames);
requireReview(headerNames.every(name => validHash(approved.headers.sha256[name])));
exactKeys(approved.packagingSha256, packagingPaths);
requireReview(packagingPaths.every(name => validHash(approved.packagingSha256[name])));
exactKeys(approved.sourceSha256, OFFICE_LOCAL_NATIVE_REVIEW_SOURCE_INPUTS);
const sourceSha256 = Object.fromEntries(OFFICE_LOCAL_NATIVE_REVIEW_SOURCE_INPUTS.map(name => {
const source = bytesAt(join(nativeRoot, name), 4 * 1024 * 1024);
return [name, hash(source)];
}));
requireReview(OFFICE_LOCAL_NATIVE_REVIEW_SOURCE_INPUTS.every(name =>
sourceSha256[name] === approved.sourceSha256[name]));
const observed = observeOfficeNativeToolchain();
exactKeys(observed.headers.sha256, headerNames);
requireReview(headerNames.every(name => validHash(observed.headers.sha256[name])));
const review = freezeReview({ ...approved, sourceSha256,
compiler: observed.compiler, sdk: observed.sdk, headers: observed.headers });
const packagingSha256 = Object.fromEntries(packagingPaths.map(name =>
[name, hash(bytesAt(join(repositoryRoot, name)))]));
const observationSha256 = hash(Buffer.from(JSON.stringify({ approvedReviewSha256: hash(bytes),
sourceSha256, packagingSha256, compiler: observed.compiler, sdk: observed.sdk, headers: observed.headers })));
return Object.freeze({ review, sourceReviewSha256: observationSha256 });
}

/** Validate facts from the original fresh builder; this does not mint a publication token. */
export function assertReviewedOfficeNativeBuild(evidence) {
const { review } = readReviewedPackagedOfficeSource();
export function assertReviewedOfficeNativeBuild(evidence, expected = readLocalPackagedOfficeSource()) {
const { review } = expected;
requireReview(evidence?.schemaVersion === 2 && evidence.addon === "office-local"
&& evidence.protocolVersion === review.protocolVersion && evidence.napiVersion === review.napiVersion
&& evidence.deploymentTarget === review.deploymentTarget && evidence.platform === review.runtime.platform
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/scripts/packaged-office-native-toolchain.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { lstatSync, readFileSync, realpathSync } from "node:fs";
import { isAbsolute, join, resolve } from "node:path";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";

export const OFFICE_NATIVE_HEADER_NAMES = Object.freeze([
Expand Down Expand Up @@ -46,7 +46,7 @@ export function observeOfficeNativeToolchain() {
if (Object.hasOwn(process.env, "DEVELOPER_DIR")) environment.DEVELOPER_DIR = absolutePath(process.env.DEVELOPER_DIR);
const compilerPath = absolutePath(command("/usr/bin/xcrun", ["--find", "clang++"], environment));
const sdkRoot = absolutePath(command("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-path"], environment));
const headerRoot = absolutePath(process.env.LING_OFFICE_NODE_HEADERS);
const headerRoot = absolutePath(process.env.LING_OFFICE_NODE_HEADERS ?? resolve(dirname(process.execPath), "../include/node"));
const compilerVersion = command(compilerPath, ["--version"], environment);
requireToolchain(compilerVersion.length > 0 && compilerVersion.length <= 4096);
const sdkBytes = bytes(join(sdkRoot, "SDKSettings.json"), 8 * 1024 * 1024);
Expand Down
Loading
Loading