"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports._setResourcesForTest = _setResourcesForTest;
exports._setOpenApiGeneratorForTest = _setOpenApiGeneratorForTest;
exports._setHttpUrlPrefixForTest = _setHttpUrlPrefixForTest;
exports.listResources = listResources;
exports.listResourceTemplates = listResourceTemplates;
exports.readResource = readResource;
/**
 * MCP resources capability — implements `resources/list`, `resources/read`,
 * and `resources/templates/list` per MCP §server/resources (rev 2025-06-18).
 *
 * Two URI schemes:
 *   - `https://<host>[:<port>]/<path>` for app-exported Resources. The same
 *     URL the REST API uses. Resolved **in-process** via
 *     `Resources.getMatch(path)` — never makes an outbound HTTP request.
 *   - `harper://...` for synthetic / metadata resources that don't have a
 *     real HTTP endpoint:
 *       harper://about              — server version, profile, capabilities
 *       harper://schema/{database}/{table} — Table.attributes (RBAC-filtered at read time)
 *       harper://openapi             — OpenAPI 3.0.3 document
 *       harper://operations          — ops-profile only; canonical ops list
 *
 * Unlike the tool registry, resources aren't *registered* — they're
 * *discovered* at request time. Apps register their `Resource` classes
 * through Harper's normal flow; this module enumerates the global
 * `resources` registry and adds the synthetic URIs that v1 exposes.
 *
 * Security model: list time only checks REST-method presence on the
 * Resource class. Resource access is determined programmatically (per-
 * record `allow{Read,Create,Update,Delete}` predicates), so list-time
 * RBAC walks would be both incomplete and misleading. Every read goes
 * through the corresponding read path's permission enforcement.
 *
 * For `harper://schema/{database}/{table}`, read enforcement uses the
 * static `user.role.permission[db].tables[table].{read,describe}` walk —
 * that's the same path Harper's describe permission uses
 * (`dataLayer/schemaDescribe.ts:29-49`).
 */
const env = __importStar(require("../../utility/environment/environmentManager.js"));
const hdbTerms_ts_1 = require("../../utility/hdbTerms.js");
const harper_logger_ts_1 = __importDefault(require("../../utility/logging/harper_logger.js"));
const lifecycle_ts_1 = require("./lifecycle.js");
const HARPER_SCHEME = 'harper:';
const HTTPS_SCHEME = 'https:';
const HTTP_SCHEME = 'http:';
const DEFAULT_LIMIT = 200;
// Test seams. Injecting these lets us unit-test without standing up a full
// Resources registry or running openapi generation.
let _resourcesOverride;
let _openApiOverride;
let _httpUrlPrefixOverride;
/** Test seam: replace the Resources registry the module reads. */
function _setResourcesForTest(r) {
    _resourcesOverride = r;
}
/** Test seam: replace the openapi generator. */
function _setOpenApiGeneratorForTest(fn) {
    _openApiOverride = fn;
}
/** Test seam: replace the inferred application HTTP URL prefix. */
function _setHttpUrlPrefixForTest(prefix) {
    _httpUrlPrefixOverride = prefix;
}
function getResources() {
    if (_resourcesOverride)
        return _resourcesOverride;
    // Lazy import — see file-top comment on Harper graph initialization.
    const { resources } = require('../../resources/Resources.ts');
    return resources;
}
function getOpenApiGenerator() {
    if (_openApiOverride)
        return _openApiOverride;
    const { generateJsonApi } = require('../../resources/openApi.ts');
    return generateJsonApi;
}
function listResources(args) {
    const all = enumerate(args.profile);
    const offset = args.cursor ? decodeCursor(args.cursor) : 0;
    const limit = args.limit && args.limit > 0 ? args.limit : DEFAULT_LIMIT;
    const slice = all.slice(offset, offset + limit);
    const next = offset + slice.length;
    return {
        resources: slice,
        nextCursor: next < all.length ? encodeCursor(next) : undefined,
    };
}
function listResourceTemplates(profile) {
    const templates = [];
    if (profile === 'application') {
        templates.push({
            uriTemplate: 'harper://schema/{database}/{table}',
            name: 'Table schema',
            description: 'Attribute definitions for a Harper table, RBAC-filtered by attribute_permissions',
            mimeType: 'application/json',
        });
        const serverHttpURL = guessAppHttpUrlPrefix();
        if (serverHttpURL) {
            templates.push({
                uriTemplate: `${serverHttpURL}/{resourcePath}`,
                name: 'Application resource',
                description: 'A Resource exported on the HTTP port. The URI is the canonical REST URL; resolution is in-process.',
                mimeType: 'application/json',
            });
        }
    }
    return templates;
}
async function readResource(args) {
    const { uri, user, profile } = args;
    let parsed;
    try {
        parsed = new URL(uri);
    }
    catch {
        return { ok: false, reason: `invalid uri: ${uri}` };
    }
    if (parsed.protocol === HARPER_SCHEME) {
        return readHarperUri(parsed, user, profile);
    }
    if (parsed.protocol === HTTPS_SCHEME || parsed.protocol === HTTP_SCHEME) {
        if (profile !== 'application') {
            return { ok: false, reason: 'https:// resources are only available on the application profile' };
        }
        return readAppResource(parsed);
    }
    return { ok: false, reason: `unsupported uri scheme: ${parsed.protocol}` };
}
// ─── Enumeration ───────────────────────────────────────────────────────
function enumerate(profile) {
    const out = [];
    // `harper://about` is available on both profiles.
    out.push({
        uri: 'harper://about',
        name: 'Server metadata',
        description: 'Harper server version, profile, and the MCP protocol versions advertised by this server.',
        mimeType: 'application/json',
    });
    if (profile === 'operations') {
        out.push({
            uri: 'harper://operations',
            name: 'Operations catalog',
            description: 'User-filtered list of Harper operations with their JSON Schemas.',
            mimeType: 'application/json',
        });
    }
    if (profile === 'application') {
        out.push({
            uri: 'harper://openapi',
            name: 'OpenAPI document',
            description: "OpenAPI 3.0.3 spec for the application's HTTP REST surface.",
            mimeType: 'application/json',
        });
        // harper://schema/{database}/{table} — one entry per table backing a Resource.
        // No list-time RBAC filter; readTableSchema enforces describe/read perms.
        for (const { db, table, description: tableDoc } of enumerateTableBackedResources()) {
            const base = `Attribute definitions for ${db}.${table}, filtered at read time by your role's attribute_permissions.`;
            out.push({
                uri: `harper://schema/${db}/${table}`,
                name: `${db}.${table} schema`,
                description: tableDoc ? `${tableDoc} ${base}` : base,
                mimeType: 'application/json',
            });
        }
        // https://... — one per exported Resource that has class-level REST methods.
        // Per-record access is decided by each Resource's `allow{Read,...}` predicate
        // at read time, so the list filter only checks method presence.
        for (const entry of enumerateAppHttpResources())
            out.push(entry);
    }
    out.sort((a, b) => (a.uri < b.uri ? -1 : a.uri > b.uri ? 1 : 0));
    return out;
}
function enumerateTableBackedResources() {
    const seen = new Set();
    const result = [];
    for (const entry of getResources().values()) {
        if (!isMcpExposed(entry))
            continue;
        const ResourceClass = entry.Resource;
        // @hidden suppresses the Resource from descriptive surfaces (MCP + OpenAPI).
        if (ResourceClass?.hidden === true)
            continue;
        const db = ResourceClass?.databaseName;
        const table = ResourceClass?.tableName;
        if (!db || !table)
            continue;
        const key = `${db}/${table}`;
        if (seen.has(key))
            continue;
        seen.add(key);
        result.push({ db, table, description: ResourceClass?.description });
    }
    return result;
}
function enumerateAppHttpResources() {
    const prefix = guessAppHttpUrlPrefix();
    if (!prefix)
        return [];
    const out = [];
    for (const [path, entry] of getResources()) {
        if (!isMcpExposed(entry))
            continue;
        const ResourceClass = entry.Resource;
        // @hidden suppresses the Resource from descriptive surfaces (MCP + OpenAPI).
        if (ResourceClass?.hidden === true)
            continue;
        if (!hasRestVerbs(ResourceClass?.prototype))
            continue;
        const tableDoc = ResourceClass?.description;
        const description = tableDoc
            ? `${tableDoc} Application resource at /${path}. Resolves in-process via Resources.getMatch.`
            : `Application resource at /${path}. Resolves in-process via Resources.getMatch.`;
        out.push({
            uri: `${prefix}/${path}`,
            name: path,
            description,
            mimeType: 'application/json',
        });
    }
    return out;
}
/**
 * Honor the per-protocol export controls operators use to scope a
 * Resource's surface. A Resource registered with `exportTypes.mcp === false`
 * is explicitly opted out of MCP enumeration entirely. Missing flag = opted
 * in (mirror of how `Resources.getMatch` treats unspecified types).
 */
function isMcpExposed(entry) {
    const types = entry.exportTypes;
    if (!types)
        return true;
    return types.mcp !== false;
}
/**
 * Mirrors the verb-presence check at `resources/openApi.ts:149-153`.
 * Returns true if the Resource subclass defines any REST verb on its
 * prototype, which is what makes it visible over HTTP. `update()` is
 * counted because openApi treats it as an implicit POST override.
 */
function hasRestVerbs(prototype) {
    if (!prototype || typeof prototype !== 'object')
        return false;
    const p = prototype;
    return (typeof p.get === 'function' ||
        typeof p.put === 'function' ||
        typeof p.post === 'function' ||
        typeof p.patch === 'function' ||
        typeof p.delete === 'function' ||
        typeof p.update === 'function');
}
// ─── Read dispatchers ──────────────────────────────────────────────────
async function readHarperUri(uri, user, profile) {
    // `harper://about` parses with host='about' (URL treats it like a host).
    // We use `host + pathname` to recover the full opaque path. Empty host
    // is also valid (some parsers strip it); fall back to pathname.
    const opaque = (uri.host || '') + (uri.pathname || '');
    if (opaque === '' || opaque === 'about')
        return readAbout(profile, uri.href);
    if (opaque === 'openapi') {
        if (profile !== 'application') {
            return { ok: false, reason: 'harper://openapi is only available on the application profile' };
        }
        return readOpenApi(uri.href);
    }
    if (opaque === 'operations') {
        if (profile !== 'operations') {
            return { ok: false, reason: 'harper://operations is only available on the operations profile' };
        }
        return readOperationsCatalog(user, uri.href);
    }
    const schemaMatch = /^schema\/([^/]+)\/([^/]+)$/.exec(opaque);
    if (schemaMatch) {
        return readTableSchema(schemaMatch[1], schemaMatch[2], user, uri.href);
    }
    return { ok: false, reason: `unknown harper:// resource: ${uri.href}` };
}
function readAbout(profile, href) {
    // Reuse the lifecycle module's constants so this resource and the
    // `initialize` handshake never drift.
    const body = {
        serverInfo: lifecycle_ts_1.SERVER_INFO,
        profile,
        protocolVersions: lifecycle_ts_1.SUPPORTED_PROTOCOL_VERSIONS,
        capabilities: lifecycle_ts_1.SERVER_CAPABILITIES,
    };
    return jsonContent(href, body);
}
function readOpenApi(href) {
    try {
        const generator = getOpenApiGenerator();
        const serverHttpURL = guessAppHttpUrlPrefix() ?? '';
        const api = generator(getResources(), serverHttpURL);
        return jsonContent(href, api);
    }
    catch (err) {
        harper_logger_ts_1.default.warn(`MCP harper://openapi generation failed: ${err.message}`);
        return { ok: false, reason: 'failed to generate openapi document' };
    }
}
function readTableSchema(db, table, user, href) {
    const perm = userTablePermissions(user, db, table);
    if (!perm?.read && !perm?.describe) {
        return { ok: false, reason: `permission denied: cannot describe ${db}.${table}` };
    }
    // Find the Resource backing this table.
    let resource;
    for (const entry of getResources().values()) {
        const r = entry.Resource;
        if (r?.databaseName === db && r?.tableName === table) {
            resource = r;
            break;
        }
    }
    if (!resource)
        return { ok: false, reason: `table not found: ${db}.${table}` };
    const attributes = resource.attributes ?? [];
    const filteredAttributes = filterAttributesByPermissions(attributes, perm?.attribute_permissions);
    const body = {
        database: db,
        table,
        primaryKey: resource.primaryKey,
        attributes: filteredAttributes,
        // `attribute_permissions` is forwarded as informational — lets the LLM
        // surface "fields you can't write to" hints without us having to
        // pre-compute them.
        attribute_permissions: perm?.attribute_permissions,
    };
    return jsonContent(href, body);
}
function readOperationsCatalog(user, href) {
    const out = [];
    for (const opName of Object.values(hdbTerms_ts_1.OPERATIONS_ENUM)) {
        if (canRoleInvokeOperation(user, opName)) {
            out.push({ name: opName });
        }
    }
    out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
    return jsonContent(href, { operations: out });
}
async function readAppResource(uri) {
    // Strip the host:port and leading slash to get the path that
    // Resources.getMatch expects. Passing the 'mcp' export type runs the
    // existing per-protocol gate at resources/Resources.ts:97 — an entry
    // registered with exportTypes.mcp === false short-circuits to "no match".
    const path = uri.pathname.replace(/^\/+/, '');
    const entry = getResources().getMatch(path, 'mcp');
    if (!entry)
        return { ok: false, reason: `no resource matches: ${uri.href}` };
    const ResourceClass = entry.Resource;
    // v1 returns a descriptor of the Resource class — enough for an LLM to
    // understand what's available. Full content reads (a record fetch, a
    // search result) go through the tools surface, where each Resource's
    // existing `transactional()` + `allow{Read,Create,Update,Delete}`
    // predicates run per-record. This keeps `resources/read` a fast,
    // side-effect-free metadata view; the descriptor itself isn't a
    // capability token, just a hint that this URI exists in the registry.
    const body = {
        uri: uri.href,
        path: entry.path,
        database: ResourceClass?.databaseName,
        table: ResourceClass?.tableName,
        hint: 'Use the corresponding `get_*` or `search_*` tool from `tools/list` to fetch records.',
    };
    return jsonContent(uri.href, body);
}
function isSuperUser(user) {
    return user?.role?.permission?.super_user === true;
}
function userTablePermissions(user, db, table) {
    if (isSuperUser(user)) {
        return { read: true, describe: true };
    }
    const dbPerm = user?.role?.permission?.[db];
    if (!dbPerm || typeof dbPerm !== 'object' || Array.isArray(dbPerm))
        return null;
    const tablePerm = dbPerm.tables?.[table];
    if (!tablePerm)
        return null;
    return {
        read: tablePerm.read === true,
        describe: tablePerm.describe === true,
        attribute_permissions: tablePerm.attribute_permissions,
    };
}
const SCHEMA_STRUCTURE_OPERATIONS = new Set([
    'create_schema',
    'create_database',
    'drop_schema',
    'drop_database',
    'create_table',
    'drop_table',
    'create_attribute',
    'drop_attribute',
]);
function canRoleInvokeOperation(user, operation) {
    if (isSuperUser(user))
        return true;
    const perm = user?.role?.permission;
    if (!perm)
        return false;
    if (perm.structure_user && SCHEMA_STRUCTURE_OPERATIONS.has(operation))
        return true;
    if (Array.isArray(perm.operations) && perm.operations.includes(operation))
        return true;
    return false;
}
function filterAttributesByPermissions(attributes, attributePermissions) {
    if (!attributePermissions || !Array.isArray(attributes))
        return attributes;
    // `attribute_permissions` is an array of `{ attribute_name, read, insert,
    // update }` per Harper's existing role-permissions shape. v1 simply
    // drops attributes the user has no read on; finer-grained handling
    // (write-only / partial visibility) is documented but unimplemented.
    const denied = new Set();
    if (Array.isArray(attributePermissions)) {
        for (const ap of attributePermissions) {
            if (ap && ap.attribute_name && ap.read === false)
                denied.add(ap.attribute_name);
        }
    }
    if (denied.size === 0)
        return attributes;
    return attributes.filter((a) => !denied.has(a?.name));
}
function guessAppHttpUrlPrefix() {
    // Best-effort URL prefix construction. Hostname comes from the server
    // module post-boot; the port comes from config. In tests where neither
    // is available we return undefined and skip the https:// enumeration —
    // callers handle the absence gracefully.
    if (_httpUrlPrefixOverride !== undefined)
        return _httpUrlPrefixOverride || undefined;
    let hostname;
    try {
        const { server } = require('../../server/Server.ts');
        hostname = server?.hostname;
    }
    catch {
        return undefined;
    }
    if (!hostname)
        return undefined;
    // On Fabric, secure ports are shadowed through Unix domain sockets and
    // the published surface lives behind a load balancer (9926 → 443). Drop
    // the port so the URL points at the public surface, not the internal one.
    if (env.get(hdbTerms_ts_1.CONFIG_PARAMS.TLS_UNIXDOMAINSOCKETS)) {
        return `https://${hostname}`;
    }
    // Standard deployment: prefer the HTTPS port. Fall back to the plain
    // HTTP port for dev setups that don't configure TLS.
    const securePort = env.get(hdbTerms_ts_1.CONFIG_PARAMS.HTTP_SECUREPORT);
    if (securePort)
        return `https://${hostname}:${securePort}`;
    const httpPort = env.get(hdbTerms_ts_1.CONFIG_PARAMS.HTTP_PORT);
    if (httpPort)
        return `http://${hostname}:${httpPort}`;
    return undefined;
}
function jsonContent(uri, body) {
    return {
        ok: true,
        contents: [
            {
                uri,
                mimeType: 'application/json',
                text: JSON.stringify(body),
            },
        ],
    };
}
function encodeCursor(offset) {
    return Buffer.from(JSON.stringify({ offset }), 'utf8').toString('base64url');
}
function decodeCursor(cursor) {
    try {
        const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
        const offset = decoded?.offset;
        if (typeof offset !== 'number' || offset < 0 || !Number.isFinite(offset) || !Number.isInteger(offset)) {
            harper_logger_ts_1.default.trace('MCP resources cursor decoded but missing/invalid offset; treating as 0');
            return 0;
        }
        return offset;
    }
    catch (err) {
        harper_logger_ts_1.default.trace(`MCP resources cursor decode failed (${err.message}); treating as 0`);
        return 0;
    }
}
//# sourceMappingURL=resources.js.map