@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/storage.js
CHANGED
|
@@ -1,35 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
2
|
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
function __accessProp(key) {
|
|
8
|
-
return this[key];
|
|
9
|
-
}
|
|
10
|
-
var __toESMCache_node;
|
|
11
|
-
var __toESMCache_esm;
|
|
12
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
-
var canCache = mod != null && typeof mod === "object";
|
|
14
|
-
if (canCache) {
|
|
15
|
-
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
-
var cached = cache.get(mod);
|
|
17
|
-
if (cached)
|
|
18
|
-
return cached;
|
|
19
|
-
}
|
|
20
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
-
for (let key of __getOwnPropNames(mod))
|
|
23
|
-
if (!__hasOwnProp.call(to, key))
|
|
24
|
-
__defProp(to, key, {
|
|
25
|
-
get: __accessProp.bind(mod, key),
|
|
26
|
-
enumerable: true
|
|
27
|
-
});
|
|
28
|
-
if (canCache)
|
|
29
|
-
cache.set(mod, to);
|
|
30
|
-
return to;
|
|
31
|
-
};
|
|
32
|
-
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
33
3
|
var __returnValue = (v) => v;
|
|
34
4
|
function __exportSetter(name, newValue) {
|
|
35
5
|
this[name] = __returnValue.bind(null, newValue);
|
|
@@ -206,51 +176,6 @@ function getAdapter(path = getDbPath()) {
|
|
|
206
176
|
function getDb(path = getDbPath()) {
|
|
207
177
|
return getAdapter(path).raw;
|
|
208
178
|
}
|
|
209
|
-
function closeDb() {
|
|
210
|
-
if (adapter) {
|
|
211
|
-
adapter.close();
|
|
212
|
-
adapter = null;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
function upsertHeartbeat(machineId, pid = process.pid, status = "online", metadata = {}) {
|
|
216
|
-
const db = getDb();
|
|
217
|
-
db.query(`INSERT INTO agent_heartbeats (
|
|
218
|
-
machine_id,
|
|
219
|
-
pid,
|
|
220
|
-
status,
|
|
221
|
-
updated_at,
|
|
222
|
-
daemon_version,
|
|
223
|
-
agent_mode,
|
|
224
|
-
platform,
|
|
225
|
-
os_version,
|
|
226
|
-
os_build,
|
|
227
|
-
arch,
|
|
228
|
-
uptime_seconds,
|
|
229
|
-
tool_versions_json,
|
|
230
|
-
tailscale_json,
|
|
231
|
-
storage_sync_status,
|
|
232
|
-
storage_sync_last_error,
|
|
233
|
-
doctor_summary_json,
|
|
234
|
-
private_metadata
|
|
235
|
-
)
|
|
236
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
237
|
-
ON CONFLICT(machine_id, pid) DO UPDATE SET
|
|
238
|
-
status = excluded.status,
|
|
239
|
-
updated_at = excluded.updated_at,
|
|
240
|
-
daemon_version = excluded.daemon_version,
|
|
241
|
-
agent_mode = excluded.agent_mode,
|
|
242
|
-
platform = excluded.platform,
|
|
243
|
-
os_version = excluded.os_version,
|
|
244
|
-
os_build = excluded.os_build,
|
|
245
|
-
arch = excluded.arch,
|
|
246
|
-
uptime_seconds = excluded.uptime_seconds,
|
|
247
|
-
tool_versions_json = excluded.tool_versions_json,
|
|
248
|
-
tailscale_json = excluded.tailscale_json,
|
|
249
|
-
storage_sync_status = excluded.storage_sync_status,
|
|
250
|
-
storage_sync_last_error = excluded.storage_sync_last_error,
|
|
251
|
-
doctor_summary_json = excluded.doctor_summary_json,
|
|
252
|
-
private_metadata = excluded.private_metadata`).run(machineId, pid, status, new Date().toISOString(), metadata.daemonVersion ?? null, metadata.agentMode ?? null, metadata.platform ?? null, metadata.osVersion ?? null, metadata.osBuild ?? null, metadata.arch ?? null, metadata.uptimeSeconds == null ? null : Math.max(0, Math.floor(metadata.uptimeSeconds)), metadata.toolVersions ? JSON.stringify(metadata.toolVersions) : null, metadata.tailscale ? JSON.stringify(metadata.tailscale) : null, metadata.storageSyncStatus ?? null, metadata.storageSyncLastError ?? null, metadata.doctorSummary ? JSON.stringify(metadata.doctorSummary) : null, metadata.privateMetadata ? 1 : 0);
|
|
253
|
-
}
|
|
254
179
|
function getLocalMachineId() {
|
|
255
180
|
return process.env["HASNA_MACHINES_MACHINE_ID"] || hostname();
|
|
256
181
|
}
|
|
@@ -280,12 +205,6 @@ function countRuns(table) {
|
|
|
280
205
|
const row = db.query(`SELECT COUNT(*) as count FROM ${table}`).get();
|
|
281
206
|
return row.count;
|
|
282
207
|
}
|
|
283
|
-
function setHeartbeatStatus(machineId, pid, status) {
|
|
284
|
-
const db = getDb();
|
|
285
|
-
db.query(`UPDATE agent_heartbeats
|
|
286
|
-
SET status = ?, updated_at = ?
|
|
287
|
-
WHERE machine_id = ? AND pid = ?`).run(status, new Date().toISOString(), machineId, pid);
|
|
288
|
-
}
|
|
289
208
|
function recordSetupRun(machineId, status, details) {
|
|
290
209
|
const db = getDb();
|
|
291
210
|
const now = new Date().toISOString();
|
|
@@ -359,6 +278,8 @@ var PG_MIGRATIONS = [
|
|
|
359
278
|
|
|
360
279
|
// src/remote-storage.ts
|
|
361
280
|
import pg from "pg";
|
|
281
|
+
var MACHINES_DATABASE_ALLOW_INSECURE_TLS_ENV = "HASNA_MACHINES_ALLOW_INSECURE_DATABASE_TLS";
|
|
282
|
+
var MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED_ENV = "HASNA_MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED";
|
|
362
283
|
function translatePlaceholders(sql) {
|
|
363
284
|
let index = 0;
|
|
364
285
|
return sql.replace(/\?/g, () => `$${++index}`);
|
|
@@ -367,7 +288,18 @@ function normalizeParams(params) {
|
|
|
367
288
|
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
368
289
|
return flat.map((value) => value === undefined ? null : value);
|
|
369
290
|
}
|
|
370
|
-
function
|
|
291
|
+
function envFlag(env, name) {
|
|
292
|
+
const value = env[name]?.trim().toLowerCase();
|
|
293
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
294
|
+
}
|
|
295
|
+
function isLoopbackHost(hostname2) {
|
|
296
|
+
const normalized = hostname2.replace(/^\[|\]$/g, "").toLowerCase();
|
|
297
|
+
return normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1" || /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
298
|
+
}
|
|
299
|
+
function allowsLocalInsecureTls(url, env) {
|
|
300
|
+
return isLoopbackHost(url.hostname) && envFlag(env, MACHINES_DATABASE_ALLOW_INSECURE_TLS_ENV);
|
|
301
|
+
}
|
|
302
|
+
function sslConfigFor(connectionString, env = process.env) {
|
|
371
303
|
let url;
|
|
372
304
|
try {
|
|
373
305
|
url = new URL(connectionString);
|
|
@@ -376,12 +308,21 @@ function sslConfigFor(connectionString) {
|
|
|
376
308
|
}
|
|
377
309
|
const sslMode = url.searchParams.get("sslmode")?.toLowerCase();
|
|
378
310
|
const ssl = url.searchParams.get("ssl")?.toLowerCase();
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
311
|
+
const rejectUnauthorizedOverride = env[MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED_ENV]?.trim() === "0";
|
|
312
|
+
if (sslMode === "disable" || ssl === "false") {
|
|
313
|
+
if (allowsLocalInsecureTls(url, env))
|
|
314
|
+
return;
|
|
315
|
+
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.`);
|
|
316
|
+
}
|
|
317
|
+
if (sslMode === "no-verify" || rejectUnauthorizedOverride) {
|
|
318
|
+
if (!allowsLocalInsecureTls(url, env)) {
|
|
319
|
+
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.`);
|
|
320
|
+
}
|
|
382
321
|
return { rejectUnauthorized: false };
|
|
383
322
|
}
|
|
384
|
-
|
|
323
|
+
if (sslMode || ssl === "true")
|
|
324
|
+
return { rejectUnauthorized: true };
|
|
325
|
+
return isLoopbackHost(url.hostname) ? undefined : { rejectUnauthorized: true };
|
|
385
326
|
}
|
|
386
327
|
|
|
387
328
|
class PgAdapterAsync {
|
|
@@ -666,16 +607,376 @@ function coerceForSqlite(value) {
|
|
|
666
607
|
return JSON.stringify(value);
|
|
667
608
|
return String(value);
|
|
668
609
|
}
|
|
610
|
+
|
|
611
|
+
// src/commands/mutation-approval.ts
|
|
612
|
+
import { createHash, createHmac, randomUUID, timingSafeEqual } from "crypto";
|
|
613
|
+
import { resolve as resolve2 } from "path";
|
|
614
|
+
var MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_ALLOW_MUTATIONS";
|
|
615
|
+
var LEGACY_MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_MUTATION_APPROVAL";
|
|
616
|
+
var MUTATION_APPROVAL_TOKEN_ENV = "HASNA_MACHINES_MUTATION_TOKEN";
|
|
617
|
+
var MUTATION_APPROVAL_CALLER_ENV = "HASNA_MACHINES_MUTATION_CALLER_ID";
|
|
618
|
+
var MUTATION_APPROVAL_RUN_ENV = "HASNA_MACHINES_MUTATION_RUN_ID";
|
|
619
|
+
var MUTATION_APPROVAL_REPLAY_PATH_ENV = "HASNA_MACHINES_MUTATION_REPLAY_PATH";
|
|
620
|
+
var TOKEN_PREFIX = "machines-mut-v1";
|
|
621
|
+
var DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
622
|
+
var MAX_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
623
|
+
var MAX_CLOCK_SKEW_MS = 30000;
|
|
624
|
+
var trustedSdkMutationApprovals = new WeakSet;
|
|
625
|
+
function isTrustedSdkMutationApproval(approval) {
|
|
626
|
+
return typeof approval === "object" && approval !== null && trustedSdkMutationApprovals.has(approval);
|
|
627
|
+
}
|
|
628
|
+
function isTruthy(value) {
|
|
629
|
+
return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
|
|
630
|
+
}
|
|
631
|
+
function nowMs(now) {
|
|
632
|
+
if (typeof now === "number")
|
|
633
|
+
return now;
|
|
634
|
+
if (now instanceof Date)
|
|
635
|
+
return now.getTime();
|
|
636
|
+
return Date.now();
|
|
637
|
+
}
|
|
638
|
+
function signingSecret(env, explicitSecret) {
|
|
639
|
+
return explicitSecret?.trim() || env[MUTATION_APPROVAL_TOKEN_ENV]?.trim();
|
|
640
|
+
}
|
|
641
|
+
function base64Url(value) {
|
|
642
|
+
return Buffer.from(value).toString("base64url");
|
|
643
|
+
}
|
|
644
|
+
function hmac(payload, secret) {
|
|
645
|
+
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
646
|
+
}
|
|
647
|
+
function sha256Hex(payload) {
|
|
648
|
+
return createHash("sha256").update(payload).digest("hex");
|
|
649
|
+
}
|
|
650
|
+
function replayDbPath(env) {
|
|
651
|
+
const configured = env[MUTATION_APPROVAL_REPLAY_PATH_ENV]?.trim();
|
|
652
|
+
return configured ? resolve2(configured) : undefined;
|
|
653
|
+
}
|
|
654
|
+
function replayNonceKey(claims) {
|
|
655
|
+
return sha256Hex(JSON.stringify({ nonce: claims.nonce }));
|
|
656
|
+
}
|
|
657
|
+
function recordReplayNonce(env, claims, tokenPayload, now) {
|
|
658
|
+
const dbPath = replayDbPath(env);
|
|
659
|
+
if (!dbPath)
|
|
660
|
+
return;
|
|
661
|
+
if (!claims.nonce) {
|
|
662
|
+
return { approved: false, reason: "approval_token nonce claim is required for replay protection." };
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
const db = getDb(dbPath);
|
|
666
|
+
db.query("DELETE FROM mutation_approval_nonces WHERE expires_at <= ?").run(now);
|
|
667
|
+
const result = db.query(`
|
|
668
|
+
INSERT OR IGNORE INTO mutation_approval_nonces (
|
|
669
|
+
nonce_sha256,
|
|
670
|
+
token_sha256,
|
|
671
|
+
surface,
|
|
672
|
+
operation,
|
|
673
|
+
caller_id,
|
|
674
|
+
run_id,
|
|
675
|
+
transport,
|
|
676
|
+
expires_at,
|
|
677
|
+
used_at
|
|
678
|
+
)
|
|
679
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
680
|
+
`).run(replayNonceKey(claims), sha256Hex(tokenPayload), claims.surface, claims.operation, claims.callerId ?? "", claims.runId ?? "", claims.transport ?? "", claims.expiresAt, now);
|
|
681
|
+
if (result.changes !== 1) {
|
|
682
|
+
return { approved: false, reason: "approval_token nonce has already been used." };
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
} catch (error) {
|
|
686
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
687
|
+
return { approved: false, reason: `approval_token replay store is unavailable: ${message}` };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function safeEqual(left, right) {
|
|
691
|
+
const leftBuffer = Buffer.from(left);
|
|
692
|
+
const rightBuffer = Buffer.from(right);
|
|
693
|
+
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
694
|
+
}
|
|
695
|
+
function normalizeScope(scope) {
|
|
696
|
+
return {
|
|
697
|
+
surface: scope.surface,
|
|
698
|
+
operation: scope.operation,
|
|
699
|
+
machineId: scope.machineId || undefined,
|
|
700
|
+
resourceId: scope.resourceId || undefined,
|
|
701
|
+
callerId: scope.callerId || undefined,
|
|
702
|
+
runId: scope.runId || undefined,
|
|
703
|
+
transport: scope.transport || undefined,
|
|
704
|
+
argsSha256: scope.argsSha256 || (scope.args === undefined ? undefined : mutationArgsSha256(scope.args))
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function canonicalizeMutationArg(value, inArray = false) {
|
|
708
|
+
if (value === undefined)
|
|
709
|
+
return inArray ? null : undefined;
|
|
710
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
711
|
+
return value;
|
|
712
|
+
if (typeof value === "number")
|
|
713
|
+
return Number.isFinite(value) ? value : null;
|
|
714
|
+
if (Array.isArray(value)) {
|
|
715
|
+
return value.map((entry) => canonicalizeMutationArg(entry, true) ?? null);
|
|
716
|
+
}
|
|
717
|
+
if (value instanceof Date)
|
|
718
|
+
return value.toISOString();
|
|
719
|
+
if (typeof value === "object") {
|
|
720
|
+
const result = {};
|
|
721
|
+
for (const key of Object.keys(value).sort()) {
|
|
722
|
+
if (key === "approval_token" || key === "approvalToken")
|
|
723
|
+
continue;
|
|
724
|
+
const canonicalValue = canonicalizeMutationArg(value[key]);
|
|
725
|
+
if (canonicalValue !== undefined)
|
|
726
|
+
result[key] = canonicalValue;
|
|
727
|
+
}
|
|
728
|
+
return result;
|
|
729
|
+
}
|
|
730
|
+
return inArray ? null : undefined;
|
|
731
|
+
}
|
|
732
|
+
function canonicalMutationArgs(value) {
|
|
733
|
+
return JSON.stringify(canonicalizeMutationArg(value) ?? {});
|
|
734
|
+
}
|
|
735
|
+
function mutationArgsSha256(value) {
|
|
736
|
+
return sha256Hex(canonicalMutationArgs(value));
|
|
737
|
+
}
|
|
738
|
+
function stripPlanRuntimeFields(value) {
|
|
739
|
+
if (Array.isArray(value))
|
|
740
|
+
return value.map(stripPlanRuntimeFields);
|
|
741
|
+
if (value instanceof Date)
|
|
742
|
+
return value;
|
|
743
|
+
if (value && typeof value === "object") {
|
|
744
|
+
const result = {};
|
|
745
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
746
|
+
if (key === "planDigest" || key === "plan_digest" || key === "mode" || key === "executed")
|
|
747
|
+
continue;
|
|
748
|
+
result[key] = stripPlanRuntimeFields(entry);
|
|
749
|
+
}
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
return value;
|
|
753
|
+
}
|
|
754
|
+
function mutationPlanDigest(plan) {
|
|
755
|
+
return mutationArgsSha256(stripPlanRuntimeFields(plan));
|
|
756
|
+
}
|
|
757
|
+
function attachMutationPlanDigest(plan) {
|
|
758
|
+
return {
|
|
759
|
+
...plan,
|
|
760
|
+
planDigest: mutationPlanDigest(plan)
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function assertMutationPlanDigest(plan, expectedPlanDigest) {
|
|
764
|
+
if (expectedPlanDigest && mutationPlanDigest(plan) !== expectedPlanDigest) {
|
|
765
|
+
throw new Error("Approved plan digest does not match the current execution plan.");
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function createMutationApprovalToken(scope, options = {}) {
|
|
769
|
+
const env = options.env ?? process.env;
|
|
770
|
+
const secret = signingSecret(env, options.secret);
|
|
771
|
+
if (!secret)
|
|
772
|
+
throw new Error(`${MUTATION_APPROVAL_TOKEN_ENV} is required to sign mutation approval tokens.`);
|
|
773
|
+
const issuedAt = nowMs(options.now);
|
|
774
|
+
const claims = {
|
|
775
|
+
version: 1,
|
|
776
|
+
...normalizeScope(scope),
|
|
777
|
+
callerId: scope.callerId,
|
|
778
|
+
runId: scope.runId,
|
|
779
|
+
transport: scope.transport,
|
|
780
|
+
issuedAt,
|
|
781
|
+
expiresAt: issuedAt + Math.max(1, options.ttlMs ?? DEFAULT_TOKEN_TTL_MS),
|
|
782
|
+
nonce: options.nonce ?? randomUUID()
|
|
783
|
+
};
|
|
784
|
+
claims.args_sha256 = claims.argsSha256;
|
|
785
|
+
delete claims.args;
|
|
786
|
+
delete claims.argsSha256;
|
|
787
|
+
const payload = base64Url(JSON.stringify(claims));
|
|
788
|
+
return `${TOKEN_PREFIX}.${payload}.${hmac(payload, secret)}`;
|
|
789
|
+
}
|
|
790
|
+
function parseToken(token) {
|
|
791
|
+
if (!token)
|
|
792
|
+
return null;
|
|
793
|
+
const parts = token.split(".");
|
|
794
|
+
if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX)
|
|
795
|
+
return null;
|
|
796
|
+
try {
|
|
797
|
+
const claims = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8"));
|
|
798
|
+
return { payload: parts[1] ?? "", signature: parts[2] ?? "", claims };
|
|
799
|
+
} catch {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function claimMatches(expected, actual) {
|
|
804
|
+
if (expected === undefined)
|
|
805
|
+
return actual === undefined;
|
|
806
|
+
return actual === expected;
|
|
807
|
+
}
|
|
808
|
+
function verifyMutationApprovalToken(options) {
|
|
809
|
+
const env = options.env ?? process.env;
|
|
810
|
+
const secret = signingSecret(env);
|
|
811
|
+
if (!secret)
|
|
812
|
+
return { approved: false, reason: `${MUTATION_APPROVAL_TOKEN_ENV} is not configured.` };
|
|
813
|
+
const parsed = parseToken(options.approvalToken);
|
|
814
|
+
if (!parsed)
|
|
815
|
+
return { approved: false, reason: "approval_token is not a scoped mutation token." };
|
|
816
|
+
if (!safeEqual(hmac(parsed.payload, secret), parsed.signature)) {
|
|
817
|
+
return { approved: false, reason: "approval_token signature is invalid." };
|
|
818
|
+
}
|
|
819
|
+
const claims = parsed.claims;
|
|
820
|
+
if (claims.version !== 1)
|
|
821
|
+
return { approved: false, reason: "approval_token version is unsupported." };
|
|
822
|
+
if (!claims.callerId || !claims.runId) {
|
|
823
|
+
return { approved: false, reason: "approval_token must include caller and run claims." };
|
|
824
|
+
}
|
|
825
|
+
if (!claims.transport) {
|
|
826
|
+
return { approved: false, reason: "approval_token must include a transport claim." };
|
|
827
|
+
}
|
|
828
|
+
if (!Number.isFinite(claims.expiresAt) || claims.expiresAt <= nowMs(options.now)) {
|
|
829
|
+
return { approved: false, reason: "approval_token is expired." };
|
|
830
|
+
}
|
|
831
|
+
const now = nowMs(options.now);
|
|
832
|
+
if (!Number.isFinite(claims.issuedAt) || claims.issuedAt > now + MAX_CLOCK_SKEW_MS) {
|
|
833
|
+
return { approved: false, reason: "approval_token issue time is invalid." };
|
|
834
|
+
}
|
|
835
|
+
if (claims.expiresAt - claims.issuedAt > MAX_TOKEN_TTL_MS) {
|
|
836
|
+
return { approved: false, reason: "approval_token TTL is too long." };
|
|
837
|
+
}
|
|
838
|
+
for (const key of ["surface", "operation", "machineId", "resourceId", "transport"]) {
|
|
839
|
+
if (!claimMatches(options[key], claims[key])) {
|
|
840
|
+
return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
for (const key of ["callerId", "runId"]) {
|
|
844
|
+
if (options[key] !== undefined && options[key] !== claims[key]) {
|
|
845
|
+
return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const expectedArgsSha256 = options.argsSha256 || (options.args === undefined ? undefined : mutationArgsSha256(options.args));
|
|
849
|
+
if (expectedArgsSha256 !== undefined && claims.args_sha256 !== expectedArgsSha256) {
|
|
850
|
+
return { approved: false, reason: "approval_token args_sha256 claim does not match this mutation." };
|
|
851
|
+
}
|
|
852
|
+
const replayDecision = recordReplayNonce(env, claims, parsed.payload, now);
|
|
853
|
+
if (replayDecision)
|
|
854
|
+
return replayDecision;
|
|
855
|
+
return { approved: true, claims };
|
|
856
|
+
}
|
|
857
|
+
function isMutationApproved(options = {}) {
|
|
858
|
+
const env = options.env ?? process.env;
|
|
859
|
+
const surface = options.surface ?? "cli";
|
|
860
|
+
if (surface === "mcp") {
|
|
861
|
+
if (!options.operation)
|
|
862
|
+
return false;
|
|
863
|
+
return verifyMutationApprovalToken({
|
|
864
|
+
surface,
|
|
865
|
+
operation: options.operation,
|
|
866
|
+
machineId: options.machineId,
|
|
867
|
+
resourceId: options.resourceId,
|
|
868
|
+
callerId: options.callerId,
|
|
869
|
+
runId: options.runId,
|
|
870
|
+
transport: options.transport ?? "mcp",
|
|
871
|
+
args: options.args,
|
|
872
|
+
argsSha256: options.argsSha256,
|
|
873
|
+
approvalToken: options.approvalToken,
|
|
874
|
+
env,
|
|
875
|
+
now: options.now
|
|
876
|
+
}).approved;
|
|
877
|
+
}
|
|
878
|
+
if (options.approvalToken) {
|
|
879
|
+
const decision = options.operation ? verifyMutationApprovalToken({
|
|
880
|
+
surface,
|
|
881
|
+
operation: options.operation,
|
|
882
|
+
machineId: options.machineId,
|
|
883
|
+
resourceId: options.resourceId,
|
|
884
|
+
callerId: options.callerId,
|
|
885
|
+
runId: options.runId,
|
|
886
|
+
transport: options.transport ?? surface,
|
|
887
|
+
args: options.args,
|
|
888
|
+
argsSha256: options.argsSha256,
|
|
889
|
+
approvalToken: options.approvalToken,
|
|
890
|
+
env,
|
|
891
|
+
now: options.now
|
|
892
|
+
}) : { approved: false };
|
|
893
|
+
if (decision.approved)
|
|
894
|
+
return true;
|
|
895
|
+
if (env[MUTATION_APPROVAL_TOKEN_ENV]?.trim())
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
return isTruthy(env[MUTATION_APPROVAL_FLAG_ENV]) || isTruthy(env[LEGACY_MUTATION_APPROVAL_FLAG_ENV]);
|
|
899
|
+
}
|
|
900
|
+
function assertMutationApproved(options) {
|
|
901
|
+
if (isMutationApproved(options)) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const env = options.env ?? process.env;
|
|
905
|
+
const tokenConfigured = Boolean(env[MUTATION_APPROVAL_TOKEN_ENV]?.trim());
|
|
906
|
+
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}`;
|
|
907
|
+
throw new Error(`Fleet mutation blocked: ${options.surface}.${options.operation} requires operator approval; ${approvalHint}.`);
|
|
908
|
+
}
|
|
909
|
+
function assertSdkMutationApproved(scope, options = {}) {
|
|
910
|
+
if (isTrustedSdkMutationApproval(options.trustedLocalMutation))
|
|
911
|
+
return;
|
|
912
|
+
const decision = verifyMutationApprovalToken({
|
|
913
|
+
surface: "sdk",
|
|
914
|
+
operation: scope.operation,
|
|
915
|
+
machineId: scope.machineId,
|
|
916
|
+
resourceId: scope.resourceId,
|
|
917
|
+
callerId: options.callerId,
|
|
918
|
+
runId: options.runId,
|
|
919
|
+
transport: "sdk",
|
|
920
|
+
args: scope.args,
|
|
921
|
+
argsSha256: scope.argsSha256,
|
|
922
|
+
approvalToken: options.approvalToken,
|
|
923
|
+
env: process.env
|
|
924
|
+
});
|
|
925
|
+
if (decision.approved)
|
|
926
|
+
return;
|
|
927
|
+
throw new Error(`Fleet mutation blocked: sdk.${scope.operation} requires a scoped SDK approval token.`);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/storage.ts
|
|
931
|
+
function storageArgs(options = {}) {
|
|
932
|
+
return {
|
|
933
|
+
tables: options.tables?.length ? [...options.tables] : null
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function storageResourceId(operation, options = {}) {
|
|
937
|
+
return `storage:${operation}:${mutationArgsSha256(storageArgs(options))}`;
|
|
938
|
+
}
|
|
939
|
+
async function runStorageMigrations2(remote, options = {}) {
|
|
940
|
+
assertSdkMutationApproved({
|
|
941
|
+
operation: "machines_storage_migrate",
|
|
942
|
+
resourceId: "storage:migrations",
|
|
943
|
+
args: {}
|
|
944
|
+
}, options);
|
|
945
|
+
return runStorageMigrations(remote);
|
|
946
|
+
}
|
|
947
|
+
async function storagePush2(options = {}) {
|
|
948
|
+
assertSdkMutationApproved({
|
|
949
|
+
operation: "machines_storage_push",
|
|
950
|
+
resourceId: storageResourceId("push", options),
|
|
951
|
+
args: storageArgs(options)
|
|
952
|
+
}, options);
|
|
953
|
+
return storagePush({ tables: options.tables });
|
|
954
|
+
}
|
|
955
|
+
async function storagePull2(options = {}) {
|
|
956
|
+
assertSdkMutationApproved({
|
|
957
|
+
operation: "machines_storage_pull",
|
|
958
|
+
resourceId: storageResourceId("pull", options),
|
|
959
|
+
args: storageArgs(options)
|
|
960
|
+
}, options);
|
|
961
|
+
return storagePull({ tables: options.tables });
|
|
962
|
+
}
|
|
963
|
+
async function storageSync2(options = {}) {
|
|
964
|
+
assertSdkMutationApproved({
|
|
965
|
+
operation: "machines_storage_sync",
|
|
966
|
+
resourceId: storageResourceId("sync", options),
|
|
967
|
+
args: storageArgs(options)
|
|
968
|
+
}, options);
|
|
969
|
+
return storageSync({ tables: options.tables });
|
|
970
|
+
}
|
|
669
971
|
export {
|
|
670
|
-
storageSync,
|
|
671
|
-
storagePush,
|
|
672
|
-
storagePull,
|
|
673
|
-
runStorageMigrations,
|
|
972
|
+
storageSync2 as storageSync,
|
|
973
|
+
storagePush2 as storagePush,
|
|
974
|
+
storagePull2 as storagePull,
|
|
975
|
+
runStorageMigrations2 as runStorageMigrations,
|
|
674
976
|
resolveTables,
|
|
675
977
|
parseStorageTables,
|
|
676
978
|
getSyncMetaAll,
|
|
677
979
|
getStorageStatus,
|
|
678
|
-
getStoragePg,
|
|
679
980
|
getStorageMode,
|
|
680
981
|
getStorageDatabaseUrl,
|
|
681
982
|
getStorageDatabaseEnvName,
|
|
@@ -683,7 +984,6 @@ export {
|
|
|
683
984
|
STORAGE_TABLES,
|
|
684
985
|
STORAGE_MODE_ENV,
|
|
685
986
|
STORAGE_DATABASE_ENV,
|
|
686
|
-
PgAdapterAsync,
|
|
687
987
|
PG_MIGRATIONS,
|
|
688
988
|
MACHINES_STORAGE_TABLES,
|
|
689
989
|
MACHINES_STORAGE_MODE_FALLBACK_ENV,
|
package/dist/types.d.ts
CHANGED
|
@@ -211,8 +211,15 @@ export interface SelfTestCheck {
|
|
|
211
211
|
summary: string;
|
|
212
212
|
detail: string;
|
|
213
213
|
}
|
|
214
|
+
export interface SelfTestCounts {
|
|
215
|
+
ok: number;
|
|
216
|
+
warn: number;
|
|
217
|
+
fail: number;
|
|
218
|
+
}
|
|
214
219
|
export interface SelfTestResult {
|
|
215
220
|
machineId: string;
|
|
221
|
+
overall: SelfTestCheck["status"];
|
|
222
|
+
counts: SelfTestCounts;
|
|
216
223
|
checks: SelfTestCheck[];
|
|
217
224
|
}
|
|
218
225
|
export interface ClipboardEntry {
|