@hasna/machines 0.0.48 → 0.0.49
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 +19 -0
- package/dist/agent/index.js +34 -12
- package/dist/cli/index.js +504 -387
- package/dist/commands/mutation-approval.d.ts +11 -0
- package/dist/commands/runtime.d.ts +1 -0
- package/dist/consumer.js +0 -81
- package/dist/index.d.ts +57 -29
- package/dist/index.js +9202 -25314
- package/dist/mcp/index.js +120 -22
- package/dist/remote-storage.d.ts +5 -1
- package/dist/sdk-mutations.d.ts +54 -0
- package/dist/storage.d.ts +18 -3
- package/dist/storage.js +392 -92
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
993
993
|
this._exitCallback = (err) => {
|
|
994
994
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
995
995
|
throw err;
|
|
996
|
-
}
|
|
996
|
+
}
|
|
997
997
|
};
|
|
998
998
|
}
|
|
999
999
|
return this;
|
|
@@ -2283,6 +2283,289 @@ var init_db = __esm(() => {
|
|
|
2283
2283
|
];
|
|
2284
2284
|
});
|
|
2285
2285
|
|
|
2286
|
+
// src/commands/mutation-approval.ts
|
|
2287
|
+
import { createHash, createHmac, randomUUID, timingSafeEqual } from "crypto";
|
|
2288
|
+
import { resolve as resolve2 } from "path";
|
|
2289
|
+
function createTrustedSdkMutationApproval() {
|
|
2290
|
+
const approval = {};
|
|
2291
|
+
trustedSdkMutationApprovals.add(approval);
|
|
2292
|
+
return approval;
|
|
2293
|
+
}
|
|
2294
|
+
function isTrustedSdkMutationApproval(approval) {
|
|
2295
|
+
return typeof approval === "object" && approval !== null && trustedSdkMutationApprovals.has(approval);
|
|
2296
|
+
}
|
|
2297
|
+
function isTruthy(value) {
|
|
2298
|
+
return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
|
|
2299
|
+
}
|
|
2300
|
+
function nowMs(now) {
|
|
2301
|
+
if (typeof now === "number")
|
|
2302
|
+
return now;
|
|
2303
|
+
if (now instanceof Date)
|
|
2304
|
+
return now.getTime();
|
|
2305
|
+
return Date.now();
|
|
2306
|
+
}
|
|
2307
|
+
function signingSecret(env2, explicitSecret) {
|
|
2308
|
+
return explicitSecret?.trim() || env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim();
|
|
2309
|
+
}
|
|
2310
|
+
function hmac(payload, secret) {
|
|
2311
|
+
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
2312
|
+
}
|
|
2313
|
+
function sha256Hex(payload) {
|
|
2314
|
+
return createHash("sha256").update(payload).digest("hex");
|
|
2315
|
+
}
|
|
2316
|
+
function replayDbPath(env2) {
|
|
2317
|
+
const configured = env2[MUTATION_APPROVAL_REPLAY_PATH_ENV]?.trim();
|
|
2318
|
+
return configured ? resolve2(configured) : undefined;
|
|
2319
|
+
}
|
|
2320
|
+
function replayNonceKey(claims) {
|
|
2321
|
+
return sha256Hex(JSON.stringify({ nonce: claims.nonce }));
|
|
2322
|
+
}
|
|
2323
|
+
function recordReplayNonce(env2, claims, tokenPayload, now) {
|
|
2324
|
+
const dbPath = replayDbPath(env2);
|
|
2325
|
+
if (!dbPath)
|
|
2326
|
+
return;
|
|
2327
|
+
if (!claims.nonce) {
|
|
2328
|
+
return { approved: false, reason: "approval_token nonce claim is required for replay protection." };
|
|
2329
|
+
}
|
|
2330
|
+
try {
|
|
2331
|
+
const db = getDb(dbPath);
|
|
2332
|
+
db.query("DELETE FROM mutation_approval_nonces WHERE expires_at <= ?").run(now);
|
|
2333
|
+
const result = db.query(`
|
|
2334
|
+
INSERT OR IGNORE INTO mutation_approval_nonces (
|
|
2335
|
+
nonce_sha256,
|
|
2336
|
+
token_sha256,
|
|
2337
|
+
surface,
|
|
2338
|
+
operation,
|
|
2339
|
+
caller_id,
|
|
2340
|
+
run_id,
|
|
2341
|
+
transport,
|
|
2342
|
+
expires_at,
|
|
2343
|
+
used_at
|
|
2344
|
+
)
|
|
2345
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2346
|
+
`).run(replayNonceKey(claims), sha256Hex(tokenPayload), claims.surface, claims.operation, claims.callerId ?? "", claims.runId ?? "", claims.transport ?? "", claims.expiresAt, now);
|
|
2347
|
+
if (result.changes !== 1) {
|
|
2348
|
+
return { approved: false, reason: "approval_token nonce has already been used." };
|
|
2349
|
+
}
|
|
2350
|
+
return;
|
|
2351
|
+
} catch (error) {
|
|
2352
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2353
|
+
return { approved: false, reason: `approval_token replay store is unavailable: ${message}` };
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
function safeEqual(left, right) {
|
|
2357
|
+
const leftBuffer = Buffer.from(left);
|
|
2358
|
+
const rightBuffer = Buffer.from(right);
|
|
2359
|
+
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
2360
|
+
}
|
|
2361
|
+
function canonicalizeMutationArg(value, inArray = false) {
|
|
2362
|
+
if (value === undefined)
|
|
2363
|
+
return inArray ? null : undefined;
|
|
2364
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
2365
|
+
return value;
|
|
2366
|
+
if (typeof value === "number")
|
|
2367
|
+
return Number.isFinite(value) ? value : null;
|
|
2368
|
+
if (Array.isArray(value)) {
|
|
2369
|
+
return value.map((entry) => canonicalizeMutationArg(entry, true) ?? null);
|
|
2370
|
+
}
|
|
2371
|
+
if (value instanceof Date)
|
|
2372
|
+
return value.toISOString();
|
|
2373
|
+
if (typeof value === "object") {
|
|
2374
|
+
const result = {};
|
|
2375
|
+
for (const key of Object.keys(value).sort()) {
|
|
2376
|
+
if (key === "approval_token" || key === "approvalToken")
|
|
2377
|
+
continue;
|
|
2378
|
+
const canonicalValue = canonicalizeMutationArg(value[key]);
|
|
2379
|
+
if (canonicalValue !== undefined)
|
|
2380
|
+
result[key] = canonicalValue;
|
|
2381
|
+
}
|
|
2382
|
+
return result;
|
|
2383
|
+
}
|
|
2384
|
+
return inArray ? null : undefined;
|
|
2385
|
+
}
|
|
2386
|
+
function canonicalMutationArgs(value) {
|
|
2387
|
+
return JSON.stringify(canonicalizeMutationArg(value) ?? {});
|
|
2388
|
+
}
|
|
2389
|
+
function mutationArgsSha256(value) {
|
|
2390
|
+
return sha256Hex(canonicalMutationArgs(value));
|
|
2391
|
+
}
|
|
2392
|
+
function stripPlanRuntimeFields(value) {
|
|
2393
|
+
if (Array.isArray(value))
|
|
2394
|
+
return value.map(stripPlanRuntimeFields);
|
|
2395
|
+
if (value instanceof Date)
|
|
2396
|
+
return value;
|
|
2397
|
+
if (value && typeof value === "object") {
|
|
2398
|
+
const result = {};
|
|
2399
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2400
|
+
if (key === "planDigest" || key === "plan_digest" || key === "mode" || key === "executed")
|
|
2401
|
+
continue;
|
|
2402
|
+
result[key] = stripPlanRuntimeFields(entry);
|
|
2403
|
+
}
|
|
2404
|
+
return result;
|
|
2405
|
+
}
|
|
2406
|
+
return value;
|
|
2407
|
+
}
|
|
2408
|
+
function mutationPlanDigest(plan) {
|
|
2409
|
+
return mutationArgsSha256(stripPlanRuntimeFields(plan));
|
|
2410
|
+
}
|
|
2411
|
+
function attachMutationPlanDigest(plan) {
|
|
2412
|
+
return {
|
|
2413
|
+
...plan,
|
|
2414
|
+
planDigest: mutationPlanDigest(plan)
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
function assertMutationPlanDigest(plan, expectedPlanDigest) {
|
|
2418
|
+
if (expectedPlanDigest && mutationPlanDigest(plan) !== expectedPlanDigest) {
|
|
2419
|
+
throw new Error("Approved plan digest does not match the current execution plan.");
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
function parseToken(token) {
|
|
2423
|
+
if (!token)
|
|
2424
|
+
return null;
|
|
2425
|
+
const parts = token.split(".");
|
|
2426
|
+
if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX)
|
|
2427
|
+
return null;
|
|
2428
|
+
try {
|
|
2429
|
+
const claims = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8"));
|
|
2430
|
+
return { payload: parts[1] ?? "", signature: parts[2] ?? "", claims };
|
|
2431
|
+
} catch {
|
|
2432
|
+
return null;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
function claimMatches(expected, actual) {
|
|
2436
|
+
if (expected === undefined)
|
|
2437
|
+
return actual === undefined;
|
|
2438
|
+
return actual === expected;
|
|
2439
|
+
}
|
|
2440
|
+
function verifyMutationApprovalToken(options) {
|
|
2441
|
+
const env2 = options.env ?? process.env;
|
|
2442
|
+
const secret = signingSecret(env2);
|
|
2443
|
+
if (!secret)
|
|
2444
|
+
return { approved: false, reason: `${MUTATION_APPROVAL_TOKEN_ENV} is not configured.` };
|
|
2445
|
+
const parsed = parseToken(options.approvalToken);
|
|
2446
|
+
if (!parsed)
|
|
2447
|
+
return { approved: false, reason: "approval_token is not a scoped mutation token." };
|
|
2448
|
+
if (!safeEqual(hmac(parsed.payload, secret), parsed.signature)) {
|
|
2449
|
+
return { approved: false, reason: "approval_token signature is invalid." };
|
|
2450
|
+
}
|
|
2451
|
+
const claims = parsed.claims;
|
|
2452
|
+
if (claims.version !== 1)
|
|
2453
|
+
return { approved: false, reason: "approval_token version is unsupported." };
|
|
2454
|
+
if (!claims.callerId || !claims.runId) {
|
|
2455
|
+
return { approved: false, reason: "approval_token must include caller and run claims." };
|
|
2456
|
+
}
|
|
2457
|
+
if (!claims.transport) {
|
|
2458
|
+
return { approved: false, reason: "approval_token must include a transport claim." };
|
|
2459
|
+
}
|
|
2460
|
+
if (!Number.isFinite(claims.expiresAt) || claims.expiresAt <= nowMs(options.now)) {
|
|
2461
|
+
return { approved: false, reason: "approval_token is expired." };
|
|
2462
|
+
}
|
|
2463
|
+
const now = nowMs(options.now);
|
|
2464
|
+
if (!Number.isFinite(claims.issuedAt) || claims.issuedAt > now + MAX_CLOCK_SKEW_MS) {
|
|
2465
|
+
return { approved: false, reason: "approval_token issue time is invalid." };
|
|
2466
|
+
}
|
|
2467
|
+
if (claims.expiresAt - claims.issuedAt > MAX_TOKEN_TTL_MS) {
|
|
2468
|
+
return { approved: false, reason: "approval_token TTL is too long." };
|
|
2469
|
+
}
|
|
2470
|
+
for (const key of ["surface", "operation", "machineId", "resourceId", "transport"]) {
|
|
2471
|
+
if (!claimMatches(options[key], claims[key])) {
|
|
2472
|
+
return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
for (const key of ["callerId", "runId"]) {
|
|
2476
|
+
if (options[key] !== undefined && options[key] !== claims[key]) {
|
|
2477
|
+
return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
const expectedArgsSha256 = options.argsSha256 || (options.args === undefined ? undefined : mutationArgsSha256(options.args));
|
|
2481
|
+
if (expectedArgsSha256 !== undefined && claims.args_sha256 !== expectedArgsSha256) {
|
|
2482
|
+
return { approved: false, reason: "approval_token args_sha256 claim does not match this mutation." };
|
|
2483
|
+
}
|
|
2484
|
+
const replayDecision = recordReplayNonce(env2, claims, parsed.payload, now);
|
|
2485
|
+
if (replayDecision)
|
|
2486
|
+
return replayDecision;
|
|
2487
|
+
return { approved: true, claims };
|
|
2488
|
+
}
|
|
2489
|
+
function isMutationApproved(options = {}) {
|
|
2490
|
+
const env2 = options.env ?? process.env;
|
|
2491
|
+
const surface = options.surface ?? "cli";
|
|
2492
|
+
if (surface === "mcp") {
|
|
2493
|
+
if (!options.operation)
|
|
2494
|
+
return false;
|
|
2495
|
+
return verifyMutationApprovalToken({
|
|
2496
|
+
surface,
|
|
2497
|
+
operation: options.operation,
|
|
2498
|
+
machineId: options.machineId,
|
|
2499
|
+
resourceId: options.resourceId,
|
|
2500
|
+
callerId: options.callerId,
|
|
2501
|
+
runId: options.runId,
|
|
2502
|
+
transport: options.transport ?? "mcp",
|
|
2503
|
+
args: options.args,
|
|
2504
|
+
argsSha256: options.argsSha256,
|
|
2505
|
+
approvalToken: options.approvalToken,
|
|
2506
|
+
env: env2,
|
|
2507
|
+
now: options.now
|
|
2508
|
+
}).approved;
|
|
2509
|
+
}
|
|
2510
|
+
if (options.approvalToken) {
|
|
2511
|
+
const decision = options.operation ? verifyMutationApprovalToken({
|
|
2512
|
+
surface,
|
|
2513
|
+
operation: options.operation,
|
|
2514
|
+
machineId: options.machineId,
|
|
2515
|
+
resourceId: options.resourceId,
|
|
2516
|
+
callerId: options.callerId,
|
|
2517
|
+
runId: options.runId,
|
|
2518
|
+
transport: options.transport ?? surface,
|
|
2519
|
+
args: options.args,
|
|
2520
|
+
argsSha256: options.argsSha256,
|
|
2521
|
+
approvalToken: options.approvalToken,
|
|
2522
|
+
env: env2,
|
|
2523
|
+
now: options.now
|
|
2524
|
+
}) : { approved: false };
|
|
2525
|
+
if (decision.approved)
|
|
2526
|
+
return true;
|
|
2527
|
+
if (env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim())
|
|
2528
|
+
return false;
|
|
2529
|
+
}
|
|
2530
|
+
return isTruthy(env2[MUTATION_APPROVAL_FLAG_ENV]) || isTruthy(env2[LEGACY_MUTATION_APPROVAL_FLAG_ENV]);
|
|
2531
|
+
}
|
|
2532
|
+
function assertMutationApproved(options) {
|
|
2533
|
+
if (isMutationApproved(options)) {
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
const env2 = options.env ?? process.env;
|
|
2537
|
+
const tokenConfigured = Boolean(env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim());
|
|
2538
|
+
const approvalHint = options.surface === "mcp" ? `pass a scoped approval_token signed with ${MUTATION_APPROVAL_TOKEN_ENV}` : tokenConfigured ? `pass a scoped approval_token signed with ${MUTATION_APPROVAL_TOKEN_ENV} or set ${MUTATION_APPROVAL_FLAG_ENV}=1 for a trusted local session` : `set ${MUTATION_APPROVAL_FLAG_ENV}=1 for a trusted local session or configure ${MUTATION_APPROVAL_TOKEN_ENV}`;
|
|
2539
|
+
throw new Error(`Fleet mutation blocked: ${options.surface}.${options.operation} requires operator approval; ${approvalHint}.`);
|
|
2540
|
+
}
|
|
2541
|
+
function assertSdkMutationApproved(scope, options = {}) {
|
|
2542
|
+
if (isTrustedSdkMutationApproval(options.trustedLocalMutation))
|
|
2543
|
+
return;
|
|
2544
|
+
const decision = verifyMutationApprovalToken({
|
|
2545
|
+
surface: "sdk",
|
|
2546
|
+
operation: scope.operation,
|
|
2547
|
+
machineId: scope.machineId,
|
|
2548
|
+
resourceId: scope.resourceId,
|
|
2549
|
+
callerId: options.callerId,
|
|
2550
|
+
runId: options.runId,
|
|
2551
|
+
transport: "sdk",
|
|
2552
|
+
args: scope.args,
|
|
2553
|
+
argsSha256: scope.argsSha256,
|
|
2554
|
+
approvalToken: options.approvalToken,
|
|
2555
|
+
env: process.env
|
|
2556
|
+
});
|
|
2557
|
+
if (decision.approved)
|
|
2558
|
+
return;
|
|
2559
|
+
throw new Error(`Fleet mutation blocked: sdk.${scope.operation} requires a scoped SDK approval token.`);
|
|
2560
|
+
}
|
|
2561
|
+
var MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_ALLOW_MUTATIONS", LEGACY_MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_MUTATION_APPROVAL", MUTATION_APPROVAL_TOKEN_ENV = "HASNA_MACHINES_MUTATION_TOKEN", MUTATION_APPROVAL_CALLER_ENV = "HASNA_MACHINES_MUTATION_CALLER_ID", MUTATION_APPROVAL_RUN_ENV = "HASNA_MACHINES_MUTATION_RUN_ID", MUTATION_APPROVAL_REPLAY_PATH_ENV = "HASNA_MACHINES_MUTATION_REPLAY_PATH", TOKEN_PREFIX = "machines-mut-v1", DEFAULT_TOKEN_TTL_MS, MAX_TOKEN_TTL_MS, MAX_CLOCK_SKEW_MS = 30000, trustedSdkMutationApprovals;
|
|
2562
|
+
var init_mutation_approval = __esm(() => {
|
|
2563
|
+
init_db();
|
|
2564
|
+
DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
2565
|
+
MAX_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
2566
|
+
trustedSdkMutationApprovals = new WeakSet;
|
|
2567
|
+
});
|
|
2568
|
+
|
|
2286
2569
|
// src/pg-migrations.ts
|
|
2287
2570
|
var PG_MIGRATIONS;
|
|
2288
2571
|
var init_pg_migrations = __esm(() => {
|
|
@@ -2354,7 +2637,18 @@ function normalizeParams(params) {
|
|
|
2354
2637
|
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
2355
2638
|
return flat.map((value) => value === undefined ? null : value);
|
|
2356
2639
|
}
|
|
2357
|
-
function
|
|
2640
|
+
function envFlag(env2, name) {
|
|
2641
|
+
const value = env2[name]?.trim().toLowerCase();
|
|
2642
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
2643
|
+
}
|
|
2644
|
+
function isLoopbackHost(hostname6) {
|
|
2645
|
+
const normalized = hostname6.replace(/^\[|\]$/g, "").toLowerCase();
|
|
2646
|
+
return normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1" || /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
2647
|
+
}
|
|
2648
|
+
function allowsLocalInsecureTls(url, env2) {
|
|
2649
|
+
return isLoopbackHost(url.hostname) && envFlag(env2, MACHINES_DATABASE_ALLOW_INSECURE_TLS_ENV);
|
|
2650
|
+
}
|
|
2651
|
+
function sslConfigFor(connectionString, env2 = process.env) {
|
|
2358
2652
|
let url;
|
|
2359
2653
|
try {
|
|
2360
2654
|
url = new URL(connectionString);
|
|
@@ -2363,12 +2657,21 @@ function sslConfigFor(connectionString) {
|
|
|
2363
2657
|
}
|
|
2364
2658
|
const sslMode = url.searchParams.get("sslmode")?.toLowerCase();
|
|
2365
2659
|
const ssl = url.searchParams.get("ssl")?.toLowerCase();
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2660
|
+
const rejectUnauthorizedOverride = env2[MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED_ENV]?.trim() === "0";
|
|
2661
|
+
if (sslMode === "disable" || ssl === "false") {
|
|
2662
|
+
if (allowsLocalInsecureTls(url, env2))
|
|
2663
|
+
return;
|
|
2664
|
+
throw new Error(`Insecure PostgreSQL TLS mode is rejected for remote storage; use sslmode=require or set ${MACHINES_DATABASE_ALLOW_INSECURE_TLS_ENV}=1 only for loopback development databases.`);
|
|
2665
|
+
}
|
|
2666
|
+
if (sslMode === "no-verify" || rejectUnauthorizedOverride) {
|
|
2667
|
+
if (!allowsLocalInsecureTls(url, env2)) {
|
|
2668
|
+
throw new Error(`PostgreSQL TLS certificate verification cannot be disabled for remote storage; set ${MACHINES_DATABASE_ALLOW_INSECURE_TLS_ENV}=1 only for loopback development databases.`);
|
|
2669
|
+
}
|
|
2369
2670
|
return { rejectUnauthorized: false };
|
|
2370
2671
|
}
|
|
2371
|
-
|
|
2672
|
+
if (sslMode || ssl === "true")
|
|
2673
|
+
return { rejectUnauthorized: true };
|
|
2674
|
+
return isLoopbackHost(url.hostname) ? undefined : { rejectUnauthorized: true };
|
|
2372
2675
|
}
|
|
2373
2676
|
|
|
2374
2677
|
class PgAdapterAsync {
|
|
@@ -2388,6 +2691,7 @@ class PgAdapterAsync {
|
|
|
2388
2691
|
await this.pool.end();
|
|
2389
2692
|
}
|
|
2390
2693
|
}
|
|
2694
|
+
var MACHINES_DATABASE_ALLOW_INSECURE_TLS_ENV = "HASNA_MACHINES_ALLOW_INSECURE_DATABASE_TLS", MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED_ENV = "HASNA_MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED";
|
|
2391
2695
|
var init_remote_storage = () => {};
|
|
2392
2696
|
|
|
2393
2697
|
// src/storage-sync.ts
|
|
@@ -2660,15 +2964,14 @@ var init_storage_sync = __esm(() => {
|
|
|
2660
2964
|
// src/storage.ts
|
|
2661
2965
|
var exports_storage = {};
|
|
2662
2966
|
__export(exports_storage, {
|
|
2663
|
-
storageSync: () =>
|
|
2664
|
-
storagePush: () =>
|
|
2665
|
-
storagePull: () =>
|
|
2666
|
-
runStorageMigrations: () =>
|
|
2967
|
+
storageSync: () => storageSync2,
|
|
2968
|
+
storagePush: () => storagePush2,
|
|
2969
|
+
storagePull: () => storagePull2,
|
|
2970
|
+
runStorageMigrations: () => runStorageMigrations2,
|
|
2667
2971
|
resolveTables: () => resolveTables,
|
|
2668
2972
|
parseStorageTables: () => parseStorageTables,
|
|
2669
2973
|
getSyncMetaAll: () => getSyncMetaAll,
|
|
2670
2974
|
getStorageStatus: () => getStorageStatus,
|
|
2671
|
-
getStoragePg: () => getStoragePg,
|
|
2672
2975
|
getStorageMode: () => getStorageMode,
|
|
2673
2976
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
2674
2977
|
getStorageDatabaseEnvName: () => getStorageDatabaseEnvName,
|
|
@@ -2676,7 +2979,6 @@ __export(exports_storage, {
|
|
|
2676
2979
|
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
2677
2980
|
STORAGE_MODE_ENV: () => STORAGE_MODE_ENV,
|
|
2678
2981
|
STORAGE_DATABASE_ENV: () => STORAGE_DATABASE_ENV,
|
|
2679
|
-
PgAdapterAsync: () => PgAdapterAsync,
|
|
2680
2982
|
PG_MIGRATIONS: () => PG_MIGRATIONS,
|
|
2681
2983
|
MACHINES_STORAGE_TABLES: () => MACHINES_STORAGE_TABLES,
|
|
2682
2984
|
MACHINES_STORAGE_MODE_FALLBACK_ENV: () => MACHINES_STORAGE_MODE_FALLBACK_ENV,
|
|
@@ -2684,10 +2986,50 @@ __export(exports_storage, {
|
|
|
2684
2986
|
MACHINES_STORAGE_FALLBACK_ENV: () => MACHINES_STORAGE_FALLBACK_ENV,
|
|
2685
2987
|
MACHINES_STORAGE_ENV: () => MACHINES_STORAGE_ENV
|
|
2686
2988
|
});
|
|
2989
|
+
function storageArgs(options = {}) {
|
|
2990
|
+
return {
|
|
2991
|
+
tables: options.tables?.length ? [...options.tables] : null
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
function storageResourceId(operation, options = {}) {
|
|
2995
|
+
return `storage:${operation}:${mutationArgsSha256(storageArgs(options))}`;
|
|
2996
|
+
}
|
|
2997
|
+
async function runStorageMigrations2(remote, options = {}) {
|
|
2998
|
+
assertSdkMutationApproved({
|
|
2999
|
+
operation: "machines_storage_migrate",
|
|
3000
|
+
resourceId: "storage:migrations",
|
|
3001
|
+
args: {}
|
|
3002
|
+
}, options);
|
|
3003
|
+
return runStorageMigrations(remote);
|
|
3004
|
+
}
|
|
3005
|
+
async function storagePush2(options = {}) {
|
|
3006
|
+
assertSdkMutationApproved({
|
|
3007
|
+
operation: "machines_storage_push",
|
|
3008
|
+
resourceId: storageResourceId("push", options),
|
|
3009
|
+
args: storageArgs(options)
|
|
3010
|
+
}, options);
|
|
3011
|
+
return storagePush({ tables: options.tables });
|
|
3012
|
+
}
|
|
3013
|
+
async function storagePull2(options = {}) {
|
|
3014
|
+
assertSdkMutationApproved({
|
|
3015
|
+
operation: "machines_storage_pull",
|
|
3016
|
+
resourceId: storageResourceId("pull", options),
|
|
3017
|
+
args: storageArgs(options)
|
|
3018
|
+
}, options);
|
|
3019
|
+
return storagePull({ tables: options.tables });
|
|
3020
|
+
}
|
|
3021
|
+
async function storageSync2(options = {}) {
|
|
3022
|
+
assertSdkMutationApproved({
|
|
3023
|
+
operation: "machines_storage_sync",
|
|
3024
|
+
resourceId: storageResourceId("sync", options),
|
|
3025
|
+
args: storageArgs(options)
|
|
3026
|
+
}, options);
|
|
3027
|
+
return storageSync({ tables: options.tables });
|
|
3028
|
+
}
|
|
2687
3029
|
var init_storage = __esm(() => {
|
|
2688
3030
|
init_storage_sync();
|
|
3031
|
+
init_mutation_approval();
|
|
2689
3032
|
init_pg_migrations();
|
|
2690
|
-
init_remote_storage();
|
|
2691
3033
|
});
|
|
2692
3034
|
|
|
2693
3035
|
// node_modules/commander/esm.mjs
|
|
@@ -7432,378 +7774,120 @@ function readManifestWithSource(options = {}) {
|
|
|
7432
7774
|
const manifest2 = options.adapter.readManifest({ source, rawRef });
|
|
7433
7775
|
if (manifest2) {
|
|
7434
7776
|
return {
|
|
7435
|
-
manifest: fleetSchema.parse(manifest2),
|
|
7436
|
-
info: {
|
|
7437
|
-
source,
|
|
7438
|
-
loadedFrom: "private-ref",
|
|
7439
|
-
warnings
|
|
7440
|
-
}
|
|
7441
|
-
};
|
|
7442
|
-
}
|
|
7443
|
-
warnings.push(`private_manifest_adapter_empty:${redactIdentifier(options.adapter.id)}`);
|
|
7444
|
-
} catch (error) {
|
|
7445
|
-
warnings.push(`private_manifest_adapter_failed:${redactIdentifier(options.adapter.id)}`);
|
|
7446
|
-
}
|
|
7447
|
-
} else {
|
|
7448
|
-
warnings.push("private_manifest_ref_without_adapter");
|
|
7449
|
-
}
|
|
7450
|
-
const fallbackSource = fileSourceRef(path);
|
|
7451
|
-
const manifest = readManifest(path);
|
|
7452
|
-
return {
|
|
7453
|
-
manifest,
|
|
7454
|
-
info: {
|
|
7455
|
-
source,
|
|
7456
|
-
loadedFrom: existsSync3(path) ? "fallback" : "default",
|
|
7457
|
-
fallbackSource,
|
|
7458
|
-
warnings
|
|
7459
|
-
}
|
|
7460
|
-
};
|
|
7461
|
-
}
|
|
7462
|
-
return {
|
|
7463
|
-
manifest: readManifest(path),
|
|
7464
|
-
info: {
|
|
7465
|
-
source,
|
|
7466
|
-
loadedFrom: existsSync3(path) ? "file" : "default",
|
|
7467
|
-
warnings
|
|
7468
|
-
}
|
|
7469
|
-
};
|
|
7470
|
-
}
|
|
7471
|
-
function validateManifest(path = getManifestPath()) {
|
|
7472
|
-
return readManifest(path);
|
|
7473
|
-
}
|
|
7474
|
-
function writeManifest(manifest, path = getManifestPath()) {
|
|
7475
|
-
ensureParentDir(path);
|
|
7476
|
-
const payload = {
|
|
7477
|
-
...manifest,
|
|
7478
|
-
version: 1,
|
|
7479
|
-
generatedAt: new Date().toISOString(),
|
|
7480
|
-
machines: normalizeMachines(manifest.machines)
|
|
7481
|
-
};
|
|
7482
|
-
fleetSchema.parse(payload);
|
|
7483
|
-
writeFileSync(path, `${JSON.stringify(payload, null, 2)}
|
|
7484
|
-
`, "utf8");
|
|
7485
|
-
return path;
|
|
7486
|
-
}
|
|
7487
|
-
function getManifestMachine(machineId, path = getManifestPath()) {
|
|
7488
|
-
return readManifest(path).machines.find((machine) => machine.id === machineId) || null;
|
|
7489
|
-
}
|
|
7490
|
-
function detectCurrentMachineManifest() {
|
|
7491
|
-
const machineId = process.env["HASNA_MACHINES_MACHINE_ID"] || hostname();
|
|
7492
|
-
const user = userInfo().username;
|
|
7493
|
-
const bunDir = dirname3(process.execPath);
|
|
7494
|
-
return {
|
|
7495
|
-
id: machineId,
|
|
7496
|
-
hostname: hostname(),
|
|
7497
|
-
sshAddress: `${user}@${machineId}`,
|
|
7498
|
-
tailscaleName: machineId,
|
|
7499
|
-
platform: normalizePlatform(),
|
|
7500
|
-
connection: "local",
|
|
7501
|
-
workspacePath: detectWorkspacePath(),
|
|
7502
|
-
bunPath: bunDir,
|
|
7503
|
-
tags: [`arch:${arch()}`],
|
|
7504
|
-
packages: [],
|
|
7505
|
-
files: []
|
|
7506
|
-
};
|
|
7507
|
-
}
|
|
7508
|
-
|
|
7509
|
-
// src/commands/manifest.ts
|
|
7510
|
-
init_paths();
|
|
7511
|
-
function manifestInit() {
|
|
7512
|
-
return writeManifest(getDefaultManifest(), getManifestPath());
|
|
7513
|
-
}
|
|
7514
|
-
function manifestList() {
|
|
7515
|
-
return readManifest();
|
|
7516
|
-
}
|
|
7517
|
-
function manifestAdd(machine) {
|
|
7518
|
-
const validatedMachine = machineSchema.parse(machine);
|
|
7519
|
-
const manifest = readManifest();
|
|
7520
|
-
const nextMachines = manifest.machines.filter((entry) => entry.id !== validatedMachine.id);
|
|
7521
|
-
nextMachines.push(validatedMachine);
|
|
7522
|
-
const nextManifest = { ...manifest, machines: nextMachines };
|
|
7523
|
-
writeManifest(nextManifest);
|
|
7524
|
-
return nextManifest;
|
|
7525
|
-
}
|
|
7526
|
-
function manifestBootstrapCurrentMachine() {
|
|
7527
|
-
return manifestAdd(detectCurrentMachineManifest());
|
|
7528
|
-
}
|
|
7529
|
-
function manifestGet(machineId) {
|
|
7530
|
-
return getManifestMachine(machineId);
|
|
7531
|
-
}
|
|
7532
|
-
function manifestRemove(machineId) {
|
|
7533
|
-
const manifest = readManifest();
|
|
7534
|
-
const nextManifest = {
|
|
7535
|
-
...manifest,
|
|
7536
|
-
machines: manifest.machines.filter((machine) => machine.id !== machineId)
|
|
7537
|
-
};
|
|
7538
|
-
writeManifest(nextManifest);
|
|
7539
|
-
return nextManifest;
|
|
7540
|
-
}
|
|
7541
|
-
function manifestValidate() {
|
|
7542
|
-
return validateManifest(getManifestPath());
|
|
7543
|
-
}
|
|
7544
|
-
|
|
7545
|
-
// src/commands/setup.ts
|
|
7546
|
-
import { homedir as homedir2 } from "os";
|
|
7547
|
-
init_db();
|
|
7548
|
-
|
|
7549
|
-
// src/commands/mutation-approval.ts
|
|
7550
|
-
init_db();
|
|
7551
|
-
import { createHash, createHmac, randomUUID, timingSafeEqual } from "crypto";
|
|
7552
|
-
import { resolve as resolve2 } from "path";
|
|
7553
|
-
var MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_ALLOW_MUTATIONS";
|
|
7554
|
-
var LEGACY_MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_MUTATION_APPROVAL";
|
|
7555
|
-
var MUTATION_APPROVAL_TOKEN_ENV = "HASNA_MACHINES_MUTATION_TOKEN";
|
|
7556
|
-
var MUTATION_APPROVAL_CALLER_ENV = "HASNA_MACHINES_MUTATION_CALLER_ID";
|
|
7557
|
-
var MUTATION_APPROVAL_RUN_ENV = "HASNA_MACHINES_MUTATION_RUN_ID";
|
|
7558
|
-
var MUTATION_APPROVAL_REPLAY_PATH_ENV = "HASNA_MACHINES_MUTATION_REPLAY_PATH";
|
|
7559
|
-
var TOKEN_PREFIX = "machines-mut-v1";
|
|
7560
|
-
var DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
7561
|
-
var MAX_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
7562
|
-
var MAX_CLOCK_SKEW_MS = 30000;
|
|
7563
|
-
function isTruthy(value) {
|
|
7564
|
-
return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
|
|
7565
|
-
}
|
|
7566
|
-
function nowMs(now) {
|
|
7567
|
-
if (typeof now === "number")
|
|
7568
|
-
return now;
|
|
7569
|
-
if (now instanceof Date)
|
|
7570
|
-
return now.getTime();
|
|
7571
|
-
return Date.now();
|
|
7572
|
-
}
|
|
7573
|
-
function signingSecret(env2, explicitSecret) {
|
|
7574
|
-
return explicitSecret?.trim() || env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim();
|
|
7575
|
-
}
|
|
7576
|
-
function hmac(payload, secret) {
|
|
7577
|
-
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
7578
|
-
}
|
|
7579
|
-
function sha256Hex(payload) {
|
|
7580
|
-
return createHash("sha256").update(payload).digest("hex");
|
|
7581
|
-
}
|
|
7582
|
-
function replayDbPath(env2) {
|
|
7583
|
-
const configured = env2[MUTATION_APPROVAL_REPLAY_PATH_ENV]?.trim();
|
|
7584
|
-
return configured ? resolve2(configured) : undefined;
|
|
7585
|
-
}
|
|
7586
|
-
function replayNonceKey(claims) {
|
|
7587
|
-
return sha256Hex(JSON.stringify({ nonce: claims.nonce }));
|
|
7588
|
-
}
|
|
7589
|
-
function recordReplayNonce(env2, claims, tokenPayload, now) {
|
|
7590
|
-
const dbPath = replayDbPath(env2);
|
|
7591
|
-
if (!dbPath)
|
|
7592
|
-
return;
|
|
7593
|
-
if (!claims.nonce) {
|
|
7594
|
-
return { approved: false, reason: "approval_token nonce claim is required for replay protection." };
|
|
7595
|
-
}
|
|
7596
|
-
try {
|
|
7597
|
-
const db = getDb(dbPath);
|
|
7598
|
-
db.query("DELETE FROM mutation_approval_nonces WHERE expires_at <= ?").run(now);
|
|
7599
|
-
const result = db.query(`
|
|
7600
|
-
INSERT OR IGNORE INTO mutation_approval_nonces (
|
|
7601
|
-
nonce_sha256,
|
|
7602
|
-
token_sha256,
|
|
7603
|
-
surface,
|
|
7604
|
-
operation,
|
|
7605
|
-
caller_id,
|
|
7606
|
-
run_id,
|
|
7607
|
-
transport,
|
|
7608
|
-
expires_at,
|
|
7609
|
-
used_at
|
|
7610
|
-
)
|
|
7611
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7612
|
-
`).run(replayNonceKey(claims), sha256Hex(tokenPayload), claims.surface, claims.operation, claims.callerId ?? "", claims.runId ?? "", claims.transport ?? "", claims.expiresAt, now);
|
|
7613
|
-
if (result.changes !== 1) {
|
|
7614
|
-
return { approved: false, reason: "approval_token nonce has already been used." };
|
|
7615
|
-
}
|
|
7616
|
-
return;
|
|
7617
|
-
} catch (error) {
|
|
7618
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7619
|
-
return { approved: false, reason: `approval_token replay store is unavailable: ${message}` };
|
|
7620
|
-
}
|
|
7621
|
-
}
|
|
7622
|
-
function safeEqual(left, right) {
|
|
7623
|
-
const leftBuffer = Buffer.from(left);
|
|
7624
|
-
const rightBuffer = Buffer.from(right);
|
|
7625
|
-
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
7626
|
-
}
|
|
7627
|
-
function canonicalizeMutationArg(value, inArray = false) {
|
|
7628
|
-
if (value === undefined)
|
|
7629
|
-
return inArray ? null : undefined;
|
|
7630
|
-
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
7631
|
-
return value;
|
|
7632
|
-
if (typeof value === "number")
|
|
7633
|
-
return Number.isFinite(value) ? value : null;
|
|
7634
|
-
if (Array.isArray(value)) {
|
|
7635
|
-
return value.map((entry) => canonicalizeMutationArg(entry, true) ?? null);
|
|
7636
|
-
}
|
|
7637
|
-
if (value instanceof Date)
|
|
7638
|
-
return value.toISOString();
|
|
7639
|
-
if (typeof value === "object") {
|
|
7640
|
-
const result = {};
|
|
7641
|
-
for (const key of Object.keys(value).sort()) {
|
|
7642
|
-
if (key === "approval_token" || key === "approvalToken")
|
|
7643
|
-
continue;
|
|
7644
|
-
const canonicalValue = canonicalizeMutationArg(value[key]);
|
|
7645
|
-
if (canonicalValue !== undefined)
|
|
7646
|
-
result[key] = canonicalValue;
|
|
7777
|
+
manifest: fleetSchema.parse(manifest2),
|
|
7778
|
+
info: {
|
|
7779
|
+
source,
|
|
7780
|
+
loadedFrom: "private-ref",
|
|
7781
|
+
warnings
|
|
7782
|
+
}
|
|
7783
|
+
};
|
|
7784
|
+
}
|
|
7785
|
+
warnings.push(`private_manifest_adapter_empty:${redactIdentifier(options.adapter.id)}`);
|
|
7786
|
+
} catch (error) {
|
|
7787
|
+
warnings.push(`private_manifest_adapter_failed:${redactIdentifier(options.adapter.id)}`);
|
|
7788
|
+
}
|
|
7789
|
+
} else {
|
|
7790
|
+
warnings.push("private_manifest_ref_without_adapter");
|
|
7647
7791
|
}
|
|
7648
|
-
|
|
7792
|
+
const fallbackSource = fileSourceRef(path);
|
|
7793
|
+
const manifest = readManifest(path);
|
|
7794
|
+
return {
|
|
7795
|
+
manifest,
|
|
7796
|
+
info: {
|
|
7797
|
+
source,
|
|
7798
|
+
loadedFrom: existsSync3(path) ? "fallback" : "default",
|
|
7799
|
+
fallbackSource,
|
|
7800
|
+
warnings
|
|
7801
|
+
}
|
|
7802
|
+
};
|
|
7649
7803
|
}
|
|
7650
|
-
return
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7804
|
+
return {
|
|
7805
|
+
manifest: readManifest(path),
|
|
7806
|
+
info: {
|
|
7807
|
+
source,
|
|
7808
|
+
loadedFrom: existsSync3(path) ? "file" : "default",
|
|
7809
|
+
warnings
|
|
7810
|
+
}
|
|
7811
|
+
};
|
|
7654
7812
|
}
|
|
7655
|
-
function
|
|
7656
|
-
return
|
|
7813
|
+
function validateManifest(path = getManifestPath()) {
|
|
7814
|
+
return readManifest(path);
|
|
7657
7815
|
}
|
|
7658
|
-
function
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
return result;
|
|
7671
|
-
}
|
|
7672
|
-
return value;
|
|
7816
|
+
function writeManifest(manifest, path = getManifestPath()) {
|
|
7817
|
+
ensureParentDir(path);
|
|
7818
|
+
const payload = {
|
|
7819
|
+
...manifest,
|
|
7820
|
+
version: 1,
|
|
7821
|
+
generatedAt: new Date().toISOString(),
|
|
7822
|
+
machines: normalizeMachines(manifest.machines)
|
|
7823
|
+
};
|
|
7824
|
+
fleetSchema.parse(payload);
|
|
7825
|
+
writeFileSync(path, `${JSON.stringify(payload, null, 2)}
|
|
7826
|
+
`, "utf8");
|
|
7827
|
+
return path;
|
|
7673
7828
|
}
|
|
7674
|
-
function
|
|
7675
|
-
return
|
|
7829
|
+
function getManifestMachine(machineId, path = getManifestPath()) {
|
|
7830
|
+
return readManifest(path).machines.find((machine) => machine.id === machineId) || null;
|
|
7676
7831
|
}
|
|
7677
|
-
function
|
|
7832
|
+
function detectCurrentMachineManifest() {
|
|
7833
|
+
const machineId = process.env["HASNA_MACHINES_MACHINE_ID"] || hostname();
|
|
7834
|
+
const user = userInfo().username;
|
|
7835
|
+
const bunDir = dirname3(process.execPath);
|
|
7678
7836
|
return {
|
|
7679
|
-
|
|
7680
|
-
|
|
7837
|
+
id: machineId,
|
|
7838
|
+
hostname: hostname(),
|
|
7839
|
+
sshAddress: `${user}@${machineId}`,
|
|
7840
|
+
tailscaleName: machineId,
|
|
7841
|
+
platform: normalizePlatform(),
|
|
7842
|
+
connection: "local",
|
|
7843
|
+
workspacePath: detectWorkspacePath(),
|
|
7844
|
+
bunPath: bunDir,
|
|
7845
|
+
tags: [`arch:${arch()}`],
|
|
7846
|
+
packages: [],
|
|
7847
|
+
files: []
|
|
7681
7848
|
};
|
|
7682
7849
|
}
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7850
|
+
|
|
7851
|
+
// src/commands/manifest.ts
|
|
7852
|
+
init_paths();
|
|
7853
|
+
function manifestInit() {
|
|
7854
|
+
return writeManifest(getDefaultManifest(), getManifestPath());
|
|
7687
7855
|
}
|
|
7688
|
-
function
|
|
7689
|
-
|
|
7690
|
-
return null;
|
|
7691
|
-
const parts = token.split(".");
|
|
7692
|
-
if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX)
|
|
7693
|
-
return null;
|
|
7694
|
-
try {
|
|
7695
|
-
const claims = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8"));
|
|
7696
|
-
return { payload: parts[1] ?? "", signature: parts[2] ?? "", claims };
|
|
7697
|
-
} catch {
|
|
7698
|
-
return null;
|
|
7699
|
-
}
|
|
7856
|
+
function manifestList() {
|
|
7857
|
+
return readManifest();
|
|
7700
7858
|
}
|
|
7701
|
-
function
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7859
|
+
function manifestAdd(machine) {
|
|
7860
|
+
const validatedMachine = machineSchema.parse(machine);
|
|
7861
|
+
const manifest = readManifest();
|
|
7862
|
+
const nextMachines = manifest.machines.filter((entry) => entry.id !== validatedMachine.id);
|
|
7863
|
+
nextMachines.push(validatedMachine);
|
|
7864
|
+
const nextManifest = { ...manifest, machines: nextMachines };
|
|
7865
|
+
writeManifest(nextManifest);
|
|
7866
|
+
return nextManifest;
|
|
7705
7867
|
}
|
|
7706
|
-
function
|
|
7707
|
-
|
|
7708
|
-
const secret = signingSecret(env2);
|
|
7709
|
-
if (!secret)
|
|
7710
|
-
return { approved: false, reason: `${MUTATION_APPROVAL_TOKEN_ENV} is not configured.` };
|
|
7711
|
-
const parsed = parseToken(options.approvalToken);
|
|
7712
|
-
if (!parsed)
|
|
7713
|
-
return { approved: false, reason: "approval_token is not a scoped mutation token." };
|
|
7714
|
-
if (!safeEqual(hmac(parsed.payload, secret), parsed.signature)) {
|
|
7715
|
-
return { approved: false, reason: "approval_token signature is invalid." };
|
|
7716
|
-
}
|
|
7717
|
-
const claims = parsed.claims;
|
|
7718
|
-
if (claims.version !== 1)
|
|
7719
|
-
return { approved: false, reason: "approval_token version is unsupported." };
|
|
7720
|
-
if (!claims.callerId || !claims.runId) {
|
|
7721
|
-
return { approved: false, reason: "approval_token must include caller and run claims." };
|
|
7722
|
-
}
|
|
7723
|
-
if (!claims.transport) {
|
|
7724
|
-
return { approved: false, reason: "approval_token must include a transport claim." };
|
|
7725
|
-
}
|
|
7726
|
-
if (!Number.isFinite(claims.expiresAt) || claims.expiresAt <= nowMs(options.now)) {
|
|
7727
|
-
return { approved: false, reason: "approval_token is expired." };
|
|
7728
|
-
}
|
|
7729
|
-
const now = nowMs(options.now);
|
|
7730
|
-
if (!Number.isFinite(claims.issuedAt) || claims.issuedAt > now + MAX_CLOCK_SKEW_MS) {
|
|
7731
|
-
return { approved: false, reason: "approval_token issue time is invalid." };
|
|
7732
|
-
}
|
|
7733
|
-
if (claims.expiresAt - claims.issuedAt > MAX_TOKEN_TTL_MS) {
|
|
7734
|
-
return { approved: false, reason: "approval_token TTL is too long." };
|
|
7735
|
-
}
|
|
7736
|
-
for (const key of ["surface", "operation", "machineId", "resourceId", "transport"]) {
|
|
7737
|
-
if (!claimMatches(options[key], claims[key])) {
|
|
7738
|
-
return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
|
|
7739
|
-
}
|
|
7740
|
-
}
|
|
7741
|
-
for (const key of ["callerId", "runId"]) {
|
|
7742
|
-
if (options[key] !== undefined && options[key] !== claims[key]) {
|
|
7743
|
-
return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
|
|
7744
|
-
}
|
|
7745
|
-
}
|
|
7746
|
-
const expectedArgsSha256 = options.argsSha256 || (options.args === undefined ? undefined : mutationArgsSha256(options.args));
|
|
7747
|
-
if (expectedArgsSha256 !== undefined && claims.args_sha256 !== expectedArgsSha256) {
|
|
7748
|
-
return { approved: false, reason: "approval_token args_sha256 claim does not match this mutation." };
|
|
7749
|
-
}
|
|
7750
|
-
const replayDecision = recordReplayNonce(env2, claims, parsed.payload, now);
|
|
7751
|
-
if (replayDecision)
|
|
7752
|
-
return replayDecision;
|
|
7753
|
-
return { approved: true, claims };
|
|
7868
|
+
function manifestBootstrapCurrentMachine() {
|
|
7869
|
+
return manifestAdd(detectCurrentMachineManifest());
|
|
7754
7870
|
}
|
|
7755
|
-
function
|
|
7756
|
-
|
|
7757
|
-
const surface = options.surface ?? "cli";
|
|
7758
|
-
if (surface === "mcp") {
|
|
7759
|
-
if (!options.operation)
|
|
7760
|
-
return false;
|
|
7761
|
-
return verifyMutationApprovalToken({
|
|
7762
|
-
surface,
|
|
7763
|
-
operation: options.operation,
|
|
7764
|
-
machineId: options.machineId,
|
|
7765
|
-
resourceId: options.resourceId,
|
|
7766
|
-
callerId: options.callerId,
|
|
7767
|
-
runId: options.runId,
|
|
7768
|
-
transport: options.transport ?? "mcp",
|
|
7769
|
-
args: options.args,
|
|
7770
|
-
argsSha256: options.argsSha256,
|
|
7771
|
-
approvalToken: options.approvalToken,
|
|
7772
|
-
env: env2,
|
|
7773
|
-
now: options.now
|
|
7774
|
-
}).approved;
|
|
7775
|
-
}
|
|
7776
|
-
if (options.approvalToken) {
|
|
7777
|
-
const decision = options.operation ? verifyMutationApprovalToken({
|
|
7778
|
-
surface,
|
|
7779
|
-
operation: options.operation,
|
|
7780
|
-
machineId: options.machineId,
|
|
7781
|
-
resourceId: options.resourceId,
|
|
7782
|
-
callerId: options.callerId,
|
|
7783
|
-
runId: options.runId,
|
|
7784
|
-
transport: options.transport ?? surface,
|
|
7785
|
-
args: options.args,
|
|
7786
|
-
argsSha256: options.argsSha256,
|
|
7787
|
-
approvalToken: options.approvalToken,
|
|
7788
|
-
env: env2,
|
|
7789
|
-
now: options.now
|
|
7790
|
-
}) : { approved: false };
|
|
7791
|
-
if (decision.approved)
|
|
7792
|
-
return true;
|
|
7793
|
-
if (env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim())
|
|
7794
|
-
return false;
|
|
7795
|
-
}
|
|
7796
|
-
return isTruthy(env2[MUTATION_APPROVAL_FLAG_ENV]) || isTruthy(env2[LEGACY_MUTATION_APPROVAL_FLAG_ENV]);
|
|
7871
|
+
function manifestGet(machineId) {
|
|
7872
|
+
return getManifestMachine(machineId);
|
|
7797
7873
|
}
|
|
7798
|
-
function
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7874
|
+
function manifestRemove(machineId) {
|
|
7875
|
+
const manifest = readManifest();
|
|
7876
|
+
const nextManifest = {
|
|
7877
|
+
...manifest,
|
|
7878
|
+
machines: manifest.machines.filter((machine) => machine.id !== machineId)
|
|
7879
|
+
};
|
|
7880
|
+
writeManifest(nextManifest);
|
|
7881
|
+
return nextManifest;
|
|
7806
7882
|
}
|
|
7883
|
+
function manifestValidate() {
|
|
7884
|
+
return validateManifest(getManifestPath());
|
|
7885
|
+
}
|
|
7886
|
+
|
|
7887
|
+
// src/commands/setup.ts
|
|
7888
|
+
import { homedir as homedir2 } from "os";
|
|
7889
|
+
init_db();
|
|
7890
|
+
init_mutation_approval();
|
|
7807
7891
|
|
|
7808
7892
|
// src/remote.ts
|
|
7809
7893
|
init_db();
|
|
@@ -9810,6 +9894,7 @@ function diffMachines(leftMachineId, rightMachineId) {
|
|
|
9810
9894
|
}
|
|
9811
9895
|
|
|
9812
9896
|
// src/commands/apps.ts
|
|
9897
|
+
init_mutation_approval();
|
|
9813
9898
|
function getPackageName(app) {
|
|
9814
9899
|
return app.packageName || app.name;
|
|
9815
9900
|
}
|
|
@@ -9950,6 +10035,7 @@ function runAppsPlan(plan, options = {}, runner = runMachineCommand) {
|
|
|
9950
10035
|
}
|
|
9951
10036
|
|
|
9952
10037
|
// src/commands/install-claude.ts
|
|
10038
|
+
init_mutation_approval();
|
|
9953
10039
|
var AI_CLI_PACKAGES = {
|
|
9954
10040
|
claude: "@anthropic-ai/claude-code",
|
|
9955
10041
|
codex: "@openai/codex",
|
|
@@ -10057,6 +10143,7 @@ function runClaudeInstallPlan(plan, options = {}, runner = runMachineCommand) {
|
|
|
10057
10143
|
}
|
|
10058
10144
|
|
|
10059
10145
|
// src/commands/install-tailscale.ts
|
|
10146
|
+
init_mutation_approval();
|
|
10060
10147
|
function buildInstallSteps2(machine) {
|
|
10061
10148
|
if (machine.platform === "macos") {
|
|
10062
10149
|
return [
|
|
@@ -10124,6 +10211,7 @@ function runTailscaleInstallPlan(plan, options = {}, runner = runMachineCommand)
|
|
|
10124
10211
|
import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
10125
10212
|
import { delimiter, isAbsolute, join as join7 } from "path";
|
|
10126
10213
|
init_paths();
|
|
10214
|
+
init_mutation_approval();
|
|
10127
10215
|
var notificationChannelSchema = exports_external.object({
|
|
10128
10216
|
id: exports_external.string(),
|
|
10129
10217
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
@@ -10467,9 +10555,20 @@ import { EventsClient } from "@hasna/events";
|
|
|
10467
10555
|
function shellQuote5(value) {
|
|
10468
10556
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
10469
10557
|
}
|
|
10558
|
+
function commandBasename(command) {
|
|
10559
|
+
const withoutArgs = command.trim().split(/\s+/, 1)[0] ?? "";
|
|
10560
|
+
return withoutArgs.split(/[\\/]/).filter(Boolean).at(-1) ?? withoutArgs;
|
|
10561
|
+
}
|
|
10562
|
+
function assertMachinesCommandSafe(command) {
|
|
10563
|
+
const basename = commandBasename(command);
|
|
10564
|
+
if (basename === "events" || basename === "hasna-events") {
|
|
10565
|
+
throw new Error("tmux-hook-plan machines command must invoke the machines CLI, not dependency-owned @hasna/events bins.");
|
|
10566
|
+
}
|
|
10567
|
+
}
|
|
10470
10568
|
function buildTmuxPaneDiedHookPlan(options = {}) {
|
|
10471
10569
|
const tmuxCommand = options.tmuxCommand ?? process.env["HASNA_MACHINES_TMUX_BIN"] ?? "tmux";
|
|
10472
10570
|
const machinesCommand = options.machinesCommand ?? "machines";
|
|
10571
|
+
assertMachinesCommandSafe(machinesCommand);
|
|
10473
10572
|
const deliver = options.deliver === true;
|
|
10474
10573
|
const approvalToken = options.approvalToken?.trim();
|
|
10475
10574
|
const trustedLocalMutation = approvalToken ? false : options.trustedLocalMutation === true;
|
|
@@ -10710,6 +10809,7 @@ import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync7, sy
|
|
|
10710
10809
|
import { homedir as homedir5 } from "os";
|
|
10711
10810
|
init_paths();
|
|
10712
10811
|
init_db();
|
|
10812
|
+
init_mutation_approval();
|
|
10713
10813
|
function quote4(value) {
|
|
10714
10814
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
10715
10815
|
}
|
|
@@ -11494,6 +11594,9 @@ function runDoctor(machineId, options = {}) {
|
|
|
11494
11594
|
};
|
|
11495
11595
|
}
|
|
11496
11596
|
|
|
11597
|
+
// src/cli/index.ts
|
|
11598
|
+
init_mutation_approval();
|
|
11599
|
+
|
|
11497
11600
|
// src/commands/daemon.ts
|
|
11498
11601
|
import { execFileSync } from "child_process";
|
|
11499
11602
|
import { chmodSync, existsSync as existsSync10, readFileSync as readFileSync8, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
@@ -12053,6 +12156,7 @@ function getAgentStatus(machineId, options = {}) {
|
|
|
12053
12156
|
}
|
|
12054
12157
|
|
|
12055
12158
|
// src/commands/serve.ts
|
|
12159
|
+
init_mutation_approval();
|
|
12056
12160
|
function escapeHtml(value) {
|
|
12057
12161
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
12058
12162
|
}
|
|
@@ -12512,6 +12616,16 @@ function startDashboardServer(options = {}) {
|
|
|
12512
12616
|
function check(id, status, summary, detail) {
|
|
12513
12617
|
return { id, status, summary, detail };
|
|
12514
12618
|
}
|
|
12619
|
+
function summarize(checks) {
|
|
12620
|
+
const counts = checks.reduce((acc, entry) => {
|
|
12621
|
+
acc[entry.status] += 1;
|
|
12622
|
+
return acc;
|
|
12623
|
+
}, { ok: 0, warn: 0, fail: 0 });
|
|
12624
|
+
return {
|
|
12625
|
+
overall: counts.fail > 0 ? "fail" : counts.warn > 0 ? "warn" : "ok",
|
|
12626
|
+
counts
|
|
12627
|
+
};
|
|
12628
|
+
}
|
|
12515
12629
|
function runSelfTest() {
|
|
12516
12630
|
const version = getPackageVersion();
|
|
12517
12631
|
const status = getStatus();
|
|
@@ -12523,19 +12637,21 @@ function runSelfTest() {
|
|
|
12523
12637
|
const apps = listApps(machineId);
|
|
12524
12638
|
const appsDiff = diffApps(machineId);
|
|
12525
12639
|
const cliPlan = buildClaudeInstallPlan(machineId);
|
|
12640
|
+
const checks = [
|
|
12641
|
+
check("package-version", version === "0.0.0" ? "fail" : "ok", "Package version resolves", version),
|
|
12642
|
+
check("status", "ok", "Status loads", JSON.stringify({ machines: status.manifestMachineCount, heartbeats: status.heartbeatCount })),
|
|
12643
|
+
check("doctor", doctor.checks.some((entry) => entry.status === "fail") ? "warn" : "ok", "Doctor completed", `${doctor.checks.length} checks`),
|
|
12644
|
+
check("serve-info", "ok", "Dashboard info renders", `${serveInfo.url} routes=${serveInfo.routes.length}`),
|
|
12645
|
+
check("dashboard-html", html.includes("Machines Dashboard") ? "ok" : "fail", "Dashboard HTML renders", html.slice(0, 80)),
|
|
12646
|
+
check("notifications", "ok", "Notifications config loads", `${notifications.channels.length} channels`),
|
|
12647
|
+
check("apps", "ok", "Apps manifest loads", `${apps.apps.length} apps`),
|
|
12648
|
+
check("apps-diff", appsDiff.missing.length === 0 ? "ok" : "warn", "Apps diff computed", `missing=${appsDiff.missing.length} installed=${appsDiff.installed.length}`),
|
|
12649
|
+
check("install-claude-plan", cliPlan.steps.length > 0 ? "ok" : "warn", "Install plan renders", `${cliPlan.steps.length} steps`)
|
|
12650
|
+
];
|
|
12526
12651
|
return {
|
|
12527
12652
|
machineId,
|
|
12528
|
-
checks
|
|
12529
|
-
|
|
12530
|
-
check("status", "ok", "Status loads", JSON.stringify({ machines: status.manifestMachineCount, heartbeats: status.heartbeatCount })),
|
|
12531
|
-
check("doctor", doctor.checks.some((entry) => entry.status === "fail") ? "warn" : "ok", "Doctor completed", `${doctor.checks.length} checks`),
|
|
12532
|
-
check("serve-info", "ok", "Dashboard info renders", `${serveInfo.url} routes=${serveInfo.routes.length}`),
|
|
12533
|
-
check("dashboard-html", html.includes("Machines Dashboard") ? "ok" : "fail", "Dashboard HTML renders", html.slice(0, 80)),
|
|
12534
|
-
check("notifications", "ok", "Notifications config loads", `${notifications.channels.length} channels`),
|
|
12535
|
-
check("apps", "ok", "Apps manifest loads", `${apps.apps.length} apps`),
|
|
12536
|
-
check("apps-diff", appsDiff.missing.length === 0 ? "ok" : "warn", "Apps diff computed", `missing=${appsDiff.missing.length} installed=${appsDiff.installed.length}`),
|
|
12537
|
-
check("install-claude-plan", cliPlan.steps.length > 0 ? "ok" : "warn", "Install plan renders", `${cliPlan.steps.length} steps`)
|
|
12538
|
-
]
|
|
12653
|
+
...summarize(checks),
|
|
12654
|
+
checks
|
|
12539
12655
|
};
|
|
12540
12656
|
}
|
|
12541
12657
|
|
|
@@ -13632,6 +13748,7 @@ function renderDoctorResult(report) {
|
|
|
13632
13748
|
function renderSelfTestResult(result) {
|
|
13633
13749
|
return [
|
|
13634
13750
|
`machine: ${result.machineId}`,
|
|
13751
|
+
`overall: ${result.overall} ok=${result.counts.ok} warn=${result.counts.warn} fail=${result.counts.fail}`,
|
|
13635
13752
|
...result.checks.map((check2) => {
|
|
13636
13753
|
const status = check2.status === "ok" ? source_default.green(check2.status) : check2.status === "warn" ? source_default.yellow(check2.status) : source_default.red(check2.status);
|
|
13637
13754
|
return `${check2.id.padEnd(20)} ${status} ${check2.detail}`;
|
|
@@ -14765,10 +14882,10 @@ storageCommand.command("status").description("Show storage sync status").option(
|
|
|
14765
14882
|
});
|
|
14766
14883
|
storageCommand.command("push").description("Push local machine runtime data to storage PostgreSQL").option("--tables <tables>", "Comma-separated table names").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
14767
14884
|
try {
|
|
14768
|
-
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePush:
|
|
14885
|
+
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePush: storagePush3 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14769
14886
|
const tables = resolveTables2(parseStorageTables2(options.tables));
|
|
14770
14887
|
requireCliMutation("storage_push", options.approvalToken, { resourceId: cliResourceId("storage-push", tables.join(",")), args: { tables } });
|
|
14771
|
-
const results = await
|
|
14888
|
+
const results = await storagePush3({ tables, trustedLocalMutation: createTrustedSdkMutationApproval() });
|
|
14772
14889
|
printStorageResults(results, options.json);
|
|
14773
14890
|
} catch (error) {
|
|
14774
14891
|
printStorageError(error);
|
|
@@ -14776,10 +14893,10 @@ storageCommand.command("push").description("Push local machine runtime data to s
|
|
|
14776
14893
|
});
|
|
14777
14894
|
storageCommand.command("pull").description("Pull machine runtime data from storage PostgreSQL to local SQLite").option("--tables <tables>", "Comma-separated table names").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
14778
14895
|
try {
|
|
14779
|
-
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePull:
|
|
14896
|
+
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePull: storagePull3 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14780
14897
|
const tables = resolveTables2(parseStorageTables2(options.tables));
|
|
14781
14898
|
requireCliMutation("storage_pull", options.approvalToken, { resourceId: cliResourceId("storage-pull", tables.join(",")), args: { tables } });
|
|
14782
|
-
const results = await
|
|
14899
|
+
const results = await storagePull3({ tables, trustedLocalMutation: createTrustedSdkMutationApproval() });
|
|
14783
14900
|
printStorageResults(results, options.json);
|
|
14784
14901
|
} catch (error) {
|
|
14785
14902
|
printStorageError(error);
|
|
@@ -14787,10 +14904,10 @@ storageCommand.command("pull").description("Pull machine runtime data from stora
|
|
|
14787
14904
|
});
|
|
14788
14905
|
storageCommand.command("sync").description("Bidirectional storage sync: pull then push").option("--tables <tables>", "Comma-separated table names").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
14789
14906
|
try {
|
|
14790
|
-
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storageSync:
|
|
14907
|
+
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storageSync: storageSync3 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14791
14908
|
const tables = resolveTables2(parseStorageTables2(options.tables));
|
|
14792
14909
|
requireCliMutation("storage_sync", options.approvalToken, { resourceId: cliResourceId("storage-sync", tables.join(",")), args: { tables } });
|
|
14793
|
-
const result = await
|
|
14910
|
+
const result = await storageSync3({ tables, trustedLocalMutation: createTrustedSdkMutationApproval() });
|
|
14794
14911
|
if (options.json) {
|
|
14795
14912
|
console.log(JSON.stringify(result, null, 2));
|
|
14796
14913
|
return;
|