fix: allow vault info under path scope and clarify API errors
Build and Push Docker Container / build-and-push (push) Successful in 4m29s

- Allow vault and vaults commands when a token has path restrictions

- Treat vault/vaults as pathless metadata commands in policy checks

- Add allowed commands, vaults and paths to 403 permission errors

- Surface real JWT failure reason in 401 instead of generic Unauthorized

- Report missing, malformed and empty Bearer tokens distinctly

- Catch httpRequest failures in the n8n node to avoid circular JSON

- Map API errors to clean NodeApiError with status code and detail

- Support continueOnFail with per-item error output in the n8n node

- Push a versioned image tag from pyproject in the CI workflow

- Bump server to 1.0.2 and n8n node package to 0.2.1

- Add tests for vault info access and richer auth error messages
This commit is contained in:
2026-06-24 11:41:18 +02:00
parent be78e3aa00
commit 3bda4002fe
9 changed files with 184 additions and 53 deletions
@@ -3,7 +3,7 @@ import type {
IDataObject,
IHttpRequestOptions,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
export interface ObsidianCommandResult extends IDataObject {
id: string;
@@ -64,7 +64,12 @@ export async function executeObsidianCommand(
returnFullResponse: false,
};
const response = await context.helpers.httpRequest(request) as unknown;
let response: unknown;
try {
response = await context.helpers.httpRequest(request) as unknown;
} catch (error) {
throw toCleanApiError(context, error, itemIndex);
}
const result = normalizeCommandResult(response);
if (result.exit_code !== 0) {
@@ -78,6 +83,62 @@ export async function executeObsidianCommand(
return result;
}
function toCleanApiError(
context: IExecuteFunctions,
error: unknown,
itemIndex: number,
): Error {
const err = isObject(error) ? error : {};
const statusCode = Number(
err.statusCode ?? err.status ?? (isObject(err.response) ? err.response.statusCode : undefined) ?? 0,
);
const body = extractErrorBody(err);
const detail = extractErrorMessage(body) || (typeof err.message === 'string' ? err.message : '');
const statusText = statusCode ? `HTTP ${statusCode}` : 'Request failed';
const message = detail
? `Obsidian API error (${statusText}): ${detail}`
: `Obsidian API error (${statusText})`;
// Build a plain-object error so n8n never tries to serialize the raw HTTP
// response (it contains circular fields like res.socket._httpMessage).
return new NodeApiError(
context.getNode(),
{ message, ...(statusCode ? { statusCode } : {}), ...(detail ? { detail } : {}) },
{ message, description: detail || undefined, httpCode: statusCode ? String(statusCode) : undefined, itemIndex },
);
}
function extractErrorBody(err: Record<string, unknown>): unknown {
if ('body' in err && err.body !== undefined) return err.body;
const response = err.response;
if (isObject(response)) {
if ('body' in response && response.body !== undefined) return response.body;
if ('data' in response && response.data !== undefined) return response.data;
}
if ('error' in err && err.error !== undefined) return err.error;
return undefined;
}
function extractErrorMessage(body: unknown): string {
let data: unknown = body;
if (typeof data === 'string') {
const trimmed = data.trim();
if (!trimmed) return '';
try {
data = JSON.parse(trimmed);
} catch {
return trimmed;
}
}
if (isObject(data)) {
const candidate = data.detail ?? data.message ?? data.error;
if (typeof candidate === 'string' && candidate) return candidate;
if (isObject(candidate) && typeof candidate.message === 'string') return candidate.message;
}
return '';
}
function normalizeCommandResult(response: unknown): ObsidianCommandResult {
const data = extractResponseBody(response);
const result = typeof data === 'string' ? JSON.parse(data) : data;