"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 (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __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.QuirrelClient = exports.cron = exports.timezone = exports.cronExpression = void 0;
const config = __importStar(require("./config"));
const z = __importStar(require("zod"));
const secure_e2ee_1 = __importDefault(require("secure-e2ee"));
const secure_webhooks_1 = require("secure-webhooks");
const ms_1 = __importDefault(require("ms"));
const cross_fetch_1 = __importDefault(require("cross-fetch"));
const fetch_retry_1 = __importDefault(require("@vercel/fetch-retry"));
const package_json_1 = __importDefault(require("../../package.json"));
const EnhancedJSON = __importStar(require("./enhanced-json"));
const is_valid_cron_1 = require("../shared/is-valid-cron");
const repeat_1 = require("../shared/repeat");
const defaultLogger = {
    receivedJob: (route, data) => console.log(`Received job to ${route}`, data),
    processingError: (route, data, error) => console.error(`Error in job at ${route}`, data, error),
};
const vercelMs = z
    .string()
    .regex(/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i, "Please provide a valid time span, according to https://github.com/vercel/ms");
const timeDuration = (fieldName = "duration") => z.union([
    z.number().min(1, { message: `${fieldName} must be positive` }),
    vercelMs,
]);
exports.cronExpression = z
    .string()
    .refine(is_valid_cron_1.isValidCronExpression, "Please provide a valid Cron expression. See https://github.com/harrisiirak/cron-parser for reference");
exports.timezone = z
    .string()
    .refine(repeat_1.isValidTimezone, "Please provide a valid IANA timezone.");
exports.cron = z.union([
    exports.cronExpression,
    z.tuple([exports.cronExpression, exports.timezone]),
]);
const EnqueueJobOptionsSchema = z.object({
    id: z.string().optional(),
    exclusive: z.boolean().optional(),
    override: z.boolean().optional(),
    retry: z.array(timeDuration("retry")).min(1).max(10).optional(),
    delay: timeDuration("delay").optional(),
    runAt: z
        .date()
        .refine((date) => Date.now() <= +date, {
        message: "runAt must not be in the past",
    })
        .optional(),
    repeat: z
        .object({
        every: timeDuration("every").optional(),
        times: z.number().nonnegative().optional(),
        cron: exports.cron.optional(),
    })
        .optional(),
});
function parseDuration(value) {
    if (!value) {
        return undefined;
    }
    if (typeof value === "string") {
        return (0, ms_1.default)(value);
    }
    return value;
}
function runAtToDelay(value) {
    return +value - Date.now();
}
function getEncryptor(encryptionSecret, oldSecrets = []) {
    if (!encryptionSecret) {
        return undefined;
    }
    return new secure_e2ee_1.default(encryptionSecret, [encryptionSecret, ...oldSecrets]);
}
function getAuthHeaders(token) {
    if (!token) {
        return {};
    }
    return { Authorization: `Bearer ${token}` };
}
class QuirrelClient {
    constructor(args) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
        this.fetchAgainstCurrent = async (input, init) => {
            return await this.fetch(this.baseUrl + input, {
                ...init,
                headers: {
                    ...this.defaultHeaders,
                    ...init === null || init === void 0 ? void 0 : init.headers,
                },
            });
        };
        this.fetchAgainstOld = async (input, init) => {
            if (!this.oldBaseUrl) {
                throw new Error("only call when oldBaseUrl is defined");
            }
            return await this.fetch(this.oldBaseUrl + input, {
                ...init,
                headers: {
                    ...this.oldDefaultHeaders,
                    ...init === null || init === void 0 ? void 0 : init.headers,
                },
            });
        };
        this.fetchAndRetryNotFound = async (input, init) => {
            const res = await this.fetchAgainstCurrent(input, init);
            if (!this.oldBaseUrl) {
                return res;
            }
            if (res.status !== 404) {
                return res;
            }
            return await this.fetchAgainstOld(input, init);
        };
        this.handler = args.handler;
        this.defaultJobOptions = args.options;
        const token = (_b = (_a = args.config) === null || _a === void 0 ? void 0 : _a.token) !== null && _b !== void 0 ? _b : config.getQuirrelToken();
        this.defaultHeaders = {
            ...getAuthHeaders(token),
            "X-QuirrelClient-Version": package_json_1.default.version,
        };
        this.logger = (_d = (_c = args.options) === null || _c === void 0 ? void 0 : _c.logger) !== null && _d !== void 0 ? _d : defaultLogger;
        const quirrelBaseUrl = (_f = (_e = args.config) === null || _e === void 0 ? void 0 : _e.quirrelBaseUrl) !== null && _f !== void 0 ? _f : config.getQuirrelBaseUrl();
        this.applicationBaseUrl = config.withoutTrailingSlash(config.prefixWithProtocol((_h = (_g = args.config) === null || _g === void 0 ? void 0 : _g.applicationBaseUrl) !== null && _h !== void 0 ? _h : config.getApplicationBaseUrl()));
        this.quirrelBaseUrl = quirrelBaseUrl;
        this.token = (_k = (_j = args.config) === null || _j === void 0 ? void 0 : _j.token) !== null && _k !== void 0 ? _k : config.getQuirrelToken();
        this.route = config.withoutLeadingSlash(args.route);
        this.quirrelOldBaseUrl =
            (_m = (_l = args.config) === null || _l === void 0 ? void 0 : _l.quirrelOldBaseUrl) !== null && _m !== void 0 ? _m : config.getOldQuirrelBaseUrl();
        this.quirrelOldToken = (_p = (_o = args.config) === null || _o === void 0 ? void 0 : _o.oldToken) !== null && _p !== void 0 ? _p : config.getOldQuirrelToken();
        this.oldDefaultHeaders = {
            ...getAuthHeaders(this.quirrelOldToken),
            "X-QuirrelClient-Version": package_json_1.default.version,
        };
        this.encryptor = getEncryptor((_r = (_q = args.config) === null || _q === void 0 ? void 0 : _q.encryptionSecret) !== null && _r !== void 0 ? _r : config.getEncryptionSecret(), (_u = (_t = (_s = args.config) === null || _s === void 0 ? void 0 : _s.oldSecrets) !== null && _t !== void 0 ? _t : config.getOldEncryptionSecrets()) !== null && _u !== void 0 ? _u : undefined);
        this.catchDecryptionErrors = args.catchDecryptionErrors;
        this.fetch = (_v = args.fetch) !== null && _v !== void 0 ? _v : (0, fetch_retry_1.default)(cross_fetch_1.default);
        this.signaturePublicKey =
            (_x = (_w = args.config) === null || _w === void 0 ? void 0 : _w.signaturePublicKey) !== null && _x !== void 0 ? _x : config.getSignaturePublicKey();
    }
    get baseUrl() {
        return (this.quirrelBaseUrl +
            "/queues/" +
            encodeURIComponent(this.applicationBaseUrl + "/" + this.route));
    }
    get oldBaseUrl() {
        if (!this.quirrelOldBaseUrl) {
            return undefined;
        }
        return (this.quirrelOldBaseUrl +
            "/queues/" +
            encodeURIComponent(this.applicationBaseUrl + "/" + this.route));
    }
    async makeRequest(uri, init) {
        return await this.fetch(this.quirrelBaseUrl + uri, {
            credentials: "omit",
            ...init,
            headers: {
                ...this.defaultHeaders,
                ...init === null || init === void 0 ? void 0 : init.headers,
            },
        });
    }
    async payloadAndOptionsToBody(payload, options) {
        var _a, _b, _c, _d, _e, _f, _g, _h;
        if (typeof payload === "undefined") {
            throw new Error("Passing `undefined` as Payload is not allowed");
        }
        if (options.repeat && ((_a = options.retry) === null || _a === void 0 ? void 0 : _a.length)) {
            throw new Error("retry and repeat cannot be used together");
        }
        if (options.override && !options.id) {
            throw new Error("override requires id");
        }
        options = EnqueueJobOptionsSchema.parse(options);
        let delay = parseDuration(options.delay);
        if ("runAt" in options && options.runAt) {
            delay = runAtToDelay(options.runAt);
        }
        if (options.repeat) {
            options.repeat.every = parseDuration((_b = options.repeat) === null || _b === void 0 ? void 0 : _b.every);
        }
        let stringifiedBody = EnhancedJSON.stringify(payload);
        if (this.encryptor) {
            stringifiedBody = await this.encryptor.encrypt(stringifiedBody);
        }
        let cron = {};
        if ((_c = options.repeat) === null || _c === void 0 ? void 0 : _c.cron) {
            if (typeof options.repeat.cron === "string") {
                cron = { cron: options.repeat.cron };
            }
            else {
                cron = {
                    cron: options.repeat.cron[0],
                    cronTimezone: options.repeat.cron[1],
                };
            }
        }
        return {
            body: stringifiedBody,
            delay,
            id: options.id,
            repeat: options.repeat
                ? {
                    ...options.repeat,
                    ...cron,
                }
                : undefined,
            retry: (_f = ((_d = options.retry) !== null && _d !== void 0 ? _d : (_e = this.defaultJobOptions) === null || _e === void 0 ? void 0 : _e.retry)) === null || _f === void 0 ? void 0 : _f.map(parseDuration),
            override: options.override,
            exclusive: (_g = options.exclusive) !== null && _g !== void 0 ? _g : (_h = this.defaultJobOptions) === null || _h === void 0 ? void 0 : _h.exclusive,
        };
    }
    /**
     * Enqueue a job to the specified endpoint.
     * @param options job options
     */
    async enqueue(payload, options = {}) {
        const body = await this.payloadAndOptionsToBody(payload, options);
        if (this.oldBaseUrl && !options.override && options.id) {
            const res = await this.fetchAgainstOld(`/${encodeURIComponent(options.id)}`);
            if (res.status === 200) {
                return await this.toJob(await res.json());
            }
        }
        const res = await this.fetch(this.baseUrl, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                ...this.defaultHeaders,
            },
            credentials: "omit",
            body: JSON.stringify(body),
        });
        if (res.status === 201) {
            if (this.oldBaseUrl && options.override && options.id) {
                await this.fetchAgainstOld(`/${encodeURIComponent(options.id)}`, {
                    method: "DELETE",
                });
            }
            return await this.toJob(await res.json());
        }
        throw new Error(`Unexpected response while trying to enqueue "${JSON.stringify(body)}" to ${this.applicationBaseUrl}: ${await res.text()}`);
    }
    /**
     * Enqueue multiple jobs
     */
    async enqueueMany(jobs) {
        const body = await Promise.all(jobs.map(({ payload, options = {} }) => this.payloadAndOptionsToBody(payload, options)));
        const res = await this.fetch(this.baseUrl + "/batch", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                ...this.defaultHeaders,
            },
            credentials: "omit",
            body: JSON.stringify(body),
        });
        if (res.status === 201) {
            const response = (await res.json());
            return await Promise.all(response.map((job) => this.toJob(job)));
        }
        throw new Error(`Unexpected response while trying to enqueue "${body}" to ${this.applicationBaseUrl}: ${await res.text()}`);
    }
    async decryptAndDecodeBody(body) {
        if (this.encryptor) {
            if (body === "null") {
                return null;
            }
            if (this.catchDecryptionErrors) {
                try {
                    body = await this.encryptor.decrypt(body);
                }
                catch (error) {
                    this.catchDecryptionErrors(error);
                    return body;
                }
            }
            else {
                body = await this.encryptor.decrypt(body);
            }
        }
        if (body === "") {
            return null;
        }
        return EnhancedJSON.parse(body);
    }
    async toJob(dto) {
        return {
            ...dto,
            body: await this.decryptAndDecodeBody(dto.body),
            runAt: new Date(dto.runAt),
            delete: () => this.delete(dto.id),
            invoke: () => this.invoke(dto.id),
        };
    }
    /**
     * Iterate through scheduled jobs.
     * @example
     * for await (const jobs of queue.get()) {
     *   // do smth
     * }
     */
    async *get() {
        let cursor = 0;
        while (cursor !== null) {
            const res = await this.fetchAgainstCurrent("?cursor=" + cursor);
            const json = await res.json();
            const { cursor: newCursor, jobs } = json;
            cursor = newCursor;
            yield await Promise.all(jobs.map((dto) => this.toJob(dto)));
        }
        if (this.quirrelOldBaseUrl) {
            let cursor = 0;
            while (cursor !== null) {
                const res = await this.fetchAgainstOld("?cursor=" + cursor);
                const json = await res.json();
                const { cursor: newCursor, jobs } = json;
                cursor = newCursor;
                yield await Promise.all(jobs.map((dto) => this.toJob(dto)));
            }
        }
    }
    /**
     * Get a specific job.
     * @returns null if no job was found.
     */
    async getById(id) {
        const res = await this.fetchAndRetryNotFound("/" + encodeURIComponent(id));
        if (res.status === 404) {
            return null;
        }
        if (res.status === 200) {
            return await this.toJob(await res.json());
        }
        throw new Error("Unexpected response: " + (await res.text()));
    }
    /**
     * Schedule a job for immediate execution.
     * @returns false if job could not be found.
     */
    async invoke(id) {
        const res = await this.fetchAndRetryNotFound("/" + encodeURIComponent(id), {
            method: "POST",
        });
        if (res.status === 404) {
            return false;
        }
        if (res.status === 204) {
            return true;
        }
        throw new Error("Unexpected response: " + (await res.text()));
    }
    /**
     * Delete a job, preventing it from executing.
     * @returns false if job could not be found.
     */
    async delete(id) {
        const res = await this.fetchAndRetryNotFound("/" + encodeURIComponent(id), {
            method: "DELETE",
        });
        if (res.status === 404) {
            return false;
        }
        if (res.status === 204) {
            return true;
        }
        throw new Error("Unexpected response: " + (await res.text()));
    }
    isValidSignature(body, signature) {
        var _a;
        if (this.signaturePublicKey) {
            return secure_webhooks_1.asymmetric.verify(body, this.signaturePublicKey, signature);
        }
        else {
            return (secure_webhooks_1.symmetric.verify(body, this.token, signature) ||
                secure_webhooks_1.symmetric.verify(body, (_a = this.quirrelOldToken) !== null && _a !== void 0 ? _a : "", signature));
        }
    }
    async respondTo(body, headers) {
        var _a;
        if (process.env.NODE_ENV === "production") {
            const signature = headers["x-quirrel-signature"];
            if (typeof signature !== "string") {
                return {
                    status: 401,
                    headers: {},
                    body: "Signature missing",
                };
            }
            if (!this.isValidSignature(body, signature)) {
                return {
                    status: 401,
                    headers: {},
                    body: "Signature invalid",
                };
            }
        }
        const payload = await this.decryptAndDecodeBody(body);
        const { id, count, retry, nextRepetition, exclusive } = JSON.parse((_a = headers["x-quirrel-meta"]) !== null && _a !== void 0 ? _a : "{}");
        this.logger.receivedJob(this.route, payload);
        try {
            await this.handler(payload, {
                id,
                count,
                retry,
                nextRepetition,
                exclusive,
            });
            return {
                status: 200,
                headers: {},
                body: "OK",
            };
        }
        catch (error) {
            this.logger.processingError(this.route, payload, error);
            return {
                status: 500,
                headers: {},
                body: String(error),
            };
        }
    }
}
exports.QuirrelClient = QuirrelClient;
//# sourceMappingURL=index.js.map