@hasna/machines 0.0.47 → 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 +523 -390
- package/dist/commands/hosts.d.ts +2 -1
- 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
|
|
@@ -7439,372 +7781,114 @@ function readManifestWithSource(options = {}) {
|
|
|
7439
7781
|
warnings
|
|
7440
7782
|
}
|
|
7441
7783
|
};
|
|
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;
|
|
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;
|
|
7882
|
+
}
|
|
7883
|
+
function manifestValidate() {
|
|
7884
|
+
return validateManifest(getManifestPath());
|
|
7806
7885
|
}
|
|
7807
7886
|
|
|
7887
|
+
// src/commands/setup.ts
|
|
7888
|
+
import { homedir as homedir2 } from "os";
|
|
7889
|
+
init_db();
|
|
7890
|
+
init_mutation_approval();
|
|
7891
|
+
|
|
7808
7892
|
// src/remote.ts
|
|
7809
7893
|
init_db();
|
|
7810
7894
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -9660,12 +9744,13 @@ ${after}` : `
|
|
|
9660
9744
|
return `${prefix}${block}
|
|
9661
9745
|
`;
|
|
9662
9746
|
}
|
|
9663
|
-
function
|
|
9664
|
-
if (
|
|
9665
|
-
warnings.
|
|
9747
|
+
function loadTailscaleStatus(runner, binary, warnings) {
|
|
9748
|
+
if (!binary) {
|
|
9749
|
+
if (!warnings.includes("tailscale_not_available"))
|
|
9750
|
+
warnings.push("tailscale_not_available");
|
|
9666
9751
|
return null;
|
|
9667
9752
|
}
|
|
9668
|
-
const result = runner("
|
|
9753
|
+
const result = runner(`"${binary}" status --json`);
|
|
9669
9754
|
if (result.exitCode !== 0) {
|
|
9670
9755
|
warnings.push("tailscale_status_failed");
|
|
9671
9756
|
return null;
|
|
@@ -9694,9 +9779,23 @@ function collectPingTargets(tailscale, localSubnets) {
|
|
|
9694
9779
|
}
|
|
9695
9780
|
return targets;
|
|
9696
9781
|
}
|
|
9697
|
-
|
|
9782
|
+
var TAILSCALE_CANDIDATES = [
|
|
9783
|
+
"tailscale",
|
|
9784
|
+
"/usr/local/bin/tailscale",
|
|
9785
|
+
"/opt/homebrew/bin/tailscale",
|
|
9786
|
+
"/Applications/Tailscale.app/Contents/MacOS/Tailscale"
|
|
9787
|
+
];
|
|
9788
|
+
function resolveTailscaleBinary(runner) {
|
|
9789
|
+
for (const candidate of TAILSCALE_CANDIDATES) {
|
|
9790
|
+
const check = candidate.includes("/") ? `test -x "${candidate}"` : `command -v ${candidate} >/dev/null 2>&1`;
|
|
9791
|
+
if (runner(check).exitCode === 0)
|
|
9792
|
+
return candidate;
|
|
9793
|
+
}
|
|
9794
|
+
return null;
|
|
9795
|
+
}
|
|
9796
|
+
function warmDirectPaths(runner, targets, binary, timeoutSeconds = 2) {
|
|
9698
9797
|
for (const target of targets) {
|
|
9699
|
-
runner(`
|
|
9798
|
+
runner(`"${binary}" ping --c 1 --timeout ${timeoutSeconds}s ${target} >/dev/null 2>&1 || true`);
|
|
9700
9799
|
}
|
|
9701
9800
|
}
|
|
9702
9801
|
function resolveLocalMachineId(tailscale, explicit) {
|
|
@@ -9707,14 +9806,15 @@ function resolveLocalMachineId(tailscale, explicit) {
|
|
|
9707
9806
|
function planFleetHosts(options = {}) {
|
|
9708
9807
|
const runner = options.runner ?? defaultRunner2;
|
|
9709
9808
|
const warnings = [];
|
|
9710
|
-
|
|
9809
|
+
const binary = resolveTailscaleBinary(runner);
|
|
9810
|
+
let tailscale = loadTailscaleStatus(runner, binary, warnings);
|
|
9711
9811
|
const manifest = readManifest();
|
|
9712
9812
|
const localSubnets = options.localSubnets ?? localPrivateSubnets();
|
|
9713
|
-
if (options.warm !== false && tailscale && localSubnets.length > 0) {
|
|
9813
|
+
if (options.warm !== false && tailscale && binary && localSubnets.length > 0) {
|
|
9714
9814
|
const targets = collectPingTargets(tailscale, localSubnets);
|
|
9715
9815
|
if (targets.length > 0) {
|
|
9716
|
-
warmDirectPaths(runner, targets, options.warmTimeoutSeconds);
|
|
9717
|
-
tailscale =
|
|
9816
|
+
warmDirectPaths(runner, targets, binary, options.warmTimeoutSeconds);
|
|
9817
|
+
tailscale = loadTailscaleStatus(runner, binary, warnings) ?? tailscale;
|
|
9718
9818
|
}
|
|
9719
9819
|
}
|
|
9720
9820
|
const localMachineId = resolveLocalMachineId(tailscale, options.localMachineId);
|
|
@@ -9794,6 +9894,7 @@ function diffMachines(leftMachineId, rightMachineId) {
|
|
|
9794
9894
|
}
|
|
9795
9895
|
|
|
9796
9896
|
// src/commands/apps.ts
|
|
9897
|
+
init_mutation_approval();
|
|
9797
9898
|
function getPackageName(app) {
|
|
9798
9899
|
return app.packageName || app.name;
|
|
9799
9900
|
}
|
|
@@ -9934,6 +10035,7 @@ function runAppsPlan(plan, options = {}, runner = runMachineCommand) {
|
|
|
9934
10035
|
}
|
|
9935
10036
|
|
|
9936
10037
|
// src/commands/install-claude.ts
|
|
10038
|
+
init_mutation_approval();
|
|
9937
10039
|
var AI_CLI_PACKAGES = {
|
|
9938
10040
|
claude: "@anthropic-ai/claude-code",
|
|
9939
10041
|
codex: "@openai/codex",
|
|
@@ -10041,6 +10143,7 @@ function runClaudeInstallPlan(plan, options = {}, runner = runMachineCommand) {
|
|
|
10041
10143
|
}
|
|
10042
10144
|
|
|
10043
10145
|
// src/commands/install-tailscale.ts
|
|
10146
|
+
init_mutation_approval();
|
|
10044
10147
|
function buildInstallSteps2(machine) {
|
|
10045
10148
|
if (machine.platform === "macos") {
|
|
10046
10149
|
return [
|
|
@@ -10108,6 +10211,7 @@ function runTailscaleInstallPlan(plan, options = {}, runner = runMachineCommand)
|
|
|
10108
10211
|
import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
10109
10212
|
import { delimiter, isAbsolute, join as join7 } from "path";
|
|
10110
10213
|
init_paths();
|
|
10214
|
+
init_mutation_approval();
|
|
10111
10215
|
var notificationChannelSchema = exports_external.object({
|
|
10112
10216
|
id: exports_external.string(),
|
|
10113
10217
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
@@ -10451,9 +10555,20 @@ import { EventsClient } from "@hasna/events";
|
|
|
10451
10555
|
function shellQuote5(value) {
|
|
10452
10556
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
10453
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
|
+
}
|
|
10454
10568
|
function buildTmuxPaneDiedHookPlan(options = {}) {
|
|
10455
10569
|
const tmuxCommand = options.tmuxCommand ?? process.env["HASNA_MACHINES_TMUX_BIN"] ?? "tmux";
|
|
10456
10570
|
const machinesCommand = options.machinesCommand ?? "machines";
|
|
10571
|
+
assertMachinesCommandSafe(machinesCommand);
|
|
10457
10572
|
const deliver = options.deliver === true;
|
|
10458
10573
|
const approvalToken = options.approvalToken?.trim();
|
|
10459
10574
|
const trustedLocalMutation = approvalToken ? false : options.trustedLocalMutation === true;
|
|
@@ -10694,6 +10809,7 @@ import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync7, sy
|
|
|
10694
10809
|
import { homedir as homedir5 } from "os";
|
|
10695
10810
|
init_paths();
|
|
10696
10811
|
init_db();
|
|
10812
|
+
init_mutation_approval();
|
|
10697
10813
|
function quote4(value) {
|
|
10698
10814
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
10699
10815
|
}
|
|
@@ -11478,6 +11594,9 @@ function runDoctor(machineId, options = {}) {
|
|
|
11478
11594
|
};
|
|
11479
11595
|
}
|
|
11480
11596
|
|
|
11597
|
+
// src/cli/index.ts
|
|
11598
|
+
init_mutation_approval();
|
|
11599
|
+
|
|
11481
11600
|
// src/commands/daemon.ts
|
|
11482
11601
|
import { execFileSync } from "child_process";
|
|
11483
11602
|
import { chmodSync, existsSync as existsSync10, readFileSync as readFileSync8, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
@@ -12037,6 +12156,7 @@ function getAgentStatus(machineId, options = {}) {
|
|
|
12037
12156
|
}
|
|
12038
12157
|
|
|
12039
12158
|
// src/commands/serve.ts
|
|
12159
|
+
init_mutation_approval();
|
|
12040
12160
|
function escapeHtml(value) {
|
|
12041
12161
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
12042
12162
|
}
|
|
@@ -12496,6 +12616,16 @@ function startDashboardServer(options = {}) {
|
|
|
12496
12616
|
function check(id, status, summary, detail) {
|
|
12497
12617
|
return { id, status, summary, detail };
|
|
12498
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
|
+
}
|
|
12499
12629
|
function runSelfTest() {
|
|
12500
12630
|
const version = getPackageVersion();
|
|
12501
12631
|
const status = getStatus();
|
|
@@ -12507,19 +12637,21 @@ function runSelfTest() {
|
|
|
12507
12637
|
const apps = listApps(machineId);
|
|
12508
12638
|
const appsDiff = diffApps(machineId);
|
|
12509
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
|
+
];
|
|
12510
12651
|
return {
|
|
12511
12652
|
machineId,
|
|
12512
|
-
checks
|
|
12513
|
-
|
|
12514
|
-
check("status", "ok", "Status loads", JSON.stringify({ machines: status.manifestMachineCount, heartbeats: status.heartbeatCount })),
|
|
12515
|
-
check("doctor", doctor.checks.some((entry) => entry.status === "fail") ? "warn" : "ok", "Doctor completed", `${doctor.checks.length} checks`),
|
|
12516
|
-
check("serve-info", "ok", "Dashboard info renders", `${serveInfo.url} routes=${serveInfo.routes.length}`),
|
|
12517
|
-
check("dashboard-html", html.includes("Machines Dashboard") ? "ok" : "fail", "Dashboard HTML renders", html.slice(0, 80)),
|
|
12518
|
-
check("notifications", "ok", "Notifications config loads", `${notifications.channels.length} channels`),
|
|
12519
|
-
check("apps", "ok", "Apps manifest loads", `${apps.apps.length} apps`),
|
|
12520
|
-
check("apps-diff", appsDiff.missing.length === 0 ? "ok" : "warn", "Apps diff computed", `missing=${appsDiff.missing.length} installed=${appsDiff.installed.length}`),
|
|
12521
|
-
check("install-claude-plan", cliPlan.steps.length > 0 ? "ok" : "warn", "Install plan renders", `${cliPlan.steps.length} steps`)
|
|
12522
|
-
]
|
|
12653
|
+
...summarize(checks),
|
|
12654
|
+
checks
|
|
12523
12655
|
};
|
|
12524
12656
|
}
|
|
12525
12657
|
|
|
@@ -13616,6 +13748,7 @@ function renderDoctorResult(report) {
|
|
|
13616
13748
|
function renderSelfTestResult(result) {
|
|
13617
13749
|
return [
|
|
13618
13750
|
`machine: ${result.machineId}`,
|
|
13751
|
+
`overall: ${result.overall} ok=${result.counts.ok} warn=${result.counts.warn} fail=${result.counts.fail}`,
|
|
13619
13752
|
...result.checks.map((check2) => {
|
|
13620
13753
|
const status = check2.status === "ok" ? source_default.green(check2.status) : check2.status === "warn" ? source_default.yellow(check2.status) : source_default.red(check2.status);
|
|
13621
13754
|
return `${check2.id.padEnd(20)} ${status} ${check2.detail}`;
|
|
@@ -14749,10 +14882,10 @@ storageCommand.command("status").description("Show storage sync status").option(
|
|
|
14749
14882
|
});
|
|
14750
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) => {
|
|
14751
14884
|
try {
|
|
14752
|
-
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePush:
|
|
14885
|
+
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePush: storagePush3 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14753
14886
|
const tables = resolveTables2(parseStorageTables2(options.tables));
|
|
14754
14887
|
requireCliMutation("storage_push", options.approvalToken, { resourceId: cliResourceId("storage-push", tables.join(",")), args: { tables } });
|
|
14755
|
-
const results = await
|
|
14888
|
+
const results = await storagePush3({ tables, trustedLocalMutation: createTrustedSdkMutationApproval() });
|
|
14756
14889
|
printStorageResults(results, options.json);
|
|
14757
14890
|
} catch (error) {
|
|
14758
14891
|
printStorageError(error);
|
|
@@ -14760,10 +14893,10 @@ storageCommand.command("push").description("Push local machine runtime data to s
|
|
|
14760
14893
|
});
|
|
14761
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) => {
|
|
14762
14895
|
try {
|
|
14763
|
-
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePull:
|
|
14896
|
+
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePull: storagePull3 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14764
14897
|
const tables = resolveTables2(parseStorageTables2(options.tables));
|
|
14765
14898
|
requireCliMutation("storage_pull", options.approvalToken, { resourceId: cliResourceId("storage-pull", tables.join(",")), args: { tables } });
|
|
14766
|
-
const results = await
|
|
14899
|
+
const results = await storagePull3({ tables, trustedLocalMutation: createTrustedSdkMutationApproval() });
|
|
14767
14900
|
printStorageResults(results, options.json);
|
|
14768
14901
|
} catch (error) {
|
|
14769
14902
|
printStorageError(error);
|
|
@@ -14771,10 +14904,10 @@ storageCommand.command("pull").description("Pull machine runtime data from stora
|
|
|
14771
14904
|
});
|
|
14772
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) => {
|
|
14773
14906
|
try {
|
|
14774
|
-
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storageSync:
|
|
14907
|
+
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storageSync: storageSync3 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14775
14908
|
const tables = resolveTables2(parseStorageTables2(options.tables));
|
|
14776
14909
|
requireCliMutation("storage_sync", options.approvalToken, { resourceId: cliResourceId("storage-sync", tables.join(",")), args: { tables } });
|
|
14777
|
-
const result = await
|
|
14910
|
+
const result = await storageSync3({ tables, trustedLocalMutation: createTrustedSdkMutationApproval() });
|
|
14778
14911
|
if (options.json) {
|
|
14779
14912
|
console.log(JSON.stringify(result, null, 2));
|
|
14780
14913
|
return;
|