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
112 changes: 94 additions & 18 deletions packages/shared/src/generatedBuildOwnership.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,24 +81,79 @@ it("reclaims unchanged staged emissions, retains additions/modified files, and r
);
});

it("never claims preexisting matching output or overwrites unknown collisions", () => {
const { cwd, emit } = fixture();
const root = "apps/server/dist";
emit(NodePath.join(cwd, root), "same.js", "same emission");
emit(NodePath.join(cwd, root), "notes.js", "unknown addition");
const stage = createGeneratedBuildStage(cwd, root)!;
emit(stage.directory, "same.js", "same emission");
stage.publish(100);
assert.deepEqual(inspectGeneratedBuildOwnership(cwd, root)!.files, []);
const collision = createGeneratedBuildStage(cwd, root)!;
emit(collision.directory, "notes.js", "different emission");
assert.throws(() => collision.publish(200), "requires file review");
collision.discard();
assert.equal(
NodeFS.readFileSync(NodePath.join(cwd, root, "notes.js"), "utf8"),
"unknown addition",
);
});
it.each(["absent", "device", "inode", "birthtime", "path"] as const)(
"never claims preexisting matching output or overwrites unknown collisions (prior owner: %s)",
(identityChange) => {
const { cwd, metadata, emit } = fixture();
const root = "apps/server/dist";
if (identityChange !== "absent") {
const prior = createGeneratedBuildStage(cwd, root)!;
emit(prior.directory, "same.js", "same emission");
emit(prior.directory, "notes.js", "unknown addition");
emit(prior.directory, "obsolete.js", "prior output");
prior.publish(50);
const previous = inspectGeneratedBuildOwnership(cwd, root)!;
const recordPath = NodePath.join(
metadata,
NodeFS.readdirSync(metadata).find(
(file) => file.startsWith("t3-build-") && file.endsWith(".json"),
)!,
);
const record = JSON.parse(NodeFS.readFileSync(recordPath, "utf8"));
const stat = NodeFS.lstatSync(cwd);
const allocation = [stat.dev, stat.ino, stat.birthtimeMs];
if (identityChange !== "path") {
const index = { device: 0, inode: 1, birthtime: 2 }[identityChange];
allocation[index]! += 1;
}
record.worktreeIdentity = `${identityChange === "path" ? `${cwd}-prior` : cwd}:${JSON.stringify(allocation)}`;
const stale = JSON.stringify(record);
NodeFS.writeFileSync(recordPath, stale);
assert.isNull(inspectGeneratedBuildOwnership(cwd, root));
assert.isFalse(recordGeneratedBuildUse(cwd, root, 75));
assert.equal(NodeFS.readFileSync(recordPath, "utf8"), stale);
assert.throws(
() => removeGeneratedBuildFiles(cwd, root, previous.token, ["same.js"]),
"ownership changed",
);
const changed = createGeneratedBuildStage(cwd, root)!;
emit(changed.directory, "same.js", "changed compiler emission");
assert.throws(() => changed.publish(75), "requires file review");
changed.discard();
assert.equal(
NodeFS.readFileSync(NodePath.join(cwd, root, "same.js"), "utf8"),
"same emission",
);
assert.equal(NodeFS.readFileSync(recordPath, "utf8"), stale);
}
emit(NodePath.join(cwd, root), "same.js", "same emission");
emit(NodePath.join(cwd, root), "notes.js", "unknown addition");
const stage = createGeneratedBuildStage(cwd, root)!;
emit(stage.directory, "same.js", "same emission");
emit(stage.directory, "fresh.js", "fresh emission");
stage.publish(100);
const after = inspectGeneratedBuildOwnership(cwd, root)!;
assert.equal(after.lastUsedAt, 100);
assert.deepEqual(
after.files.map((file) => file.path),
["fresh.js"],
);
if (identityChange !== "absent") {
assert.equal(
NodeFS.readFileSync(NodePath.join(cwd, root, "obsolete.js"), "utf8"),
"prior output",
);
}
const collision = createGeneratedBuildStage(cwd, root)!;
emit(collision.directory, "notes.js", "different emission");
assert.throws(() => collision.publish(200), "requires file review");
collision.discard();
assert.equal(
NodeFS.readFileSync(NodePath.join(cwd, root, "notes.js"), "utf8"),
"unknown addition",
);
},
);

it("rejects linked or hardlinked destinations and forged roots; ordinary checkout builds keep their behavior", () => {
const { cwd, temporary, emit } = fixture();
Expand Down Expand Up @@ -180,6 +235,27 @@ it("positive compiled-output use renews tokens, while malformed provenance is ne
const record = NodeFS.readdirSync(metadata).find(
(file) => file.startsWith("t3-build-") && file.endsWith(".json"),
)!;
const recordPath = NodePath.join(metadata, record);
const malformed = JSON.parse(NodeFS.readFileSync(recordPath, "utf8"));
for (const identity of [
"not-an-identity",
`${cwd}:[1,2]`,
`${cwd}:[1,"2",3]`,
`${cwd}:[1,2,null]`,
`${cwd}:[1,2,3,4]`,
"relative:[1,2,3]",
]) {
const invalid = JSON.stringify({ ...malformed, worktreeIdentity: identity });
NodeFS.writeFileSync(recordPath, invalid);
assert.throws(() => createGeneratedBuildStage(cwd, root), "Invalid build ownership record");
assert.equal(NodeFS.readFileSync(recordPath, "utf8"), invalid);
}
malformed.worktreeIdentity = malformed.worktreeIdentity.replace(cwd, `${cwd}-prior`);
malformed.files[0].path = "../retained";
const invalid = JSON.stringify(malformed);
NodeFS.writeFileSync(recordPath, invalid);
assert.throws(() => createGeneratedBuildStage(cwd, root), "Invalid owned build file");
assert.equal(NodeFS.readFileSync(recordPath, "utf8"), invalid);
NodeFS.writeFileSync(NodePath.join(metadata, record), "invalid record");
assert.throws(() => recordGeneratedBuildUse(cwd, root, 300));
assert.equal(NodeFS.readFileSync(NodePath.join(metadata, record), "utf8"), "invalid record");
Expand Down
42 changes: 35 additions & 7 deletions packages/shared/src/generatedBuildOwnership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ const safeRelative = (path: string) =>
!NodePath.posix.isAbsolute(path) &&
path.split("/").every((part) => part !== "" && part !== "." && part !== "..");

function validWorktreeIdentity(value: unknown): value is string {
if (typeof value !== "string") return false;
const separator = value.lastIndexOf(":");
if (separator < 1 || !NodePath.isAbsolute(value.slice(0, separator))) return false;
try {
const identity: unknown = JSON.parse(value.slice(separator + 1));
return (
Array.isArray(identity) &&
identity.length === 3 &&
identity.every((part) => typeof part === "number" && Number.isFinite(part))
);
} catch {
return false;
}
}

function context(worktreePath: string, root: string) {
if (!allowedRoot(root)) throw new Error("Unrecognized generated build root");
const cwd = NodeFS.realpathSync(worktreePath);
Expand Down Expand Up @@ -187,23 +203,30 @@ function readRecord(ctx: Context, allowPending = false) {
throw new Error("Unsafe build ownership record");
const raw = NodeFS.readFileSync(ctx.recordPath, "utf8");
const value: unknown = JSON.parse(raw);
validateRecord(ctx, value);
validateRecord(ctx, value, false);
if (signature(NodeFS.lstatSync(ctx.recordPath)) !== signature(stat))
throw new Error("Build ownership changed during observation");
// Native identities can go stale across boots; stale provenance grants no ownership.
if (value.worktreeIdentity !== ctx.worktreeIdentity) return null;
return {
record: value as BuildRecord,
recordIdentity: signature(stat),
token: NodeCrypto.createHash("sha256").update(raw).update(signature(stat)).digest("hex"),
};
}
function validateRecord(ctx: Context, value: unknown): asserts value is BuildRecord {
function validateRecord(
ctx: Context,
value: unknown,
requireCurrentOwner = true,
): asserts value is BuildRecord {
if (
typeof value !== "object" ||
value === null ||
!("version" in value) ||
value.version !== 1 ||
!("worktreeIdentity" in value) ||
value.worktreeIdentity !== ctx.worktreeIdentity ||
!validWorktreeIdentity(value.worktreeIdentity) ||
(requireCurrentOwner && value.worktreeIdentity !== ctx.worktreeIdentity) ||
!("root" in value) ||
value.root !== ctx.root ||
!("lastUsedAt" in value) ||
Expand Down Expand Up @@ -1069,7 +1092,9 @@ export function createGeneratedBuildStage(worktreePath: string, root: string) {
return { file, destination, prior, skip: false };
if (existing.hash === file.hash)
return { file, destination, prior: undefined, skip: true };
throw new Error(`Build output collision requires file review: ${root}/${file.path}`);
throw new Error(
`Build output collision requires file review: ${root}/${file.path}. The existing file differs from the new build output and no current build owns these bytes, so it was left in place. Review it or move it aside, then rebuild.`,
);
} catch (error) {
if (missing(error)) return { file, destination, prior: undefined, skip: false };
throw error;
Expand Down Expand Up @@ -1221,7 +1246,7 @@ function rootAllocation(ctx: Context) {
return allocation(stat);
}

function readReview(ctx: Context): ReviewSnapshot | null {
function readReview(ctx: Context, includeStaleObservation = false): ReviewSnapshot | null {
const path = reviewPath(ctx);
let fd;
try {
Expand All @@ -1240,7 +1265,8 @@ function readReview(ctx: Context): ReviewSnapshot | null {
typeof value !== "object" ||
value === null ||
value.version !== 1 ||
value.worktreeIdentity !== ctx.worktreeIdentity ||
!validWorktreeIdentity(value.worktreeIdentity) ||
(value.protected && value.worktreeIdentity !== ctx.worktreeIdentity) ||
value.root !== ctx.root ||
typeof value.rootAllocation !== "string" ||
typeof value.lastUsedAt !== "number" ||
Expand Down Expand Up @@ -1277,6 +1303,8 @@ function readReview(ctx: Context): ReviewSnapshot | null {
signature(NodeFS.fstatSync(fd)) !== signature(stat)
)
throw new Error("Build review changed during observation");
// Observation-only clocks hold no consent; retained protection still binds to its owner.
if (value.worktreeIdentity !== ctx.worktreeIdentity && !includeStaleObservation) return null;
return {
record: value as ReviewRecord,
token: digest(raw + signature(stat)),
Expand Down Expand Up @@ -1414,7 +1442,7 @@ export function generatedBuildReviewProtection(worktreePath: string, relativeCan
digest(value.root) !== match[1]
)
return true;
const previous = readReview({ ...ctx, root: value.root });
const previous = readReview({ ...ctx, root: value.root }, true);
if (!previous) return true;
if (!previous.record.protected) continue;
const root = previous.record.root;
Expand Down
49 changes: 49 additions & 0 deletions packages/shared/src/generatedBuildReview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,51 @@ it("starts a conservative first-observation clock, never adopts mtimes or compil
assert.isFalse(generatedBuildReviewProtection(cwd, "node_modules"));
});

it.each(["observe", "rebuild"] as const)(
"stale compiler observations retain no consent across another identity change (%s)",
(operation) => {
const { cwd, metadata, output, receipt } = fixture();
const compiler = createGeneratedBuildStage(cwd, root)!;
NodeFS.writeFileSync(NodePath.join(compiler.directory, "owned.js"), "prior compiler output");
compiler.publish(100);
const recordPath = NodePath.join(
metadata,
NodeFS.readdirSync(metadata).find(
(name) => name.startsWith("t3-build-") && name.endsWith(".json"),
)!,
);
const record = JSON.parse(NodeFS.readFileSync(recordPath, "utf8"));
const stat = NodeFS.lstatSync(cwd);
record.worktreeIdentity = `${cwd}:${JSON.stringify([stat.dev + 1, stat.ino, stat.birthtimeMs])}`;
NodeFS.writeFileSync(recordPath, JSON.stringify(record));
const observation = observeLegacyBuild(cwd, root, 200)!;
assert.isFalse(generatedBuildReviewProtection(cwd));

const reviewPath = receipt();
const review = JSON.parse(NodeFS.readFileSync(reviewPath, "utf8"));
review.worktreeIdentity = `${cwd}:${JSON.stringify([stat.dev + 2, stat.ino, stat.birthtimeMs])}`;
NodeFS.writeFileSync(reviewPath, JSON.stringify(review));
assert.isFalse(generatedBuildReviewProtection(cwd));
assert.isNull(legacyBuildActivity(cwd, root));
if (operation === "observe") {
const refreshed = observeLegacyBuild(cwd, root, 300)!;
assert.equal(refreshed.lastUsedAt, 300);
assert.notEqual(refreshed.token, observation.token);
assert.isFalse(refreshed.hasSelection);
}
const rebuilt = createGeneratedBuildStage(cwd, root)!;
NodeFS.writeFileSync(NodePath.join(rebuilt.directory, "fresh.js"), "fresh compiler output");
rebuilt.publish(400);
const owned = inspectGeneratedBuildOwnership(cwd, root)!;
assert.equal(removeGeneratedBuildFiles(cwd, root, owned.token, ["fresh.js"]).files, 1);
assert.equal(
NodeFS.readFileSync(NodePath.join(output, "owned.js"), "utf8"),
"prior compiler output",
);
assert.isFalse(generatedBuildReviewProtection(cwd));
},
);

it("removes only unchanged selected files once and preserves changed, recreated, new and unselected files", async () => {
const { cwd, emit, output, approve } = fixture();
emit("edited.js");
Expand Down Expand Up @@ -179,6 +224,10 @@ it("malformed, unreadable and mismatched markers fail closed without any survivi
valid.replace(/"worktreeIdentity":"[^"]+"/, '"worktreeIdentity":"replacement"'),
);
assert.isTrue(generatedBuildReviewProtection(cwd, "node_modules"));
const stale = JSON.parse(valid);
stale.worktreeIdentity = stale.worktreeIdentity.replace(cwd, `${cwd}-prior`);
NodeFS.writeFileSync(path, JSON.stringify(stale));
assert.isTrue(generatedBuildReviewProtection(cwd));
});

it("context changes and positive actual use revoke authority while preview/save do not renew age", async () => {
Expand Down
Loading