feat(FN-4847): add actionable denial reason to sync-status
Fusion-Task-Id: FN-4847 Fusion-Task-Lineage: af551b65-a484-496a-8bab-b46193e06104
This commit is contained in:
committed by
gsxdsm
parent
af8999f6c6
commit
ab31aaceb4
@@ -5,6 +5,43 @@ import { getAuthFileCandidates, type StoredAuthProvider } from "../auth-paths.js
|
||||
|
||||
export const MISSING_REMOTE_NODE_API_KEY_MESSAGE = "Remote node requires an apiKey for authenticated sync";
|
||||
|
||||
// FN-4847: Stable denial-reason enum surfaced by sync-status for actionable remote probe failures.
|
||||
export const SYNC_STATUS_DENIAL_REASONS = ["missing-remote-api-key", "auth-failed", "unreachable", "unknown"] as const;
|
||||
|
||||
// FN-4847: Public contract type for actionable sync-status denial diagnostics.
|
||||
export type SyncStatusDenialReason = (typeof SYNC_STATUS_DENIAL_REASONS)[number];
|
||||
|
||||
// FN-4847: Classify remote probe failures to a non-leaking enum suitable for API responses.
|
||||
export function classifySyncStatusDenialReason(err: unknown): SyncStatusDenialReason {
|
||||
if (err instanceof ApiError && err.message === MISSING_REMOTE_NODE_API_KEY_MESSAGE) {
|
||||
return "missing-remote-api-key";
|
||||
}
|
||||
|
||||
if (err instanceof ApiError && err.message === "Remote node authentication failed") {
|
||||
return "auth-failed";
|
||||
}
|
||||
|
||||
if (err instanceof ApiError && err.message === "Remote node unreachable") {
|
||||
return "unreachable";
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
const message = err.message ?? "";
|
||||
const causeCode = (err as { cause?: { code?: unknown } }).cause?.code;
|
||||
if (
|
||||
err.name === "AbortError"
|
||||
|| /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network/i.test(message)
|
||||
|| causeCode === "ECONNREFUSED"
|
||||
|| causeCode === "ENOTFOUND"
|
||||
|| causeCode === "ETIMEDOUT"
|
||||
) {
|
||||
return "unreachable";
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export async function readStoredAuthProvidersFromDisk(): Promise<Record<string, StoredAuthProvider>> {
|
||||
const merged: Record<string, StoredAuthProvider> = {};
|
||||
for (const authJsonPath of getAuthFileCandidates()) {
|
||||
|
||||
@@ -3,10 +3,12 @@ import { basename } from "node:path";
|
||||
import { ApiError, badRequest, notFound } from "../api-error.js";
|
||||
import { getFusionAuthPath } from "../auth-paths.js";
|
||||
import {
|
||||
classifySyncStatusDenialReason,
|
||||
fetchFromRemoteNode,
|
||||
MISSING_REMOTE_NODE_API_KEY_MESSAGE,
|
||||
readStoredAuthProvidersFromDisk,
|
||||
toProviderAuthEntries,
|
||||
type SyncStatusDenialReason,
|
||||
} from "./register-settings-sync-helpers.js";
|
||||
import type { ApiRouteRegistrar } from "./types.js";
|
||||
|
||||
@@ -294,6 +296,7 @@ export const registerSettingsSyncRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
let remoteSettings: { global: Record<string, unknown>; project: Record<string, unknown> } | null = null;
|
||||
let diffGlobal: string[] = [];
|
||||
let diffProject: string[] = [];
|
||||
let denialReason: SyncStatusDenialReason | null = null; // FN-4847: stable, non-leaking denial classification for degraded probes.
|
||||
|
||||
try {
|
||||
remoteSettings = await fetchFromRemoteNode(node, "/api/settings/scopes") as {
|
||||
@@ -311,8 +314,9 @@ export const registerSettingsSyncRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
);
|
||||
diffGlobal = diff.global;
|
||||
diffProject = diff.project;
|
||||
} catch {
|
||||
// Remote unreachable - diff will be empty arrays
|
||||
} catch (err) {
|
||||
// FN-4847: Remote probe failures are classified into actionable, enum-only denial reasons.
|
||||
denialReason = classifySyncStatusDenialReason(err);
|
||||
}
|
||||
|
||||
await central.close();
|
||||
@@ -322,6 +326,7 @@ export const registerSettingsSyncRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
lastSyncDirection: syncState ? "sync" : null, // Direction not tracked in new schema
|
||||
localUpdatedAt: syncState?.updatedAt ?? new Date().toISOString(),
|
||||
remoteReachable,
|
||||
actionableDenialReason: denialReason, // FN-4847: explicit null on success, enum value on degraded failures.
|
||||
diff: { global: diffGlobal, project: diffProject },
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
|
||||
Reference in New Issue
Block a user