import * as __typia_transform__assertGuard from "typia/lib/internal/_assertGuard.js";
import * as __typia_transform__accessExpressionAsString from "typia/lib/internal/_accessExpressionAsString.js";
import { isKeetaStorageAnchorDeleteResponse, isKeetaStorageAnchorPutResponse, isKeetaStorageAnchorSearchResponse, isKeetaStorageAnchorQuotaResponse } from './common.generated.js';
import { getKeetaStorageAnchorDeleteRequestSigningData, getKeetaStorageAnchorPutRequestSigningData, getKeetaStorageAnchorGetRequestSigningData, getKeetaStorageAnchorSearchRequestSigningData, getKeetaStorageAnchorQuotaRequestSigningData, getKeetaStorageAnchorUpdateMetadataRequestSigningData, parseContainerPayload, Errors, CONTENT_TYPE_JSON, CONTENT_TYPE_OCTET_STREAM, DEFAULT_SIGNED_URL_TTL_SECONDS } from './common.js';
import { KeetaNet } from '../../client/index.js';
import { getDefaultResolver } from '../../config.js';
import { EncryptedContainer } from '../../lib/encrypted-container.js';
import { createAssertEquals } from 'typia';
import { addSignatureToURL } from '../../lib/http-server/common.js';
import { SignData } from '../../lib/utils/signing.js';
import { KeetaAnchorError } from '../../lib/error.js';
import { arrayBufferLikeToBuffer } from '../../lib/utils/buffer.js';
import { StorageContactsClient } from './clients/contacts.js';
import { StorageIconsClient } from './clients/icon.js';
import Resolver from '../../lib/resolver.js';
import crypto from '../../lib/utils/crypto.js';
const KeetaStorageAnchorClientAccessToken = Symbol('KeetaStorageAnchorClientAccessToken');
const assertServiceMetadataEndpoint = (() => { const _io0 = (input, _exceptionable = true) => "string" === typeof input.url && (undefined === input.options || "object" === typeof input.options && null !== input.options && false === Array.isArray(input.options) && _io1(input.options, true && _exceptionable)) && (1 === Object.keys(input).length || Object.keys(input).every(key => {
    if (["url", "options"].some(prop => key === prop))
        return true;
    const value = input[key];
    if (undefined === value)
        return true;
    return false;
})); const _io1 = (input, _exceptionable = true) => (undefined === input.authentication || "object" === typeof input.authentication && null !== input.authentication && _io2(input.authentication, true && _exceptionable)) && (0 === Object.keys(input).length || Object.keys(input).every(key => {
    if (["authentication"].some(prop => key === prop))
        return true;
    const value = input[key];
    if (undefined === value)
        return true;
    return false;
})); const _io2 = (input, _exceptionable = true) => "keeta-account" === input.method && ("optional" === input.type || "required" === input.type || "none" === input.type) && (2 === Object.keys(input).length || Object.keys(input).every(key => {
    if (["method", "type"].some(prop => key === prop))
        return true;
    const value = input[key];
    if (undefined === value)
        return true;
    return false;
})); const _ao0 = (input, _path, _exceptionable = true) => ("string" === typeof input.url || __typia_transform__assertGuard._assertGuard(_exceptionable, {
    method: "createAssertEquals",
    path: _path + ".url",
    expected: "string",
    value: input.url
}, _errorFactory)) && (undefined === input.options || ("object" === typeof input.options && null !== input.options && false === Array.isArray(input.options) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
    method: "createAssertEquals",
    path: _path + ".options",
    expected: "(__type.o1 | undefined)",
    value: input.options
}, _errorFactory)) && _ao1(input.options, _path + ".options", true && _exceptionable) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
    method: "createAssertEquals",
    path: _path + ".options",
    expected: "(__type.o1 | undefined)",
    value: input.options
}, _errorFactory)) && (1 === Object.keys(input).length || (false === _exceptionable || Object.keys(input).every(key => {
    if (["url", "options"].some(prop => key === prop))
        return true;
    const value = input[key];
    if (undefined === value)
        return true;
    return __typia_transform__assertGuard._assertGuard(_exceptionable, {
        method: "createAssertEquals",
        path: _path + __typia_transform__accessExpressionAsString._accessExpressionAsString(key),
        expected: "undefined",
        value: value
    }, _errorFactory);
}))); const _ao1 = (input, _path, _exceptionable = true) => (undefined === input.authentication || ("object" === typeof input.authentication && null !== input.authentication || __typia_transform__assertGuard._assertGuard(_exceptionable, {
    method: "createAssertEquals",
    path: _path + ".authentication",
    expected: "(ServiceMetadataAuthenticationType | undefined)",
    value: input.authentication
}, _errorFactory)) && _ao2(input.authentication, _path + ".authentication", true && _exceptionable) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
    method: "createAssertEquals",
    path: _path + ".authentication",
    expected: "(ServiceMetadataAuthenticationType | undefined)",
    value: input.authentication
}, _errorFactory)) && (0 === Object.keys(input).length || (false === _exceptionable || Object.keys(input).every(key => {
    if (["authentication"].some(prop => key === prop))
        return true;
    const value = input[key];
    if (undefined === value)
        return true;
    return __typia_transform__assertGuard._assertGuard(_exceptionable, {
        method: "createAssertEquals",
        path: _path + __typia_transform__accessExpressionAsString._accessExpressionAsString(key),
        expected: "undefined",
        value: value
    }, _errorFactory);
}))); const _ao2 = (input, _path, _exceptionable = true) => ("keeta-account" === input.method || __typia_transform__assertGuard._assertGuard(_exceptionable, {
    method: "createAssertEquals",
    path: _path + ".method",
    expected: "\"keeta-account\"",
    value: input.method
}, _errorFactory)) && ("optional" === input.type || "required" === input.type || "none" === input.type || __typia_transform__assertGuard._assertGuard(_exceptionable, {
    method: "createAssertEquals",
    path: _path + ".type",
    expected: "(\"none\" | \"optional\" | \"required\")",
    value: input.type
}, _errorFactory)) && (2 === Object.keys(input).length || (false === _exceptionable || Object.keys(input).every(key => {
    if (["method", "type"].some(prop => key === prop))
        return true;
    const value = input[key];
    if (undefined === value)
        return true;
    return __typia_transform__assertGuard._assertGuard(_exceptionable, {
        method: "createAssertEquals",
        path: _path + __typia_transform__accessExpressionAsString._accessExpressionAsString(key),
        expected: "undefined",
        value: value
    }, _errorFactory);
}))); const __is = (input, _exceptionable = true) => null !== input && undefined !== input && ("string" === typeof input || "object" === typeof input && null !== input && _io0(input, true)); let _errorFactory; return (input, errorFactory) => {
    if (false === __is(input)) {
        _errorFactory = errorFactory;
        ((input, _path, _exceptionable = true) => (null !== input || __typia_transform__assertGuard._assertGuard(true, {
            method: "createAssertEquals",
            path: _path + "",
            expected: "(__type | string)",
            value: input
        }, _errorFactory)) && (undefined !== input || __typia_transform__assertGuard._assertGuard(true, {
            method: "createAssertEquals",
            path: _path + "",
            expected: "(__type | string)",
            value: input
        }, _errorFactory)) && ("string" === typeof input || ("object" === typeof input && null !== input || __typia_transform__assertGuard._assertGuard(true, {
            method: "createAssertEquals",
            path: _path + "",
            expected: "(__type | string)",
            value: input
        }, _errorFactory)) && _ao0(input, _path + "", true) || __typia_transform__assertGuard._assertGuard(true, {
            method: "createAssertEquals",
            path: _path + "",
            expected: "(__type | string)",
            value: input
        }, _errorFactory)))(input, "$input", true);
    }
    return input;
}; })();
function validateURL(url) {
    if (url === undefined || url === null) {
        throw (new Errors.InvalidPath('Invalid URL: null or undefined'));
    }
    try {
        return (new URL(url));
    }
    catch {
        throw (new Errors.InvalidResponse(`Invalid URL in service metadata: ${url}`));
    }
}
async function getEndpoints(resolver, logger) {
    const response = await resolver.lookup('storage', {});
    if (response === undefined) {
        return (null);
    }
    const serviceInfoPromises = Object.entries(response).map(async function ([id, serviceInfo]) {
        const operations = await serviceInfo.operations('object');
        const operationsFunctions = {};
        for (const [key, operation] of Object.entries(operations)) {
            if (operation === undefined) {
                continue;
            }
            Object.defineProperty(operationsFunctions, key, {
                get: async function () {
                    const endpointInfo = assertServiceMetadataEndpoint(await Resolver.Metadata.fullyResolveValuizable(operation));
                    let url;
                    let authentication = {
                        type: 'none',
                        method: 'keeta-account'
                    };
                    if (typeof endpointInfo === 'string') {
                        url = endpointInfo;
                    }
                    else {
                        url = endpointInfo.url;
                        if (endpointInfo.options?.authentication) {
                            authentication = endpointInfo.options.authentication;
                        }
                    }
                    return ({
                        url: function (params) {
                            let substitutedURL;
                            try {
                                substitutedURL = decodeURI(url);
                            }
                            catch (error) {
                                logger?.debug('getEndpoints', 'Failed to decode URI, using original URL for substitution', error, url);
                                substitutedURL = url;
                            }
                            for (const [paramKey, paramValue] of Object.entries(params ?? {})) {
                                substitutedURL = substitutedURL.replace(`{${paramKey}}`, encodeURIComponent(paramValue));
                            }
                            return (validateURL(substitutedURL));
                        },
                        options: { authentication }
                    });
                },
                enumerable: true,
                configurable: true
            });
        }
        // Extract anchor account public key from service metadata
        const result = { operations: operationsFunctions };
        if ('anchorAccount' in serviceInfo && typeof serviceInfo.anchorAccount === 'function') {
            const anchorAccountValue = await serviceInfo.anchorAccount('primitive');
            if (typeof anchorAccountValue === 'string') {
                result.anchorAccountPublicKey = anchorAccountValue;
            }
        }
        return ([
            // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
            id,
            result
        ]);
    });
    const retval = Object.fromEntries(await Promise.all(serviceInfoPromises));
    return (retval);
}
class KeetaStorageAnchorBase {
    logger;
    client;
    constructor(config) {
        this.client = config.client;
        this.logger = config.logger;
    }
}
/**
 * A session provides a simplified API for storage operations with default account,
 * working directory, and visibility settings.
 */
export class KeetaStorageAnchorSession {
    provider;
    account;
    workingDirectory;
    #defaultVisibility;
    constructor(provider, config) {
        this.provider = provider;
        this.account = config.account;
        this.workingDirectory = config.workingDirectory ?? '/';
        this.#defaultVisibility = config.defaultVisibility ?? 'private';
    }
    /**
     * Resolve a relative path to a full storage path.
     *
     * @param relativePath - Path to resolve (absolute paths returned as-is)
     *
     * @returns The full storage path with working directory prepended if relative
     */
    #resolvePath(relativePath) {
        // If path is already absolute (starts with /), use it as-is
        if (relativePath.startsWith('/')) {
            return (relativePath);
        }
        // Otherwise, prepend working directory
        return (this.workingDirectory + relativePath);
    }
    /**
     * Store data at a relative path.
     * For public visibility, the anchor account is automatically fetched from the provider.
     *
     * @param relativePath - The relative path
     * @param data - The binary data to store
     * @param options.mimeType - MIME type of the data
     * @param options.tags - Optional plaintext tags
     * @param options.visibility - Object visibility
     *
     * @returns The created/updated object metadata
     */
    async put(relativePath, data, options) {
        const fullPath = this.#resolvePath(relativePath);
        const visibility = options.visibility ?? this.#defaultVisibility;
        const putOpts = {
            path: fullPath,
            data,
            mimeType: options.mimeType,
            visibility,
            account: this.account
        };
        if (options.tags) {
            putOpts.tags = options.tags;
        }
        if (options.additionalPrincipals) {
            putOpts.additionalPrincipals = options.additionalPrincipals;
        }
        if (visibility === 'public' && this.provider.anchorAccount) {
            putOpts.anchorAccount = this.provider.anchorAccount;
        }
        return (await this.provider.put(putOpts));
    }
    /**
     * Get data from a relative path.
     *
     * @param relativePath - The relative path
     *
     * @returns The decrypted data and mime-type, or null if not found
     */
    async get(relativePath) {
        const fullPath = this.#resolvePath(relativePath);
        return (await this.provider.get({ path: fullPath, account: this.account }));
    }
    /**
     * Get metadata for an object at a relative path.
     *
     * @param relativePath - The relative Path
     *
     * @returns The object metadata, or null if not found
     */
    async getMetadata(relativePath) {
        const fullPath = this.#resolvePath(relativePath);
        return (await this.provider.getMetadata({ path: fullPath, account: this.account }));
    }
    /**
     * Update metadata for an object at a relative path.
     *
     * @param relativePath - The relative path
     * @param options.tags - New tags for the object
     * @param options.visibility - New visibility setting
     *
     * @returns The updated object metadata, or null if not found
     */
    async updateMetadata(relativePath, options) {
        const fullPath = this.#resolvePath(relativePath);
        const visibility = options.visibility ?? this.#defaultVisibility;
        return (await this.provider.updateMetadata({
            path: fullPath,
            tags: options.tags,
            visibility,
            account: this.account
        }));
    }
    /**
     * Delete data at a relative path.
     *
     * @param relativePath - The relative path
     *
     * @returns True if the object was deleted, false if it didn't exist
     */
    async delete(relativePath) {
        const fullPath = this.#resolvePath(relativePath);
        return (await this.provider.delete({ path: fullPath, account: this.account }));
    }
    /**
     * Search for objects. Owner is automatically set to the session account.
     *
     * @param criteria - Optional search criteria (owner is set automatically)
     * @param pagination - Optional pagination settings
     *
     * @returns Search results with optional nextCursor for pagination
     */
    async search(criteria, pagination) {
        const fullCriteria = {
            ...criteria,
            owner: this.account.publicKeyString.get()
        };
        const searchOpts = {
            criteria: fullCriteria,
            account: this.account
        };
        if (pagination) {
            searchOpts.pagination = pagination;
        }
        return (await this.provider.search(searchOpts));
    }
    /**
     * Get a pre-signed public URL for a relative path.
     *
     * @param relativePath - The relative path
     * @param options.ttl - Optional TTL in seconds
     *
     * @returns The pre-signed URL for public access
     */
    async getPublicUrl(relativePath, options) {
        const fullPath = this.#resolvePath(relativePath);
        const urlOpts = {
            path: fullPath,
            account: this.account
        };
        if (options?.ttl) {
            urlOpts.ttl = options.ttl;
        }
        return (await this.provider.getPublicUrl(urlOpts));
    }
}
/**
 * Represents a Storage Anchor provider for performing storage operations.
 */
export class KeetaStorageAnchorProvider extends KeetaStorageAnchorBase {
    /**
     * Service information including available operations and endpoints.
     */
    serviceInfo;
    /**
     * Unique identifier for this provider.
     */
    providerID;
    /**
     * The anchor account for this provider.
     */
    anchorAccount;
    constructor(serviceInfo, providerID, parent) {
        const parentPrivate = parent._internals(KeetaStorageAnchorClientAccessToken);
        super(parentPrivate);
        this.serviceInfo = serviceInfo;
        this.providerID = providerID;
        // Convert anchor account public key string to Account
        if (serviceInfo.anchorAccountPublicKey) {
            try {
                this.anchorAccount = KeetaNet.lib.Account.fromPublicKeyString(serviceInfo.anchorAccountPublicKey).assertAccount();
            }
            catch {
                throw (new Errors.InvalidAnchorAccount(serviceInfo.anchorAccountPublicKey));
            }
        }
        else {
            this.anchorAccount = null;
        }
    }
    /**
     * Validate tags for client-side constraints before sending to server.
     * @throws Errors.InvalidTag if any tag is empty or contains commas
     */
    #validateTags(tags) {
        for (const tag of tags) {
            if (!tag || tag.trim().length === 0) {
                throw (new Errors.InvalidTag('Tags cannot be empty'));
            }
            if (tag.includes(',')) {
                throw (new Errors.InvalidTag('Tags cannot contain commas'));
            }
        }
    }
    /**
     * Get operation endpoint data for a given operation.
     *
     * @param operationName - The operation to get endpoint data for
     * @param params - Optional URL template parameters to substitute
     *
     * @returns The endpoint URL and authentication configuration
     *
     * @throws OperationNotSupported if the operation is not available
     * @throws UnsupportedAuthMethod if the authentication method is not supported
     */
    async #getOperationData(operationName, params) {
        const endpoint = await this.serviceInfo.operations[operationName];
        if (endpoint === undefined) {
            throw (new Errors.OperationNotSupported(operationName));
        }
        if (endpoint.options.authentication.method !== 'keeta-account') {
            throw (new Errors.UnsupportedAuthMethod(endpoint.options.authentication.method));
        }
        return ({
            url: endpoint.url(params),
            auth: endpoint.options.authentication
        });
    }
    /**
     * Parse an error response from the server.
     * Attempts to restore structured error types from JSON.
     *
     * @param data - The error response data
     *
     * @returns A KeetaAnchorError or generic Error with the error message
     *
     * @throws InvariantViolation if the response is not a valid error object
     */
    async #parseResponseError(data) {
        if (typeof data !== 'object' || data === null) {
            throw (new Errors.InvariantViolation('expected error response object'));
        }
        if (!('ok' in data) || data.ok !== false) {
            throw (new Errors.InvariantViolation('expected error response with ok=false'));
        }
        let parsedError = null;
        try {
            parsedError = await KeetaAnchorError.fromJSON(data);
        }
        catch (error) {
            this.logger?.debug('Failed to parse error response as KeetaAnchorError', error, data);
        }
        if (parsedError) {
            return (parsedError);
        }
        else {
            let errorStr;
            if ('error' in data && typeof data.error === 'string') {
                errorStr = data.error;
            }
            else {
                errorStr = 'Unknown error';
            }
            return (new Error(`storage request failed: ${errorStr}`));
        }
    }
    /**
     * Resolve account to use for signing, with private key validation.
     *
     * @param account - Optional account override (falls back to client account)
     * @param requirePrivateKey - Whether private key is required
     *
     * @returns The resolved account with optional private key validation
     *
     * @throws PrivateKeyRequired if private key is needed but not available
     */
    #resolveSignerAccount(account, requirePrivateKey = true) {
        const resolved = account ?? this.client.account;
        if (requirePrivateKey && !resolved?.hasPrivateKey) {
            throw (new Errors.PrivateKeyRequired());
        }
        if (!resolved) {
            throw (new Errors.AccountRequired());
        }
        return (resolved);
    }
    /**
     * Handle error responses from binary PUT/GET requests.
     * Attempts to parse JSON error response, otherwise throws generic error.
     *
     * @param response - The failed HTTP response to handle
     *
     * @throws The parsed error from the response body, or a generic ServiceUnavailable error
     */
    async #handleBinaryResponseError(response) {
        let errorJSON;
        try {
            errorJSON = await response.json();
        }
        catch {
            // JSON parsing failed - throw generic error
            throw (new Errors.InvalidResponse(`HTTP ${response.status}: ${response.statusText}`));
        }
        // JSON parsed successfully - attempt to extract error
        if (typeof errorJSON === 'object' && errorJSON !== null && 'ok' in errorJSON && errorJSON.ok === false) {
            throw (await this.#parseResponseError(errorJSON));
        }
        // Unexpected response shape
        throw (new Errors.InvalidResponse(`HTTP ${response.status}: ${response.statusText}`));
    }
    /**
     * Normalize a path suffix and append it to a URL pathname.
     * Handles leading slashes and trailing slashes consistently.
     * Validates against path traversal attacks.
     *
     * @param url - The URL object to modify (mutated in place)
     * @param path - The path suffix to append
     *
     * @throws InvalidPath if path contains traversal patterns
     */
    #appendPathToUrl(url, path) {
        // Validate path is not empty
        if (!path || path.trim().length === 0) {
            throw (new Errors.InvalidPath('Path cannot be empty'));
        }
        // Validate against path traversal attacks
        if (path.includes('..') || path.includes('//')) {
            throw (new Errors.InvalidPath('Path contains invalid segments'));
        }
        const pathSuffix = path.startsWith('/') ? path.slice(1) : path;
        url.pathname = url.pathname.replace(/\/$/, '') + '/' + pathSuffix;
    }
    /**
     * Make a JSON API request to the storage service.
     * Handles authentication, serialization, and response parsing.
     *
     * @typeParam Response - The expected response type
     * @typeParam Request - The request body type
     * @typeParam SerializedRequest - The serialized request type
     *
     * @param input.method - HTTP method
     * @param input.endpoint - Operation endpoint name
     * @param input.account - Account for signing the request
     * @param input.params - URL template parameters
     * @param input.queryParams - Query string parameters
     * @param input.pathSuffix - Path to append to the endpoint URL
     * @param input.body - Request body
     * @param input.serializeRequest - Function to serialize the request body
     * @param input.getSignedData - Function to get signing data from request
     * @param input.isResponse - Type guard for the response
     *
     * @returns The successful response
     *
     * @throws AccountRequired if account is required but not provided
     * @throws InvariantViolation if getSignedData is required but not provided
     * @throws InvalidResponse if the response is invalid
     */
    async #makeRequest(input) {
        const { url, auth } = await this.#getOperationData(input.endpoint, input.params);
        // Append path suffix to URL pathname if provided
        if (input.pathSuffix) {
            this.#appendPathToUrl(url, input.pathSuffix);
        }
        // Add query parameters to URL if provided
        if (input.queryParams) {
            for (const [key, value] of Object.entries(input.queryParams)) {
                url.searchParams.set(key, value);
            }
        }
        // Serialize the request body if provided.
        let serializedBody;
        if (input.body !== undefined && input.serializeRequest) {
            serializedBody = await input.serializeRequest(input.body);
        }
        let signed;
        if (auth.type === 'required' || (auth.type === 'optional' && input.account)) {
            if (!input.account) {
                throw (new Errors.AccountRequired());
            }
            if (!input.getSignedData) {
                throw (new Errors.InvariantViolation('getSignedData required for signed requests'));
            }
            const signable = input.getSignedData(serializedBody);
            signed = await SignData(input.account.assertAccount(), signable);
        }
        const headers = { 'Accept': CONTENT_TYPE_JSON };
        let usingUrl = url;
        let body = null;
        if (input.method === 'POST' || input.method === 'PUT') {
            headers['Content-Type'] = CONTENT_TYPE_JSON;
            body = JSON.stringify({ ...serializedBody, signed });
        }
        else {
            if (signed) {
                if (!input.account) {
                    throw (new Errors.InvariantViolation('Account information is required for this operation'));
                }
                usingUrl = addSignatureToURL(usingUrl, { signedField: signed, account: input.account.assertAccount() });
            }
            if (input.body) {
                throw (new Errors.InvariantViolation('body cannot be sent with GET/DELETE requests'));
            }
        }
        let httpResponse;
        try {
            httpResponse = await fetch(usingUrl, {
                method: input.method, headers, body
            });
        }
        catch (e) {
            throw (new Errors.ServiceUnavailable(`Network error: ${e instanceof Error ? e.message : String(e)}`));
        }
        const requestInformationJSON = await httpResponse.json();
        if (!input.isResponse(requestInformationJSON)) {
            throw (new Errors.InvalidResponse(JSON.stringify(requestInformationJSON)));
        }
        if (!requestInformationJSON.ok) {
            throw (await this.#parseResponseError(requestInformationJSON));
        }
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
        return requestInformationJSON;
    }
    /**
     * Make a PUT request with raw binary body.
     * Used for uploading encrypted container data to storage.
     *
     * @param input.path - The storage path for the object
     * @param input.data - The binary data (encrypted container) to upload
     * @param input.visibility - Object visibility (public or private)
     * @param input.tags - Optional tags to associate with the object
     * @param input.account - Account for signing the request
     *
     * @returns The created/updated object metadata
     *
     * @throws AccountRequired if authentication is required but no account provided
     * @throws ServiceUnavailable if the request fails
     */
    async #makeBinaryPutRequest(input) {
        const { url, auth } = await this.#getOperationData('put');
        const visibility = input.visibility ?? 'private';
        this.#appendPathToUrl(url, input.path);
        if (auth.type === 'required' && !input.account) {
            throw (new Errors.AccountRequired());
        }
        const tags = input.tags ?? [];
        this.#validateTags(tags);
        const signable = getKeetaStorageAnchorPutRequestSigningData({ path: input.path, visibility, tags });
        const signed = await SignData(input.account.assertAccount(), signable);
        // Add auth to query params using helper
        const signedUrl = addSignatureToURL(url, { signedField: signed, account: input.account.assertAccount() });
        signedUrl.searchParams.set('visibility', visibility);
        if (tags.length > 0) {
            signedUrl.searchParams.set('tags', tags.join(','));
        }
        let response;
        try {
            response = await fetch(signedUrl, {
                method: 'PUT',
                headers: { 'Content-Type': CONTENT_TYPE_OCTET_STREAM, 'Accept': CONTENT_TYPE_JSON },
                body: input.data
            });
        }
        catch (e) {
            throw (new Errors.ServiceUnavailable(`Network error: ${e instanceof Error ? e.message : String(e)}`));
        }
        if (!response.ok) {
            await this.#handleBinaryResponseError(response);
        }
        const responseJSON = await response.json();
        if (!isKeetaStorageAnchorPutResponse(responseJSON)) {
            throw (new Errors.InvalidResponse(JSON.stringify(responseJSON)));
        }
        if (!responseJSON.ok) {
            throw (await this.#parseResponseError(responseJSON));
        }
        return (responseJSON);
    }
    /**
     * Make a GET request that returns raw binary data.
     * Used for downloading encrypted container data from storage.
     *
     * @param input.path - The storage path to retrieve
     * @param input.account - Account for signing the request
     *
     * @returns The raw binary data (encrypted container)
     *
     * @throws AccountRequired if authentication is required but no account provided
     * @throws ServiceUnavailable if the request fails
     */
    async #makeBinaryGetRequest(input) {
        const { url, auth } = await this.#getOperationData('get');
        this.#appendPathToUrl(url, input.path);
        if (auth.type === 'required' && !input.account) {
            throw (new Errors.AccountRequired());
        }
        // Sign the request
        const signable = getKeetaStorageAnchorGetRequestSigningData({ path: input.path, account: input.account.publicKeyString.get() });
        const signed = await SignData(input.account.assertAccount(), signable);
        // Add auth to query params
        const signedUrl = addSignatureToURL(url, { signedField: signed, account: input.account.assertAccount() });
        let response;
        try {
            response = await fetch(signedUrl, {
                method: 'GET',
                headers: { 'Accept': CONTENT_TYPE_OCTET_STREAM }
            });
        }
        catch (e) {
            throw (new Errors.ServiceUnavailable(`Network error: ${e instanceof Error ? e.message : String(e)}`));
        }
        if (!response.ok) {
            await this.#handleBinaryResponseError(response);
        }
        const arrayBuffer = await response.arrayBuffer();
        return (arrayBufferLikeToBuffer(arrayBuffer));
    }
    /**
     * Delete an object by path.
     *
     * @param request - The delete request parameters
     * @param request.path - The storage path to delete
     * @param request.account - Optional account for signing (falls back to client account)
     *
     * @returns True if the object was deleted, false if it didn't exist
     */
    async delete(request) {
        this.logger?.debug(`Deleting object at ${request.path} for provider ID: ${String(this.providerID)}`);
        const response = await this.#makeRequest({
            method: 'DELETE',
            endpoint: 'delete',
            account: request.account,
            pathSuffix: request.path,
            getSignedData: function () {
                return (getKeetaStorageAnchorDeleteRequestSigningData({ path: request.path }));
            },
            isResponse: isKeetaStorageAnchorDeleteResponse
        });
        this.logger?.debug(`Delete request successful for path: ${request.path}`);
        return (response.deleted);
    }
    /**
     * Get (retrieve) an object by path.
     * Data is automatically decrypted from the EncryptedContainer.
     *
     * @param options.path - The storage path (e.g., "/user/<publicKey>/myfile.txt")
     * @param options.account - Optional account for signing and decryption (falls back to client account)
     *
     * @returns The decrypted data and mime-type, or null if not found
     *
     * @throws PrivateKeyRequired if no account with private key is available
     * @throws InvalidResponse if the stored data cannot be decrypted
     */
    async get(options) {
        const { path } = options;
        this.logger?.debug(`Getting object at path: ${path}`);
        const signerAccount = this.#resolveSignerAccount(options.account);
        try {
            const encodedData = await this.#makeBinaryGetRequest({
                path,
                account: signerAccount
            });
            // Decrypt the container and extract mimeType from encrypted payload
            const container = EncryptedContainer.fromEncryptedBuffer(encodedData, [signerAccount]);
            const plaintext = await container.getPlaintext();
            const { mimeType, content: data } = parseContainerPayload(plaintext);
            this.logger?.debug(`Get request successful for path: ${path}`);
            return ({
                data,
                mimeType
            });
        }
        catch (e) {
            if (Errors.DocumentNotFound.isInstance(e)) {
                return (null);
            }
            throw (e);
        }
    }
    /**
     * Get metadata for an object by path.
     *
     * @param options.path - The storage path (e.g., "/user/<publicKey>/myfile.txt")
     * @param options.account - Optional account for signing (falls back to client account)
     *
     * @returns The object metadata, or null if not found
     *
     * @throws PrivateKeyRequired if no account with private key is available
     */
    async getMetadata(options) {
        const { path } = options;
        this.logger?.debug(`Getting metadata at path: ${path}`);
        const signerAccount = this.#resolveSignerAccount(options.account);
        try {
            const response = await this.#makeRequest({
                method: 'GET',
                endpoint: 'metadata',
                account: signerAccount,
                pathSuffix: path,
                getSignedData: function () {
                    return (getKeetaStorageAnchorGetRequestSigningData({
                        path,
                        account: signerAccount.publicKeyString.get()
                    }));
                },
                isResponse: function (data) {
                    if (typeof data !== 'object' || data === null || !('ok' in data)) {
                        return (false);
                    }
                    if ('object' in data && typeof data.object === 'object' && data.object !== null) {
                        return (data.ok === true);
                    }
                    if ('error' in data && typeof data.error === 'string') {
                        return (data.ok === false);
                    }
                    return (false);
                }
            });
            this.logger?.debug(`Get metadata successful for path: ${path}`);
            return (response.object);
        }
        catch (e) {
            // Check if it's a "not found" error
            if (Errors.DocumentNotFound.isInstance(e)) {
                return (null);
            }
            throw (e);
        }
    }
    /**
     * Update metadata for an existing object without re-uploading data.
     *
     * @param options.path - The storage path
     * @param options.tags - New tags for the object
     * @param options.visibility - New visibility setting
     * @param options.account - Optional account for signing (falls back to client account)
     *
     * @returns The updated object metadata, or null if not found
     *
     * @throws PrivateKeyRequired if no account with private key is available
     */
    async updateMetadata(options) {
        const { path, tags, visibility } = options;
        this.logger?.debug(`Updating metadata at path: ${path}, visibility: ${visibility}, tags: [${tags.join(', ')}]`);
        this.#validateTags(tags);
        const signerAccount = this.#resolveSignerAccount(options.account);
        try {
            const response = await this.#makeRequest({
                method: 'PUT',
                endpoint: 'updateMetadata',
                account: signerAccount,
                pathSuffix: path,
                serializeRequest(body) {
                    return ({
                        tags: body.tags,
                        visibility: body.visibility,
                        ...(body.account ? { account: body.account.publicKeyString.get() } : {})
                    });
                },
                body: { tags, visibility, account: signerAccount },
                getSignedData: function () {
                    return (getKeetaStorageAnchorUpdateMetadataRequestSigningData({
                        path,
                        visibility,
                        tags
                    }));
                },
                isResponse: isKeetaStorageAnchorPutResponse
            });
            this.logger?.debug(`Update metadata successful for path: ${path}`);
            return (response.object);
        }
        catch (e) {
            if (Errors.DocumentNotFound.isInstance(e)) {
                return (null);
            }
            throw (e);
        }
    }
    /**
     * Put (create/update) an object.
     * Data is automatically wrapped in an EncryptedContainer.
     *
     * @param options.path - The storage path (e.g., "/user/<publicKey>/myfile.txt")
     * @param options.data - The binary data to store
     * @param options.mimeType - MIME type of the data (stored encrypted in the container)
     * @param options.tags - Optional plaintext tags for the object
     * @param options.visibility - Object visibility
     * @param options.account - Optional account for signing (falls back to client account)
     * @param options.anchorAccount - Anchor account for public object encryption (falls back to provider's anchor)
     *
     * @returns The created/updated object metadata
     *
     * @throws PrivateKeyRequired if no account with private key is available
     * @throws InvalidAnchorAccount if public visibility requires an anchor account that's not available
     */
    async put(options) {
        const { path, data, mimeType, tags, visibility, anchorAccount } = options;
        this.logger?.debug(`Putting object at path: ${path}`);
        const signerAccount = this.#resolveSignerAccount(options.account);
        const principals = [signerAccount];
        if (options.additionalPrincipals) {
            principals.push(...options.additionalPrincipals);
        }
        if (visibility === 'public') {
            const effectiveAnchor = anchorAccount ?? this.anchorAccount;
            if (!effectiveAnchor) {
                throw (new Errors.AccountRequired('anchorAccount is required for public visibility and no provider anchor account is available'));
            }
            principals.push(effectiveAnchor);
        }
        // Create payload with mimeType inside (encrypted)
        const payload = {
            mimeType,
            data: data.toString('base64')
        };
        const container = EncryptedContainer.fromPlaintext(JSON.stringify(payload), principals, { signer: signerAccount });
        const encodedBuffer = await container.getEncodedBuffer();
        const binaryData = arrayBufferLikeToBuffer(encodedBuffer);
        const response = await this.#makeBinaryPutRequest({
            path,
            data: binaryData,
            account: signerAccount,
            ...(visibility !== undefined && { visibility }),
            ...(tags !== undefined && { tags })
        });
        this.logger?.debug(`Put request successful for path: ${path}`);
        return (response.object);
    }
    /**
     * Search for objects matching criteria.
     *
     * @param options.criteria - Search criteria
     * @param options.pagination - Optional pagination settings
     * @param options.account - Optional account for signing (falls back to client account)
     *
     * @returns Search results with optional nextCursor for pagination
     */
    async search(options) {
        const { criteria, pagination } = options;
        this.logger?.debug('Searching for objects');
        const signerAccount = this.#resolveSignerAccount(options.account);
        const bodyToSend = {
            criteria,
            account: signerAccount
        };
        if (pagination !== undefined) {
            bodyToSend.pagination = pagination;
        }
        const response = await this.#makeRequest({
            method: 'POST',
            endpoint: 'search',
            account: signerAccount,
            serializeRequest(body) {
                const serialized = {
                    criteria: body.criteria
                };
                if (body.pagination !== undefined) {
                    serialized.pagination = body.pagination;
                }
                if (body.account !== undefined) {
                    serialized.account = body.account.assertAccount().publicKeyString.get();
                }
                return (serialized);
            },
            body: bodyToSend,
            getSignedData: function () {
                return (getKeetaStorageAnchorSearchRequestSigningData({
                    criteria,
                    ...(pagination !== undefined ? { pagination } : {})
                }));
            },
            isResponse: isKeetaStorageAnchorSearchResponse
        });
        this.logger?.debug(`Search returned ${response.results.length} results`);
        return ({
            results: response.results,
            ...(response.nextCursor !== undefined ? { nextCursor: response.nextCursor } : {})
        });
    }
    /**
     * Get quota status for the authenticated user.
     *
     * @param options.account - Optional account for signing (falls back to client account)
     *
     * @returns Current quota status
     */
    async getQuotaStatus(options) {
        this.logger?.debug('Getting quota status');
        const signerAccount = this.#resolveSignerAccount(options?.account);
        const response = await this.#makeRequest({
            method: 'GET',
            endpoint: 'quota',
            account: signerAccount,
            getSignedData: function () {
                return (getKeetaStorageAnchorQuotaRequestSigningData({}));
            },
            isResponse: isKeetaStorageAnchorQuotaResponse
        });
        this.logger?.debug('Quota status retrieved successfully');
        return (response.quota);
    }
    /**
     * Generate a pre-signed URL for public access to an object.
     * The URL is signed by the owner and has a limited lifetime.
     *
     * @param options.path - The storage path to the public object
     * @param options.ttl - TTL in seconds
     * @param options.account - Owner account for signing (falls back to client account, must have private key)
     *
     * @returns The pre-signed URL for unauthenticated access
     *
     * @throws PrivateKeyRequired if no account with private key is available
     * @throws OperationNotSupported if the public endpoint is not available
     */
    async getPublicUrl(options) {
        const { path } = options;
        const signerAccount = this.#resolveSignerAccount(options.account);
        const ttl = options.ttl ?? DEFAULT_SIGNED_URL_TTL_SECONDS;
        if (ttl <= 0) {
            throw (new Errors.InvariantViolation('TTL must be positive'));
        }
        const expiresAt = Math.floor(Date.now() / 1000) + ttl;
        // Sign the message with signer pubkey cryptographically bound
        const signerPubKey = signerAccount.publicKeyString.get();
        const signed = await SignData(signerAccount.assertAccount(), [path, expiresAt, signerPubKey]);
        // Get base URL from service info
        const operationInfo = await this.serviceInfo.operations.public;
        if (!operationInfo) {
            throw (new Errors.OperationNotSupported('public'));
        }
        // Construct the public URL using standard signed field convention
        const publicUrl = new URL(operationInfo.url().href);
        this.#appendPathToUrl(publicUrl, path);
        publicUrl.searchParams.set('expires', String(expiresAt));
        const finalUrl = addSignatureToURL(publicUrl, { signedField: signed, account: signerAccount.assertAccount() });
        return (finalUrl.toString());
    }
    /**
     * Create a session.
     */
    beginSession(config) {
        return (new KeetaStorageAnchorSession(this, config));
    }
    /**
     * Execute a function within a session scope.
     */
    async withSession(config, fn) {
        const session = this.beginSession(config);
        return (await fn(session));
    }
    /**
     * Get a contacts client bound to the given account.
     */
    getContactsClient(config) {
        const session = this.beginSession({ account: config.account, workingDirectory: config.basePath });
        return (new StorageContactsClient({ session, logger: this.logger }));
    }
    /**
     * Get an icons client bound to the given account.
     */
    getIconsClient(config) {
        const session = this.beginSession({
            account: config.account,
            workingDirectory: config.basePath,
            defaultVisibility: 'public'
        });
        return (new StorageIconsClient({ session, logger: this.logger }));
    }
}
class KeetaStorageAnchorClient extends KeetaStorageAnchorBase {
    resolver;
    id;
    #signer;
    #account;
    constructor(client, config = {}) {
        super({ client, logger: config.logger });
        this.resolver = config.resolver ?? getDefaultResolver(client, config);
        this.id = config.id ?? crypto.randomUUID();
        if (config.signer) {
            this.#signer = config.signer;
        }
        else if ('signer' in client && client.signer !== null) {
            this.#signer = client.signer;
        }
        else if ('account' in client && client.account.hasPrivateKey) {
            this.#signer = client.account;
        }
        else {
            throw (new Errors.SignerRequired());
        }
        if (config.account) {
            this.#account = config.account;
        }
        else if ('account' in client) {
            this.#account = client.account;
        }
        else {
            throw (new Errors.AccountRequired());
        }
    }
    async #lookup() {
        const endpoints = await getEndpoints(this.resolver, this.logger);
        if (endpoints === null) {
            return (null);
        }
        const providers = Object.entries(endpoints).map(([id, serviceInfo]) => {
            return (new KeetaStorageAnchorProvider(serviceInfo, id, this));
        });
        return (providers);
    }
    /**
     * Get all available storage providers
     */
    async getProviders() {
        return (await this.#lookup());
    }
    /**
     * Get a specific provider by ID
     */
    async getProviderByID(providerID) {
        const providers = await this.#lookup();
        if (!providers) {
            return (null);
        }
        const provider = providers.find(function (p) {
            return (p.providerID === providerID);
        });
        return (provider ?? null);
    }
    async #withProvider(fn) {
        const providers = await this.getProviders();
        const provider = providers?.[0];
        if (!provider) {
            return (null);
        }
        return (fn(provider));
    }
    /**
     * Get a contacts client bound to the given account.
     * Resolves the first available provider and constructs a StorageContactsClient.
     */
    async getContactsClient(config) {
        return (await this.#withProvider(function (provider) { return (provider.getContactsClient(config)); }));
    }
    /**
     * Get an icons client bound to the given account.
     * Resolves the first available provider and constructs a StorageIconsClient.
     */
    async getIconsClient(config) {
        return (await this.#withProvider(function (provider) { return (provider.getIconsClient(config)); }));
    }
    /** @internal */
    _internals(accessToken) {
        if (accessToken !== KeetaStorageAnchorClientAccessToken) {
            throw (new Errors.InvariantViolation('invalid internal access token'));
        }
        return ({
            resolver: this.resolver,
            logger: this.logger,
            client: this.client,
            signer: this.#signer,
            account: this.#account
        });
    }
}
export default KeetaStorageAnchorClient;
//# sourceMappingURL=client.js.map