|
|
|
@@ -113,6 +113,8 @@ const MUTATING_INTENTS = new Set([
|
|
|
|
|
"debug.reset",
|
|
|
|
|
"io.uart.write"
|
|
|
|
|
]);
|
|
|
|
|
const DEFAULT_DEVICE_LEASE_TTL_SECONDS = 15 * 60;
|
|
|
|
|
const MAX_DEVICE_LEASE_TTL_SECONDS = 60 * 60;
|
|
|
|
|
|
|
|
|
|
export function createAccessController(options = {}) {
|
|
|
|
|
return new AccessController({
|
|
|
|
@@ -288,7 +290,8 @@ class AccessController {
|
|
|
|
|
if (request.method === "DELETE" && url.pathname.startsWith("/v1/admin/device-pod-grants/")) {
|
|
|
|
|
const [, , , devicePodId, userId] = url.pathname.split("/").filter(Boolean).map((part) => decodeURIComponent(part));
|
|
|
|
|
await this.store.revokeDevicePodGrant({ devicePodId: requiredText(devicePodId, "devicePodId"), userId: requiredText(userId, "userId") });
|
|
|
|
|
return sendJson(response, 200, { revoked: true, devicePodId, userId });
|
|
|
|
|
const releasedLease = await this.store.releaseDeviceLease?.({ devicePodId, holderUserId: userId, now: this.now(), allowAdmin: false, revokeGrant: true });
|
|
|
|
|
return sendJson(response, 200, { revoked: true, devicePodId, userId, releasedLease: publicDeviceLease(releasedLease) });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sendJson(response, 404, errorPayload("not_found", "Admin route is not implemented", 404));
|
|
|
|
@@ -327,6 +330,9 @@ class AccessController {
|
|
|
|
|
if (request.method === "GET" && parsed.route === "debug-probe/chip-id") return sendJson(response, 200, this.devicePodProbe(pod, "debug.chip-id"));
|
|
|
|
|
if (request.method === "GET" && parsed.route === "io-probe/uart/1") return sendJson(response, 200, this.devicePodProbe(pod, "io.uart.status"));
|
|
|
|
|
if (request.method === "GET" && parsed.route === "io-probe/uart/1/tail") return sendJson(response, 200, this.devicePodOutputProbe(pod, url.searchParams));
|
|
|
|
|
if (request.method === "POST" && parsed.route === "leases") return this.acquireDevicePodLease(request, response, pod, auth.actor);
|
|
|
|
|
if (request.method === "GET" && parsed.route === "leases/current") return this.getDevicePodLease(response, pod, auth.actor);
|
|
|
|
|
if (request.method === "DELETE" && parsed.route === "leases/current") return this.releaseDevicePodLease(request, response, pod, auth.actor);
|
|
|
|
|
if (request.method === "POST" && parsed.route === "jobs") return this.createDevicePodJob(request, response, pod, auth.actor);
|
|
|
|
|
if (request.method === "GET" && parsed.route.startsWith("jobs/")) return this.getDevicePodJob(response, pod, parsed.route, auth.actor);
|
|
|
|
|
if (request.method === "POST" && parsed.route.startsWith("jobs/") && parsed.route.endsWith("/cancel")) return this.cancelDevicePodJob(response, pod, parsed.route, auth.actor);
|
|
|
|
@@ -430,6 +436,100 @@ class AccessController {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async acquireDevicePodLease(request, response, pod, actor) {
|
|
|
|
|
const body = await jsonBody(request);
|
|
|
|
|
const now = this.now();
|
|
|
|
|
const ttlSeconds = boundedLeaseTtlSeconds(body.ttlSeconds);
|
|
|
|
|
const expiresAt = new Date(Date.parse(now) + ttlSeconds * 1000).toISOString();
|
|
|
|
|
const token = randomBytes(32).toString("base64url");
|
|
|
|
|
const holderSessionId = safeAgentSessionId(body.agentSessionId) || `ses_devicepod_${randomUUID()}`;
|
|
|
|
|
const lease = await this.store.acquireDeviceLease?.({
|
|
|
|
|
devicePodId: pod.id,
|
|
|
|
|
holderSessionId,
|
|
|
|
|
holderUserId: actor.id,
|
|
|
|
|
leaseTokenHash: sha256(token),
|
|
|
|
|
now,
|
|
|
|
|
expiresAt
|
|
|
|
|
});
|
|
|
|
|
if (!lease) {
|
|
|
|
|
const current = await this.store.getActiveDeviceLease?.(pod.id, now);
|
|
|
|
|
return sendJson(response, 409, {
|
|
|
|
|
ok: false,
|
|
|
|
|
status: 409,
|
|
|
|
|
error: {
|
|
|
|
|
code: "device_lease_conflict",
|
|
|
|
|
message: "Device Pod already has an active lease held by another actor"
|
|
|
|
|
},
|
|
|
|
|
lease: publicDeviceLease(current),
|
|
|
|
|
devicePodId: pod.id,
|
|
|
|
|
profileHash: pod.profileHash
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
await this.store.recordAgentSessionOwner?.({
|
|
|
|
|
sessionId: holderSessionId,
|
|
|
|
|
ownerUserId: actor.id,
|
|
|
|
|
projectId: textOr(body.projectId, "prj_v02_device_pod"),
|
|
|
|
|
agentId: "hwlab-device-pod",
|
|
|
|
|
status: "active",
|
|
|
|
|
session: { source: "device-pod-lease", reason: textOr(body.reason, "") },
|
|
|
|
|
now
|
|
|
|
|
});
|
|
|
|
|
return sendJson(response, 201, {
|
|
|
|
|
serviceId: CLOUD_API_SERVICE_ID,
|
|
|
|
|
contractVersion: "device-pod-lease-v1",
|
|
|
|
|
acquired: true,
|
|
|
|
|
devicePodId: pod.id,
|
|
|
|
|
targetId: targetIdFromProfile(pod.profile),
|
|
|
|
|
profileHash: pod.profileHash,
|
|
|
|
|
lease: publicDeviceLease(lease),
|
|
|
|
|
leaseToken: token,
|
|
|
|
|
tokenUsage: "Pass as body.leaseToken or x-hwlab-device-lease-token when creating mutating jobs.",
|
|
|
|
|
source: authoritySource()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getDevicePodLease(response, pod, actor) {
|
|
|
|
|
const lease = await this.store.getActiveDeviceLease?.(pod.id, this.now());
|
|
|
|
|
if (!lease || (actor.role !== "admin" && lease.holderUserId !== actor.id)) {
|
|
|
|
|
return sendJson(response, 200, {
|
|
|
|
|
serviceId: CLOUD_API_SERVICE_ID,
|
|
|
|
|
contractVersion: "device-pod-lease-v1",
|
|
|
|
|
devicePodId: pod.id,
|
|
|
|
|
profileHash: pod.profileHash,
|
|
|
|
|
lease: null,
|
|
|
|
|
source: authoritySource()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return sendJson(response, 200, {
|
|
|
|
|
serviceId: CLOUD_API_SERVICE_ID,
|
|
|
|
|
contractVersion: "device-pod-lease-v1",
|
|
|
|
|
devicePodId: pod.id,
|
|
|
|
|
profileHash: pod.profileHash,
|
|
|
|
|
lease: publicDeviceLease(lease),
|
|
|
|
|
source: authoritySource()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async releaseDevicePodLease(request, response, pod, actor) {
|
|
|
|
|
const token = leaseTokenFromRequest(request, null);
|
|
|
|
|
const released = await this.store.releaseDeviceLease?.({
|
|
|
|
|
devicePodId: pod.id,
|
|
|
|
|
holderUserId: actor.id,
|
|
|
|
|
tokenHash: token ? sha256(token) : null,
|
|
|
|
|
now: this.now(),
|
|
|
|
|
allowAdmin: actor.role === "admin"
|
|
|
|
|
});
|
|
|
|
|
return sendJson(response, released ? 200 : 404, {
|
|
|
|
|
serviceId: CLOUD_API_SERVICE_ID,
|
|
|
|
|
contractVersion: "device-pod-lease-v1",
|
|
|
|
|
released: Boolean(released),
|
|
|
|
|
devicePodId: pod.id,
|
|
|
|
|
profileHash: pod.profileHash,
|
|
|
|
|
lease: publicDeviceLease(released),
|
|
|
|
|
source: authoritySource()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
devicePodStatus(pod, actor) {
|
|
|
|
|
const route = pod.profile.route ?? {};
|
|
|
|
|
const gatewaySessionId = textOr(route.gatewaySessionId, "");
|
|
|
|
@@ -517,6 +617,17 @@ class AccessController {
|
|
|
|
|
if (MUTATING_INTENTS.has(intent) && !reason) {
|
|
|
|
|
return sendJson(response, 400, errorPayload("device_job_reason_required", `Device job intent ${intent} requires reason`, 400));
|
|
|
|
|
}
|
|
|
|
|
let lease = null;
|
|
|
|
|
if (MUTATING_INTENTS.has(intent)) {
|
|
|
|
|
const leaseToken = leaseTokenFromRequest(request, body);
|
|
|
|
|
if (!leaseToken) {
|
|
|
|
|
return sendJson(response, 409, errorPayload("device_lease_required", `Device job intent ${intent} requires an active device lease`, 409));
|
|
|
|
|
}
|
|
|
|
|
lease = await this.store.getDeviceLeaseByToken?.({ devicePodId: pod.id, tokenHash: sha256(leaseToken), now: this.now() });
|
|
|
|
|
if (!lease || (lease.holderUserId !== actor.id && actor.role !== "admin")) {
|
|
|
|
|
return sendJson(response, 409, errorPayload("device_lease_invalid", "Device lease is missing, expired, released, or held by another actor", 409));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const traceId = `trc_devicepod_${randomUUID()}`;
|
|
|
|
|
const operationId = `op_devicepod_${randomUUID()}`;
|
|
|
|
|
const now = this.now();
|
|
|
|
@@ -541,7 +652,7 @@ class AccessController {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!blocker) this.dispatchDevicePodJob({ job, pod, actor });
|
|
|
|
|
sendJson(response, blocker ? 409 : 202, this.jobPayload(job, pod));
|
|
|
|
|
sendJson(response, blocker ? 409 : 202, this.jobPayload(job, pod, { lease }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getDevicePodJob(response, pod, route, actor) {
|
|
|
|
@@ -609,7 +720,7 @@ class AccessController {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jobPayload(job, pod) {
|
|
|
|
|
jobPayload(job, pod, { lease = null } = {}) {
|
|
|
|
|
return {
|
|
|
|
|
serviceId: CLOUD_API_SERVICE_ID,
|
|
|
|
|
contractVersion: DEVICE_JOB_CONTRACT_VERSION,
|
|
|
|
@@ -621,6 +732,7 @@ class AccessController {
|
|
|
|
|
traceId: job.traceId,
|
|
|
|
|
operationId: job.operationId,
|
|
|
|
|
job: publicJob(job),
|
|
|
|
|
lease: lease ? publicDeviceLease(lease) : null,
|
|
|
|
|
blocker: job.blocker,
|
|
|
|
|
freshness: freshness(job.updatedAt, job.blocker),
|
|
|
|
|
outputUrl: `/v1/device-pods/${encodeURIComponent(pod.id)}/jobs/${encodeURIComponent(job.id)}/output`,
|
|
|
|
@@ -647,6 +759,7 @@ class MemoryAccessStore {
|
|
|
|
|
this.sessions = new Map();
|
|
|
|
|
this.devicePods = new Map();
|
|
|
|
|
this.grants = new Set();
|
|
|
|
|
this.leases = new Map();
|
|
|
|
|
this.jobs = new Map();
|
|
|
|
|
this.agentSessions = new Map();
|
|
|
|
|
}
|
|
|
|
@@ -685,6 +798,32 @@ class MemoryAccessStore {
|
|
|
|
|
async getDevicePod(id) { return this.devicePods.get(id) ?? null; }
|
|
|
|
|
async listVisibleDevicePods(actor) { return [...this.devicePods.values()].filter((pod) => pod.status === "active" && (actor.role === "admin" || this.grants.has(grantKey(pod.id, actor.id)))); }
|
|
|
|
|
async getVisibleDevicePod(actor, id) { return (await this.listVisibleDevicePods(actor)).find((pod) => pod.id === id) ?? null; }
|
|
|
|
|
async acquireDeviceLease(input) {
|
|
|
|
|
const now = input.now ?? this.now();
|
|
|
|
|
const active = await this.getActiveDeviceLease(input.devicePodId, now);
|
|
|
|
|
if (active && active.holderUserId !== input.holderUserId) return null;
|
|
|
|
|
const lease = { devicePodId: input.devicePodId, holderSessionId: input.holderSessionId, holderUserId: input.holderUserId, leaseTokenHash: input.leaseTokenHash, createdAt: active?.createdAt ?? now, expiresAt: input.expiresAt, releasedAt: null };
|
|
|
|
|
this.leases.set(input.devicePodId, lease);
|
|
|
|
|
return lease;
|
|
|
|
|
}
|
|
|
|
|
async getActiveDeviceLease(devicePodId, now = this.now()) {
|
|
|
|
|
const lease = this.leases.get(devicePodId) ?? null;
|
|
|
|
|
return lease && !lease.releasedAt && lease.expiresAt > now ? lease : null;
|
|
|
|
|
}
|
|
|
|
|
async getDeviceLeaseByToken(input) {
|
|
|
|
|
const lease = await this.getActiveDeviceLease(input.devicePodId, input.now ?? this.now());
|
|
|
|
|
return lease?.leaseTokenHash === input.tokenHash ? lease : null;
|
|
|
|
|
}
|
|
|
|
|
async releaseDeviceLease(input) {
|
|
|
|
|
const lease = await this.getActiveDeviceLease(input.devicePodId, input.now ?? this.now());
|
|
|
|
|
if (!lease) return null;
|
|
|
|
|
const tokenMatches = input.tokenHash && lease.leaseTokenHash === input.tokenHash;
|
|
|
|
|
if (!input.allowAdmin && !input.revokeGrant && (lease.holderUserId !== input.holderUserId || !tokenMatches)) return null;
|
|
|
|
|
if (!input.allowAdmin && input.revokeGrant && lease.holderUserId !== input.holderUserId) return null;
|
|
|
|
|
const released = { ...lease, releasedAt: input.now ?? this.now() };
|
|
|
|
|
this.leases.set(input.devicePodId, released);
|
|
|
|
|
return released;
|
|
|
|
|
}
|
|
|
|
|
async createDevicePodJob(input) { const job = normalizeJob(input); this.jobs.set(job.id, job); return job; }
|
|
|
|
|
async updateDevicePodJob(devicePodId, jobId, patch) { const job = this.jobs.get(jobId); if (!job || job.devicePodId !== devicePodId) return null; const next = { ...job, ...patch, updatedAt: patch.updatedAt ?? this.now() }; this.jobs.set(jobId, next); return next; }
|
|
|
|
|
async getDevicePodJob(devicePodId, jobId) { const job = this.jobs.get(jobId); return job?.devicePodId === devicePodId ? job : null; }
|
|
|
|
@@ -731,6 +870,30 @@ class PostgresAccessStore extends MemoryAccessStore {
|
|
|
|
|
async revokeDevicePodGrant(input) { await this.ensureSchema(); await this.query("DELETE FROM device_pod_grants WHERE device_pod_id = $1 AND user_id = $2", [input.devicePodId, input.userId]); }
|
|
|
|
|
async getDevicePod(id) { await this.ensureSchema(); const result = await this.query("SELECT * FROM device_pods WHERE id = $1 LIMIT 1", [id]); return pgDevicePod(result.rows?.[0]); }
|
|
|
|
|
async listVisibleDevicePods(actor) { await this.ensureSchema(); const sql = actor.role === "admin" ? "SELECT * FROM device_pods WHERE status = 'active' ORDER BY id" : "SELECT p.* FROM device_pods p JOIN device_pod_grants g ON g.device_pod_id = p.id WHERE p.status = 'active' AND g.user_id = $1 ORDER BY p.id"; const result = await this.query(sql, actor.role === "admin" ? [] : [actor.id]); return result.rows.map(pgDevicePod); }
|
|
|
|
|
async acquireDeviceLease(input) {
|
|
|
|
|
await this.ensureSchema();
|
|
|
|
|
const active = await this.getActiveDeviceLease(input.devicePodId, input.now);
|
|
|
|
|
if (active && active.holderUserId !== input.holderUserId) return null;
|
|
|
|
|
const result = await this.query("INSERT INTO device_leases (device_pod_id, holder_session_id, holder_user_id, lease_token_hash, created_at, expires_at, released_at) VALUES ($1,$2,$3,$4,$5,$6,NULL) ON CONFLICT (device_pod_id) DO UPDATE SET holder_session_id = EXCLUDED.holder_session_id, holder_user_id = EXCLUDED.holder_user_id, lease_token_hash = EXCLUDED.lease_token_hash, expires_at = EXCLUDED.expires_at, released_at = NULL WHERE device_leases.released_at IS NOT NULL OR device_leases.expires_at <= $5 OR device_leases.holder_user_id = $3 RETURNING *", [input.devicePodId, input.holderSessionId, input.holderUserId, input.leaseTokenHash, input.now, input.expiresAt]);
|
|
|
|
|
return pgLease(result.rows?.[0]);
|
|
|
|
|
}
|
|
|
|
|
async getActiveDeviceLease(devicePodId, now = this.now()) { await this.ensureSchema(); const result = await this.query("SELECT * FROM device_leases WHERE device_pod_id = $1 AND released_at IS NULL AND expires_at > $2 LIMIT 1", [devicePodId, now]); return pgLease(result.rows?.[0]); }
|
|
|
|
|
async getDeviceLeaseByToken(input) { await this.ensureSchema(); const result = await this.query("SELECT * FROM device_leases WHERE device_pod_id = $1 AND lease_token_hash = $2 AND released_at IS NULL AND expires_at > $3 LIMIT 1", [input.devicePodId, input.tokenHash, input.now]); return pgLease(result.rows?.[0]); }
|
|
|
|
|
async releaseDeviceLease(input) {
|
|
|
|
|
await this.ensureSchema();
|
|
|
|
|
const params = input.allowAdmin
|
|
|
|
|
? [input.devicePodId, input.now]
|
|
|
|
|
: input.revokeGrant
|
|
|
|
|
? [input.devicePodId, input.now, input.holderUserId]
|
|
|
|
|
: [input.devicePodId, input.now, input.holderUserId, input.tokenHash];
|
|
|
|
|
const sql = input.allowAdmin
|
|
|
|
|
? "UPDATE device_leases SET released_at = $2 WHERE device_pod_id = $1 AND released_at IS NULL RETURNING *"
|
|
|
|
|
: input.revokeGrant
|
|
|
|
|
? "UPDATE device_leases SET released_at = $2 WHERE device_pod_id = $1 AND holder_user_id = $3 AND released_at IS NULL RETURNING *"
|
|
|
|
|
: "UPDATE device_leases SET released_at = $2 WHERE device_pod_id = $1 AND holder_user_id = $3 AND lease_token_hash = $4 AND released_at IS NULL RETURNING *";
|
|
|
|
|
const result = await this.query(sql, params);
|
|
|
|
|
return pgLease(result.rows?.[0]);
|
|
|
|
|
}
|
|
|
|
|
async createDevicePodJob(input) { await this.ensureSchema(); const job = normalizeJob(input); await this.query("INSERT INTO device_pod_jobs (id, device_pod_id, owner_user_id, status, intent, args_json, reason, trace_id, operation_id, output_json, blocker_json, created_at, updated_at, completed_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)", jobParams(job)); return job; }
|
|
|
|
|
async updateDevicePodJob(devicePodId, jobId, patch) { await this.ensureSchema(); const current = await this.getDevicePodJob(devicePodId, jobId); if (!current) return null; const next = { ...current, ...patch, updatedAt: patch.updatedAt ?? this.now() }; await this.query("UPDATE device_pod_jobs SET status=$3, output_json=$4, blocker_json=$5, updated_at=$6, completed_at=$7 WHERE device_pod_id=$1 AND id=$2", [devicePodId, jobId, next.status, stableJson(next.output ?? {}), stableJson(next.blocker ?? {}), next.updatedAt, next.completedAt]); return next; }
|
|
|
|
|
async getDevicePodJob(devicePodId, jobId) { await this.ensureSchema(); const result = await this.query("SELECT * FROM device_pod_jobs WHERE device_pod_id = $1 AND id = $2 LIMIT 1", [devicePodId, jobId]); return pgJob(result.rows?.[0]); }
|
|
|
|
@@ -759,6 +922,9 @@ async function jsonBody(request) {
|
|
|
|
|
function requiredText(value, field) { const text = textOr(value, ""); if (!text) throw Object.assign(new Error(`${field} is required`), { statusCode: 400, code: "invalid_params" }); return text; }
|
|
|
|
|
function textOr(value, fallback) { const text = String(value ?? "").trim(); return text || fallback; }
|
|
|
|
|
function normalizeObject(value) { return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {}; }
|
|
|
|
|
function boundedLeaseTtlSeconds(value) { const parsed = Number.parseInt(String(value ?? ""), 10); return Math.min(Math.max(Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_DEVICE_LEASE_TTL_SECONDS, 1), MAX_DEVICE_LEASE_TTL_SECONDS); }
|
|
|
|
|
function safeAgentSessionId(value) { const text = textOr(value, ""); return /^ses_[A-Za-z0-9_.:-]+$/u.test(text) ? text : ""; }
|
|
|
|
|
function leaseTokenFromRequest(request, body) { return textOr(body?.leaseToken, "") || textOr(getHeader(request, "x-hwlab-device-lease-token"), ""); }
|
|
|
|
|
function hashPassword(password) { const salt = randomBytes(16).toString("hex"); return `sha256:${salt}:${sha256(`${salt}:${password}`)}`; }
|
|
|
|
|
function verifyPassword(stored, password) { const [, salt, digest] = String(stored ?? "").split(":"); return Boolean(salt && digest && sha256(`${salt}:${password}`) === digest); }
|
|
|
|
|
function sha256(value) { return createHash("sha256").update(String(value)).digest("hex"); }
|
|
|
|
@@ -773,6 +939,7 @@ function freshness(observedAt, blocker) { return { observedAt, ageMs: 0, stale:
|
|
|
|
|
function publicActor(user) { return user ? { id: user.id, username: user.username, displayName: user.displayName, role: user.role, status: user.status } : null; }
|
|
|
|
|
function redactedUser(user) { return publicActor(user); }
|
|
|
|
|
function publicSession(session) { return { id: session.id, userId: session.userId, createdAt: session.createdAt, lastSeenAt: session.lastSeenAt, expiresAt: session.expiresAt, revoked: Boolean(session.revokedAt) }; }
|
|
|
|
|
function publicDeviceLease(lease) { return lease ? { devicePodId: lease.devicePodId, holderSessionId: lease.holderSessionId, holderUserId: lease.holderUserId, createdAt: lease.createdAt, expiresAt: lease.expiresAt, releasedAt: lease.releasedAt ?? null, active: !lease.releasedAt } : null; }
|
|
|
|
|
function publicDevicePod(pod, { includeAdmin = false } = {}) { return { devicePodId: pod.id, name: pod.name, status: pod.status, targetId: targetIdFromProfile(pod.profile), profileHash: pod.profileHash, profile: redactedProfile(pod.profile), createdAt: pod.createdAt, updatedAt: pod.updatedAt, blocker: null, ...(includeAdmin ? { admin: { profileStored: true, routeStored: Boolean(pod.profile.route) } } : {}) }; }
|
|
|
|
|
function redactedProfile(profile = {}) { return { schemaVersion: profile.schemaVersion ?? null, target: { id: profile.target?.id ?? null }, projectWorkspace: { projectPath: profile.projectWorkspace?.projectPath ?? null, targetName: profile.projectWorkspace?.targetName ?? null, hexPath: profile.projectWorkspace?.hexPath ?? null }, debugInterface: { type: profile.debugInterface?.type ?? null }, ioInterface: { uartCount: Array.isArray(profile.ioInterface?.uart) ? profile.ioInterface.uart.length : 0 }, route: { configured: Boolean(profile.route?.gatewaySessionId), gatewaySessionId: "redacted", resourceId: profile.route?.resourceId ? "redacted" : null, capabilityId: profile.route?.capabilityId ? "redacted" : null } }; }
|
|
|
|
|
function targetIdFromProfile(profile = {}) { return profile.target?.id ?? profile.targetId ?? null; }
|
|
|
|
@@ -793,6 +960,7 @@ function sendAccessError(response, error) { const status = error?.statusCode ??
|
|
|
|
|
function pgUser(row) { return row ? { id: row.id, username: row.username, displayName: row.display_name, role: row.role, status: row.status, passwordHash: row.password_hash, createdAt: row.created_at, updatedAt: row.updated_at } : null; }
|
|
|
|
|
function pgSession(row) { return { id: row.id, userId: row.user_id, tokenHash: row.session_token_hash, createdAt: row.created_at, lastSeenAt: row.last_seen_at, expiresAt: row.expires_at, revokedAt: row.revoked_at, user: { id: row.user_id, username: row.username, displayName: row.display_name, role: row.role, status: row.status, passwordHash: row.password_hash, createdAt: row.user_created_at, updatedAt: row.user_updated_at } }; }
|
|
|
|
|
function pgDevicePod(row) { return { id: row.id, name: row.name, status: row.status, profile: parseJson(row.profile_json, {}), profileHash: row.profile_hash, createdAt: row.created_at, updatedAt: row.updated_at }; }
|
|
|
|
|
function pgLease(row) { return row ? { devicePodId: row.device_pod_id, holderSessionId: row.holder_session_id, holderUserId: row.holder_user_id, leaseTokenHash: row.lease_token_hash, createdAt: row.created_at, expiresAt: row.expires_at, releasedAt: row.released_at } : null; }
|
|
|
|
|
function pgJob(row) { return row ? { id: row.id, devicePodId: row.device_pod_id, ownerUserId: row.owner_user_id, status: row.status, intent: row.intent, args: parseJson(row.args_json, {}), reason: row.reason, traceId: row.trace_id, operationId: row.operation_id, output: parseJson(row.output_json, {}), blocker: parseJson(row.blocker_json, null), createdAt: row.created_at, updatedAt: row.updated_at, completedAt: row.completed_at } : null; }
|
|
|
|
|
function pgAgentSession(row) { return row ? { id: row.id, projectId: row.project_id, agentId: row.agent_id, status: row.status, startedAt: row.started_at, endedAt: row.ended_at, ownerUserId: row.owner_user_id, conversationId: row.conversation_id, threadId: row.thread_id, lastTraceId: row.last_trace_id, session: parseJson(row.session_json, {}), updatedAt: row.updated_at } : null; }
|
|
|
|
|
function jobParams(job) { return [job.id, job.devicePodId, job.ownerUserId, job.status, job.intent, stableJson(job.args), job.reason, job.traceId, job.operationId, stableJson(job.output), stableJson(job.blocker ?? {}), job.createdAt, job.updatedAt, job.completedAt]; }
|
|
|
|
|