@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
103 lines • 4.73 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { IOResult } from "../../../../io/types.js";
import { NotionAPIError } from "../../../../core/notion/client.js";
const ENC = new TextEncoder();
const FAILED = 'error: Public API request failed';
// 401 is the one status upstream does not itemize: it drops the parenthesis
// entirely and answers with an actionable hint and its own exit code, because a
// token problem is the user's to fix and naming `unauthorized` twice would not
// help them do it.
const UNAUTHORIZED_HINT = ' hint: Set NOTION_API_TOKEN, or run `ntn login` to reuse a saved workspace token.\n';
const API_ERROR_EXIT = 5;
const UNAUTHORIZED_EXIT = 4;
const UNAUTHORIZED = 401;
// Upstream is a Rust program and renders the reason phrase from its http
// crate's full table, but only the statuses Notion itself documents can reach a
// caller, so those are the ones pinned here (probed one by one against ntn
// 0.21.9). An unlisted status keeps the number and drops the phrase rather than
// inventing one. Spelled out instead of read from node:http because core runs
// in the browser too.
const HTTP_REASON = {
400: 'Bad Request',
403: 'Forbidden',
404: 'Not Found',
409: 'Conflict',
429: 'Too Many Requests',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
};
// An API failure a verb has its own second line for. Upstream pairs some
// refusals with a ` hint:` line naming what the operand could have been, which
// is the verb's knowledge rather than the transport's, so it is attached here
// instead of in the client.
export class HintedAPIError extends NotionAPIError {
hint;
constructor(base, hint) {
super(base.message, base.status, base.code);
this.hint = hint;
this.name = 'HintedAPIError';
}
}
// Upstream's hint for an operand that was neither kind of id.
export function sourceHint(ref) {
return (` hint: Could not find a data source or database with ID \`${ref}\`. ` +
`Check that the ID or URL points to a data source or database shared with ` +
`your integration.\n`);
}
// Render an API failure the way the real ntn binary renders it.
export function apiFailure(err) {
const hint = err instanceof HintedAPIError ? err.hint : '';
if (err.status === UNAUTHORIZED) {
return [`${FAILED}: ${err.message}\n${UNAUTHORIZED_HINT}`, UNAUTHORIZED_EXIT];
}
const named = [];
if (err.status !== null) {
named.push(String(err.status));
const reason = HTTP_REASON[err.status];
if (reason !== undefined)
named.push(reason);
}
if (err.code !== null)
named.push(err.code);
const detail = named.length > 0 ? ` (${named.join(' ')})` : '';
return [`${FAILED}${detail}: ${err.message}\n${hint}`, API_ERROR_EXIT];
}
// Stamped on the wrapper so the invariant below is checkable rather than
// inferred: python reads it off `partial.func`, and a wrapper closure has no
// equivalent to read, so it carries the fact instead.
export const GUARDED = Symbol.for('ntn.guarded');
// Run one ntn verb, answering an API failure in upstream's voice. Every leaf is
// wrapped with this in the tree, because the executor's own fallback prints
// `ntn <verb>: <message>` and exits 1 (the GNU shape it owes every other CLI),
// which drops the status, the reason phrase and Notion's machine-readable code:
// exactly the three fields a caller branches on. failure.test.ts fails if a
// leaf is left unwrapped.
export function guarded(fn) {
const wrapped = async (accessor, paths, texts, ctx) => {
try {
return await fn(accessor, paths, texts, ctx);
}
catch (err) {
if (!(err instanceof NotionAPIError))
throw err;
const [stderr, exitCode] = apiFailure(err);
return [null, new IOResult({ stderr: ENC.encode(stderr), exitCode })];
}
};
return Object.assign(wrapped, { [GUARDED]: true });
}
//# sourceMappingURL=failure.js.map