@nanhara/hara 0.124.0 → 0.124.2
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/CHANGELOG.md +40 -0
- package/dist/cron/store.js +4 -3
- package/dist/fs-identity.js +11 -0
- package/dist/fs-open-flags.js +14 -0
- package/dist/fs-read.js +12 -15
- package/dist/fs-write.js +4 -3
- package/dist/gateway/outbound-files.js +7 -8
- package/dist/gateway/runtime-state.js +11 -20
- package/dist/gateway/wecom.js +512 -223
- package/dist/org/review-chain.js +2 -2
- package/dist/security/permissions.js +4 -3
- package/dist/security/private-state.js +55 -22
- package/dist/serve/server.js +6 -4
- package/dist/tools/search.js +6 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,46 @@ All notable changes to `@nanhara/hara`.
|
|
|
5
5
|
> Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
|
|
6
6
|
> **patch** (last) number bumps for **optimizations/fixes of existing features**.
|
|
7
7
|
|
|
8
|
+
## 0.124.2 — 2026-07-17 — reliable WeCom gateway transport
|
|
9
|
+
|
|
10
|
+
- **The WeCom gateway is ready only after WeCom accepts its credentials.** Hara now waits for the
|
|
11
|
+
`aibot_subscribe` acknowledgement, stops after five bounded credential failures, monitors heartbeat
|
|
12
|
+
acknowledgements, reconnects half-open sockets with bounded exponential backoff, and stops cleanly when
|
|
13
|
+
`disconnected_event` reports that another active connection replaced it.
|
|
14
|
+
- **A failed WeCom delivery is no longer reported as success.** Correlated requests reject on timeout,
|
|
15
|
+
cancellation, connection loss, malformed/nonzero acknowledgements, or missing upload results. Text and file
|
|
16
|
+
transfers have hard deadlines and credential-scoped per-chat FIFO ordering; chunk uploads retry within a
|
|
17
|
+
fixed bound, and transport diagnostics redact the configured Secret.
|
|
18
|
+
- **WeCom callbacks now participate in Hara's cross-restart execution boundary.** Stable `msgid` and
|
|
19
|
+
`create_time` metadata feed stale-event filtering, deduplication, and cached no-rerun outcomes so a redelivery
|
|
20
|
+
cannot silently launch the same coding/file task again.
|
|
21
|
+
- **Inbound media fails closed and keeps file types separate.** Images remain vision attachments, while files
|
|
22
|
+
and videos become explicit private local-path references. Invalid AES ciphertext/padding is rejected instead
|
|
23
|
+
of returning padded bytes, and unfinished handoffs remove adapter-owned temporary files.
|
|
24
|
+
- A deterministic local WebSocket suite covers auth failure, heartbeat reconnection, request errors, strict
|
|
25
|
+
decryption, media classification, two-chunk upload, and a spawned `hara gateway --platform wecom` allowlist
|
|
26
|
+
round trip without using real enterprise credentials.
|
|
27
|
+
- Upgrade with `npm i -g @nanhara/hara@0.124.2`.
|
|
28
|
+
|
|
29
|
+
## 0.124.1 — 2026-07-16 — Windows native private-state and file-identity portability
|
|
30
|
+
|
|
31
|
+
- **Windows private state works in both Node and the native Bun executable.** Exclusive staging creation now
|
|
32
|
+
uses the portable `wx`/`CREATE_NEW` contract instead of passing Bun numeric flags that could surface a false
|
|
33
|
+
`ENOENT`. Config, credentials, gateway state, discovery records, and other private atomic writers retain
|
|
34
|
+
create-if-absent publication, hard-link fencing, fsync, and fail-closed alias checks.
|
|
35
|
+
- **Descriptor-to-path identity checks follow the host's real guarantees.** Windows Node may report a
|
|
36
|
+
volume-derived `dev` from `fstat` but `dev=0` from `lstat` for the exact same NTFS file. Hara now correlates
|
|
37
|
+
an already-open descriptor with its already-bounded path using the stable file id and link count, while
|
|
38
|
+
POSIX still requires device, inode, and owner-only mode. Arbitrary path-to-path protected-file detection
|
|
39
|
+
continues to require device + inode, so the portability fix does not weaken hard-link protection.
|
|
40
|
+
- **The release gate now exercises the reported failure on a real Windows runtime.** CI installs the pinned
|
|
41
|
+
Bun 1.3.9 x64-baseline runtime, runs portable file-state contracts, compiles `hara.exe`, and executes the
|
|
42
|
+
hostile-cwd standalone smoke including an isolated `hara doctor`. Bounded failure annotations keep future
|
|
43
|
+
Windows-only regressions diagnosable without exposing credentials. Production dependency audits retry a
|
|
44
|
+
transient registry failure only within a fixed bound and still require a real successful audit result.
|
|
45
|
+
- Windows users on 0.124.0 should upgrade with `npm i -g @nanhara/hara@0.124.1`; Desktop releases that bundle
|
|
46
|
+
a sidecar should pin this patch or newer.
|
|
47
|
+
|
|
8
48
|
## 0.124.0 — 2026-07-16 — task-safe interaction, bounded context, and private-state hardening
|
|
9
49
|
|
|
10
50
|
- **Idle conversation no longer hijacks an unfinished task.** Ordinary input starts a new execution while
|
package/dist/cron/store.js
CHANGED
|
@@ -7,6 +7,8 @@ import { chmodSync, closeSync, constants, existsSync, fstatSync, fsyncSync, lsta
|
|
|
7
7
|
import { cronMatches, parseCron, validTz } from "./schedule.js";
|
|
8
8
|
import { compareProcessIdentity, defaultProcessIdentity } from "../process-identity.js";
|
|
9
9
|
import { sleepSync } from "../sync-sleep.js";
|
|
10
|
+
import { optionalPosixOpenFlag } from "../fs-open-flags.js";
|
|
11
|
+
import { sameOpenedFileIdentity } from "../fs-identity.js";
|
|
10
12
|
/** Hard persistence bounds: outages must apply backpressure instead of growing jobs.json forever. */
|
|
11
13
|
export const MAX_CRON_PENDING_NOTIFICATIONS = 64;
|
|
12
14
|
const CRON_RUN_NOTIFICATION_RESERVE = 2;
|
|
@@ -43,11 +45,10 @@ function readStoreLockSnapshot(path) {
|
|
|
43
45
|
const before = lstatSync(path);
|
|
44
46
|
let raw = null;
|
|
45
47
|
if (before.isFile() && before.size <= MAX_STORE_LOCK_BYTES) {
|
|
46
|
-
const
|
|
47
|
-
const fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK | noFollow);
|
|
48
|
+
const fd = openSync(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK") | optionalPosixOpenFlag("O_NOFOLLOW"));
|
|
48
49
|
try {
|
|
49
50
|
const opened = fstatSync(fd);
|
|
50
|
-
if (opened.isFile() && opened
|
|
51
|
+
if (opened.isFile() && sameOpenedFileIdentity(opened, before) && opened.size <= MAX_STORE_LOCK_BYTES) {
|
|
51
52
|
const buffer = Buffer.alloc(opened.size);
|
|
52
53
|
let offset = 0;
|
|
53
54
|
while (offset < buffer.length) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correlate an already-open descriptor with a stat of the same bounded path. Node on Windows may report
|
|
3
|
+
* a volume-derived `dev` for fstat and `0` for lstat/stat of that exact NTFS file. Its file id (`ino`) stays
|
|
4
|
+
* stable, and the caller has already fixed the path/parent boundary, so requiring `dev` there rejects valid
|
|
5
|
+
* files without adding an identity fence. POSIX retains the ordinary device + inode comparison.
|
|
6
|
+
*
|
|
7
|
+
* Do not use this to decide whether two arbitrary Windows paths are hard links across unrelated roots.
|
|
8
|
+
*/
|
|
9
|
+
export function sameOpenedFileIdentity(left, right, platform = process.platform) {
|
|
10
|
+
return left.ino === right.ino && (platform === "win32" || left.dev === right.dev);
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Bun's Windows compatibility layer may expose POSIX-only fs constants even though CreateFileW cannot
|
|
4
|
+
* consume them. Node documents that Windows supports neither O_DIRECTORY, O_NOFOLLOW nor O_NONBLOCK;
|
|
5
|
+
* passing Bun's numeric values can turn an otherwise valid create/open into a misleading ENOENT.
|
|
6
|
+
*
|
|
7
|
+
* Windows callers retain their lstat/fstat/identity checks and O_CREAT|O_EXCL no-replace semantics.
|
|
8
|
+
*/
|
|
9
|
+
export function optionalPosixOpenFlag(name, platform = process.platform) {
|
|
10
|
+
if (platform === "win32")
|
|
11
|
+
return 0;
|
|
12
|
+
const value = constants[name];
|
|
13
|
+
return typeof value === "number" ? value : 0;
|
|
14
|
+
}
|
package/dist/fs-read.js
CHANGED
|
@@ -4,6 +4,8 @@ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpat
|
|
|
4
4
|
import { open } from "node:fs/promises";
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
6
6
|
import { sensitiveFileError } from "./security/sensitive-files.js";
|
|
7
|
+
import { optionalPosixOpenFlag } from "./fs-open-flags.js";
|
|
8
|
+
import { sameOpenedFileIdentity } from "./fs-identity.js";
|
|
7
9
|
export class BinaryFileError extends Error {
|
|
8
10
|
constructor(path) {
|
|
9
11
|
super(`${path} appears to be binary (NUL byte detected)`);
|
|
@@ -100,10 +102,8 @@ export function verifyOpenedRegularFileSync(path, opened, options = {}) {
|
|
|
100
102
|
const currentLink = lstatSync(path);
|
|
101
103
|
const currentTarget = statSync(canonical);
|
|
102
104
|
if (currentLink.isSymbolicLink()
|
|
103
|
-
|| currentLink
|
|
104
|
-
||
|
|
105
|
-
|| currentTarget.dev !== opened.dev
|
|
106
|
-
|| currentTarget.ino !== opened.ino) {
|
|
105
|
+
|| !sameOpenedFileIdentity(currentLink, opened)
|
|
106
|
+
|| !sameOpenedFileIdentity(currentTarget, opened)) {
|
|
107
107
|
throw new Error(`refusing to access ${path}: path changed while opening it`);
|
|
108
108
|
}
|
|
109
109
|
return canonical;
|
|
@@ -118,8 +118,7 @@ export async function openVerifiedRegularFileNoFollow(path, options = {}) {
|
|
|
118
118
|
const before = lstatSync(path);
|
|
119
119
|
if (before.isSymbolicLink())
|
|
120
120
|
throw new NonRegularFileError(path);
|
|
121
|
-
const
|
|
122
|
-
const handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK | noFollow);
|
|
121
|
+
const handle = await open(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK") | optionalPosixOpenFlag("O_NOFOLLOW"));
|
|
123
122
|
try {
|
|
124
123
|
const info = await handle.stat();
|
|
125
124
|
const canonicalPath = verifyOpenedRegularFileSync(path, info, options);
|
|
@@ -189,8 +188,7 @@ export function readVerifiedRegularFileSnapshotSync(path, maxBytes = MAX_EDIT_RE
|
|
|
189
188
|
const before = lstatSync(path);
|
|
190
189
|
if (before.isSymbolicLink())
|
|
191
190
|
throw new NonRegularFileError(path);
|
|
192
|
-
const
|
|
193
|
-
const fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK | noFollow);
|
|
191
|
+
const fd = openSync(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK") | optionalPosixOpenFlag("O_NOFOLLOW"));
|
|
194
192
|
try {
|
|
195
193
|
const info = fstatSync(fd);
|
|
196
194
|
verifyOpenedRegularFileSync(path, info, {
|
|
@@ -243,11 +241,10 @@ function withVerifiedContextFdSync(path, read) {
|
|
|
243
241
|
throw new ProtectedContextFileError(denied);
|
|
244
242
|
// O_NOFOLLOW is not exposed on every platform. The before/after lstat checks retain fail-closed symlink
|
|
245
243
|
// behaviour there; on POSIX O_NOFOLLOW makes the critical open itself atomic.
|
|
246
|
-
const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
247
244
|
const before = lstatSync(path);
|
|
248
245
|
if (before.isSymbolicLink())
|
|
249
246
|
throw new NonRegularFileError(path);
|
|
250
|
-
const fd = openSync(path, constants.O_RDONLY |
|
|
247
|
+
const fd = openSync(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK") | optionalPosixOpenFlag("O_NOFOLLOW"));
|
|
251
248
|
try {
|
|
252
249
|
const info = fstatSync(fd);
|
|
253
250
|
verifyOpenedRegularFileSync(path, info, {
|
|
@@ -387,12 +384,11 @@ async function readRegularFileSnapshotWithFlags(path, maxBytes, flags) {
|
|
|
387
384
|
}
|
|
388
385
|
}
|
|
389
386
|
export async function readRegularFileSnapshot(path, maxBytes = MAX_EDIT_READ_BYTES) {
|
|
390
|
-
return readRegularFileSnapshotWithFlags(path, maxBytes, constants.O_RDONLY |
|
|
387
|
+
return readRegularFileSnapshotWithFlags(path, maxBytes, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK"));
|
|
391
388
|
}
|
|
392
389
|
/** Quarantine/transaction reader: reject a symlink at open(2), then validate/read that same fd. */
|
|
393
390
|
export async function readRegularFileSnapshotNoFollow(path, maxBytes = MAX_EDIT_READ_BYTES) {
|
|
394
|
-
|
|
395
|
-
return readRegularFileSnapshotWithFlags(path, maxBytes, constants.O_RDONLY | constants.O_NONBLOCK | noFollow);
|
|
391
|
+
return readRegularFileSnapshotWithFlags(path, maxBytes, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK") | optionalPosixOpenFlag("O_NOFOLLOW"));
|
|
396
392
|
}
|
|
397
393
|
export async function readRegularFileText(path, maxBytes = MAX_EDIT_READ_BYTES) {
|
|
398
394
|
return (await readRegularFileSnapshot(path, maxBytes)).text;
|
|
@@ -402,7 +398,7 @@ export function readTextPrefixSync(path, maxChars) {
|
|
|
402
398
|
const requested = Number.isFinite(maxChars) ? Math.floor(maxChars) : MAX_PREFIX_CHARS;
|
|
403
399
|
const chars = Math.min(MAX_PREFIX_CHARS, Math.max(0, requested));
|
|
404
400
|
// O_NONBLOCK makes opening a FIFO return immediately; fstat on this exact fd then rejects it before read.
|
|
405
|
-
const fd = openSync(path, constants.O_RDONLY |
|
|
401
|
+
const fd = openSync(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK"));
|
|
406
402
|
try {
|
|
407
403
|
const info = fstatSync(fd);
|
|
408
404
|
if (!info.isFile())
|
|
@@ -504,7 +500,8 @@ export async function streamFileSlice(path, offset = 1, limit = 300, options = {
|
|
|
504
500
|
const verified = options.protectSensitive
|
|
505
501
|
? await openVerifiedRegularFileNoFollow(path, { action: "read", rejectHardLinks: true, protectSensitive: true })
|
|
506
502
|
: null;
|
|
507
|
-
const handle = verified?.handle
|
|
503
|
+
const handle = verified?.handle
|
|
504
|
+
?? await open(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK"));
|
|
508
505
|
let stream;
|
|
509
506
|
try {
|
|
510
507
|
const info = verified?.info ?? await handle.stat();
|
package/dist/fs-write.js
CHANGED
|
@@ -5,6 +5,8 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
5
5
|
import { constants, linkSync, lstatSync, readlinkSync, realpathSync, renameSync, symlinkSync, unlinkSync } from "node:fs";
|
|
6
6
|
import { lstat, mkdir, open, realpath, rmdir, stat } from "node:fs/promises";
|
|
7
7
|
import { NonRegularFileError, readRegularFileSnapshotNoFollow } from "./fs-read.js";
|
|
8
|
+
import { optionalPosixOpenFlag } from "./fs-open-flags.js";
|
|
9
|
+
import { sameOpenedFileIdentity } from "./fs-identity.js";
|
|
8
10
|
import { canonicalizeProspectivePath, lexicalSensitiveFileReason, sensitiveFileError, } from "./security/sensitive-files.js";
|
|
9
11
|
export class FileChangedError extends Error {
|
|
10
12
|
code = "HARA_FILE_CHANGED";
|
|
@@ -296,7 +298,7 @@ async function syncDirectory(path) {
|
|
|
296
298
|
try {
|
|
297
299
|
// The directory name can be exchanged after rename. O_NONBLOCK plus fstat on this exact descriptor
|
|
298
300
|
// keeps best-effort durability from hanging forever on a replacement FIFO/device.
|
|
299
|
-
const handle = await open(path, constants.O_RDONLY |
|
|
301
|
+
const handle = await open(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NONBLOCK"));
|
|
300
302
|
try {
|
|
301
303
|
if (!(await handle.stat()).isDirectory())
|
|
302
304
|
return;
|
|
@@ -508,8 +510,7 @@ export async function atomicWriteText(path, content, options = {}) {
|
|
|
508
510
|
verifyCommitParent();
|
|
509
511
|
const current = lstatSync(temp);
|
|
510
512
|
if (current.isFile()
|
|
511
|
-
&& current
|
|
512
|
-
&& current.ino === writtenIdentity.ino)
|
|
513
|
+
&& sameOpenedFileIdentity(current, writtenIdentity))
|
|
513
514
|
unlinkSync(temp);
|
|
514
515
|
}
|
|
515
516
|
catch {
|
|
@@ -6,6 +6,8 @@ import { chmodSync, constants, lstatSync, mkdirSync, readdirSync, realpathSync,
|
|
|
6
6
|
import { open } from "node:fs/promises";
|
|
7
7
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
8
8
|
import { openVerifiedRegularFileNoFollow, verifyOpenedRegularFileSync } from "../fs-read.js";
|
|
9
|
+
import { optionalPosixOpenFlag } from "../fs-open-flags.js";
|
|
10
|
+
import { sameOpenedFileIdentity } from "../fs-identity.js";
|
|
9
11
|
export const OUTBOUND_FILE_MAX_BYTES = 20 * 1024 * 1024;
|
|
10
12
|
export const OUTBOUND_BATCH_MAX_BYTES = 20 * 1024 * 1024;
|
|
11
13
|
export const OUTBOUND_BATCH_MAX_FILES = 4;
|
|
@@ -90,17 +92,15 @@ async function appendOutbox(outbox, snapshot, signal) {
|
|
|
90
92
|
if (error?.code !== "ENOENT")
|
|
91
93
|
throw error;
|
|
92
94
|
}
|
|
93
|
-
const
|
|
94
|
-
const handle = await open(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | noFollow, 0o600);
|
|
95
|
+
const handle = await open(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | optionalPosixOpenFlag("O_NOFOLLOW"), 0o600);
|
|
95
96
|
try {
|
|
96
97
|
const info = await handle.stat();
|
|
97
98
|
const current = lstatSync(path);
|
|
98
99
|
if (!info.isFile()
|
|
99
100
|
|| info.nlink > 1
|
|
100
101
|
|| current.isSymbolicLink()
|
|
101
|
-
|| current
|
|
102
|
-
||
|
|
103
|
-
|| (before && (before.dev !== info.dev || before.ino !== info.ino))
|
|
102
|
+
|| !sameOpenedFileIdentity(current, info)
|
|
103
|
+
|| (before && !sameOpenedFileIdentity(before, info))
|
|
104
104
|
|| !ownedByProcess(path))
|
|
105
105
|
throw new Error(`unsafe gateway outbox: ${outbox}`);
|
|
106
106
|
await handle.chmod(0o600);
|
|
@@ -251,7 +251,7 @@ async function readOutbox(outbox) {
|
|
|
251
251
|
await verified?.handle.close().catch(() => { });
|
|
252
252
|
try {
|
|
253
253
|
const current = lstatSync(resolve(outbox));
|
|
254
|
-
if (verified && !current.isSymbolicLink() && current
|
|
254
|
+
if (verified && !current.isSymbolicLink() && sameOpenedFileIdentity(current, verified.info)) {
|
|
255
255
|
unlinkSync(resolve(outbox));
|
|
256
256
|
}
|
|
257
257
|
}
|
|
@@ -380,8 +380,7 @@ export function cleanupOutboundSnapshot(path) {
|
|
|
380
380
|
&& entry.isFile()
|
|
381
381
|
&& !entry.isSymbolicLink()
|
|
382
382
|
&& entry.nlink === 1
|
|
383
|
-
&& entry
|
|
384
|
-
&& entry.ino === expected.ino
|
|
383
|
+
&& sameOpenedFileIdentity(entry, expected)
|
|
385
384
|
&& ownedByProcess(candidate))
|
|
386
385
|
unlinkSync(candidate);
|
|
387
386
|
if (readdirSync(dir).length === 0)
|
|
@@ -13,6 +13,8 @@ import { atomicWriteText, bindHaraPrivateStateWritePath, FileChangedError } from
|
|
|
13
13
|
import { ensurePrivateStateSubdirectory, readPrivateStateFileSnapshot, } from "../security/private-state.js";
|
|
14
14
|
import { sleepSync } from "../sync-sleep.js";
|
|
15
15
|
import { compareProcessIdentity, defaultProcessIdentity } from "../process-identity.js";
|
|
16
|
+
import { optionalPosixOpenFlag } from "../fs-open-flags.js";
|
|
17
|
+
import { sameOpenedFileIdentity } from "../fs-identity.js";
|
|
16
18
|
const PLATFORM = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
17
19
|
const LOCK_BYTES = 4 * 1024;
|
|
18
20
|
const LOCK_WAIT_MS = 5_000;
|
|
@@ -100,13 +102,11 @@ function leaseStagingPath(path, record) {
|
|
|
100
102
|
return `${path}.${record.pid}.${record.token}.pending`;
|
|
101
103
|
}
|
|
102
104
|
function readLease(path, platform) {
|
|
103
|
-
const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
104
|
-
const nonBlock = typeof constants.O_NONBLOCK === "number" ? constants.O_NONBLOCK : 0;
|
|
105
105
|
let fd;
|
|
106
106
|
try {
|
|
107
107
|
// O_NONBLOCK is inert for regular files and prevents a hostile FIFO/device at the lock path from hanging
|
|
108
108
|
// gateway startup before fstat can reject it.
|
|
109
|
-
fd = openSync(path, constants.O_RDONLY |
|
|
109
|
+
fd = openSync(path, constants.O_RDONLY | optionalPosixOpenFlag("O_NOFOLLOW") | optionalPosixOpenFlag("O_NONBLOCK"));
|
|
110
110
|
}
|
|
111
111
|
catch (error) {
|
|
112
112
|
if (errorCode(error) === "ENOENT")
|
|
@@ -175,8 +175,7 @@ function readLease(path, platform) {
|
|
|
175
175
|
if (!staging.isFile()
|
|
176
176
|
|| staging.isSymbolicLink()
|
|
177
177
|
|| staging.nlink !== 2
|
|
178
|
-
|| staging
|
|
179
|
-
|| staging.ino !== info.ino)
|
|
178
|
+
|| !sameOpenedFileIdentity(staging, info))
|
|
180
179
|
throw new Error(`refusing malformed gateway instance lock: ${path}`);
|
|
181
180
|
return { record, dev: info.dev, ino: info.ino, stagingPath };
|
|
182
181
|
}
|
|
@@ -187,8 +186,7 @@ function readLease(path, platform) {
|
|
|
187
186
|
function sameLease(left, right) {
|
|
188
187
|
return Boolean(left
|
|
189
188
|
&& right
|
|
190
|
-
&& left
|
|
191
|
-
&& left.ino === right.ino
|
|
189
|
+
&& sameOpenedFileIdentity(left, right)
|
|
192
190
|
&& left.record.pid === right.record.pid
|
|
193
191
|
&& left.record.token === right.record.token);
|
|
194
192
|
}
|
|
@@ -198,8 +196,7 @@ function unlinkLease(path, expected) {
|
|
|
198
196
|
|| current.isSymbolicLink()
|
|
199
197
|
|| current.nlink < 1
|
|
200
198
|
|| current.nlink > 2
|
|
201
|
-
|| current
|
|
202
|
-
|| current.ino !== expected.ino)
|
|
199
|
+
|| !sameOpenedFileIdentity(current, expected))
|
|
203
200
|
throw new Error(`gateway instance lock changed before removal: ${path}`);
|
|
204
201
|
if (current.nlink === 2) {
|
|
205
202
|
if (!expected.stagingPath)
|
|
@@ -208,30 +205,26 @@ function unlinkLease(path, expected) {
|
|
|
208
205
|
if (!staging.isFile()
|
|
209
206
|
|| staging.isSymbolicLink()
|
|
210
207
|
|| staging.nlink !== 2
|
|
211
|
-
|| staging
|
|
212
|
-
|| staging.ino !== expected.ino)
|
|
208
|
+
|| !sameOpenedFileIdentity(staging, expected))
|
|
213
209
|
throw new Error(`gateway instance lock changed before removal: ${path}`);
|
|
214
210
|
unlinkSync(expected.stagingPath);
|
|
215
211
|
current = lstatSync(path);
|
|
216
212
|
if (!current.isFile()
|
|
217
213
|
|| current.isSymbolicLink()
|
|
218
214
|
|| current.nlink !== 1
|
|
219
|
-
|| current
|
|
220
|
-
|| current.ino !== expected.ino)
|
|
215
|
+
|| !sameOpenedFileIdentity(current, expected))
|
|
221
216
|
throw new Error(`gateway instance lock changed before removal: ${path}`);
|
|
222
217
|
}
|
|
223
218
|
unlinkSync(path);
|
|
224
219
|
}
|
|
225
220
|
function writeLease(path, record) {
|
|
226
|
-
const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
227
|
-
const nonBlock = typeof constants.O_NONBLOCK === "number" ? constants.O_NONBLOCK : 0;
|
|
228
221
|
const stagingPath = leaseStagingPath(path, record);
|
|
229
222
|
let fd;
|
|
230
223
|
let published = false;
|
|
231
224
|
try {
|
|
232
225
|
// Never expose an empty/partial owner record at the canonical path. The private staging inode is fully
|
|
233
226
|
// written + fsynced first; link(2) then publishes it atomically and refuses to replace an existing owner.
|
|
234
|
-
fd = openSync(stagingPath,
|
|
227
|
+
fd = openSync(stagingPath, "wx", 0o600);
|
|
235
228
|
writeFileSync(fd, JSON.stringify(record), "utf8");
|
|
236
229
|
fsyncSync(fd);
|
|
237
230
|
closeSync(fd);
|
|
@@ -853,8 +846,7 @@ export class GatewayFlowRunStore {
|
|
|
853
846
|
if (!current.isFile()
|
|
854
847
|
|| current.isSymbolicLink()
|
|
855
848
|
|| current.nlink !== 1
|
|
856
|
-
|| current
|
|
857
|
-
|| current.ino !== snapshot.ino)
|
|
849
|
+
|| !sameOpenedFileIdentity(current, snapshot))
|
|
858
850
|
throw new Error(`gateway flow run changed before removal: ${path}`);
|
|
859
851
|
unlinkSync(path);
|
|
860
852
|
}
|
|
@@ -1172,8 +1164,7 @@ export class GatewayRunOutcomeStore {
|
|
|
1172
1164
|
if (!current.isFile()
|
|
1173
1165
|
|| current.isSymbolicLink()
|
|
1174
1166
|
|| current.nlink !== 1
|
|
1175
|
-
|| current
|
|
1176
|
-
|| current.ino !== snapshot.ino)
|
|
1167
|
+
|| !sameOpenedFileIdentity(current, snapshot))
|
|
1177
1168
|
throw new Error(`gateway run outcome changed before removal: ${path}`);
|
|
1178
1169
|
unlinkSync(path);
|
|
1179
1170
|
}
|