@hardkas/core 0.2.2-alpha → 0.3.0-alpha
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/dist/index.d.ts +141 -1
- package/dist/index.js +367 -1
- package/package.json +5 -2
package/dist/index.d.ts
CHANGED
|
@@ -276,6 +276,136 @@ type UnknownEventPayload = {
|
|
|
276
276
|
*/
|
|
277
277
|
type Branded<K, T> = Brand<T, K extends string ? K : string>;
|
|
278
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Redacts sensitive information from strings and objects recursively.
|
|
281
|
+
* Masks Kaspa private keys (64 hex chars) and mnemonics.
|
|
282
|
+
*/
|
|
283
|
+
declare function maskSecrets(data: any): any;
|
|
284
|
+
/**
|
|
285
|
+
* Legacy single-value redaction for backward compatibility.
|
|
286
|
+
*/
|
|
287
|
+
declare function redactSecret(value: string): string;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Options for atomic file writing.
|
|
291
|
+
*/
|
|
292
|
+
interface WriteFileAtomicOptions {
|
|
293
|
+
/** Encoding for string data (default: utf-8) */
|
|
294
|
+
encoding?: BufferEncoding;
|
|
295
|
+
/** File mode (permissions) */
|
|
296
|
+
mode?: number;
|
|
297
|
+
/** If true, calls fsync on the parent directory (Linux/macOS) */
|
|
298
|
+
fsyncParent?: boolean;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Writes a file atomically using the temp-file-and-rename pattern.
|
|
302
|
+
* Ensures that either the entire file is written or no changes are made.
|
|
303
|
+
*
|
|
304
|
+
* Pattern:
|
|
305
|
+
* 1. Write data to a temporary file in the same directory.
|
|
306
|
+
* 2. fsync the temporary file to ensure data is on disk.
|
|
307
|
+
* 3. Close the temporary file.
|
|
308
|
+
* 4. Rename the temporary file to the target path (atomic operation).
|
|
309
|
+
* 5. Optional: fsync the parent directory to ensure metadata is on disk.
|
|
310
|
+
*/
|
|
311
|
+
declare function writeFileAtomic(targetPath: string, data: string | Buffer, options?: WriteFileAtomicOptions): Promise<void>;
|
|
312
|
+
/**
|
|
313
|
+
* Synchronous version of writeFileAtomic.
|
|
314
|
+
*/
|
|
315
|
+
declare function writeFileAtomicSync(targetPath: string, data: string | Buffer, options?: WriteFileAtomicOptions): void;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* HardKAS Corruption Taxonomy & Issue Reporting
|
|
319
|
+
*/
|
|
320
|
+
type CorruptionCode = "ARTIFACT_JSON_INVALID" | "ARTIFACT_SCHEMA_MISSING" | "ARTIFACT_SCHEMA_INVALID" | "ARTIFACT_HASH_MISMATCH" | "ARTIFACT_ID_INVALID" | "ARTIFACT_LINEAGE_INVALID" | "EVENT_JSON_INVALID" | "EVENT_SCHEMA_INVALID" | "EVENT_LINE_CORRUPT" | "STORE_STALE" | "STORE_CORRUPT" | "STORE_REBUILD_REQUIRED" | "DUPLICATE_ARTIFACT" | "DUPLICATE_EVENT" | "SEMANTIC_DIVERGENCE" | "REPLAY_PARTIAL" | "REPLAY_UNSUPPORTED_CHECK" | "LOCK_HELD" | "LOCK_TIMEOUT" | "STALE_LOCK" | "LOCK_RELEASE_FAILED" | "LOCK_OWNER_MISMATCH" | "LOCK_METADATA_INVALID" | "STORE_MIGRATION_REQUIRED" | "STORE_MIGRATION_FAILED" | "STORE_MIGRATION_CHECKSUM_MISMATCH" | "STORE_SCHEMA_UNSUPPORTED" | "STORE_LEGACY_BOOTSTRAPPED";
|
|
321
|
+
type CorruptionSeverity = "warning" | "error";
|
|
322
|
+
interface CorruptionIssue {
|
|
323
|
+
readonly code: CorruptionCode;
|
|
324
|
+
readonly severity: CorruptionSeverity;
|
|
325
|
+
readonly message: string;
|
|
326
|
+
readonly path?: string;
|
|
327
|
+
readonly lineNumber?: number;
|
|
328
|
+
readonly artifactId?: string;
|
|
329
|
+
readonly contentHash?: string;
|
|
330
|
+
readonly suggestion?: string;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Formats a corruption issue for human-readable output.
|
|
334
|
+
*/
|
|
335
|
+
declare function formatCorruptionIssue(issue: CorruptionIssue): string;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* HardKAS Lock Metadata schema v1
|
|
339
|
+
*/
|
|
340
|
+
interface LockMetadata {
|
|
341
|
+
schema: "hardkas.lock.v1";
|
|
342
|
+
name: string;
|
|
343
|
+
pid: number;
|
|
344
|
+
command: string;
|
|
345
|
+
cwd: string;
|
|
346
|
+
hostname: string;
|
|
347
|
+
createdAt: string;
|
|
348
|
+
expiresAt: string | null;
|
|
349
|
+
}
|
|
350
|
+
interface LockHandle {
|
|
351
|
+
readonly path: string;
|
|
352
|
+
readonly metadata: LockMetadata;
|
|
353
|
+
release(): Promise<void>;
|
|
354
|
+
}
|
|
355
|
+
interface AcquireLockArgs {
|
|
356
|
+
rootDir: string;
|
|
357
|
+
name: string;
|
|
358
|
+
command?: string;
|
|
359
|
+
staleMs?: number;
|
|
360
|
+
wait?: boolean;
|
|
361
|
+
timeoutMs?: number;
|
|
362
|
+
pollMs?: number;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Deterministic lock ordering to avoid deadlocks.
|
|
366
|
+
* workspace > node > accounts > artifacts > events > query-store
|
|
367
|
+
*/
|
|
368
|
+
declare const LOCK_ORDER: string[];
|
|
369
|
+
/**
|
|
370
|
+
* Acquires a named lock for the workspace.
|
|
371
|
+
*/
|
|
372
|
+
declare function acquireLock(args: AcquireLockArgs): Promise<LockHandle>;
|
|
373
|
+
/**
|
|
374
|
+
* Helper to run a task with a single lock.
|
|
375
|
+
*/
|
|
376
|
+
declare function withLock<T>(args: AcquireLockArgs, fn: (handle: LockHandle) => Promise<T>): Promise<T>;
|
|
377
|
+
/**
|
|
378
|
+
* Helper to run a task with multiple locks in deterministic order.
|
|
379
|
+
*/
|
|
380
|
+
declare function withLocks<T>(rootDir: string, names: string[], fn: () => Promise<T>, options?: {
|
|
381
|
+
command?: string;
|
|
382
|
+
wait?: boolean;
|
|
383
|
+
timeoutMs?: number;
|
|
384
|
+
}): Promise<T>;
|
|
385
|
+
/**
|
|
386
|
+
* Checks if a process is alive.
|
|
387
|
+
*/
|
|
388
|
+
declare function isProcessAlive(pid: number): boolean;
|
|
389
|
+
/**
|
|
390
|
+
* Lists all active locks in the workspace.
|
|
391
|
+
*/
|
|
392
|
+
declare function listLocks(rootDir: string): Array<{
|
|
393
|
+
name: string;
|
|
394
|
+
metadata: LockMetadata;
|
|
395
|
+
path: string;
|
|
396
|
+
isAlive: boolean;
|
|
397
|
+
}>;
|
|
398
|
+
/**
|
|
399
|
+
* Safely clears a lock if criteria are met.
|
|
400
|
+
*/
|
|
401
|
+
declare function clearLock(rootDir: string, name: string, options?: {
|
|
402
|
+
force?: boolean;
|
|
403
|
+
ifDead?: boolean;
|
|
404
|
+
}): {
|
|
405
|
+
cleared: boolean;
|
|
406
|
+
reason?: string;
|
|
407
|
+
};
|
|
408
|
+
|
|
279
409
|
declare const SOMPI_PER_KAS = 100000000n;
|
|
280
410
|
declare const kaspaNetworkIdSchema: z.ZodEnum<["mainnet", "testnet-10", "testnet-11", "testnet-12", "simnet", "simnet-1", "devnet"]>;
|
|
281
411
|
type NetworkId = Brand<z.infer<typeof kaspaNetworkIdSchema>, "NetworkId">;
|
|
@@ -352,8 +482,18 @@ declare class HardkasError extends Error {
|
|
|
352
482
|
cause?: unknown;
|
|
353
483
|
});
|
|
354
484
|
}
|
|
485
|
+
type InvariantDomain = "semantic" | "replay" | "provenance" | "structural" | "operational";
|
|
486
|
+
type InvariantSeverity = "warning" | "error" | "fatal";
|
|
487
|
+
declare class InvariantViolationError extends HardkasError {
|
|
488
|
+
readonly domain: InvariantDomain;
|
|
489
|
+
readonly severity: InvariantSeverity;
|
|
490
|
+
constructor(domain: InvariantDomain, message: string, options?: {
|
|
491
|
+
severity?: InvariantSeverity;
|
|
492
|
+
cause?: unknown;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
355
495
|
declare function parseHardkasConfig(input: unknown): HardkasConfig;
|
|
356
496
|
declare function parseKasToSompi(input: string): bigint;
|
|
357
497
|
declare function formatSompi(amountSompi: bigint): string;
|
|
358
498
|
|
|
359
|
-
export { type ArtifactId, type ArtifactType, ArtifactTypeSchema, type Brand, type Branded, type ContentHash, type CoreEvent, type CoreEventListener, type CorrelationId, type DaaScore, type EventDomain, type EventEnvelope, type EventId, type EventKind, type EventPayloadByKind, type EventSequence, type ExecutionMode, ExecutionModeSchema, type HardkasConfig, HardkasError, type KaspaAddress, type LineageId, type NetworkId, NetworkIdSchema, type RpcEndpointId, SOMPI_PER_KAS, type StampedEvent, type TxId, type UnknownEventPayload, type WorkflowId, artifactTypeSchema, asArtifactId, asContentHash, asCorrelationId, asDaaScore, asEventId, asEventSequence, asKaspaAddress, asLineageId, asNetworkId, asRpcEndpointId, asTxId, asWorkflowId, coreEvents, createEventEnvelope, executionModeSchema, formatSompi, hardkasConfigSchema, kaspaNetworkIdSchema, parseHardkasConfig, parseKasToSompi, validateEventEnvelope };
|
|
499
|
+
export { type AcquireLockArgs, type ArtifactId, type ArtifactType, ArtifactTypeSchema, type Brand, type Branded, type ContentHash, type CoreEvent, type CoreEventListener, type CorrelationId, type CorruptionCode, type CorruptionIssue, type CorruptionSeverity, type DaaScore, type EventDomain, type EventEnvelope, type EventId, type EventKind, type EventPayloadByKind, type EventSequence, type ExecutionMode, ExecutionModeSchema, type HardkasConfig, HardkasError, type InvariantDomain, type InvariantSeverity, InvariantViolationError, type KaspaAddress, LOCK_ORDER, type LineageId, type LockHandle, type LockMetadata, type NetworkId, NetworkIdSchema, type RpcEndpointId, SOMPI_PER_KAS, type StampedEvent, type TxId, type UnknownEventPayload, type WorkflowId, type WriteFileAtomicOptions, acquireLock, artifactTypeSchema, asArtifactId, asContentHash, asCorrelationId, asDaaScore, asEventId, asEventSequence, asKaspaAddress, asLineageId, asNetworkId, asRpcEndpointId, asTxId, asWorkflowId, clearLock, coreEvents, createEventEnvelope, executionModeSchema, formatCorruptionIssue, formatSompi, hardkasConfigSchema, isProcessAlive, kaspaNetworkIdSchema, listLocks, maskSecrets, parseHardkasConfig, parseKasToSompi, redactSecret, validateEventEnvelope, withLock, withLocks, writeFileAtomic, writeFileAtomicSync };
|
package/dist/index.js
CHANGED
|
@@ -77,6 +77,349 @@ var asNetworkId = (id) => id;
|
|
|
77
77
|
var asEventSequence = (seq) => seq;
|
|
78
78
|
var asDaaScore = (score) => score;
|
|
79
79
|
|
|
80
|
+
// src/security.ts
|
|
81
|
+
function maskSecrets(data) {
|
|
82
|
+
if (data === null || data === void 0) return data;
|
|
83
|
+
if (typeof data === "string") {
|
|
84
|
+
let redacted = data.replace(/\b[0-9a-fA-F]{64}\b/g, (match) => {
|
|
85
|
+
return `${match.slice(0, 6)}...${match.slice(-4)} [REDACTED]`;
|
|
86
|
+
});
|
|
87
|
+
redacted = redacted.replace(/\b([a-z]{3,10}\s+){11,23}[a-z]{3,10}\b/g, "[MNEMONIC REDACTED]");
|
|
88
|
+
return redacted;
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(data)) {
|
|
91
|
+
return data.map((item) => maskSecrets(item));
|
|
92
|
+
}
|
|
93
|
+
if (typeof data === "object") {
|
|
94
|
+
const redactedObj = {};
|
|
95
|
+
for (const key in data) {
|
|
96
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
97
|
+
if (key.toLowerCase().includes("secret") || key.toLowerCase().includes("privatekey") || key.toLowerCase().includes("mnemonic") || key.toLowerCase().includes("password")) {
|
|
98
|
+
redactedObj[key] = "[REDACTED]";
|
|
99
|
+
} else {
|
|
100
|
+
redactedObj[key] = maskSecrets(data[key]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return redactedObj;
|
|
105
|
+
}
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
function redactSecret(value) {
|
|
109
|
+
if (!value) return "";
|
|
110
|
+
if (value.length <= 10) return "***";
|
|
111
|
+
return `${value.slice(0, 6)}...${value.slice(-4)}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/fs.ts
|
|
115
|
+
import fs from "fs";
|
|
116
|
+
import path from "path";
|
|
117
|
+
async function writeFileAtomic(targetPath, data, options = {}) {
|
|
118
|
+
const dir = path.dirname(targetPath);
|
|
119
|
+
const base = path.basename(targetPath);
|
|
120
|
+
const tempPath = path.join(dir, `.tmp.${base}.${Math.random().toString(36).slice(2)}`);
|
|
121
|
+
let fd = null;
|
|
122
|
+
try {
|
|
123
|
+
if (!fs.existsSync(dir)) {
|
|
124
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
fd = fs.openSync(tempPath, "w", options.mode);
|
|
127
|
+
const buffer = typeof data === "string" ? Buffer.from(data, options.encoding || "utf-8") : data;
|
|
128
|
+
fs.writeSync(fd, buffer, 0, buffer.length);
|
|
129
|
+
fs.fsyncSync(fd);
|
|
130
|
+
fs.closeSync(fd);
|
|
131
|
+
fd = null;
|
|
132
|
+
let attempts = 0;
|
|
133
|
+
const maxAttempts = process.platform === "win32" ? 5 : 1;
|
|
134
|
+
while (attempts < maxAttempts) {
|
|
135
|
+
try {
|
|
136
|
+
fs.renameSync(tempPath, targetPath);
|
|
137
|
+
break;
|
|
138
|
+
} catch (e) {
|
|
139
|
+
attempts++;
|
|
140
|
+
if (attempts >= maxAttempts) throw e;
|
|
141
|
+
if (e.code === "EPERM" || e.code === "EBUSY") {
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 10 * attempts));
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (options.fsyncParent && process.platform !== "win32") {
|
|
149
|
+
let dirFd = null;
|
|
150
|
+
try {
|
|
151
|
+
dirFd = fs.openSync(dir, "r");
|
|
152
|
+
fs.fsyncSync(dirFd);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
} finally {
|
|
155
|
+
if (dirFd !== null) fs.closeSync(dirFd);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
throw new HardkasError(
|
|
160
|
+
"IO_ERROR",
|
|
161
|
+
`Failed to write file atomically: ${targetPath}`,
|
|
162
|
+
{ cause: err }
|
|
163
|
+
);
|
|
164
|
+
} finally {
|
|
165
|
+
if (fs.existsSync(tempPath)) {
|
|
166
|
+
try {
|
|
167
|
+
fs.unlinkSync(tempPath);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (fd !== null) {
|
|
172
|
+
try {
|
|
173
|
+
fs.closeSync(fd);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function writeFileAtomicSync(targetPath, data, options = {}) {
|
|
180
|
+
const dir = path.dirname(targetPath);
|
|
181
|
+
const base = path.basename(targetPath);
|
|
182
|
+
const tempPath = path.join(dir, `.tmp.${base}.${Math.random().toString(36).slice(2)}`);
|
|
183
|
+
let fd = null;
|
|
184
|
+
try {
|
|
185
|
+
if (!fs.existsSync(dir)) {
|
|
186
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
fd = fs.openSync(tempPath, "w", options.mode);
|
|
189
|
+
const buffer = typeof data === "string" ? Buffer.from(data, options.encoding || "utf-8") : data;
|
|
190
|
+
fs.writeSync(fd, buffer, 0, buffer.length);
|
|
191
|
+
fs.fsyncSync(fd);
|
|
192
|
+
fs.closeSync(fd);
|
|
193
|
+
fd = null;
|
|
194
|
+
let attempts = 0;
|
|
195
|
+
const maxAttempts = process.platform === "win32" ? 5 : 1;
|
|
196
|
+
while (attempts < maxAttempts) {
|
|
197
|
+
try {
|
|
198
|
+
fs.renameSync(tempPath, targetPath);
|
|
199
|
+
break;
|
|
200
|
+
} catch (e) {
|
|
201
|
+
attempts++;
|
|
202
|
+
if (attempts >= maxAttempts) throw e;
|
|
203
|
+
if (e.code === "EPERM" || e.code === "EBUSY") {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
throw e;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (options.fsyncParent && process.platform !== "win32") {
|
|
210
|
+
let dirFd = null;
|
|
211
|
+
try {
|
|
212
|
+
dirFd = fs.openSync(dir, "r");
|
|
213
|
+
fs.fsyncSync(dirFd);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
} finally {
|
|
216
|
+
if (dirFd !== null) fs.closeSync(dirFd);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
throw new HardkasError(
|
|
221
|
+
"IO_ERROR",
|
|
222
|
+
`Failed to write file atomically (sync): ${targetPath}`,
|
|
223
|
+
{ cause: err }
|
|
224
|
+
);
|
|
225
|
+
} finally {
|
|
226
|
+
if (fs.existsSync(tempPath)) {
|
|
227
|
+
try {
|
|
228
|
+
fs.unlinkSync(tempPath);
|
|
229
|
+
} catch (e) {
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (fd !== null) {
|
|
233
|
+
try {
|
|
234
|
+
fs.closeSync(fd);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/corruption.ts
|
|
242
|
+
function formatCorruptionIssue(issue) {
|
|
243
|
+
const parts = [];
|
|
244
|
+
const icon = issue.severity === "error" ? "\u274C" : "\u26A0\uFE0F";
|
|
245
|
+
parts.push(`${icon} [${issue.code}] ${issue.message}`);
|
|
246
|
+
if (issue.path) {
|
|
247
|
+
const loc = issue.lineNumber ? `${issue.path}:${issue.lineNumber}` : issue.path;
|
|
248
|
+
parts.push(` Location: ${loc}`);
|
|
249
|
+
}
|
|
250
|
+
if (issue.artifactId) {
|
|
251
|
+
parts.push(` Artifact: ${issue.artifactId}`);
|
|
252
|
+
}
|
|
253
|
+
if (issue.suggestion) {
|
|
254
|
+
parts.push(` Suggestion: ${issue.suggestion}`);
|
|
255
|
+
}
|
|
256
|
+
return parts.join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/lock.ts
|
|
260
|
+
import fs2 from "fs";
|
|
261
|
+
import path2 from "path";
|
|
262
|
+
import os from "os";
|
|
263
|
+
var LOCK_ORDER = [
|
|
264
|
+
"workspace",
|
|
265
|
+
"node",
|
|
266
|
+
"accounts",
|
|
267
|
+
"artifacts",
|
|
268
|
+
"events",
|
|
269
|
+
"query-store"
|
|
270
|
+
];
|
|
271
|
+
async function acquireLock(args) {
|
|
272
|
+
const lockDir = path2.join(args.rootDir, ".hardkas", "locks");
|
|
273
|
+
const lockPath = path2.join(lockDir, `${args.name}.lock`);
|
|
274
|
+
const timeoutMs = args.timeoutMs ?? 3e4;
|
|
275
|
+
const pollMs = args.pollMs ?? 250;
|
|
276
|
+
const start = Date.now();
|
|
277
|
+
if (!fs2.existsSync(lockDir)) {
|
|
278
|
+
fs2.mkdirSync(lockDir, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
while (true) {
|
|
281
|
+
try {
|
|
282
|
+
const metadata = {
|
|
283
|
+
schema: "hardkas.lock.v1",
|
|
284
|
+
name: args.name,
|
|
285
|
+
pid: process.pid,
|
|
286
|
+
command: args.command || process.argv.join(" "),
|
|
287
|
+
cwd: process.cwd(),
|
|
288
|
+
hostname: os.hostname(),
|
|
289
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
290
|
+
expiresAt: null
|
|
291
|
+
};
|
|
292
|
+
const fd = fs2.openSync(lockPath, "wx");
|
|
293
|
+
fs2.writeSync(fd, JSON.stringify(metadata, null, 2));
|
|
294
|
+
fs2.closeSync(fd);
|
|
295
|
+
return {
|
|
296
|
+
path: lockPath,
|
|
297
|
+
metadata,
|
|
298
|
+
release: async () => {
|
|
299
|
+
if (fs2.existsSync(lockPath)) {
|
|
300
|
+
try {
|
|
301
|
+
const current = JSON.parse(fs2.readFileSync(lockPath, "utf-8"));
|
|
302
|
+
if (current.pid === process.pid) {
|
|
303
|
+
fs2.unlinkSync(lockPath);
|
|
304
|
+
}
|
|
305
|
+
} catch (e) {
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
} catch (e) {
|
|
311
|
+
if (e.code === "EEXIST") {
|
|
312
|
+
let existingMetadata = null;
|
|
313
|
+
try {
|
|
314
|
+
existingMetadata = JSON.parse(fs2.readFileSync(lockPath, "utf-8"));
|
|
315
|
+
} catch (err) {
|
|
316
|
+
throw new HardkasError("LOCK_METADATA_INVALID", `Lock file at ${lockPath} is corrupted.`, { cause: err });
|
|
317
|
+
}
|
|
318
|
+
if (existingMetadata) {
|
|
319
|
+
const isAlive = isProcessAlive(existingMetadata.pid);
|
|
320
|
+
if (!isAlive) {
|
|
321
|
+
throw new HardkasError(
|
|
322
|
+
"STALE_LOCK",
|
|
323
|
+
`Workspace is locked by a dead process (PID: ${existingMetadata.pid}).`,
|
|
324
|
+
{ cause: existingMetadata }
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (args.wait && Date.now() - start < timeoutMs) {
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
throw new HardkasError(
|
|
332
|
+
args.wait ? "LOCK_TIMEOUT" : "LOCK_HELD",
|
|
333
|
+
`Workspace is locked by another HardKAS process (PID: ${existingMetadata.pid}).`,
|
|
334
|
+
{ cause: existingMetadata }
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
throw e;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async function withLock(args, fn) {
|
|
343
|
+
const handle = await acquireLock(args);
|
|
344
|
+
try {
|
|
345
|
+
return await fn(handle);
|
|
346
|
+
} finally {
|
|
347
|
+
await handle.release();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function withLocks(rootDir, names, fn, options = {}) {
|
|
351
|
+
const sortedNames = [...names].sort((a, b) => {
|
|
352
|
+
const idxA = LOCK_ORDER.indexOf(a);
|
|
353
|
+
const idxB = LOCK_ORDER.indexOf(b);
|
|
354
|
+
return idxA - idxB;
|
|
355
|
+
});
|
|
356
|
+
const handles = [];
|
|
357
|
+
try {
|
|
358
|
+
for (const name of sortedNames) {
|
|
359
|
+
handles.push(await acquireLock({ rootDir, name, ...options }));
|
|
360
|
+
}
|
|
361
|
+
return await fn();
|
|
362
|
+
} finally {
|
|
363
|
+
for (const handle of handles.reverse()) {
|
|
364
|
+
await handle.release();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function isProcessAlive(pid) {
|
|
369
|
+
try {
|
|
370
|
+
process.kill(pid, 0);
|
|
371
|
+
return true;
|
|
372
|
+
} catch (e) {
|
|
373
|
+
return e.code !== "ESRCH";
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function listLocks(rootDir) {
|
|
377
|
+
const lockDir = path2.join(rootDir, ".hardkas", "locks");
|
|
378
|
+
if (!fs2.existsSync(lockDir)) return [];
|
|
379
|
+
const files = fs2.readdirSync(lockDir).filter((f) => f.endsWith(".lock"));
|
|
380
|
+
const result = [];
|
|
381
|
+
for (const file of files) {
|
|
382
|
+
const lockPath = path2.join(lockDir, file);
|
|
383
|
+
try {
|
|
384
|
+
const metadata = JSON.parse(fs2.readFileSync(lockPath, "utf-8"));
|
|
385
|
+
result.push({
|
|
386
|
+
name: path2.basename(file, ".lock"),
|
|
387
|
+
metadata,
|
|
388
|
+
path: lockPath,
|
|
389
|
+
isAlive: metadata.hostname === os.hostname() ? isProcessAlive(metadata.pid) : true
|
|
390
|
+
// Assume alive if remote
|
|
391
|
+
});
|
|
392
|
+
} catch (e) {
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
function clearLock(rootDir, name, options = {}) {
|
|
398
|
+
const lockDir = path2.join(rootDir, ".hardkas", "locks");
|
|
399
|
+
const lockPath = path2.join(lockDir, `${name}.lock`);
|
|
400
|
+
if (!fs2.existsSync(lockPath)) return { cleared: false, reason: "Lock not found" };
|
|
401
|
+
let metadata;
|
|
402
|
+
try {
|
|
403
|
+
metadata = JSON.parse(fs2.readFileSync(lockPath, "utf-8"));
|
|
404
|
+
} catch (e) {
|
|
405
|
+
if (options.force) {
|
|
406
|
+
fs2.unlinkSync(lockPath);
|
|
407
|
+
return { cleared: true };
|
|
408
|
+
}
|
|
409
|
+
return { cleared: false, reason: "Corrupt metadata (use --force to clear)" };
|
|
410
|
+
}
|
|
411
|
+
const isLocal = metadata.hostname === os.hostname();
|
|
412
|
+
const isAlive = isLocal ? isProcessAlive(metadata.pid) : true;
|
|
413
|
+
if (options.ifDead) {
|
|
414
|
+
if (!isLocal) return { cleared: false, reason: "Cannot verify liveness of remote lock (host: " + metadata.hostname + ")" };
|
|
415
|
+
if (isAlive) return { cleared: false, reason: `Process (PID: ${metadata.pid}) is still alive` };
|
|
416
|
+
} else if (!options.force) {
|
|
417
|
+
return { cleared: false, reason: "Lock is potentially active. Use --force or --if-dead." };
|
|
418
|
+
}
|
|
419
|
+
fs2.unlinkSync(lockPath);
|
|
420
|
+
return { cleared: true };
|
|
421
|
+
}
|
|
422
|
+
|
|
80
423
|
// src/index.ts
|
|
81
424
|
var SOMPI_PER_KAS = 100000000n;
|
|
82
425
|
var kaspaNetworkIdSchema = z.enum([
|
|
@@ -127,6 +470,16 @@ var HardkasError = class extends Error {
|
|
|
127
470
|
this.cause = options?.cause;
|
|
128
471
|
}
|
|
129
472
|
};
|
|
473
|
+
var InvariantViolationError = class extends HardkasError {
|
|
474
|
+
domain;
|
|
475
|
+
severity;
|
|
476
|
+
constructor(domain, message, options) {
|
|
477
|
+
super(`INVARIANT_VIOLATION_${domain.toUpperCase()}`, message, { cause: options?.cause });
|
|
478
|
+
this.name = "InvariantViolationError";
|
|
479
|
+
this.domain = domain;
|
|
480
|
+
this.severity = options?.severity || "fatal";
|
|
481
|
+
}
|
|
482
|
+
};
|
|
130
483
|
function parseHardkasConfig(input) {
|
|
131
484
|
const result = hardkasConfigSchema.safeParse(input);
|
|
132
485
|
if (!result.success) {
|
|
@@ -160,8 +513,11 @@ export {
|
|
|
160
513
|
ArtifactTypeSchema,
|
|
161
514
|
ExecutionModeSchema,
|
|
162
515
|
HardkasError,
|
|
516
|
+
InvariantViolationError,
|
|
517
|
+
LOCK_ORDER,
|
|
163
518
|
NetworkIdSchema,
|
|
164
519
|
SOMPI_PER_KAS,
|
|
520
|
+
acquireLock,
|
|
165
521
|
artifactTypeSchema,
|
|
166
522
|
asArtifactId,
|
|
167
523
|
asContentHash,
|
|
@@ -175,13 +531,23 @@ export {
|
|
|
175
531
|
asRpcEndpointId,
|
|
176
532
|
asTxId,
|
|
177
533
|
asWorkflowId,
|
|
534
|
+
clearLock,
|
|
178
535
|
coreEvents,
|
|
179
536
|
createEventEnvelope,
|
|
180
537
|
executionModeSchema,
|
|
538
|
+
formatCorruptionIssue,
|
|
181
539
|
formatSompi,
|
|
182
540
|
hardkasConfigSchema,
|
|
541
|
+
isProcessAlive,
|
|
183
542
|
kaspaNetworkIdSchema,
|
|
543
|
+
listLocks,
|
|
544
|
+
maskSecrets,
|
|
184
545
|
parseHardkasConfig,
|
|
185
546
|
parseKasToSompi,
|
|
186
|
-
|
|
547
|
+
redactSecret,
|
|
548
|
+
validateEventEnvelope,
|
|
549
|
+
withLock,
|
|
550
|
+
withLocks,
|
|
551
|
+
writeFileAtomic,
|
|
552
|
+
writeFileAtomicSync
|
|
187
553
|
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hardkas/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-alpha",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
|
-
".":
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
9
12
|
},
|
|
10
13
|
"dependencies": {
|
|
11
14
|
"pino": "^9.5.0",
|