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.
Files changed (101) hide show
  1. package/README.md +46 -110
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/command-manifest-lite.js +0 -2
  5. package/dist/cli/command-registry.js +0 -2
  6. package/dist/commands/codex.js +3 -3
  7. package/dist/commands/doctor.js +5 -157
  8. package/dist/config/skills-manifest.json +1 -1
  9. package/dist/core/codex/codex-cli-syntax-builder.js +0 -8
  10. package/dist/core/codex-app/codex-models-cache.js +2 -1
  11. package/dist/core/codex-app/menubar/constants.js +0 -7
  12. package/dist/core/codex-compat/codex-capability-matrix.js +7 -7
  13. package/dist/core/codex-compat/codex-compat-report.js +8 -12
  14. package/dist/core/codex-compat/codex-runtime-contract.js +62 -0
  15. package/dist/core/codex-compat/codex-version-policy.js +7 -7
  16. package/dist/core/codex-control/codex-current-app-capability.js +3 -3
  17. package/dist/core/codex-control/codex-current-capability.js +67 -94
  18. package/dist/core/codex-control/codex-current-core-capability.js +3 -3
  19. package/dist/core/codex-control/codex-current-core-probe-runner.js +4 -4
  20. package/dist/core/codex-control/codex-current-core-real-probes.js +2 -2
  21. package/dist/core/codex-control/codex-current-feature-capability.js +4 -4
  22. package/dist/core/codex-control/codex-current-image-path-real-probe.js +1 -1
  23. package/dist/core/codex-control/codex-current-web-search-probe.js +1 -1
  24. package/dist/core/codex-control/codex-sdk-capability.js +2 -2
  25. package/dist/core/commands/remote-command.js +2 -2
  26. package/dist/core/context7-client.js +5 -2
  27. package/dist/core/doctor/codex-current-core-probe-doctor.js +2 -2
  28. package/dist/core/doctor/doctor-dirty-planner.js +1 -1
  29. package/dist/core/doctor/doctor-readiness-matrix.js +0 -7
  30. package/dist/core/feature-fixtures.js +0 -4
  31. package/dist/core/feature-registry.js +0 -1
  32. package/dist/core/image-ux-review.js +3 -3
  33. package/dist/core/install/installed-package-smoke.js +2 -3
  34. package/dist/core/mad-sks/sql-plane/mcp-executor.js +4 -3
  35. package/dist/core/mcp/modern-protocol.js +36 -0
  36. package/dist/core/mcp-config/health-check.js +45 -30
  37. package/dist/core/perf/release-latency-slo.js +1 -2
  38. package/dist/core/proof/runtime-truth-matrix.js +1 -1
  39. package/dist/core/release/canonical-test-proof.js +1 -2
  40. package/dist/core/release/gate-manifest.js +5 -2
  41. package/dist/core/release/physical-release-gates.js +0 -26
  42. package/dist/core/release/release-gate-affected-selector.js +2 -1
  43. package/dist/core/release/release-gate-contract.js +3 -1
  44. package/dist/core/release/release-pack-receipt.js +0 -1
  45. package/dist/core/release/stage-publish.js +1 -1
  46. package/dist/core/version-manager.js +4 -0
  47. package/dist/core/version.js +1 -1
  48. package/dist/native/sks-menubar/Sources/AppDelegate.swift +1 -12
  49. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +2 -6
  50. package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +28 -471
  51. package/dist/native/sks-menubar/Sources/SecureProcessEnvelope.swift +1 -43
  52. package/dist/scripts/codex-current-app-server-v2-check.js +9 -8
  53. package/dist/scripts/codex-current-binary-identity-check.js +2 -2
  54. package/dist/scripts/codex-current-capability-check.js +9 -7
  55. package/dist/scripts/codex-current-dependency-check.js +21 -0
  56. package/dist/scripts/docs-truthfulness-check.js +4 -19
  57. package/dist/scripts/fast-codex-service-tier-proof-check.js +0 -1
  58. package/dist/scripts/install-surface-ssot-check.js +2 -2
  59. package/dist/scripts/latest-version-guidance-check.js +1 -1
  60. package/dist/scripts/packlist-performance-check.js +0 -1
  61. package/dist/scripts/release-metadata-check.js +4 -1
  62. package/dist/scripts/release-version-truth-check.js +149 -0
  63. package/dist/vendor/openai-codex/latest/hooks/permission-request.command.input.schema.json +6 -0
  64. package/dist/vendor/openai-codex/latest/hooks/permission-request.command.output.schema.json +2 -16
  65. package/dist/vendor/openai-codex/latest/hooks/post-compact.command.input.schema.json +6 -0
  66. package/dist/vendor/openai-codex/latest/hooks/post-tool-use.command.input.schema.json +6 -0
  67. package/dist/vendor/openai-codex/latest/hooks/post-tool-use.command.output.schema.json +2 -16
  68. package/dist/vendor/openai-codex/latest/hooks/pre-compact.command.input.schema.json +6 -0
  69. package/dist/vendor/openai-codex/latest/hooks/pre-tool-use.command.input.schema.json +6 -0
  70. package/dist/vendor/openai-codex/latest/hooks/pre-tool-use.command.output.schema.json +2 -16
  71. package/dist/vendor/openai-codex/latest/hooks/session-start.command.output.schema.json +2 -16
  72. package/dist/vendor/openai-codex/latest/hooks/snapshot-metadata.json +7 -5
  73. package/dist/vendor/openai-codex/latest/hooks/subagent-start.command.output.schema.json +4 -18
  74. package/dist/vendor/openai-codex/latest/hooks/user-prompt-submit.command.input.schema.json +6 -0
  75. package/dist/vendor/openai-codex/latest/hooks/user-prompt-submit.command.output.schema.json +2 -16
  76. package/package.json +4 -7
  77. package/release-gates.v2.json +49 -19
  78. package/config/codex-releases/rust-v0.146.0.json +0 -45
  79. package/dist/core/agents/agent-runner-codex-exec.js +0 -177
  80. package/dist/core/codex-compat/codex-release-manifest.js +0 -72
  81. package/dist/core/commands/telegram-command.js +0 -447
  82. package/dist/core/telegram/access.js +0 -58
  83. package/dist/core/telegram/client.js +0 -157
  84. package/dist/core/telegram/confirmation.js +0 -37
  85. package/dist/core/telegram/doctor.js +0 -154
  86. package/dist/core/telegram/index.js +0 -9
  87. package/dist/core/telegram/keychain.js +0 -317
  88. package/dist/core/telegram/liveness.js +0 -124
  89. package/dist/core/telegram/poller.js +0 -90
  90. package/dist/core/telegram/redaction.js +0 -15
  91. package/dist/core/telegram/state-lock.js +0 -416
  92. package/dist/core/telegram/types.js +0 -1
  93. package/dist/native/sks-menubar/Sources/TelegramPrivateFileStore.swift +0 -498
  94. package/dist/native/sks-menubar/Sources/TelegramPrivateFileSupport.swift +0 -236
  95. package/dist/native/sks-menubar/Sources/TelegramProcessGateway.swift +0 -454
  96. package/dist/native/sks-menubar/Sources/TelegramRuntimeSupport.swift +0 -56
  97. package/dist/native/sks-menubar/Sources/TelegramStateLock.swift +0 -448
  98. package/dist/native/sks-menubar/Sources/TelegramSupport.swift +0 -243
  99. package/dist/native/sks-menubar/Sources/TelegramTransport.swift +0 -499
  100. package/dist/scripts/codex-current-manifest-check.js +0 -29
  101. 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() };