fix: allow vault info under path scope and clarify API errors
Build and Push Docker Container / build-and-push (push) Successful in 4m29s
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:
@@ -225,41 +225,50 @@ export class ObsidianApi implements INodeType {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
const resource = this.getNodeParameter('resource', itemIndex) as string;
|
||||
const operation = this.getNodeParameter('operation', itemIndex) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as Record<string, unknown>;
|
||||
const vault = String(additionalFields.vault || '');
|
||||
const timeout = Number(additionalFields.timeout ?? 60);
|
||||
try {
|
||||
const resource = this.getNodeParameter('resource', itemIndex) as string;
|
||||
const operation = this.getNodeParameter('operation', itemIndex) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as Record<string, unknown>;
|
||||
const vault = String(additionalFields.vault || '');
|
||||
const timeout = Number(additionalFields.timeout ?? 60);
|
||||
|
||||
let args: string[];
|
||||
let args: string[];
|
||||
|
||||
switch (resource) {
|
||||
case 'note': {
|
||||
args = buildNoteArgs(this, operation, itemIndex);
|
||||
break;
|
||||
switch (resource) {
|
||||
case 'note': {
|
||||
args = buildNoteArgs(this, operation, itemIndex);
|
||||
break;
|
||||
}
|
||||
case 'daily': {
|
||||
args = buildDailyArgs(this, operation, itemIndex);
|
||||
break;
|
||||
}
|
||||
case 'search': {
|
||||
args = buildSearchArgs(this, operation, itemIndex);
|
||||
break;
|
||||
}
|
||||
case 'vault': {
|
||||
args = buildVaultArgs(this, operation, itemIndex, additionalFields);
|
||||
break;
|
||||
}
|
||||
case 'raw': {
|
||||
args = parseRawArgs(this, itemIndex);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new NodeOperationError(this.getNode(), `Unsupported resource: ${resource}`, { itemIndex });
|
||||
}
|
||||
case 'daily': {
|
||||
args = buildDailyArgs(this, operation, itemIndex);
|
||||
break;
|
||||
|
||||
const result = await executeObsidianCommand(this, itemIndex, args, { vault, timeout });
|
||||
returnData.push({ json: result, pairedItem: { item: itemIndex } });
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
returnData.push({ json: { error: message }, pairedItem: { item: itemIndex } });
|
||||
continue;
|
||||
}
|
||||
case 'search': {
|
||||
args = buildSearchArgs(this, operation, itemIndex);
|
||||
break;
|
||||
}
|
||||
case 'vault': {
|
||||
args = buildVaultArgs(this, operation, itemIndex, additionalFields);
|
||||
break;
|
||||
}
|
||||
case 'raw': {
|
||||
args = parseRawArgs(this, itemIndex);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new NodeOperationError(this.getNode(), `Unsupported resource: ${resource}`, { itemIndex });
|
||||
throw error;
|
||||
}
|
||||
|
||||
const result = await executeObsidianCommand(this, itemIndex, args, { vault, timeout });
|
||||
returnData.push({ json: result, pairedItem: { item: itemIndex } });
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
@@ -285,7 +294,7 @@ function buildNoteArgs(context: IExecuteFunctions, operation: string, itemIndex:
|
||||
return compactArgs(['files', kv('folder', context.getNodeParameter('folder', itemIndex, '') as string)]);
|
||||
default:
|
||||
throw new NodeOperationError(context.getNode(), `Unsupported note operation: ${operation}`, { itemIndex });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildDailyArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] {
|
||||
@@ -300,7 +309,7 @@ function buildDailyArgs(context: IExecuteFunctions, operation: string, itemIndex
|
||||
return ['daily:path'];
|
||||
default:
|
||||
throw new NodeOperationError(context.getNode(), `Unsupported daily operation: ${operation}`, { itemIndex });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildSearchArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] {
|
||||
@@ -322,7 +331,7 @@ function buildVaultArgs(context: IExecuteFunctions, operation: string, itemIndex
|
||||
}
|
||||
default:
|
||||
throw new NodeOperationError(context.getNode(), `Unsupported vault operation: ${operation}`, { itemIndex });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseRawArgs(context: IExecuteFunctions, itemIndex: number): string[] {
|
||||
@@ -330,7 +339,7 @@ function parseRawArgs(context: IExecuteFunctions, itemIndex: number): string[] {
|
||||
const parsed = Array.isArray(raw) ? raw : JSON.parse(raw);
|
||||
if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === 'string')) {
|
||||
throw new NodeOperationError(context.getNode(), 'Args JSON must be an array of strings', { itemIndex });
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "n8n-nodes-obsidian-cli-api",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "n8n-nodes-obsidian-cli-api",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-nodes-obsidian-cli-api",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "n8n community nodes for the Obsidian API Docker service backed by the official Obsidian CLI.",
|
||||
"license": "MIT",
|
||||
"homepage": "https://git.yiprawr.dev/Docker/obsidian-api",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user