FN-7647: route dashboard sync/mesh AuthStorage writes through createFusionAuthStorage
Route node settings-sync and mesh credential writes through the coordinated @fusion/engine auth store to prevent concurrent clobbers of ~/.fusion/agent/auth.json. - register-settings-sync-routes.ts, register-settings-sync-inbound-routes.ts, and register-mesh-routes.ts now persist received credentials via createFusionAuthStorage() instead of raw AuthStorage.create(getFusionAuthPath()) - Shares FN-7646's reload-before-persist + per-provider locked-merge path, avoiding lost writes from concurrent Fusion processes - Uses a static top-level import of @fusion/engine (not dynamic import) per FN-3049's bundler-safety rule - Adds route-level regression coverage in mesh-routes.test.ts, routes-nodes-sync-contract.test.ts, and routes-nodes-sync.test.ts - Adds a patch changeset for @runfusion/fusion Files changed: .../fn-7647-auth-storage-routes-coordination.md | 7 ++ .../dashboard/src/__tests__/mesh-routes.test.ts | 118 +++++++++++++++++++++ .../__tests__/routes-nodes-sync-contract.test.ts | 27 ++++- .../src/__tests__/routes-nodes-sync.test.ts | 110 ++++++++++++++++++- .../dashboard/src/routes/register-mesh-routes.ts | 14 ++- .../register-settings-sync-inbound-routes.ts | 15 ++- .../src/routes/register-settings-sync-routes.ts | 15 ++- 7 files changed, 290 insertions(+), 16 deletions(-) Fusion-Task-Id: FN-7647 Fusion-Task-Lineage: cfab9679-0baa-4eca-8c7a-23db017aa54f Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7647-auth-storage-routes-coordination.md
Normal file
7
.changeset/fn-7647-auth-storage-routes-coordination.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Route node settings-sync and mesh credential writes through the coordinated auth store to prevent concurrent clobbers.
|
||||
category: fix
|
||||
dev: register-settings-sync-routes.ts, register-settings-sync-inbound-routes.ts, and register-mesh-routes.ts now persist received credentials via @fusion/engine createFusionAuthStorage() instead of raw AuthStorage.create(getFusionAuthPath()), sharing FN-7646's reload-before-persist + per-provider locked-merge path over ~/.fusion/agent/auth.json. Adds route-level regression coverage.
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { Task, TaskStore } from "@fusion/core";
|
||||
import { createAuthMaterialSnapshot } from "@fusion/core";
|
||||
import { request } from "../test-request.js";
|
||||
import { createServer } from "../server.js";
|
||||
import type { RuntimeLogger } from "../runtime-logger.js";
|
||||
@@ -31,6 +32,7 @@ const mockCommitDistributedTaskIdReservation = vi.fn();
|
||||
const mockAbortDistributedTaskIdReservation = vi.fn();
|
||||
const mockGetDistributedTaskIdState = vi.fn();
|
||||
const mockApplyReplicatedTaskCreate = vi.fn();
|
||||
const mockApplyAuthMaterialSnapshot = vi.fn();
|
||||
|
||||
// Mock GlobalSettingsStore
|
||||
const mockGetSettings = vi.fn().mockResolvedValue({});
|
||||
@@ -55,10 +57,38 @@ vi.mock("@fusion/core", async () => {
|
||||
getLocalMeshSnapshot: mockGetLocalMeshSnapshot,
|
||||
getSettingsForSync: mockGetSettingsForSync,
|
||||
applyRemoteSettings: mockApplyRemoteSettings,
|
||||
applyAuthMaterialSnapshot: mockApplyAuthMaterialSnapshot,
|
||||
}; }),
|
||||
};
|
||||
});
|
||||
|
||||
// FNXC:ProviderAuth 2026-07-07-00:00: FN-7647 routed register-mesh-routes.ts's inline
|
||||
// auth-material shared-state domain through @fusion/engine's createFusionAuthStorage() instead of
|
||||
// a raw AuthStorage.create(getFusionAuthPath()). Mock the factory so mesh sync auth-material writes
|
||||
// are observable and never touch the real ~/.fusion/agent/auth.json during tests; preserve every
|
||||
// other real @fusion/engine export other routers rely on at module load time. vi.mock factories are
|
||||
// hoisted above top-level const declarations, so the referenced mocks must be created via
|
||||
// vi.hoisted to avoid a temporal-dead-zone ReferenceError.
|
||||
const { mockMeshAuthStorageSet, mockCreateFusionAuthStorage } = vi.hoisted(() => {
|
||||
const mockMeshAuthStorageSet = vi.fn().mockResolvedValue(undefined);
|
||||
const mockCreateFusionAuthStorage = vi.fn(() => ({
|
||||
set: mockMeshAuthStorageSet,
|
||||
get: vi.fn(),
|
||||
getApiKey: vi.fn(),
|
||||
getOAuthProviders: vi.fn().mockReturnValue([]),
|
||||
reload: vi.fn(),
|
||||
}));
|
||||
return { mockMeshAuthStorageSet, mockCreateFusionAuthStorage };
|
||||
});
|
||||
|
||||
vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@fusion/engine")>();
|
||||
return {
|
||||
...actual,
|
||||
createFusionAuthStorage: mockCreateFusionAuthStorage,
|
||||
};
|
||||
});
|
||||
|
||||
class MockStore extends EventEmitter {
|
||||
getRootDir(): string {
|
||||
return "/tmp/fn-1224";
|
||||
@@ -703,6 +733,94 @@ describe("POST /api/mesh/sync", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── FN-7647 Symptom Verification: auth-material shared-state sync ─────────────
|
||||
// Original symptom (FN-7646 class): a raw independent AuthStorage instance can persist a stale
|
||||
// in-memory snapshot over ~/.fusion/agent/auth.json, wiping a key another process just saved.
|
||||
// Asserts the mesh sync auth-material domain writes via the coordinated createFusionAuthStorage()
|
||||
// proxy (not a raw instance) and that an unrelated provider's saved credential survives the write.
|
||||
describe("auth-material shared-state sync", () => {
|
||||
beforeEach(() => {
|
||||
mockApplyAuthMaterialSnapshot.mockReset();
|
||||
mockCreateFusionAuthStorage.mockClear();
|
||||
mockMeshAuthStorageSet.mockReset().mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("writes received auth-material credentials via the coordinated createFusionAuthStorage() proxy", async () => {
|
||||
mockApplyAuthMaterialSnapshot.mockReturnValue({
|
||||
success: true,
|
||||
authCount: 1,
|
||||
providerAuth: { anthropic: { type: "api_key", key: "sk-ant-mesh-received" } },
|
||||
});
|
||||
|
||||
const authMaterial = createAuthMaterialSnapshot({
|
||||
anthropic: { type: "api_key", key: "sk-ant-mesh-received" },
|
||||
});
|
||||
|
||||
const response = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/mesh/sync",
|
||||
JSON.stringify({
|
||||
senderNodeId: "node_remote",
|
||||
senderNodeUrl: "https://remote.example.com",
|
||||
knownPeers: [],
|
||||
timestamp: "2026-04-01T12:00:00.000Z",
|
||||
sharedState: { authMaterial },
|
||||
}),
|
||||
{ "Content-Type": "application/json" }
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockApplyAuthMaterialSnapshot).toHaveBeenCalledWith(authMaterial);
|
||||
// Proves the write path is the coordinated proxy, not a raw independent AuthStorage instance.
|
||||
expect(mockCreateFusionAuthStorage).toHaveBeenCalled();
|
||||
expect(mockMeshAuthStorageSet).toHaveBeenCalledWith("anthropic", { type: "api_key", key: "sk-ant-mesh-received" });
|
||||
});
|
||||
|
||||
it("concurrent-writer survival: an unrelated provider's saved key is never clobbered by the mesh sync write", async () => {
|
||||
mockApplyAuthMaterialSnapshot.mockReturnValue({
|
||||
success: true,
|
||||
authCount: 1,
|
||||
providerAuth: { anthropic: { type: "api_key", key: "sk-ant-mesh-received" } },
|
||||
});
|
||||
|
||||
// Simulate another Fusion instance's provider credential already persisted on disk by having
|
||||
// the mocked coordinated proxy merge per-provider (as the real reload-before-persist proxy
|
||||
// does) rather than overwrite the whole file.
|
||||
const diskState: Record<string, unknown> = {
|
||||
openai: { type: "api_key", key: "sk-openai-from-other-instance" },
|
||||
};
|
||||
mockMeshAuthStorageSet.mockImplementation(async (providerId: string, credential: unknown) => {
|
||||
diskState[providerId] = credential;
|
||||
});
|
||||
|
||||
const authMaterial = createAuthMaterialSnapshot({
|
||||
anthropic: { type: "api_key", key: "sk-ant-mesh-received" },
|
||||
});
|
||||
|
||||
const response = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/mesh/sync",
|
||||
JSON.stringify({
|
||||
senderNodeId: "node_remote",
|
||||
senderNodeUrl: "https://remote.example.com",
|
||||
knownPeers: [],
|
||||
timestamp: "2026-04-01T12:00:00.000Z",
|
||||
sharedState: { authMaterial },
|
||||
}),
|
||||
{ "Content-Type": "application/json" }
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
// The unrelated provider saved by another instance before this handler ran must still be present.
|
||||
expect(diskState.openai).toEqual({ type: "api_key", key: "sk-openai-from-other-instance" });
|
||||
// And the received provider was merged in alongside it — no full-snapshot clobber.
|
||||
expect(diskState.anthropic).toEqual({ type: "api_key", key: "sk-ant-mesh-received" });
|
||||
expect(mockMeshAuthStorageSet).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("/api/mesh/task-ids routes", () => {
|
||||
|
||||
@@ -62,8 +62,23 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const mockAuthStorageSet = vi.fn();
|
||||
const mockAuthStorageGetOAuthProviders = vi.fn().mockReturnValue([]);
|
||||
// FNXC:ProviderAuth 2026-07-07-00:00: FN-7647 routed register-settings-sync-routes.ts and
|
||||
// register-settings-sync-inbound-routes.ts through @fusion/engine's createFusionAuthStorage()
|
||||
// instead of a raw AuthStorage.create(getFusionAuthPath()). Mock the factory (not just the raw
|
||||
// pi-coding-agent AuthStorage) so this contract matrix never falls through to the real proxy and
|
||||
// touches the developer's actual ~/.fusion/agent/auth.json during the auth-sync/auth-receive cases.
|
||||
const { mockAuthStorageSet, mockAuthStorageGetOAuthProviders, mockCreateFusionAuthStorage } = vi.hoisted(() => {
|
||||
const mockAuthStorageSet = vi.fn();
|
||||
const mockAuthStorageGetOAuthProviders = vi.fn().mockReturnValue([]);
|
||||
const mockCreateFusionAuthStorage = vi.fn(() => ({
|
||||
set: mockAuthStorageSet,
|
||||
get: vi.fn(),
|
||||
getApiKey: vi.fn(),
|
||||
getOAuthProviders: mockAuthStorageGetOAuthProviders,
|
||||
reload: vi.fn(),
|
||||
}));
|
||||
return { mockAuthStorageSet, mockAuthStorageGetOAuthProviders, mockCreateFusionAuthStorage };
|
||||
});
|
||||
|
||||
vi.mock("@earendil-works/pi-coding-agent", () => {
|
||||
return {
|
||||
@@ -79,6 +94,14 @@ vi.mock("@earendil-works/pi-coding-agent", () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@fusion/engine")>();
|
||||
return {
|
||||
...actual,
|
||||
createFusionAuthStorage: mockCreateFusionAuthStorage,
|
||||
};
|
||||
});
|
||||
|
||||
class MockStore extends EventEmitter {
|
||||
getRootDir(): string {
|
||||
return "/tmp/fn-4755-test";
|
||||
|
||||
@@ -71,9 +71,24 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
||||
});
|
||||
|
||||
// ── Mock AuthStorage ───────────────────────────────────────────────────
|
||||
// FNXC:ProviderAuth 2026-07-07-00:00: FN-7647 routed register-settings-sync-routes.ts and
|
||||
// register-settings-sync-inbound-routes.ts through @fusion/engine's createFusionAuthStorage()
|
||||
// instead of a raw AuthStorage.create(getFusionAuthPath()). Mock the factory (not the raw
|
||||
// pi-coding-agent AuthStorage) so mockAuthStorageSet still observes the coordinated write path,
|
||||
// and preserve every other real @fusion/engine export other routers rely on at module load time.
|
||||
|
||||
const mockAuthStorageSet = vi.fn();
|
||||
const mockAuthStorageGetOAuthProviders = vi.fn().mockReturnValue([]);
|
||||
const { mockAuthStorageSet, mockAuthStorageGetOAuthProviders, mockCreateFusionAuthStorage } = vi.hoisted(() => {
|
||||
const mockAuthStorageSet = vi.fn();
|
||||
const mockAuthStorageGetOAuthProviders = vi.fn().mockReturnValue([]);
|
||||
const mockCreateFusionAuthStorage = vi.fn(() => ({
|
||||
set: mockAuthStorageSet,
|
||||
get: vi.fn(),
|
||||
getApiKey: vi.fn(),
|
||||
getOAuthProviders: mockAuthStorageGetOAuthProviders,
|
||||
reload: vi.fn(),
|
||||
}));
|
||||
return { mockAuthStorageSet, mockAuthStorageGetOAuthProviders, mockCreateFusionAuthStorage };
|
||||
});
|
||||
|
||||
vi.mock("@earendil-works/pi-coding-agent", () => {
|
||||
return {
|
||||
@@ -89,6 +104,14 @@ vi.mock("@earendil-works/pi-coding-agent", () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@fusion/engine")>();
|
||||
return {
|
||||
...actual,
|
||||
createFusionAuthStorage: mockCreateFusionAuthStorage,
|
||||
};
|
||||
});
|
||||
|
||||
// ── Mock Store ────────────────────────────────────────────────────────
|
||||
|
||||
class MockStore extends EventEmitter {
|
||||
@@ -1072,6 +1095,11 @@ describe("Node settings sync routes", () => {
|
||||
lastSyncedAt: expect.any(String),
|
||||
}),
|
||||
);
|
||||
// FN-7647: pull-mode credential writes go through the coordinated createFusionAuthStorage()
|
||||
// proxy, not a raw independent AuthStorage instance. (applyAuthMaterialSnapshot resolves the
|
||||
// default beforeEach mock here — the anthropic credential — since this test doesn't override it.)
|
||||
expect(mockCreateFusionAuthStorage).toHaveBeenCalled();
|
||||
expect(mockAuthStorageSet).toHaveBeenCalledWith("anthropic", { type: "api_key", key: "sk-ant-received" });
|
||||
});
|
||||
|
||||
it("emits structured redacted diagnostics for pull-mode auth sync", async () => {
|
||||
@@ -1420,6 +1448,82 @@ describe("Node settings sync routes", () => {
|
||||
expect(res.body.receivedProviders).toContain("anthropic");
|
||||
});
|
||||
|
||||
// ── FN-7647 Symptom Verification ─────────────────────────────────
|
||||
// Original symptom (FN-7646 class): a raw independent AuthStorage instance can persist a
|
||||
// stale in-memory snapshot over ~/.fusion/agent/auth.json, wiping a key another process just
|
||||
// saved. This asserts the inbound handler writes through the coordinated
|
||||
// createFusionAuthStorage() proxy (not a raw instance) and that per-provider writes never
|
||||
// clobber an unrelated provider's credential already present on disk.
|
||||
it("FN-7647: persists received credentials via the coordinated createFusionAuthStorage() proxy", async () => {
|
||||
const localNode = createMockLocalNode();
|
||||
mockListNodes.mockResolvedValue([localNode]);
|
||||
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/settings/auth-receive",
|
||||
JSON.stringify({
|
||||
authMaterial: {
|
||||
version: 1,
|
||||
exportedAt: "2026-04-14T10:00:00.000Z",
|
||||
checksum: "auth-checksum",
|
||||
payload: { providerAuth: { anthropic: { type: "api_key", key: "sk-ant-received" } } },
|
||||
},
|
||||
sourceNodeId: "node-remote-001",
|
||||
timestamp: "2026-04-14T10:00:00.000Z",
|
||||
}),
|
||||
{ "content-type": "application/json", "Authorization": `Bearer ${localNode.apiKey}` },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
// Proves the write path is the coordinated proxy, not a raw independent AuthStorage instance.
|
||||
expect(mockCreateFusionAuthStorage).toHaveBeenCalled();
|
||||
expect(mockAuthStorageSet).toHaveBeenCalledWith("anthropic", { type: "api_key", key: "sk-ant-received" });
|
||||
});
|
||||
|
||||
it("FN-7647: concurrent-writer survival — an unrelated provider's saved key is never clobbered by this handler's write", async () => {
|
||||
const localNode = createMockLocalNode();
|
||||
mockListNodes.mockResolvedValue([localNode]);
|
||||
|
||||
// Simulate another Fusion instance's provider credential already persisted on disk by having
|
||||
// the mocked coordinated proxy merge per-provider (as the real reload-before-persist proxy does)
|
||||
// rather than overwrite the whole file. `openai` was saved by a concurrent instance and must
|
||||
// still be observable after this handler's write — this handler only ever calls .set() for the
|
||||
// providers it received (anthropic), never a whole-snapshot overwrite that would drop `openai`.
|
||||
const diskState: Record<string, unknown> = {
|
||||
openai: { type: "api_key", key: "sk-openai-from-other-instance" },
|
||||
};
|
||||
mockAuthStorageSet.mockImplementation(async (providerId: string, credential: unknown) => {
|
||||
diskState[providerId] = credential;
|
||||
});
|
||||
|
||||
const res = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/settings/auth-receive",
|
||||
JSON.stringify({
|
||||
authMaterial: {
|
||||
version: 1,
|
||||
exportedAt: "2026-04-14T10:00:00.000Z",
|
||||
checksum: "auth-checksum",
|
||||
payload: { providerAuth: { anthropic: { type: "api_key", key: "sk-ant-received" } } },
|
||||
},
|
||||
sourceNodeId: "node-remote-001",
|
||||
timestamp: "2026-04-14T10:00:00.000Z",
|
||||
}),
|
||||
{ "content-type": "application/json", "Authorization": `Bearer ${localNode.apiKey}` },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
// The unrelated provider saved by another instance before this handler ran must still be present.
|
||||
expect(diskState.openai).toEqual({ type: "api_key", key: "sk-openai-from-other-instance" });
|
||||
// And the received provider was merged in alongside it — no full-snapshot clobber.
|
||||
expect(diskState.anthropic).toEqual({ type: "api_key", key: "sk-ant-received" });
|
||||
// .set() was only invoked for the received provider — never a whole-file overwrite call.
|
||||
expect(mockAuthStorageSet).toHaveBeenCalledTimes(1);
|
||||
expect(mockAuthStorageSet).toHaveBeenCalledWith("anthropic", { type: "api_key", key: "sk-ant-received" });
|
||||
});
|
||||
|
||||
it("returns 401 when auth header is missing", async () => {
|
||||
const res = await request(
|
||||
app,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createFusionAuthStorage } from "@fusion/engine";
|
||||
import { ApiError, badRequest } from "../api-error.js";
|
||||
import type { ApiRouteRegistrar } from "./types.js";
|
||||
import { fetchFromRemoteNode } from "./register-settings-sync-helpers.js";
|
||||
@@ -531,9 +532,16 @@ export const registerMeshRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
if (!sharedState.authMaterial) return;
|
||||
validateSnapshotEnvelope(sharedState.authMaterial);
|
||||
const applied = central.applyAuthMaterialSnapshot(sharedState.authMaterial as Parameters<typeof central.applyAuthMaterialSnapshot>[0]);
|
||||
const { AuthStorage } = await import("@earendil-works/pi-coding-agent");
|
||||
const { getFusionAuthPath } = await import("../auth-paths.js");
|
||||
const authStorage = AuthStorage.create(getFusionAuthPath());
|
||||
/*
|
||||
* FNXC:ProviderAuth 2026-07-07-00:00:
|
||||
* Dashboard sync/mesh credential writes must go through the coordinated createFusionAuthStorage()
|
||||
* proxy (reload-before-persist, supplemental-credential sync, logout suppression, Anthropic aliasing)
|
||||
* instead of a raw AuthStorage.create(getFusionAuthPath()) instance, so concurrent Fusion processes
|
||||
* sharing ~/.fusion/agent/auth.json do not clobber each other's saved provider keys. FN-7647,
|
||||
* follow-up to FN-7646's engine-side hardening. Uses a static top-level import (not dynamic
|
||||
* import) per FN-3049's bundler-safety rule against dynamic engine imports (`await` + `import(...)` of the engine package).
|
||||
*/
|
||||
const authStorage = createFusionAuthStorage();
|
||||
for (const [providerId, credential] of Object.entries(applied.providerAuth)) {
|
||||
if (credential.type === "api_key" && credential.key) {
|
||||
authStorage.set(providerId, { type: "api_key", key: credential.key });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isMovedSettingsKey } from "@fusion/core";
|
||||
import { createFusionAuthStorage } from "@fusion/engine";
|
||||
import { ApiError, badRequest } from "../api-error.js";
|
||||
import { invalidateAllGlobalSettingsCaches } from "../project-store-resolver.js";
|
||||
import { getFusionAuthPath } from "../auth-paths.js";
|
||||
import { readStoredAuthProvidersFromDisk, toProviderAuthEntries } from "./register-settings-sync-helpers.js";
|
||||
import type { ApiRouteRegistrar } from "./types.js";
|
||||
|
||||
@@ -228,9 +228,16 @@ export const registerSettingsSyncInboundRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
throw badRequest("Missing required field: timestamp");
|
||||
}
|
||||
|
||||
// Import AuthStorage and write credentials
|
||||
const { AuthStorage } = await import("@earendil-works/pi-coding-agent");
|
||||
const authStorage = AuthStorage.create(getFusionAuthPath());
|
||||
/*
|
||||
* FNXC:ProviderAuth 2026-07-07-00:00:
|
||||
* Dashboard sync/mesh credential writes must go through the coordinated createFusionAuthStorage()
|
||||
* proxy (reload-before-persist, supplemental-credential sync, logout suppression, Anthropic aliasing)
|
||||
* instead of a raw AuthStorage.create(getFusionAuthPath()) instance, so concurrent Fusion processes
|
||||
* sharing ~/.fusion/agent/auth.json do not clobber each other's saved provider keys. FN-7647,
|
||||
* follow-up to FN-7646's engine-side hardening. Uses a static top-level import (not dynamic
|
||||
* import) per FN-3049's bundler-safety rule against dynamic engine imports (`await` + `import(...)` of the engine package).
|
||||
*/
|
||||
const authStorage = createFusionAuthStorage();
|
||||
|
||||
const applyResult = central.applyAuthMaterialSnapshot(authMaterial);
|
||||
const receivedProviders: string[] = [];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ProjectSettings } from "@fusion/core";
|
||||
import { isMovedSettingsKey } from "@fusion/core";
|
||||
import { createFusionAuthStorage } from "@fusion/engine";
|
||||
import { basename } from "node:path";
|
||||
import { ApiError, badRequest, notFound } from "../api-error.js";
|
||||
import { getFusionAuthPath } from "../auth-paths.js";
|
||||
import { invalidateAllGlobalSettingsCaches } from "../project-store-resolver.js";
|
||||
import {
|
||||
classifySyncStatusDenialReason,
|
||||
@@ -501,9 +501,16 @@ export const registerSettingsSyncRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
const localPeerInfo = await central.getLocalPeerInfo();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Import AuthStorage
|
||||
const { AuthStorage } = await import("@earendil-works/pi-coding-agent");
|
||||
const authStorage = AuthStorage.create(getFusionAuthPath());
|
||||
/*
|
||||
* FNXC:ProviderAuth 2026-07-07-00:00:
|
||||
* Dashboard sync/mesh credential writes must go through the coordinated createFusionAuthStorage()
|
||||
* proxy (reload-before-persist, supplemental-credential sync, logout suppression, Anthropic aliasing)
|
||||
* instead of a raw AuthStorage.create(getFusionAuthPath()) instance, so concurrent Fusion processes
|
||||
* sharing ~/.fusion/agent/auth.json do not clobber each other's saved provider keys. FN-7647,
|
||||
* follow-up to FN-7646's engine-side hardening. Uses a static top-level import (not dynamic
|
||||
* import) per FN-3049's bundler-safety rule against dynamic engine imports (`await` + `import(...)` of the engine package).
|
||||
*/
|
||||
const authStorage = createFusionAuthStorage();
|
||||
|
||||
if (direction === "push") {
|
||||
const allProviders = await readStoredAuthProvidersFromDisk();
|
||||
|
||||
Reference in New Issue
Block a user