sneakoscope 8.2.0 → 8.3.0
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/README.md +46 -110
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/command-manifest-lite.js +0 -2
- package/dist/cli/command-registry.js +0 -2
- package/dist/commands/codex.js +3 -3
- package/dist/commands/doctor.js +5 -157
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/codex/codex-cli-syntax-builder.js +0 -8
- package/dist/core/codex-app/codex-models-cache.js +2 -1
- package/dist/core/codex-app/menubar/constants.js +0 -7
- package/dist/core/codex-compat/codex-capability-matrix.js +7 -7
- package/dist/core/codex-compat/codex-compat-report.js +8 -12
- package/dist/core/codex-compat/codex-runtime-contract.js +62 -0
- package/dist/core/codex-compat/codex-version-policy.js +7 -7
- package/dist/core/codex-control/codex-current-app-capability.js +3 -3
- package/dist/core/codex-control/codex-current-capability.js +67 -94
- package/dist/core/codex-control/codex-current-core-capability.js +3 -3
- package/dist/core/codex-control/codex-current-core-probe-runner.js +4 -4
- package/dist/core/codex-control/codex-current-core-real-probes.js +2 -2
- package/dist/core/codex-control/codex-current-feature-capability.js +4 -4
- package/dist/core/codex-control/codex-current-image-path-real-probe.js +1 -1
- package/dist/core/codex-control/codex-current-web-search-probe.js +1 -1
- package/dist/core/codex-control/codex-sdk-capability.js +2 -2
- package/dist/core/commands/remote-command.js +2 -2
- package/dist/core/context7-client.js +5 -2
- package/dist/core/doctor/codex-current-core-probe-doctor.js +2 -2
- package/dist/core/doctor/doctor-dirty-planner.js +1 -1
- package/dist/core/doctor/doctor-readiness-matrix.js +0 -7
- package/dist/core/feature-fixtures.js +0 -4
- package/dist/core/feature-registry.js +0 -1
- package/dist/core/image-ux-review.js +3 -3
- package/dist/core/install/installed-package-smoke.js +2 -3
- package/dist/core/mad-sks/sql-plane/mcp-executor.js +4 -3
- package/dist/core/mcp/modern-protocol.js +36 -0
- package/dist/core/mcp-config/health-check.js +45 -30
- package/dist/core/perf/release-latency-slo.js +1 -2
- package/dist/core/proof/runtime-truth-matrix.js +1 -1
- package/dist/core/release/canonical-test-proof.js +1 -2
- package/dist/core/release/gate-manifest.js +5 -2
- package/dist/core/release/physical-release-gates.js +0 -26
- package/dist/core/release/release-gate-affected-selector.js +2 -1
- package/dist/core/release/release-gate-contract.js +3 -1
- package/dist/core/release/release-pack-receipt.js +0 -1
- package/dist/core/release/stage-publish.js +1 -1
- package/dist/core/version-manager.js +4 -0
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/AppDelegate.swift +1 -12
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +2 -6
- package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +28 -471
- package/dist/native/sks-menubar/Sources/SecureProcessEnvelope.swift +1 -43
- package/dist/scripts/codex-current-app-server-v2-check.js +9 -8
- package/dist/scripts/codex-current-binary-identity-check.js +2 -2
- package/dist/scripts/codex-current-capability-check.js +9 -7
- package/dist/scripts/codex-current-dependency-check.js +21 -0
- package/dist/scripts/docs-truthfulness-check.js +4 -19
- package/dist/scripts/fast-codex-service-tier-proof-check.js +0 -1
- package/dist/scripts/install-surface-ssot-check.js +2 -2
- package/dist/scripts/latest-version-guidance-check.js +1 -1
- package/dist/scripts/packlist-performance-check.js +0 -1
- package/dist/scripts/release-metadata-check.js +4 -1
- package/dist/scripts/release-version-truth-check.js +149 -0
- package/dist/vendor/openai-codex/latest/hooks/permission-request.command.input.schema.json +6 -0
- package/dist/vendor/openai-codex/latest/hooks/permission-request.command.output.schema.json +2 -16
- package/dist/vendor/openai-codex/latest/hooks/post-compact.command.input.schema.json +6 -0
- package/dist/vendor/openai-codex/latest/hooks/post-tool-use.command.input.schema.json +6 -0
- package/dist/vendor/openai-codex/latest/hooks/post-tool-use.command.output.schema.json +2 -16
- package/dist/vendor/openai-codex/latest/hooks/pre-compact.command.input.schema.json +6 -0
- package/dist/vendor/openai-codex/latest/hooks/pre-tool-use.command.input.schema.json +6 -0
- package/dist/vendor/openai-codex/latest/hooks/pre-tool-use.command.output.schema.json +2 -16
- package/dist/vendor/openai-codex/latest/hooks/session-start.command.output.schema.json +2 -16
- package/dist/vendor/openai-codex/latest/hooks/snapshot-metadata.json +7 -5
- package/dist/vendor/openai-codex/latest/hooks/subagent-start.command.output.schema.json +4 -18
- package/dist/vendor/openai-codex/latest/hooks/user-prompt-submit.command.input.schema.json +6 -0
- package/dist/vendor/openai-codex/latest/hooks/user-prompt-submit.command.output.schema.json +2 -16
- package/package.json +4 -7
- package/release-gates.v2.json +49 -19
- package/config/codex-releases/rust-v0.146.0.json +0 -45
- package/dist/core/agents/agent-runner-codex-exec.js +0 -177
- package/dist/core/codex-compat/codex-release-manifest.js +0 -72
- package/dist/core/commands/telegram-command.js +0 -447
- package/dist/core/telegram/access.js +0 -58
- package/dist/core/telegram/client.js +0 -157
- package/dist/core/telegram/confirmation.js +0 -37
- package/dist/core/telegram/doctor.js +0 -154
- package/dist/core/telegram/index.js +0 -9
- package/dist/core/telegram/keychain.js +0 -317
- package/dist/core/telegram/liveness.js +0 -124
- package/dist/core/telegram/poller.js +0 -90
- package/dist/core/telegram/redaction.js +0 -15
- package/dist/core/telegram/state-lock.js +0 -416
- package/dist/core/telegram/types.js +0 -1
- package/dist/native/sks-menubar/Sources/TelegramPrivateFileStore.swift +0 -498
- package/dist/native/sks-menubar/Sources/TelegramPrivateFileSupport.swift +0 -236
- package/dist/native/sks-menubar/Sources/TelegramProcessGateway.swift +0 -454
- package/dist/native/sks-menubar/Sources/TelegramRuntimeSupport.swift +0 -56
- package/dist/native/sks-menubar/Sources/TelegramStateLock.swift +0 -448
- package/dist/native/sks-menubar/Sources/TelegramSupport.swift +0 -243
- package/dist/native/sks-menubar/Sources/TelegramTransport.swift +0 -499
- package/dist/scripts/codex-current-manifest-check.js +0 -29
- package/schemas/codex/app-server-0.146/codex_app_server_protocol.v2.schemas.json +0 -19858
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { TelegramApiError } from './client.js';
|
|
2
|
-
import { redactTelegramError } from './redaction.js';
|
|
3
|
-
export class TelegramPoller {
|
|
4
|
-
options;
|
|
5
|
-
controller = null;
|
|
6
|
-
state = {
|
|
7
|
-
schema: 'sks.telegram-poller-state.v1', running: false, offset: 0,
|
|
8
|
-
consecutive_failures: 0, last_poll_at: null, last_success_at: null,
|
|
9
|
-
last_update_at: null, last_error: null
|
|
10
|
-
};
|
|
11
|
-
constructor(options) {
|
|
12
|
-
this.options = {
|
|
13
|
-
...options,
|
|
14
|
-
timeoutSeconds: Math.max(1, Math.min(options.timeoutSeconds ?? 30, 50)),
|
|
15
|
-
minBackoffMs: Math.max(100, options.minBackoffMs ?? 500),
|
|
16
|
-
maxBackoffMs: Math.max(options.minBackoffMs ?? 500, Math.min(options.maxBackoffMs ?? 30_000, 60_000)),
|
|
17
|
-
initialOffset: Number.isSafeInteger(options.initialOffset) && Number(options.initialOffset) >= 0
|
|
18
|
-
? Number(options.initialOffset)
|
|
19
|
-
: 0,
|
|
20
|
-
sleep: options.sleep ?? abortableSleep
|
|
21
|
-
};
|
|
22
|
-
this.state = { ...this.state, offset: this.options.initialOffset };
|
|
23
|
-
}
|
|
24
|
-
snapshot() { return { ...this.state }; }
|
|
25
|
-
start() {
|
|
26
|
-
if (this.controller)
|
|
27
|
-
throw new Error('telegram_poller_already_running');
|
|
28
|
-
this.controller = new AbortController();
|
|
29
|
-
this.state = { ...this.state, running: true, last_error: null };
|
|
30
|
-
return this.run(this.controller.signal).finally(() => {
|
|
31
|
-
this.controller = null;
|
|
32
|
-
this.state = { ...this.state, running: false };
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
stop() { this.controller?.abort(); }
|
|
36
|
-
async run(signal) {
|
|
37
|
-
while (!signal.aborted) {
|
|
38
|
-
this.state = { ...this.state, last_poll_at: new Date().toISOString() };
|
|
39
|
-
try {
|
|
40
|
-
const updates = await this.options.client.getUpdates({
|
|
41
|
-
offset: this.state.offset,
|
|
42
|
-
timeoutSeconds: this.options.timeoutSeconds,
|
|
43
|
-
allowedUpdates: ['message'],
|
|
44
|
-
signal
|
|
45
|
-
});
|
|
46
|
-
for (const update of [...updates].sort((a, b) => a.update_id - b.update_id)) {
|
|
47
|
-
if (signal.aborted)
|
|
48
|
-
return;
|
|
49
|
-
if (!Number.isSafeInteger(update.update_id) || update.update_id < this.state.offset)
|
|
50
|
-
continue;
|
|
51
|
-
const claimed = {
|
|
52
|
-
...this.state,
|
|
53
|
-
offset: Math.max(this.state.offset, update.update_id + 1),
|
|
54
|
-
last_update_at: new Date().toISOString()
|
|
55
|
-
};
|
|
56
|
-
if (this.options.checkpoint)
|
|
57
|
-
await this.options.checkpoint(claimed);
|
|
58
|
-
this.state = claimed;
|
|
59
|
-
await this.options.handleUpdate(update);
|
|
60
|
-
}
|
|
61
|
-
this.state = {
|
|
62
|
-
...this.state,
|
|
63
|
-
consecutive_failures: 0,
|
|
64
|
-
last_success_at: new Date().toISOString(),
|
|
65
|
-
last_error: null
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
if (signal.aborted)
|
|
70
|
-
return;
|
|
71
|
-
const failures = this.state.consecutive_failures + 1;
|
|
72
|
-
this.state = { ...this.state, consecutive_failures: failures, last_error: redactTelegramError(error) };
|
|
73
|
-
const retryAfter = error instanceof TelegramApiError ? error.retryAfterSeconds : null;
|
|
74
|
-
const exponential = Math.min(this.options.maxBackoffMs, this.options.minBackoffMs * (2 ** Math.min(failures - 1, 8)));
|
|
75
|
-
const delay = retryAfter == null ? exponential : Math.min(this.options.maxBackoffMs, Math.max(exponential, retryAfter * 1_000));
|
|
76
|
-
await this.options.sleep(delay, signal).catch(() => undefined);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
function abortableSleep(ms, signal) {
|
|
82
|
-
return new Promise((resolve, reject) => {
|
|
83
|
-
if (signal.aborted)
|
|
84
|
-
return reject(signal.reason);
|
|
85
|
-
const timer = setTimeout(done, ms);
|
|
86
|
-
signal.addEventListener('abort', aborted, { once: true });
|
|
87
|
-
function done() { signal.removeEventListener('abort', aborted); resolve(); }
|
|
88
|
-
function aborted() { clearTimeout(timer); reject(signal.reason); }
|
|
89
|
-
});
|
|
90
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
const BOT_URL = /\/bot[^/\s]+\//gi;
|
|
3
|
-
const TOKEN_ASSIGNMENT = /((?:bot[_-]?token|token)\s*[:=]\s*)[^\s,"'}]+/gi;
|
|
4
|
-
export function redactTelegramError(value, token) {
|
|
5
|
-
let text = value instanceof Error ? value.message : String(value ?? 'telegram_error');
|
|
6
|
-
if (token)
|
|
7
|
-
text = text.split(token).join('[redacted]');
|
|
8
|
-
return text
|
|
9
|
-
.replace(BOT_URL, '/bot[redacted]/')
|
|
10
|
-
.replace(TOKEN_ASSIGNMENT, '$1[redacted]')
|
|
11
|
-
.slice(0, 512);
|
|
12
|
-
}
|
|
13
|
-
export function redactedChatActor(chatId, senderId = chatId) {
|
|
14
|
-
return `actor:${createHash('sha256').update(`${chatId}:${senderId}`).digest('hex').slice(0, 12)}`;
|
|
15
|
-
}
|
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import { constants as fsConstants } from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { assertTestHomeWriteAllowed } from '../fsx.js';
|
|
7
|
-
const LOCK_SCHEMA = 'sks.telegram-lock.v1';
|
|
8
|
-
const LOCK_MAX_BYTES = 4096;
|
|
9
|
-
const LOCK_ATTEMPTS = 200;
|
|
10
|
-
const LOCK_RETRY_MS = 25;
|
|
11
|
-
const LOCK_TOKEN_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
|
-
export async function withTelegramStateLock(paths, operation) {
|
|
13
|
-
const identity = await acquireStateLock(paths);
|
|
14
|
-
try {
|
|
15
|
-
return await operation();
|
|
16
|
-
}
|
|
17
|
-
finally {
|
|
18
|
-
await releaseStateLock(paths.stateLockPath, identity);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
async function acquireStateLock(paths) {
|
|
22
|
-
assertTestHomeWriteAllowed(paths.stateLockPath);
|
|
23
|
-
const candidate = await createOwnerCandidate(paths.stateDir, '.telegram.lock');
|
|
24
|
-
let published = false;
|
|
25
|
-
try {
|
|
26
|
-
for (let attempt = 0; attempt < LOCK_ATTEMPTS; attempt += 1) {
|
|
27
|
-
if (await reclaimBarrierExists(paths)) {
|
|
28
|
-
await settleReclaimBarrier(paths);
|
|
29
|
-
await retryDelay(attempt);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
await fs.link(candidate.path, paths.stateLockPath);
|
|
34
|
-
published = true;
|
|
35
|
-
const acquired = await inspectOwnerFile(paths.stateLockPath, 'lock');
|
|
36
|
-
if (!sameIdentity(acquired, candidate.identity)) {
|
|
37
|
-
throw new Error('telegram_state_lock_publish_identity_mismatch');
|
|
38
|
-
}
|
|
39
|
-
if (await reclaimBarrierExists(paths)) {
|
|
40
|
-
await releaseStateLock(paths.stateLockPath, candidate.identity);
|
|
41
|
-
published = false;
|
|
42
|
-
await settleReclaimBarrier(paths);
|
|
43
|
-
await retryDelay(attempt);
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
await fs.unlink(candidate.path);
|
|
47
|
-
published = false;
|
|
48
|
-
return candidate.identity;
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
if (error?.code !== 'EEXIST')
|
|
52
|
-
throw error;
|
|
53
|
-
const existing = await inspectOwnerFile(paths.stateLockPath, 'lock').catch((inspectionError) => {
|
|
54
|
-
if (isTransientLockInspection(inspectionError))
|
|
55
|
-
return null;
|
|
56
|
-
throw inspectionError;
|
|
57
|
-
});
|
|
58
|
-
if (existing && ownerIsDead(existing.owner)) {
|
|
59
|
-
if (await reclaimDeadStateLock(paths, existing))
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
await retryDelay(attempt);
|
|
64
|
-
}
|
|
65
|
-
throw new Error('telegram_state_lock_timeout');
|
|
66
|
-
}
|
|
67
|
-
finally {
|
|
68
|
-
await fs.unlink(candidate.path).catch(() => undefined);
|
|
69
|
-
if (published)
|
|
70
|
-
await releaseStateLock(paths.stateLockPath, candidate.identity);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
async function retryDelay(attempt) {
|
|
74
|
-
if (attempt + 1 >= LOCK_ATTEMPTS)
|
|
75
|
-
return;
|
|
76
|
-
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
|
|
77
|
-
}
|
|
78
|
-
async function reclaimDeadStateLock(paths, expected) {
|
|
79
|
-
const participant = await joinReclaimBarrier(paths);
|
|
80
|
-
try {
|
|
81
|
-
await cleanupDeadParticipants(paths);
|
|
82
|
-
const current = await inspectOwnerFile(paths.stateLockPath, 'lock').catch((error) => {
|
|
83
|
-
if (isTransientLockInspection(error))
|
|
84
|
-
return null;
|
|
85
|
-
throw error;
|
|
86
|
-
});
|
|
87
|
-
if (!current || !sameInspectedLock(current, expected) || !ownerIsDead(current.owner))
|
|
88
|
-
return false;
|
|
89
|
-
try {
|
|
90
|
-
await fs.unlink(paths.stateLockPath);
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
if (error?.code === 'ENOENT')
|
|
95
|
-
return false;
|
|
96
|
-
throw error;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
await leaveReclaimBarrier(paths, participant);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
async function settleReclaimBarrier(paths) {
|
|
104
|
-
const participant = await joinReclaimBarrier(paths);
|
|
105
|
-
try {
|
|
106
|
-
await cleanupDeadParticipants(paths);
|
|
107
|
-
const current = await inspectOwnerFile(paths.stateLockPath, 'lock').catch((error) => {
|
|
108
|
-
if (isTransientLockInspection(error))
|
|
109
|
-
return null;
|
|
110
|
-
throw error;
|
|
111
|
-
});
|
|
112
|
-
if (current && ownerIsDead(current.owner)) {
|
|
113
|
-
await fs.unlink(paths.stateLockPath).catch((error) => {
|
|
114
|
-
if (error?.code !== 'ENOENT')
|
|
115
|
-
throw error;
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
finally {
|
|
120
|
-
await leaveReclaimBarrier(paths, participant);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
async function joinReclaimBarrier(paths) {
|
|
124
|
-
const candidate = await createOwnerCandidate(paths.stateDir, '.telegram.reclaim');
|
|
125
|
-
const barrierPath = reclaimBarrierPath(paths);
|
|
126
|
-
const participantPath = path.join(barrierPath, `.owner.${candidate.identity.pid}.${candidate.identity.token}`);
|
|
127
|
-
try {
|
|
128
|
-
for (let attempt = 0; attempt < LOCK_ATTEMPTS; attempt += 1) {
|
|
129
|
-
await ensureReclaimBarrier(paths);
|
|
130
|
-
try {
|
|
131
|
-
await fs.link(candidate.path, participantPath);
|
|
132
|
-
const inspected = await inspectOwnerFile(participantPath, 'reclaim');
|
|
133
|
-
if (!sameIdentity(inspected, candidate.identity)) {
|
|
134
|
-
throw new Error('telegram_state_reclaim_participant_identity_mismatch');
|
|
135
|
-
}
|
|
136
|
-
await fs.unlink(candidate.path);
|
|
137
|
-
return { ...candidate.identity, path: participantPath };
|
|
138
|
-
}
|
|
139
|
-
catch (error) {
|
|
140
|
-
const code = error?.code;
|
|
141
|
-
if (code !== 'ENOENT' && code !== 'EEXIST' && code !== 'EINVAL')
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
await retryDelay(attempt);
|
|
145
|
-
}
|
|
146
|
-
throw new Error('telegram_state_reclaim_barrier_timeout');
|
|
147
|
-
}
|
|
148
|
-
finally {
|
|
149
|
-
await fs.unlink(candidate.path).catch(() => undefined);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
async function leaveReclaimBarrier(paths, participant) {
|
|
153
|
-
await unlinkOwnedFile(participant.path, participant, 'reclaim');
|
|
154
|
-
await cleanupDeadParticipants(paths);
|
|
155
|
-
await fs.rmdir(reclaimBarrierPath(paths)).catch((error) => {
|
|
156
|
-
const code = error?.code;
|
|
157
|
-
if (code !== 'ENOENT' && code !== 'ENOTEMPTY' && code !== 'EEXIST')
|
|
158
|
-
throw error;
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
async function cleanupDeadParticipants(paths) {
|
|
162
|
-
const barrierPath = reclaimBarrierPath(paths);
|
|
163
|
-
let entries;
|
|
164
|
-
try {
|
|
165
|
-
entries = await fs.readdir(barrierPath, { withFileTypes: true });
|
|
166
|
-
}
|
|
167
|
-
catch (error) {
|
|
168
|
-
if (error?.code === 'ENOENT')
|
|
169
|
-
return;
|
|
170
|
-
throw error;
|
|
171
|
-
}
|
|
172
|
-
for (const entry of entries) {
|
|
173
|
-
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
174
|
-
throw new Error('telegram_state_reclaim_participant_unsafe_type');
|
|
175
|
-
}
|
|
176
|
-
const participantPath = path.join(barrierPath, entry.name);
|
|
177
|
-
const inspected = await inspectOwnerFile(participantPath, 'reclaim').catch((error) => {
|
|
178
|
-
if (error?.code === 'ENOENT')
|
|
179
|
-
return null;
|
|
180
|
-
throw error;
|
|
181
|
-
});
|
|
182
|
-
if (!inspected)
|
|
183
|
-
continue;
|
|
184
|
-
if (entry.name !== `.owner.${inspected.owner.pid}.${inspected.owner.token}`) {
|
|
185
|
-
throw new Error('telegram_state_reclaim_participant_name_mismatch');
|
|
186
|
-
}
|
|
187
|
-
if (ownerIsDead(inspected.owner)) {
|
|
188
|
-
await unlinkOwnedFile(participantPath, {
|
|
189
|
-
dev: inspected.dev,
|
|
190
|
-
ino: inspected.ino,
|
|
191
|
-
pid: inspected.owner.pid,
|
|
192
|
-
token: inspected.owner.token
|
|
193
|
-
}, 'reclaim');
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
async function ensureReclaimBarrier(paths) {
|
|
198
|
-
const barrierPath = reclaimBarrierPath(paths);
|
|
199
|
-
assertTestHomeWriteAllowed(barrierPath);
|
|
200
|
-
for (let attempt = 0; attempt < LOCK_ATTEMPTS; attempt += 1) {
|
|
201
|
-
try {
|
|
202
|
-
await fs.mkdir(barrierPath, { mode: 0o700 });
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
if (error?.code !== 'EEXIST')
|
|
206
|
-
throw error;
|
|
207
|
-
}
|
|
208
|
-
if (await inspectReclaimBarrier(barrierPath))
|
|
209
|
-
return;
|
|
210
|
-
await retryDelay(attempt);
|
|
211
|
-
}
|
|
212
|
-
throw new Error('telegram_state_reclaim_barrier_timeout');
|
|
213
|
-
}
|
|
214
|
-
async function reclaimBarrierExists(paths) {
|
|
215
|
-
return inspectReclaimBarrier(reclaimBarrierPath(paths));
|
|
216
|
-
}
|
|
217
|
-
async function inspectReclaimBarrier(barrierPath) {
|
|
218
|
-
const pathStat = await fs.lstat(barrierPath).catch((error) => {
|
|
219
|
-
if (error?.code === 'ENOENT')
|
|
220
|
-
return null;
|
|
221
|
-
throw error;
|
|
222
|
-
});
|
|
223
|
-
if (!pathStat)
|
|
224
|
-
return false;
|
|
225
|
-
if (pathStat.isSymbolicLink() || !pathStat.isDirectory()) {
|
|
226
|
-
throw new Error('telegram_state_reclaim_barrier_unsafe_type');
|
|
227
|
-
}
|
|
228
|
-
if ((pathStat.mode & 0o777) !== 0o700)
|
|
229
|
-
throw new Error('telegram_state_reclaim_barrier_mode_not_0700');
|
|
230
|
-
const expectedUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
231
|
-
if (expectedUid !== null && pathStat.uid !== expectedUid) {
|
|
232
|
-
throw new Error('telegram_state_reclaim_barrier_owner_mismatch');
|
|
233
|
-
}
|
|
234
|
-
const handle = await fs.open(barrierPath, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW).catch((error) => {
|
|
235
|
-
if (error?.code === 'ENOENT')
|
|
236
|
-
return null;
|
|
237
|
-
throw error;
|
|
238
|
-
});
|
|
239
|
-
if (!handle)
|
|
240
|
-
return false;
|
|
241
|
-
try {
|
|
242
|
-
const stat = await handle.stat();
|
|
243
|
-
if (!stat.isDirectory())
|
|
244
|
-
throw new Error('telegram_state_reclaim_barrier_unsafe_type');
|
|
245
|
-
if ((stat.mode & 0o777) !== 0o700)
|
|
246
|
-
throw new Error('telegram_state_reclaim_barrier_mode_not_0700');
|
|
247
|
-
if (expectedUid !== null && stat.uid !== expectedUid) {
|
|
248
|
-
throw new Error('telegram_state_reclaim_barrier_owner_mismatch');
|
|
249
|
-
}
|
|
250
|
-
if (stat.dev !== pathStat.dev || stat.ino !== pathStat.ino)
|
|
251
|
-
return false;
|
|
252
|
-
}
|
|
253
|
-
finally {
|
|
254
|
-
await handle.close();
|
|
255
|
-
}
|
|
256
|
-
return true;
|
|
257
|
-
}
|
|
258
|
-
function reclaimBarrierPath(paths) {
|
|
259
|
-
return `${paths.stateLockPath}.reclaim`;
|
|
260
|
-
}
|
|
261
|
-
async function createOwnerCandidate(stateDir, prefix) {
|
|
262
|
-
const token = randomUUID();
|
|
263
|
-
const owner = {
|
|
264
|
-
schema: LOCK_SCHEMA,
|
|
265
|
-
pid: process.pid,
|
|
266
|
-
token,
|
|
267
|
-
process_start_seconds: readProcessStartSeconds(process.pid)
|
|
268
|
-
};
|
|
269
|
-
const candidatePath = path.join(stateDir, `${prefix}.${process.pid}.${token}.tmp`);
|
|
270
|
-
assertTestHomeWriteAllowed(candidatePath);
|
|
271
|
-
const handle = await fs.open(candidatePath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW, 0o600);
|
|
272
|
-
let closed = false;
|
|
273
|
-
try {
|
|
274
|
-
await handle.chmod(0o600);
|
|
275
|
-
await handle.writeFile(`${JSON.stringify(owner)}\n`, 'utf8');
|
|
276
|
-
await handle.sync();
|
|
277
|
-
const stat = await handle.stat();
|
|
278
|
-
if (!stat.isFile() || (stat.mode & 0o777) !== 0o600)
|
|
279
|
-
throw new Error('telegram_state_lock_candidate_unsafe');
|
|
280
|
-
const expectedUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
281
|
-
if (expectedUid !== null && stat.uid !== expectedUid) {
|
|
282
|
-
throw new Error('telegram_state_lock_candidate_owner_mismatch');
|
|
283
|
-
}
|
|
284
|
-
await handle.close();
|
|
285
|
-
closed = true;
|
|
286
|
-
return { path: candidatePath, identity: { dev: stat.dev, ino: stat.ino, pid: process.pid, token } };
|
|
287
|
-
}
|
|
288
|
-
catch (error) {
|
|
289
|
-
if (!closed)
|
|
290
|
-
await handle.close().catch(() => undefined);
|
|
291
|
-
await fs.unlink(candidatePath).catch(() => undefined);
|
|
292
|
-
throw error;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
async function inspectOwnerFile(file, kind) {
|
|
296
|
-
const prefix = kind === 'lock' ? 'telegram_state_lock' : 'telegram_state_reclaim_participant';
|
|
297
|
-
const pathStat = await fs.lstat(file);
|
|
298
|
-
if (pathStat.isSymbolicLink() || !pathStat.isFile())
|
|
299
|
-
throw new Error(`${prefix}_unsafe_type`);
|
|
300
|
-
if ((pathStat.mode & 0o777) !== 0o600)
|
|
301
|
-
throw new Error(`${prefix}_mode_not_0600`);
|
|
302
|
-
const expectedUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
303
|
-
if (expectedUid !== null && pathStat.uid !== expectedUid)
|
|
304
|
-
throw new Error(`${prefix}_owner_mismatch`);
|
|
305
|
-
if (pathStat.size <= 0 || pathStat.size > LOCK_MAX_BYTES)
|
|
306
|
-
throw new Error(`${prefix}_owner_record_invalid`);
|
|
307
|
-
const handle = await fs.open(file, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
308
|
-
try {
|
|
309
|
-
const before = await handle.stat();
|
|
310
|
-
if (!before.isFile() || before.dev !== pathStat.dev || before.ino !== pathStat.ino
|
|
311
|
-
|| (before.mode & 0o777) !== 0o600 || (expectedUid !== null && before.uid !== expectedUid)) {
|
|
312
|
-
throw new Error(`${prefix}_identity_mismatch`);
|
|
313
|
-
}
|
|
314
|
-
const bytes = await handle.readFile();
|
|
315
|
-
const after = await handle.stat();
|
|
316
|
-
const pathAfter = await fs.lstat(file);
|
|
317
|
-
if (bytes.length <= 0 || bytes.length > LOCK_MAX_BYTES || after.dev !== before.dev
|
|
318
|
-
|| after.ino !== before.ino || after.size !== before.size || pathAfter.dev !== after.dev
|
|
319
|
-
|| pathAfter.ino !== after.ino || pathAfter.isSymbolicLink() || !pathAfter.isFile()
|
|
320
|
-
|| (pathAfter.mode & 0o777) !== 0o600 || (expectedUid !== null && pathAfter.uid !== expectedUid)) {
|
|
321
|
-
throw new Error(`${prefix}_identity_mismatch`);
|
|
322
|
-
}
|
|
323
|
-
let parsed;
|
|
324
|
-
try {
|
|
325
|
-
parsed = JSON.parse(bytes.toString('utf8'));
|
|
326
|
-
}
|
|
327
|
-
catch {
|
|
328
|
-
throw new Error(`${prefix}_owner_record_invalid`);
|
|
329
|
-
}
|
|
330
|
-
if (!isLockOwner(parsed))
|
|
331
|
-
throw new Error(`${prefix}_owner_record_invalid`);
|
|
332
|
-
return { dev: after.dev, ino: after.ino, owner: parsed };
|
|
333
|
-
}
|
|
334
|
-
finally {
|
|
335
|
-
await handle.close();
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
async function unlinkOwnedFile(file, identity, kind) {
|
|
339
|
-
const current = await inspectOwnerFile(file, kind).catch((error) => {
|
|
340
|
-
if (error?.code === 'ENOENT'
|
|
341
|
-
|| (kind === 'lock' && isTransientLockInspection(error)))
|
|
342
|
-
return null;
|
|
343
|
-
throw error;
|
|
344
|
-
});
|
|
345
|
-
if (!current || !sameIdentity(current, identity))
|
|
346
|
-
return false;
|
|
347
|
-
try {
|
|
348
|
-
await fs.unlink(file);
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
catch (error) {
|
|
352
|
-
if (error?.code === 'ENOENT')
|
|
353
|
-
return false;
|
|
354
|
-
throw error;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
async function releaseStateLock(lockPath, identity) {
|
|
358
|
-
if (identity.pid !== process.pid)
|
|
359
|
-
return false;
|
|
360
|
-
return unlinkOwnedFile(lockPath, identity, 'lock');
|
|
361
|
-
}
|
|
362
|
-
function sameIdentity(current, expected) {
|
|
363
|
-
return current.dev === expected.dev && current.ino === expected.ino
|
|
364
|
-
&& current.owner.pid === expected.pid && current.owner.token === expected.token;
|
|
365
|
-
}
|
|
366
|
-
function sameInspectedLock(left, right) {
|
|
367
|
-
return left.dev === right.dev && left.ino === right.ino
|
|
368
|
-
&& left.owner.pid === right.owner.pid && left.owner.token === right.owner.token;
|
|
369
|
-
}
|
|
370
|
-
function ownerIsDead(owner) {
|
|
371
|
-
try {
|
|
372
|
-
process.kill(owner.pid, 0);
|
|
373
|
-
}
|
|
374
|
-
catch (error) {
|
|
375
|
-
const code = error?.code;
|
|
376
|
-
if (code === 'ESRCH')
|
|
377
|
-
return true;
|
|
378
|
-
if (code !== 'EPERM')
|
|
379
|
-
return false;
|
|
380
|
-
}
|
|
381
|
-
if (owner.process_start_seconds === undefined || owner.process_start_seconds === null)
|
|
382
|
-
return false;
|
|
383
|
-
const observed = readProcessStartSeconds(owner.pid);
|
|
384
|
-
return observed !== null && observed !== owner.process_start_seconds;
|
|
385
|
-
}
|
|
386
|
-
function isTransientLockInspection(error) {
|
|
387
|
-
return error?.code === 'ENOENT'
|
|
388
|
-
|| (error instanceof Error && error.message === 'telegram_state_lock_identity_mismatch');
|
|
389
|
-
}
|
|
390
|
-
function readProcessStartSeconds(pid) {
|
|
391
|
-
if (!Number.isSafeInteger(pid) || pid <= 0 || process.platform === 'win32')
|
|
392
|
-
return null;
|
|
393
|
-
const result = spawnSync('/bin/ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
394
|
-
encoding: 'utf8', timeout: 1_000, maxBuffer: 4_096
|
|
395
|
-
});
|
|
396
|
-
if (result.status !== 0)
|
|
397
|
-
return null;
|
|
398
|
-
const milliseconds = Date.parse(String(result.stdout || '').trim().replace(/\s+/g, ' '));
|
|
399
|
-
return Number.isFinite(milliseconds) ? Math.floor(milliseconds / 1_000) : null;
|
|
400
|
-
}
|
|
401
|
-
function isLockOwner(value) {
|
|
402
|
-
if (!isRecord(value))
|
|
403
|
-
return false;
|
|
404
|
-
const keys = Object.keys(value);
|
|
405
|
-
if (keys.some((key) => !['schema', 'pid', 'token', 'process_start_seconds'].includes(key)))
|
|
406
|
-
return false;
|
|
407
|
-
return keys.includes('schema') && keys.includes('pid') && keys.includes('token')
|
|
408
|
-
&& value.schema === LOCK_SCHEMA
|
|
409
|
-
&& Number.isSafeInteger(value.pid) && Number(value.pid) > 0 && Number(value.pid) <= 2_147_483_647
|
|
410
|
-
&& typeof value.token === 'string' && LOCK_TOKEN_PATTERN.test(value.token)
|
|
411
|
-
&& (value.process_start_seconds === undefined || value.process_start_seconds === null
|
|
412
|
-
|| (Number.isSafeInteger(value.process_start_seconds) && Number(value.process_start_seconds) > 0));
|
|
413
|
-
}
|
|
414
|
-
function isRecord(value) {
|
|
415
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
416
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const SYSTEM_TELEGRAM_CLOCK = { now: () => Date.now() };
|