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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,16 @@ deleting files. For terminal input, pass `inputExample` (such as
Behavior changes in 0.7.0: a request to a socket that doesn't exist fails with
`ControlSocketError` code `control-unavailable` (the original `ENOENT` is its
`cause`), and reading protected input from a terminal says to pipe or redirect
the value in. Each subpath is bundled separately, so check `error.code` or
`error.name` rather than `instanceof` across entrypoints.
the value in.

Changes in 0.8.0: each error class has one identity across entrypoints, so an
error thrown through `@hraness/local-custody/control-socket` passes
`instanceof` against the class imported from the package root (the build
shares modules as chunks instead of copying them into each subpath). The Rust
crate's `read_protected_descriptor` accepts a pipe or socket, like the
TypeScript reader, so `pbpaste | <cli> login --stdin` works in Rust CLIs; a
regular file must still be owned by you and private, and other descriptor
kinds are still refused.

The Rust crate has the same copy through
`describe_error(code, &DescribeOptions { .. })` and `CustodyError::describe`;
Expand Down
279 changes: 5 additions & 274 deletions dist/atomic-publish.js
Original file line number Diff line number Diff line change
@@ -1,278 +1,9 @@
// src/private-paths.ts
import {
closeSync,
constants,
fstatSync,
lstatSync,
openSync,
readSync,
realpathSync
} from "node:fs";
import { lstat, mkdir, open, realpath } from "node:fs/promises";
import { dirname, resolve } from "node:path";
var PRIVATE_DIRECTORY_MODE = 448;
var PRIVATE_FILE_MODE = 384;
var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
var kindMatches = (metadata, kind) => kind === "file" ? metadata.isFile() : kind === "directory" ? metadata.isDirectory() : metadata.isSocket();
async function assertOwnedPath(path, expectation) {
const metadata = await lstat(path, { bigint: true });
const uid = ownerUid();
const expectedLinks = expectation.links ?? (expectation.kind === "directory" ? undefined : 1n);
if (!kindMatches(metadata, expectation.kind) || metadata.isSymbolicLink() || expectedLinks !== undefined && metadata.nlink !== BigInt(expectedLinks) || uid !== undefined && metadata.uid !== BigInt(uid) || expectation.exactMode !== undefined && (metadata.mode & 0o777n) !== BigInt(expectation.exactMode) || expectation.ownerOnly === true && (metadata.mode & 0o077n) !== 0n || expectation.canonical === true && await realpath(path) !== path || expectation.minimumBytes !== undefined && metadata.size < expectation.minimumBytes || expectation.maximumBytes !== undefined && metadata.size > expectation.maximumBytes) {
throw new Error(`Unsafe local ${expectation.kind}.`);
}
return {
dev: Number(metadata.dev),
ino: Number(metadata.ino),
size: Number(metadata.size)
};
}
function assertOwnedPathSync(path, expectation) {
const metadata = lstatSync(path, { bigint: true });
const uid = ownerUid();
const expectedLinks = expectation.links ?? (expectation.kind === "directory" ? undefined : 1n);
if (!kindMatches(metadata, expectation.kind) || metadata.isSymbolicLink() || expectedLinks !== undefined && metadata.nlink !== BigInt(expectedLinks) || uid !== undefined && metadata.uid !== BigInt(uid) || expectation.exactMode !== undefined && (metadata.mode & 0o777n) !== BigInt(expectation.exactMode) || expectation.ownerOnly === true && (metadata.mode & 0o077n) !== 0n || expectation.canonical === true && realpathSync(path) !== path || expectation.minimumBytes !== undefined && metadata.size < expectation.minimumBytes || expectation.maximumBytes !== undefined && metadata.size > expectation.maximumBytes) {
throw new Error(`Unsafe local ${expectation.kind}.`);
}
return {
dev: Number(metadata.dev),
ino: Number(metadata.ino),
size: Number(metadata.size)
};
}
async function ensurePrivateDirectory(path) {
const absolute = resolve(path);
const parent = dirname(absolute);
if (await realpath(parent) !== parent) {
throw new Error("Directory parent must be physical.");
}
try {
await mkdir(absolute, { mode: PRIVATE_DIRECTORY_MODE });
} catch (error) {
if (error.code !== "EEXIST")
throw error;
}
const metadata = await lstat(absolute);
if (await realpath(absolute) !== absolute || !metadata.isDirectory() || metadata.isSymbolicLink() || ownerUid() !== undefined && metadata.uid !== ownerUid() || (metadata.mode & 63) !== 0) {
throw new Error("Directory must be physical, owned, and private.");
}
return absolute;
}
var checkStableCandidate = (before, maximumBytes, expectation) => {
const uid = ownerUid();
const links = BigInt(expectation.links ?? 1);
if (!before.isFile() || before.nlink !== links || uid !== undefined && before.uid !== BigInt(uid) || (expectation.exactMode !== undefined ? (before.mode & 0o777n) !== BigInt(expectation.exactMode) : expectation.ownerOnly !== false && (before.mode & 0o077n) !== 0n) || expectation.minimumBytes !== undefined && before.size < expectation.minimumBytes || before.size > BigInt(maximumBytes)) {
throw new Error("Unsafe private file.");
}
};
var checkStableResult = (before, after) => {
if (after.isSymbolicLink() || !after.isFile() || after.dev !== before.dev || after.ino !== before.ino || after.nlink !== before.nlink || after.mode !== before.mode || after.uid !== before.uid || after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs) {
throw new Error("Private file changed during the read.");
}
};
async function readOwnedFileStable(path, maximumBytes, expectation = {}) {
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
try {
const before = await handle.stat({ bigint: true });
checkStableCandidate(before, maximumBytes, expectation);
const buffer = Buffer.alloc(Number(before.size));
let offset = 0;
while (offset < buffer.byteLength) {
const { bytesRead } = await handle.read(buffer, offset, buffer.byteLength - offset, null);
if (bytesRead === 0)
break;
offset += bytesRead;
}
const after = await lstat(path, { bigint: true });
checkStableResult(before, after);
if (offset !== buffer.byteLength) {
throw new Error("Private file changed during the read.");
}
return { bytes: buffer, dev: Number(before.dev), ino: Number(before.ino) };
} finally {
await handle.close();
}
}
function readOwnedFileStableSync(path, maximumBytes, expectation = {}) {
const descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
try {
const before = fstatSync(descriptor, { bigint: true });
checkStableCandidate(before, maximumBytes, expectation);
const buffer = Buffer.alloc(Number(before.size));
let offset = 0;
while (offset < buffer.byteLength) {
const count = readSync(descriptor, buffer, offset, buffer.byteLength - offset, null);
if (count === 0)
break;
offset += count;
}
const after = lstatSync(path, { bigint: true });
checkStableResult(before, after);
if (offset !== buffer.byteLength) {
throw new Error("Private file changed during the read.");
}
return { bytes: buffer, dev: Number(before.dev), ino: Number(before.ino) };
} finally {
closeSync(descriptor);
}
}
async function readPrivateFile(path, maximumBytes) {
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
try {
const metadata = await handle.stat();
const uid = ownerUid();
if (!metadata.isFile() || metadata.nlink !== 1 || uid !== undefined && metadata.uid !== uid || (metadata.mode & 63) !== 0 || metadata.size > maximumBytes) {
throw new Error("Unsafe private file.");
}
const buffer = Buffer.alloc(maximumBytes + 1);
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
if (bytesRead > maximumBytes)
throw new Error("Private file exceeds its size bound.");
return buffer.subarray(0, bytesRead);
} finally {
await handle.close();
}
}

// src/atomic-publish.ts
import { randomUUID } from "node:crypto";
import {
closeSync as closeSync2,
constants as constants2,
fchmodSync,
fsyncSync,
linkSync,
openSync as openSync2,
unlinkSync,
writeSync
} from "node:fs";
import { link, open as open2, rename, unlink } from "node:fs/promises";
import { join } from "node:path";
var safeFileName = /^[A-Za-z0-9][A-Za-z0-9._-]{0,126}$/u;
var assertSafeName = (name) => {
if (!safeFileName.test(name) || Buffer.byteLength(name) > 128) {
throw new Error("Unsafe publish name.");
}
};
var syncDirectory = async (directory) => {
const handle = await open2(directory, constants2.O_RDONLY);
try {
await handle.sync();
} finally {
await handle.close();
}
};
var writeStaged = async (staged, content) => {
const handle = await open2(staged, constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY | constants2.O_NOFOLLOW, PRIVATE_FILE_MODE);
try {
await handle.chmod(PRIVATE_FILE_MODE);
await handle.writeFile(content);
await handle.sync();
} finally {
await handle.close();
}
};
async function publishPrivateFile(directory, name, content, options = {}) {
assertSafeName(name);
const target = join(directory, name);
const temporary = join(directory, `.${name}.${randomUUID()}.tmp`);
try {
await writeStaged(temporary, content);
await options.beforeCommit?.(target);
await rename(temporary, target);
await syncDirectory(directory);
await assertOwnedPath(target, {
kind: "file",
exactMode: PRIVATE_FILE_MODE,
links: 1
});
} catch (error) {
await unlink(temporary).catch(() => {
return;
});
throw error;
}
}
async function createPrivateFileOnce(directory, name, content) {
assertSafeName(name);
const target = join(directory, name);
const temporary = join(directory, `.${name}.${randomUUID()}.tmp`);
try {
await writeStaged(temporary, content);
try {
await link(temporary, target);
} catch (error) {
if (error.code === "EEXIST")
return "existing";
throw error;
}
await syncDirectory(directory);
await assertOwnedPath(target, {
kind: "file",
exactMode: PRIVATE_FILE_MODE,
links: 2
});
return "created";
} finally {
await unlink(temporary).catch(() => {
return;
});
await syncDirectory(directory).catch(() => {
return;
});
}
}
var writeStagedSync = (staged, content) => {
const descriptor = openSync2(staged, constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY | constants2.O_NOFOLLOW, PRIVATE_FILE_MODE);
try {
fchmodSync(descriptor, PRIVATE_FILE_MODE);
const bytes = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
let offset = 0;
while (offset < bytes.byteLength) {
offset += writeSync(descriptor, bytes, offset, bytes.byteLength - offset);
}
fsyncSync(descriptor);
} finally {
closeSync2(descriptor);
}
};
var syncDirectorySync = (directory) => {
const descriptor = openSync2(directory, constants2.O_RDONLY);
try {
fsyncSync(descriptor);
} finally {
closeSync2(descriptor);
}
};
function createPrivateFileOnceSync(directory, name, content) {
assertSafeName(name);
const target = join(directory, name);
const temporary = join(directory, `.${name}.${randomUUID()}.tmp`);
try {
writeStagedSync(temporary, content);
try {
linkSync(temporary, target);
} catch (error) {
if (error.code === "EEXIST")
return "existing";
throw error;
}
syncDirectorySync(directory);
assertOwnedPathSync(target, {
kind: "file",
exactMode: PRIVATE_FILE_MODE,
links: 2
});
return "created";
} finally {
try {
unlinkSync(temporary);
} catch {}
try {
syncDirectorySync(directory);
} catch {}
}
}
createPrivateFileOnce,
createPrivateFileOnceSync,
publishPrivateFile
} from "./chunk-605s349d.js";
import"./chunk-xrgz1k0h.js";
export {
publishPrivateFile,
createPrivateFileOnceSync,
Expand Down
Loading
Loading