fix: skip unchanged HWLAB test account sync

This commit is contained in:
Codex
2026-06-15 05:33:18 +00:00
parent ceb3fb4627
commit 1d11384d31
+84 -3
View File
@@ -149,7 +149,7 @@ async function sync(config: LoadedConfig, target: Target, options: Options): Pro
for (const account of target.accounts.filter((item) => item.target.kind === "kubernetes-secret")) {
const source = sources.find((item) => item.sourceRef === account.sourceRef && item.sourceKey === account.sourceKey);
if (!source?.value) throw new Error(`${account.logicalId} source material missing after preflight`);
synced.push(syncKubernetesSecret(target, account, source, options));
synced.push(syncKubernetesSecretIfNeeded(target, account, source, options));
}
const userBillingAccounts = target.accounts.filter((item) => item.target.kind === "user-billing-api-key");
if (userBillingAccounts.length === 0) {
@@ -163,6 +163,11 @@ async function sync(config: LoadedConfig, target: Target, options: Options): Pro
for (const account of userBillingAccounts) {
const source = sources.find((item) => item.sourceRef === account.sourceRef && item.sourceKey === account.sourceKey);
if (!source?.value || !source.sha256Hex || !source.serviceKeyPrefix) throw new Error(`${account.logicalId} source material missing after preflight`);
const targetStatus = await userBillingApiKeyStatus(tx, account, source);
if (userBillingAccountMatches(account, targetStatus)) {
synced.push(skippedUserBillingAccountSync(account, source, targetStatus));
continue;
}
await syncUserBillingAccount(tx, account, source);
synced.push({ logicalId: account.logicalId, targetKind: account.target.kind, mutation: true, userId: account.userId, keyId: account.target.keyId, keyPrefix: source.serviceKeyPrefix, fingerprint: source.fingerprint, valuesRedacted: true });
}
@@ -296,6 +301,45 @@ VALUES (${account.target.keyId ?? ""}, ${account.userId}, ${account.target.keyNa
ON CONFLICT (id) DO UPDATE SET user_id = EXCLUDED.user_id, name = EXCLUDED.name, key_prefix = EXCLUDED.key_prefix, key_hash = EXCLUDED.key_hash, scopes_json = EXCLUDED.scopes_json, status = 'active', revoked_at = NULL`;
}
function syncKubernetesSecretIfNeeded(target: Target, account: Account, source: SourceMaterial, options: Options): Record<string, unknown> {
const targetStatus = runtimeSecretStatus(target, account, source, options);
if (targetStatus.matchesSourceFingerprint === true) return skippedKubernetesSecretSync(target, account, source, targetStatus);
return syncKubernetesSecret(target, account, source, options);
}
function skippedKubernetesSecretSync(target: Target, account: Account, source: SourceMaterial, targetStatus: Record<string, unknown>): Record<string, unknown> {
const namespace = account.target.namespace ?? target.namespace;
const secretName = account.target.secretName ?? "";
return {
logicalId: account.logicalId,
targetKind: account.target.kind,
namespace,
secretName,
targetKey: account.target.targetKey,
rolloutDeployment: account.target.rolloutDeployment,
mutation: false,
skipped: "already-matches-source",
target: compactTargetStatus(targetStatus),
fingerprint: source.fingerprint,
valuesRedacted: true,
};
}
function skippedUserBillingAccountSync(account: Account, source: SourceMaterial, targetStatus: Record<string, unknown>): Record<string, unknown> {
return {
logicalId: account.logicalId,
targetKind: account.target.kind,
mutation: false,
skipped: "already-matches-source",
userId: account.userId,
keyId: account.target.keyId,
keyPrefix: source.serviceKeyPrefix,
fingerprint: source.fingerprint,
target: compactTargetStatus(targetStatus),
valuesRedacted: true,
};
}
function syncKubernetesSecret(target: Target, account: Account, source: SourceMaterial, options: Options): Record<string, unknown> {
const namespace = account.target.namespace ?? target.namespace;
const secretName = account.target.secretName ?? "";
@@ -359,7 +403,7 @@ function runtimeSecretStatus(target: Target, account: Account, source: SourceMat
async function userBillingApiKeyStatus(sql: any, account: Account, source: SourceMaterial): Promise<Record<string, unknown>> {
if (!sql) return { checked: false, kind: account.target.kind, reason: "database_url_source_missing", valuesRedacted: true };
const rows = await sql`
SELECT u.id AS user_id, u.username, u.role, u.status AS user_status, COALESCE(a.balance_credits, 0) AS balance_credits, COALESCE(a.reserved_credits, 0) AS reserved_credits, COALESCE(a.plan_id, '') AS plan_id, k.id AS key_id, k.key_prefix, k.key_hash, k.scopes_json::text AS scopes_json, k.status AS key_status
SELECT u.id AS user_id, u.email, u.username, u.display_name, u.role, u.status AS user_status, COALESCE(a.balance_credits, 0) AS balance_credits, COALESCE(a.reserved_credits, 0) AS reserved_credits, COALESCE(a.plan_id, '') AS plan_id, k.id AS key_id, k.key_prefix, k.key_hash, k.scopes_json::text AS scopes_json, k.status AS key_status
FROM hwlab_users u
LEFT JOIN hwlab_credit_accounts a ON a.user_id = u.id
LEFT JOIN hwlab_api_keys k ON k.id = ${account.target.keyId ?? ""} AND k.user_id = u.id
@@ -371,12 +415,41 @@ LIMIT 1`;
checked: true,
kind: account.target.kind,
exists: true,
user: { id: row.user_id, username: row.username, role: row.role, status: row.user_status, planId: row.plan_id, balanceCredits: Number(row.balance_credits ?? 0), reservedCredits: Number(row.reserved_credits ?? 0) },
user: { id: row.user_id, email: row.email, username: row.username, displayName: row.display_name, role: row.role, status: row.user_status, planId: row.plan_id, balanceCredits: Number(row.balance_credits ?? 0), reservedCredits: Number(row.reserved_credits ?? 0) },
apiKey: { exists: Boolean(row.key_id), keyId: account.target.keyId, keyPrefix: row.key_prefix || null, status: row.key_status || null, scopes: parseJsonArray(row.scopes_json), matchesSourceFingerprint: source.ok && source.sha256Hex && row.key_hash ? row.key_hash === source.sha256Hex : null },
valuesRedacted: true,
};
}
function userBillingAccountMatches(account: Account, targetStatus: Record<string, unknown>): boolean {
const user = plainObject(targetStatus.user);
const apiKey = plainObject(targetStatus.apiKey);
return targetStatus.exists === true
&& user !== null
&& apiKey !== null
&& user.id === account.userId
&& (user.email ?? "") === (account.email ?? "")
&& user.username === account.username
&& user.displayName === account.displayName
&& user.role === account.role
&& user.status === account.status
&& user.planId === account.planId
&& Number(user.balanceCredits ?? 0) >= account.initialCredits
&& apiKey.exists === true
&& apiKey.keyId === account.target.keyId
&& apiKey.status === "active"
&& apiKey.matchesSourceFingerprint === true
&& stringArraysEqual(apiKey.scopes, account.target.scopes);
}
function compactTargetStatus(status: Record<string, unknown>): Record<string, unknown> {
const target: Record<string, unknown> = { valuesRedacted: true };
for (const key of ["checked", "kind", "namespace", "secretName", "targetKey", "exists", "byteCount", "keyPrefix", "fingerprint", "matchesSourceFingerprint", "user", "apiKey"]) {
if (status[key] !== undefined) target[key] = status[key];
}
return target;
}
function report(action: string, config: LoadedConfig, target: Target, options: Options, accounts: Record<string, unknown>[], database: SourceMaterial, extra: Record<string, unknown> = {}): Record<string, unknown> {
const blockers = [
...(database.ok ? [] : [database.blocker]),
@@ -513,6 +586,14 @@ function parseJsonArray(value: unknown): string[] {
}
}
function plainObject(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
function stringArraysEqual(left: unknown, right: string[]): boolean {
return Array.isArray(left) && left.length === right.length && left.every((item, index) => item === right[index]);
}
function sha256(value: string): string {
return createHash("sha256").update(value).digest("hex");
}