mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-06-28 19:02:03 +08:00
feat: add SEND_MAIL delivery and quota controls (#986)
* feat: add SEND_MAIL delivery and quota controls * test: cover -1 unlimited runtime for send mail quota Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: split send limit validation and save * refactor: move send limit counters to settings * fix: polish send mail limit review follow-ups * docs: note SEND_MAIL breaking change * test: align send mail limit e2e with new messages * fix: address review follow-ups * fix: harden admin send mail handlers --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,12 @@ import mail_webhook_settings from './mail_webhook_settings'
|
||||
import oauth2_settings from './oauth2_settings'
|
||||
import worker_config from './worker_config'
|
||||
import admin_mail_api from './admin_mail_api'
|
||||
import { sendMailbyAdmin } from './send_mail'
|
||||
import { sendMailbyAdmin, sendMailByBindingAdmin } from './send_mail'
|
||||
import {
|
||||
getSendMailLimitConfig,
|
||||
getSendMailLimitConfigToSave,
|
||||
validateSendMailLimitConfig
|
||||
} from '../mails_api/send_mail_limit_utils'
|
||||
import db_api from './db_api'
|
||||
import ip_blacklist_settings from './ip_blacklist_settings'
|
||||
import ai_extract_settings from './ai_extract_settings'
|
||||
@@ -341,6 +346,7 @@ api.get('/admin/account_settings', async (c) => {
|
||||
const noLimitSendAddressList = await getJsonSetting(c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY);
|
||||
const addressCreationSettings = await getAddressCreationSettings(c);
|
||||
const addressCreationSubdomainMatchStatus = await getAddressCreationSubdomainMatchStatus(c, addressCreationSettings);
|
||||
const sendMailLimitConfig = await getSendMailLimitConfig(c);
|
||||
return c.json({
|
||||
blockList: blockList || [],
|
||||
sendBlockList: sendBlockList || [],
|
||||
@@ -352,6 +358,7 @@ api.get('/admin/account_settings', async (c) => {
|
||||
? { enableSubdomainMatch: addressCreationSettings.enableSubdomainMatch }
|
||||
: {},
|
||||
addressCreationSubdomainMatchStatus,
|
||||
sendMailLimitConfig,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -364,7 +371,8 @@ api.post('/admin/account_settings', async (c) => {
|
||||
/** @type {{ blockList: Array<string>, sendBlockList: Array<string> }} */
|
||||
const {
|
||||
blockList, sendBlockList, noLimitSendAddressList,
|
||||
verifiedAddressList, fromBlockList, emailRuleSettings, addressCreationSettings
|
||||
verifiedAddressList, fromBlockList, emailRuleSettings, addressCreationSettings,
|
||||
sendMailLimitConfig
|
||||
} = await c.req.json();
|
||||
if (!blockList || !sendBlockList || !verifiedAddressList) {
|
||||
return c.text(msgs.InvalidInputMsg, 400)
|
||||
@@ -380,6 +388,12 @@ api.post('/admin/account_settings', async (c) => {
|
||||
if (fromBlockList?.length > 0 && !c.env.KV) {
|
||||
return c.text(msgs.EnableKVMsg, 400)
|
||||
}
|
||||
if (sendMailLimitConfig && !validateSendMailLimitConfig(sendMailLimitConfig)) {
|
||||
return c.text(msgs.InvalidInputMsg, 400)
|
||||
}
|
||||
const sendMailLimitConfigToSave = sendMailLimitConfig
|
||||
? getSendMailLimitConfigToSave(sendMailLimitConfig)
|
||||
: null;
|
||||
await saveSetting(
|
||||
c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY,
|
||||
JSON.stringify(blockList)
|
||||
@@ -417,6 +431,12 @@ api.post('/admin/account_settings', async (c) => {
|
||||
)
|
||||
}
|
||||
}
|
||||
if (sendMailLimitConfigToSave) {
|
||||
await saveSetting(
|
||||
c, CONSTANTS.SEND_MAIL_LIMIT_CONFIG_KEY,
|
||||
JSON.stringify(sendMailLimitConfigToSave)
|
||||
)
|
||||
}
|
||||
return c.json({
|
||||
success: true
|
||||
})
|
||||
@@ -459,6 +479,7 @@ api.get("/admin/worker/configs", worker_config.getConfig);
|
||||
|
||||
// send mail by admin
|
||||
api.post("/admin/send_mail", sendMailbyAdmin);
|
||||
api.post("/admin/send_mail_by_binding", sendMailByBindingAdmin);
|
||||
|
||||
// db api
|
||||
api.get('admin/db_version', db_api.getVersion);
|
||||
|
||||
@@ -1,21 +1,89 @@
|
||||
import { Context } from "hono";
|
||||
import i18n from "../i18n";
|
||||
import { sendMail } from "../mails_api/send_mail_api";
|
||||
import { ensureSendMailLimit, increaseSendMailLimitCount } from "../mails_api/send_mail_limit_utils";
|
||||
|
||||
const getAdminSendMailErrorMessage = (
|
||||
msgs: ReturnType<typeof i18n.getMessagesbyContext>,
|
||||
error: unknown
|
||||
): string => {
|
||||
const message = error instanceof Error ? error.message : "";
|
||||
return Object.values(msgs).includes(message)
|
||||
? message
|
||||
: msgs.OperationFailedMsg;
|
||||
}
|
||||
|
||||
export const sendMailbyAdmin = async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
let reqJson;
|
||||
try {
|
||||
reqJson = await c.req.json();
|
||||
} catch (e) {
|
||||
console.error("Admin send_mail invalid json", e);
|
||||
return c.text(msgs.InvalidInputMsg, 400)
|
||||
}
|
||||
const {
|
||||
from_name, from_mail,
|
||||
to_mail, to_name,
|
||||
subject, content, is_html
|
||||
} = await c.req.json();
|
||||
await sendMail(c, from_mail, {
|
||||
from_name: from_name,
|
||||
to_name: to_name,
|
||||
to_mail: to_mail,
|
||||
subject: subject,
|
||||
content: content,
|
||||
is_html: is_html,
|
||||
}, {
|
||||
isAdmin: true
|
||||
})
|
||||
} = reqJson;
|
||||
try {
|
||||
await sendMail(c, from_mail, {
|
||||
from_name: from_name,
|
||||
to_name: to_name,
|
||||
to_mail: to_mail,
|
||||
subject: subject,
|
||||
content: content,
|
||||
is_html: is_html,
|
||||
}, {
|
||||
isAdmin: true
|
||||
})
|
||||
} catch (e) {
|
||||
console.error("Admin send_mail failed", e);
|
||||
return c.text(getAdminSendMailErrorMessage(msgs, e), 400)
|
||||
}
|
||||
return c.json({ status: "ok" });
|
||||
}
|
||||
|
||||
export const sendMailByBindingAdmin = async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!c.env.SEND_MAIL) {
|
||||
return c.text(msgs.EnableSendMailMsg, 400)
|
||||
}
|
||||
let reqJson;
|
||||
try {
|
||||
reqJson = await c.req.json();
|
||||
} catch (e) {
|
||||
console.error("Admin raw send_mail invalid json", e);
|
||||
return c.text(msgs.InvalidInputMsg, 400)
|
||||
}
|
||||
const {
|
||||
from, to, subject,
|
||||
html, text,
|
||||
cc, bcc, replyTo,
|
||||
attachments, headers,
|
||||
} = reqJson;
|
||||
if (!from || !to || !subject || (!html && !text)) {
|
||||
return c.text(msgs.InvalidInputMsg, 400)
|
||||
}
|
||||
try {
|
||||
await ensureSendMailLimit(c);
|
||||
await c.env.SEND_MAIL.send({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
...(html ? { html } : {}),
|
||||
...(text ? { text } : {}),
|
||||
...(cc ? { cc } : {}),
|
||||
...(bcc ? { bcc } : {}),
|
||||
...(replyTo ? { replyTo } : {}),
|
||||
...(attachments && attachments.length ? { attachments } : {}),
|
||||
...(headers ? { headers } : {}),
|
||||
});
|
||||
await increaseSendMailLimitCount(c);
|
||||
} catch (e) {
|
||||
console.error("Admin raw send_mail failed", e);
|
||||
return c.text(getAdminSendMailErrorMessage(msgs, e), 400)
|
||||
}
|
||||
return c.json({ status: "ok" });
|
||||
}
|
||||
|
||||
@@ -26,4 +26,6 @@ export const CONSTANTS = {
|
||||
WEBHOOK_KV_USER_SETTINGS_KEY: "temp-mail-webhook-user-settings",
|
||||
EMAIL_KV_BLACK_LIST: "temp-mail-email-black-list",
|
||||
WEBHOOK_KV_ADMIN_MAIL_SETTINGS_KEY: "temp-mail-webhook-admin-mail-settings",
|
||||
SEND_MAIL_LIMIT_COUNT_KEY_PREFIX: "send_mail_limit_count:",
|
||||
SEND_MAIL_LIMIT_CONFIG_KEY: "send_mail_limit_config",
|
||||
}
|
||||
|
||||
@@ -71,7 +71,9 @@ const messages: LocaleMessages = {
|
||||
ContentEmptyMsg: "Content is empty",
|
||||
AlreadyRequestedMsg: "Already requested",
|
||||
EnableResendOrSmtpMsg: "Please enable resend or smtp for this domain",
|
||||
EnableResendOrSmtpWithVerifiedMsg: "Please enable resend or smtp for this domain, or add recipient to verified address list",
|
||||
EnableResendOrSmtpOrSendMailMsg: "Please enable resend, smtp or SEND_MAIL for this domain",
|
||||
ServerSendMailDailyLimitMsg: "Server daily send quota has been reached",
|
||||
ServerSendMailMonthlyLimitMsg: "Server monthly send quota has been reached",
|
||||
InvalidToMailMsg: "Invalid recipient address",
|
||||
|
||||
// Admin related
|
||||
|
||||
@@ -69,7 +69,9 @@ export type LocaleMessages = {
|
||||
ContentEmptyMsg: string
|
||||
AlreadyRequestedMsg: string
|
||||
EnableResendOrSmtpMsg: string
|
||||
EnableResendOrSmtpWithVerifiedMsg: string
|
||||
EnableResendOrSmtpOrSendMailMsg: string
|
||||
ServerSendMailDailyLimitMsg: string
|
||||
ServerSendMailMonthlyLimitMsg: string
|
||||
InvalidToMailMsg: string
|
||||
|
||||
// Admin related
|
||||
|
||||
@@ -71,7 +71,9 @@ const messages: LocaleMessages = {
|
||||
ContentEmptyMsg: "内容不能为空",
|
||||
AlreadyRequestedMsg: "已经申请过了",
|
||||
EnableResendOrSmtpMsg: "请先为此域名启用 resend 或 smtp",
|
||||
EnableResendOrSmtpWithVerifiedMsg: "请先为此域名启用 resend 或 smtp,或将收件人添加到已验证地址列表",
|
||||
EnableResendOrSmtpOrSendMailMsg: "请先为此域名启用 resend、smtp 或 SEND_MAIL",
|
||||
ServerSendMailDailyLimitMsg: "服务器今日发信次数已达上限",
|
||||
ServerSendMailMonthlyLimitMsg: "服务器本月发信次数已达上限",
|
||||
InvalidToMailMsg: "收件人地址无效",
|
||||
|
||||
// Admin related
|
||||
|
||||
@@ -6,9 +6,12 @@ import { WorkerMailer, WorkerMailerOptions } from 'worker-mailer';
|
||||
|
||||
import i18n from '../i18n';
|
||||
import { CONSTANTS } from '../constants'
|
||||
import { getJsonSetting, getDomains, getIntValue, getBooleanValue, getStringValue, getJsonObjectValue, getSplitStringListValue } from '../utils';
|
||||
import {
|
||||
getJsonSetting, getDomains, getIntValue, getBooleanValue, getJsonObjectValue, getSplitStringListValue
|
||||
} from '../utils';
|
||||
import { GeoData } from '../models'
|
||||
import { handleListQuery, updateAddressUpdatedAt } from '../common'
|
||||
import { ensureSendMailLimit, increaseSendMailLimitCount } from './send_mail_limit_utils';
|
||||
|
||||
|
||||
export const api = new Hono<HonoCustomType>()
|
||||
@@ -63,6 +66,25 @@ export const sendMailToVerifyAddress = async (
|
||||
await c.env.SEND_MAIL.send(message);
|
||||
}
|
||||
|
||||
export const sendMailByBinding = async (
|
||||
c: Context<HonoCustomType>, address: string,
|
||||
reqJson: {
|
||||
from_name: string, to_mail: string, to_name: string,
|
||||
subject: string, content: string, is_html: boolean
|
||||
}
|
||||
): Promise<void> => {
|
||||
const {
|
||||
from_name, to_mail, to_name,
|
||||
subject, content, is_html
|
||||
} = reqJson;
|
||||
await c.env.SEND_MAIL.send({
|
||||
from: from_name ? { email: address, name: from_name } : address,
|
||||
to: to_name ? [`${to_name} <${to_mail}>`] : [to_mail],
|
||||
subject,
|
||||
...(is_html ? { html: content } : { text: content }),
|
||||
});
|
||||
}
|
||||
|
||||
const sendMailByResend = async (
|
||||
c: Context<HonoCustomType>, address: string,
|
||||
reqJson: {
|
||||
@@ -173,6 +195,7 @@ export const sendMail = async (
|
||||
if (!content) {
|
||||
throw new Error(msgs.ContentEmptyMsg)
|
||||
}
|
||||
await ensureSendMailLimit(c);
|
||||
|
||||
// send to verified address list, do not update balance
|
||||
const resendEnabled = c.env.RESEND_TOKEN || c.env[
|
||||
@@ -202,12 +225,13 @@ export const sendMail = async (
|
||||
else if (smtpConfig) {
|
||||
await sendMailBySmtp(c, address, reqJson, smtpConfig);
|
||||
}
|
||||
else {
|
||||
if (c.env.SEND_MAIL) {
|
||||
throw new Error(`${msgs.EnableResendOrSmtpWithVerifiedMsg} (${mailDomain})`);
|
||||
}
|
||||
throw new Error(`${msgs.EnableResendOrSmtpMsg} (${mailDomain})`);
|
||||
else if (c.env.SEND_MAIL) {
|
||||
await sendMailByBinding(c, address, reqJson);
|
||||
}
|
||||
else {
|
||||
throw new Error(`${msgs.EnableResendOrSmtpOrSendMailMsg} (${mailDomain})`);
|
||||
}
|
||||
await increaseSendMailLimitCount(c);
|
||||
|
||||
// update balance
|
||||
if (!sendByVerifiedAddressList && needCheckBalance) {
|
||||
|
||||
193
worker/src/mails_api/send_mail_limit_utils.ts
Normal file
193
worker/src/mails_api/send_mail_limit_utils.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { Context } from "hono";
|
||||
import i18n from "../i18n";
|
||||
import { SendMailLimitConfig } from "../models";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getJsonObjectValue, getSetting } from "../utils";
|
||||
|
||||
class SendMailLimitError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
const parseLimitValue = (value: unknown): number | null => {
|
||||
if (value === null || typeof value === "undefined") {
|
||||
return null;
|
||||
}
|
||||
if (!Number.isInteger(value) || (value as number) < -1) {
|
||||
return null;
|
||||
}
|
||||
return value as number;
|
||||
}
|
||||
|
||||
const isValidLimitValue = (value: number | null): boolean => {
|
||||
return value === -1 || (value !== null && value >= 0);
|
||||
}
|
||||
|
||||
const parseSendMailLimitConfig = (value: unknown): SendMailLimitConfig | null => {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const config = value as Record<string, unknown>;
|
||||
if (typeof config.dailyEnabled !== "boolean" || typeof config.monthlyEnabled !== "boolean") {
|
||||
return null;
|
||||
}
|
||||
const dailyLimit = parseLimitValue(config.dailyLimit);
|
||||
const monthlyLimit = parseLimitValue(config.monthlyLimit);
|
||||
const monthlyValid = config.monthlyEnabled
|
||||
? isValidLimitValue(monthlyLimit)
|
||||
: (config.monthlyLimit === null || typeof config.monthlyLimit === "undefined" || monthlyLimit !== null);
|
||||
const dailyValid = config.dailyEnabled
|
||||
? isValidLimitValue(dailyLimit)
|
||||
: (config.dailyLimit === null || typeof config.dailyLimit === "undefined" || dailyLimit !== null);
|
||||
if (!dailyValid || !monthlyValid) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
dailyEnabled: config.dailyEnabled,
|
||||
monthlyEnabled: config.monthlyEnabled,
|
||||
dailyLimit,
|
||||
monthlyLimit,
|
||||
};
|
||||
}
|
||||
|
||||
export const validateSendMailLimitConfig = (value: unknown): boolean => {
|
||||
return !!parseSendMailLimitConfig(value);
|
||||
}
|
||||
|
||||
export const getSendMailLimitConfigToSave = (
|
||||
value: unknown
|
||||
): SendMailLimitConfig | null => {
|
||||
const sendMailLimitConfig = parseSendMailLimitConfig(value);
|
||||
if (!sendMailLimitConfig) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
dailyEnabled: sendMailLimitConfig.dailyEnabled,
|
||||
monthlyEnabled: sendMailLimitConfig.monthlyEnabled,
|
||||
dailyLimit: sendMailLimitConfig.dailyEnabled ? sendMailLimitConfig.dailyLimit : null,
|
||||
monthlyLimit: sendMailLimitConfig.monthlyEnabled ? sendMailLimitConfig.monthlyLimit : null,
|
||||
};
|
||||
}
|
||||
|
||||
export const getSendMailLimitConfig = async (
|
||||
c: Context<HonoCustomType>
|
||||
): Promise<SendMailLimitConfig | null> => {
|
||||
return getSendMailLimitConfigToSave(getJsonObjectValue<SendMailLimitConfig>(
|
||||
await getSetting(c, CONSTANTS.SEND_MAIL_LIMIT_CONFIG_KEY)
|
||||
));
|
||||
}
|
||||
|
||||
const getDailyCountKey = (date: Date = new Date()): string => {
|
||||
const yyyy = date.getUTCFullYear();
|
||||
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
const dd = String(date.getUTCDate()).padStart(2, "0");
|
||||
return `${CONSTANTS.SEND_MAIL_LIMIT_COUNT_KEY_PREFIX}daily:${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
const getMonthlyCountKey = (date: Date = new Date()): string => {
|
||||
const yyyy = date.getUTCFullYear();
|
||||
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
return `${CONSTANTS.SEND_MAIL_LIMIT_COUNT_KEY_PREFIX}monthly:${yyyy}-${mm}`;
|
||||
}
|
||||
|
||||
const getCount = async (
|
||||
c: Context<HonoCustomType>,
|
||||
key: string
|
||||
): Promise<number> => {
|
||||
const value = await getSetting(c, key);
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isInteger(parsed) || parsed < 0) {
|
||||
return 0;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const cleanupSendMailLimitCount = async (
|
||||
c: Context<HonoCustomType>,
|
||||
currentDailyKey: string,
|
||||
currentMonthlyKey: string
|
||||
): Promise<void> => {
|
||||
await c.env.DB.batch([
|
||||
c.env.DB.prepare(
|
||||
`DELETE FROM settings
|
||||
WHERE key LIKE ?
|
||||
AND key < ?`
|
||||
).bind(`${CONSTANTS.SEND_MAIL_LIMIT_COUNT_KEY_PREFIX}daily:%`, currentDailyKey),
|
||||
c.env.DB.prepare(
|
||||
`DELETE FROM settings
|
||||
WHERE key LIKE ?
|
||||
AND key < ?`
|
||||
).bind(`${CONSTANTS.SEND_MAIL_LIMIT_COUNT_KEY_PREFIX}monthly:%`, currentMonthlyKey),
|
||||
]);
|
||||
}
|
||||
|
||||
export const ensureSendMailLimit = async (
|
||||
c: Context<HonoCustomType>
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const config = await getSendMailLimitConfig(c);
|
||||
if (!config || (!config.dailyEnabled && !config.monthlyEnabled)) {
|
||||
return;
|
||||
}
|
||||
if (config.dailyEnabled && config.dailyLimit !== null && config.dailyLimit !== -1) {
|
||||
const current = await getCount(c, getDailyCountKey());
|
||||
if (current >= config.dailyLimit) {
|
||||
throw new SendMailLimitError(msgs.ServerSendMailDailyLimitMsg);
|
||||
}
|
||||
}
|
||||
if (config.monthlyEnabled && config.monthlyLimit !== null && config.monthlyLimit !== -1) {
|
||||
const current = await getCount(c, getMonthlyCountKey());
|
||||
if (current >= config.monthlyLimit) {
|
||||
throw new SendMailLimitError(msgs.ServerSendMailMonthlyLimitMsg);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SendMailLimitError) {
|
||||
throw error;
|
||||
}
|
||||
console.warn("Failed to ensure send mail limit", error);
|
||||
}
|
||||
}
|
||||
|
||||
const increaseCount = async (
|
||||
c: Context<HonoCustomType>,
|
||||
key: string,
|
||||
): Promise<void> => {
|
||||
await c.env.DB.prepare(
|
||||
`INSERT INTO settings (key, value)
|
||||
VALUES (?, '1')
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value = CAST(COALESCE(value, '0') AS INTEGER) + 1,
|
||||
updated_at = datetime('now')`
|
||||
).bind(key).run();
|
||||
}
|
||||
|
||||
export const increaseSendMailLimitCount = async (
|
||||
c: Context<HonoCustomType>
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const config = await getSendMailLimitConfig(c);
|
||||
if (!config || (!config.dailyEnabled && !config.monthlyEnabled)) {
|
||||
return;
|
||||
}
|
||||
const dailyKey = getDailyCountKey();
|
||||
const monthlyKey = getMonthlyCountKey();
|
||||
if (config.dailyEnabled) {
|
||||
await increaseCount(c, dailyKey);
|
||||
}
|
||||
if (config.monthlyEnabled) {
|
||||
await increaseCount(c, monthlyKey);
|
||||
}
|
||||
await cleanupSendMailLimitCount(c, dailyKey, monthlyKey);
|
||||
} catch (error) {
|
||||
if (error instanceof SendMailLimitError) {
|
||||
throw error;
|
||||
}
|
||||
console.warn(`Failed to increment send_mail_limit_count`, error);
|
||||
}
|
||||
}
|
||||
@@ -184,6 +184,13 @@ export type EmailRuleSettings = {
|
||||
emailForwardingList: SubdomainForwardAddressList[]
|
||||
}
|
||||
|
||||
export type SendMailLimitConfig = {
|
||||
dailyEnabled: boolean;
|
||||
monthlyEnabled: boolean;
|
||||
dailyLimit: number | null;
|
||||
monthlyLimit: number | null;
|
||||
}
|
||||
|
||||
export type RoleConfig = {
|
||||
maxAddressCount?: number;
|
||||
// future configs can be added here
|
||||
|
||||
4
worker/src/types.d.ts
vendored
4
worker/src/types.d.ts
vendored
@@ -8,8 +8,8 @@ type Bindings = {
|
||||
// bindings
|
||||
DB: D1Database
|
||||
KV: KVNamespace
|
||||
RATE_LIMITER: any
|
||||
SEND_MAIL: any
|
||||
RATE_LIMITER: RateLimit
|
||||
SEND_MAIL: SendEmail
|
||||
ASSETS: Fetcher
|
||||
AI: Ai
|
||||
|
||||
|
||||
Reference in New Issue
Block a user