Fix OpenClaw auth propagation and debug visibility
This commit is contained in:
@@ -14,13 +14,14 @@ describe("buildJoinDefaultsPayloadForAccept", () => {
|
||||
expect(result).toMatchObject({
|
||||
url: "http://localhost:18789/v1/responses",
|
||||
paperclipApiUrl: "http://host.docker.internal:3100",
|
||||
webhookAuthHeader: "Bearer gateway-token",
|
||||
headers: {
|
||||
"x-openclaw-auth": "gateway-token",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not overwrite explicit OpenClaw defaults when already provided", () => {
|
||||
it("does not overwrite explicit OpenClaw endpoint defaults when already provided", () => {
|
||||
const result = buildJoinDefaultsPayloadForAccept({
|
||||
adapterType: "openclaw",
|
||||
defaultsPayload: {
|
||||
@@ -41,6 +42,28 @@ describe("buildJoinDefaultsPayloadForAccept", () => {
|
||||
url: "https://example.com/v1/responses",
|
||||
method: "POST",
|
||||
paperclipApiUrl: "https://paperclip.example.com",
|
||||
webhookAuthHeader: "Bearer existing-token",
|
||||
headers: {
|
||||
"x-openclaw-auth": "existing-token",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves explicit webhookAuthHeader when configured", () => {
|
||||
const result = buildJoinDefaultsPayloadForAccept({
|
||||
adapterType: "openclaw",
|
||||
defaultsPayload: {
|
||||
url: "https://example.com/v1/responses",
|
||||
webhookAuthHeader: "Bearer explicit-token",
|
||||
headers: {
|
||||
"x-openclaw-auth": "existing-token",
|
||||
},
|
||||
},
|
||||
inboundOpenClawAuthHeader: "legacy-token",
|
||||
}) as Record<string, unknown>;
|
||||
|
||||
expect(result).toMatchObject({
|
||||
webhookAuthHeader: "Bearer explicit-token",
|
||||
headers: {
|
||||
"x-openclaw-auth": "existing-token",
|
||||
},
|
||||
|
||||
@@ -223,6 +223,64 @@ describe("openclaw adapter execute", () => {
|
||||
expect(String(body.text ?? "")).toContain("PAPERCLIP_API_URL=http://dotta-macbook-pro:3100/");
|
||||
});
|
||||
|
||||
it("logs outbound header keys for auth debugging", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
sseResponse([
|
||||
"event: response.completed\n",
|
||||
'data: {"type":"response.completed","status":"completed"}\n\n',
|
||||
]),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const logs: string[] = [];
|
||||
const result = await execute(
|
||||
buildContext(
|
||||
{
|
||||
url: "https://agent.example/sse",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-openclaw-auth": "gateway-token",
|
||||
},
|
||||
},
|
||||
{
|
||||
onLog: async (_stream, chunk) => {
|
||||
logs.push(chunk);
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(
|
||||
logs.some((line) => line.includes("[openclaw] outbound header keys:") && line.includes("x-openclaw-auth")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("derives Authorization header from x-openclaw-auth when webhookAuthHeader is unset", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
sseResponse([
|
||||
"event: response.completed\n",
|
||||
'data: {"type":"response.completed","status":"completed"}\n\n',
|
||||
]),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const result = await execute(
|
||||
buildContext({
|
||||
url: "https://agent.example/sse",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-openclaw-auth": "gateway-token",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
const headers = (fetchMock.mock.calls[0]?.[1]?.headers ?? {}) as Record<string, string>;
|
||||
expect(headers["x-openclaw-auth"]).toBe("gateway-token");
|
||||
expect(headers.authorization).toBe("Bearer gateway-token");
|
||||
});
|
||||
|
||||
it("derives issue session keys when configured", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
sseResponse([
|
||||
|
||||
@@ -152,6 +152,20 @@ function headerMapHasKeyIgnoreCase(headers: Record<string, string>, targetKey: s
|
||||
return Object.keys(headers).some((key) => key.trim().toLowerCase() === normalizedTarget);
|
||||
}
|
||||
|
||||
function headerMapGetIgnoreCase(headers: Record<string, string>, targetKey: string): string | null {
|
||||
const normalizedTarget = targetKey.trim().toLowerCase();
|
||||
const key = Object.keys(headers).find((candidate) => candidate.trim().toLowerCase() === normalizedTarget);
|
||||
if (!key) return null;
|
||||
const value = headers[key];
|
||||
return typeof value === "string" ? value : null;
|
||||
}
|
||||
|
||||
function toAuthorizationHeaderValue(rawToken: string): string {
|
||||
const trimmed = rawToken.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
return /^bearer\s+/i.test(trimmed) ? trimmed : `Bearer ${trimmed}`;
|
||||
}
|
||||
|
||||
export function buildJoinDefaultsPayloadForAccept(input: {
|
||||
adapterType: string | null;
|
||||
defaultsPayload: unknown;
|
||||
@@ -211,6 +225,15 @@ export function buildJoinDefaultsPayloadForAccept(input: {
|
||||
delete merged.headers;
|
||||
}
|
||||
|
||||
const hasAuthorizationHeader = headerMapHasKeyIgnoreCase(mergedHeaders, "authorization");
|
||||
const hasWebhookAuthHeader = Boolean(nonEmptyTrimmedString(merged.webhookAuthHeader));
|
||||
if (!hasAuthorizationHeader && !hasWebhookAuthHeader) {
|
||||
const openClawAuthToken = headerMapGetIgnoreCase(mergedHeaders, "x-openclaw-auth");
|
||||
if (openClawAuthToken) {
|
||||
merged.webhookAuthHeader = toAuthorizationHeaderValue(openClawAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(merged).length > 0 ? merged : null;
|
||||
}
|
||||
|
||||
@@ -1395,6 +1418,54 @@ export function accessRoutes(
|
||||
return row;
|
||||
});
|
||||
|
||||
if (requestType === "agent" && (req.body.adapterType ?? null) === "openclaw") {
|
||||
const expectedDefaults = summarizeOpenClawDefaultsForLog(joinDefaults.normalized);
|
||||
const persistedDefaults = summarizeOpenClawDefaultsForLog(created.agentDefaultsPayload);
|
||||
const missingPersistedFields: string[] = [];
|
||||
|
||||
if (expectedDefaults.url && !persistedDefaults.url) missingPersistedFields.push("url");
|
||||
if (expectedDefaults.paperclipApiUrl && !persistedDefaults.paperclipApiUrl) {
|
||||
missingPersistedFields.push("paperclipApiUrl");
|
||||
}
|
||||
if (expectedDefaults.webhookAuthHeader && !persistedDefaults.webhookAuthHeader) {
|
||||
missingPersistedFields.push("webhookAuthHeader");
|
||||
}
|
||||
if (expectedDefaults.openClawAuthHeader && !persistedDefaults.openClawAuthHeader) {
|
||||
missingPersistedFields.push("headers.x-openclaw-auth");
|
||||
}
|
||||
if (expectedDefaults.headerKeys.length > 0 && persistedDefaults.headerKeys.length === 0) {
|
||||
missingPersistedFields.push("headers");
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{
|
||||
inviteId: invite.id,
|
||||
joinRequestId: created.id,
|
||||
joinRequestStatus: created.status,
|
||||
expectedDefaults,
|
||||
persistedDefaults,
|
||||
diagnostics: joinDefaults.diagnostics.map((diag) => ({
|
||||
code: diag.code,
|
||||
level: diag.level,
|
||||
message: diag.message,
|
||||
hint: diag.hint ?? null,
|
||||
})),
|
||||
},
|
||||
"invite accept persisted OpenClaw join request",
|
||||
);
|
||||
|
||||
if (missingPersistedFields.length > 0) {
|
||||
logger.warn(
|
||||
{
|
||||
inviteId: invite.id,
|
||||
joinRequestId: created.id,
|
||||
missingPersistedFields,
|
||||
},
|
||||
"invite accept detected missing persisted OpenClaw defaults",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: req.actor.type === "agent" ? "agent" : "user",
|
||||
|
||||
Reference in New Issue
Block a user