Files
obsidian-api/packages/n8n-nodes-obsidian-api/shared/ObsidianApiClient.ts
T
daniel156161 be78e3aa00
Build and Push Docker Container / build-and-push (push) Successful in 4m15s
feat: resolve daily-note path from Obsidian config for scoped tokens
- Add daily_note_path_for_vault to read .obsidian/daily-notes.json

- Honor configured folder and date format for daily commands

- Add moment_to_strftime to translate Obsidian date tokens

- Authorize pathless daily:* commands against the resolved path

- Pick vault root from token vault, cwd, or default vault

- Normalize n8n httpRequest response to avoid circular JSON output

- Force returnFullResponse false and parse body to plain result

- Bump n8n-nodes-obsidian-cli-api to 0.2.0

- Add tests for allowed and denied scoped daily commands
2026-06-24 11:01:57 +02:00

115 lines
3.4 KiB
TypeScript

import type {
IExecuteFunctions,
IDataObject,
IHttpRequestOptions,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
export interface ObsidianCommandResult extends IDataObject {
id: string;
command: string[];
cwd: string;
exit_code: number;
stdout: string;
stderr: string;
duration_ms: number;
timed_out: boolean;
}
export interface ObsidianCredentials extends IDataObject {
baseUrl: string;
apiToken: string;
defaultVault?: string;
}
export function compactArgs(args: Array<string | undefined | null | false>): string[] {
return args.filter((arg): arg is string => typeof arg === 'string' && arg.length > 0);
}
export function kv(name: string, value: string | number | boolean | undefined | null): string | undefined {
if (value === undefined || value === null || value === '') return undefined;
if (typeof value === 'boolean') return value ? name : undefined;
return `${name}=${value}`;
}
export function withVault(args: string[], vault?: string): string[] {
if (!vault || args[0]?.startsWith('vault=')) return args;
return [`vault=${vault}`, ...args];
}
export async function executeObsidianCommand(
context: IExecuteFunctions,
itemIndex: number,
args: string[],
options: { vault?: string; timeout?: number; stdin?: string } = {},
): Promise<ObsidianCommandResult> {
const credentials = await context.getCredentials('obsidianApi') as ObsidianCredentials;
const baseUrl = String(credentials.baseUrl).replace(/\/$/, '');
const vault = options.vault || String(credentials.defaultVault || '');
const finalArgs = withVault(args, vault);
const request: IHttpRequestOptions = {
method: 'POST',
url: `${baseUrl}/commands`,
headers: {
Authorization: `Bearer ${credentials.apiToken}`,
'Content-Type': 'application/json',
},
body: {
args: finalArgs,
stdin: options.stdin || undefined,
timeout: options.timeout,
},
json: true,
returnFullResponse: false,
};
const response = await context.helpers.httpRequest(request) as unknown;
const result = normalizeCommandResult(response);
if (result.exit_code !== 0) {
throw new NodeOperationError(
context.getNode(),
`Obsidian CLI command failed with exit code ${result.exit_code}: ${result.stderr || result.stdout}`,
{ itemIndex },
);
}
return result;
}
function normalizeCommandResult(response: unknown): ObsidianCommandResult {
const data = extractResponseBody(response);
const result = typeof data === 'string' ? JSON.parse(data) : data;
if (!isObject(result)) {
throw new Error('Obsidian API returned a non-object response');
}
return {
id: String(result.id ?? ''),
command: Array.isArray(result.command) ? result.command.map(String) : [],
cwd: String(result.cwd ?? ''),
exit_code: Number(result.exit_code ?? 0),
stdout: String(result.stdout ?? ''),
stderr: String(result.stderr ?? ''),
duration_ms: Number(result.duration_ms ?? 0),
timed_out: Boolean(result.timed_out),
};
}
function extractResponseBody(response: unknown): unknown {
if (!isObject(response)) return response;
// Some n8n versions/helpers can return a full response object. Never pass that
// through to node output because it contains circular fields like res.socket.
if ('body' in response) return response.body;
if ('data' in response) return response.data;
return response;
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}