run402-mcp 3.7.2 → 3.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,6 +38,7 @@ const SECRET_KEY_RE = /^[A-Z_][A-Z0-9_]{0,127}$/;
38
38
  const APPLY_SAFE_RETRY_CODES = new Set([
39
39
  "BASE_RELEASE_CONFLICT",
40
40
  ]);
41
+ const EMAIL_TRIGGER_EVENTS = new Set(["reply_received", "delivery", "bounced", "complained"]);
41
42
  const STATIC_ACTIVATION_FAILURE_CODES = new Set([
42
43
  "BAD_FIELD",
43
44
  "INVALID_SPEC",
@@ -718,6 +719,7 @@ function functionToCoreSpec(fn) {
718
719
  ...(fn.entrypoint !== undefined ? { entrypoint: fn.entrypoint } : {}),
719
720
  ...(fn.config !== undefined ? { config: fn.config } : {}),
720
721
  ...(fn.deps !== undefined ? { deps: fn.deps } : {}),
722
+ ...(fn.triggers !== undefined ? { triggers: fn.triggers } : {}),
721
723
  ...(fn.schedule !== undefined ? { schedule: fn.schedule } : {}),
722
724
  ...(fn.requireAuth !== undefined ? { requireAuth: fn.requireAuth } : {}),
723
725
  ...(fn.requireRole !== undefined ? { requireRole: fn.requireRole } : {}),
@@ -740,6 +742,7 @@ function functionToWire(fn) {
740
742
  }
741
743
  : {}),
742
744
  ...(fn.deps !== undefined ? { deps: fn.deps } : {}),
745
+ ...(fn.triggers !== undefined ? { triggers: fn.triggers } : {}),
743
746
  ...(fn.schedule !== undefined ? { schedule: fn.schedule } : {}),
744
747
  ...(fn.requireAuth !== undefined ? { require_auth: fn.requireAuth } : {}),
745
748
  ...(fn.requireRole !== undefined
@@ -1016,11 +1019,14 @@ async function preflightTierFunctionLimits(client, spec, ciCredentials) {
1016
1019
  max_function_memory_mb: limits.maxMemoryMb.value,
1017
1020
  });
1018
1021
  }
1019
- if (isScheduledCron(entry.fn.schedule) && limits.minCronIntervalMinutes) {
1020
- const intervalMinutes = estimateCronMinimumIntervalMinutes(entry.fn.schedule);
1022
+ for (const trigger of scheduleTriggersForFunction(entry.fn)) {
1023
+ if (!limits.minCronIntervalMinutes)
1024
+ continue;
1025
+ const intervalMinutes = estimateCronMinimumIntervalMinutes(trigger.cron);
1021
1026
  if (intervalMinutes !== null &&
1022
1027
  intervalMinutes < limits.minCronIntervalMinutes.value) {
1023
- throw tierLimitError(`Function ${entry.name} schedule runs every ${intervalMinutes} minute(s), below the ${limits.tier} tier minimum interval of ${limits.minCronIntervalMinutes.value} minutes.`, `${entry.fieldPrefix}.schedule`, entry.fn.schedule, limits, limits.minCronIntervalMinutes, {
1028
+ const triggerLabel = trigger.id === "__legacy__" ? "schedule" : `trigger ${trigger.id}`;
1029
+ throw tierLimitError(`Function ${entry.name} ${triggerLabel} runs every ${intervalMinutes} minute(s), below the ${limits.tier} tier minimum interval of ${limits.minCronIntervalMinutes.value} minutes.`, `${entry.fieldPrefix}.${trigger.fieldSuffix}`, trigger.cron, limits, limits.minCronIntervalMinutes, {
1024
1030
  interval_minutes: intervalMinutes,
1025
1031
  min_interval_minutes: limits.minCronIntervalMinutes.value,
1026
1032
  min_cron_interval_minutes: limits.minCronIntervalMinutes.value,
@@ -1044,7 +1050,7 @@ function hasFunctionTierPreflightInputs(functions) {
1044
1050
  return false;
1045
1051
  return collectFunctionPreflightEntries(functions).some((entry) => (entry.fn.config?.timeoutSeconds !== undefined ||
1046
1052
  entry.fn.config?.memoryMb !== undefined ||
1047
- isScheduledCron(entry.fn.schedule)));
1053
+ scheduleTriggersForFunction(entry.fn).length > 0));
1048
1054
  }
1049
1055
  function collectFunctionPreflightEntries(functions) {
1050
1056
  if (!functions)
@@ -1160,23 +1166,21 @@ async function computeDesiredScheduledFunctionCount(client, spec) {
1160
1166
  function countScheduledFunctionsInSetEntries(functions) {
1161
1167
  if (!functions)
1162
1168
  return 0;
1163
- const names = new Set();
1164
- for (const [name, fn] of Object.entries(functions.replace ?? {})) {
1165
- if (isScheduledCron(fn.schedule))
1166
- names.add(name);
1169
+ let count = 0;
1170
+ for (const fn of Object.values(functions.replace ?? {})) {
1171
+ count += scheduleTriggersForFunction(fn).length;
1167
1172
  }
1168
- for (const [name, fn] of Object.entries(functions.patch?.set ?? {})) {
1169
- if (isScheduledCron(fn.schedule))
1170
- names.add(name);
1173
+ for (const fn of Object.values(functions.patch?.set ?? {})) {
1174
+ count += scheduleTriggersForFunction(fn).length;
1171
1175
  }
1172
- return names.size;
1176
+ return count;
1173
1177
  }
1174
1178
  function scheduledFunctionNames(functions) {
1175
1179
  if (!functions)
1176
1180
  return null;
1177
1181
  const scheduled = new Set();
1178
1182
  for (const [name, fn] of Object.entries(functions)) {
1179
- if (isScheduledCron(fn.schedule))
1183
+ if (scheduleTriggersForFunction(fn).length > 0)
1180
1184
  scheduled.add(name);
1181
1185
  }
1182
1186
  return scheduled;
@@ -1188,7 +1192,7 @@ function applyScheduledFunctionPatch(scheduled, patch) {
1188
1192
  for (const [name, fn] of Object.entries(patch?.set ?? {})) {
1189
1193
  if (fn.schedule === null)
1190
1194
  scheduled.delete(name);
1191
- else if (isScheduledCron(fn.schedule))
1195
+ else if (scheduleTriggersForFunction(fn).length > 0)
1192
1196
  scheduled.add(name);
1193
1197
  }
1194
1198
  }
@@ -1205,11 +1209,33 @@ async function readActiveScheduledFunctionNames(client, projectId) {
1205
1209
  }
1206
1210
  const scheduled = new Set();
1207
1211
  for (const fn of inventory.functions ?? []) {
1208
- if (isScheduledCron(fn.schedule))
1212
+ if (isScheduledCron(fn.schedule) || scheduleTriggersForFunction(fn).length > 0)
1209
1213
  scheduled.add(fn.name);
1210
1214
  }
1211
1215
  return scheduled;
1212
1216
  }
1217
+ function scheduleTriggersForFunction(fn) {
1218
+ if (fn.triggers && fn.triggers.length > 0) {
1219
+ return fn.triggers
1220
+ .filter((trigger) => trigger.type === "schedule")
1221
+ .map((trigger) => ({
1222
+ ...trigger,
1223
+ fieldSuffix: `triggers.${trigger.id}.cron`,
1224
+ }));
1225
+ }
1226
+ return isScheduledCron(fn.schedule)
1227
+ ? [{
1228
+ id: "__legacy__",
1229
+ type: "schedule",
1230
+ cron: fn.schedule,
1231
+ timezone: "UTC",
1232
+ misfire_policy: "skip",
1233
+ overlap_policy: "allow",
1234
+ run: { event_type: "legacy.schedule", payload: {} },
1235
+ fieldSuffix: "schedule",
1236
+ }]
1237
+ : [];
1238
+ }
1213
1239
  function scheduledCountTierLimitError(count, limits, limit, countSource) {
1214
1240
  return tierLimitError(`Deploy would have ${count} scheduled function(s), exceeding the ${limits.tier} tier maximum of ${limit.value}.`, "functions.scheduled_count", count, limits, limit, {
1215
1241
  tier_max: limit.value,
@@ -1890,6 +1916,7 @@ const FUNCTION_SPEC_FIELDS = new Set([
1890
1916
  "entrypoint",
1891
1917
  "config",
1892
1918
  "deps",
1919
+ "triggers",
1893
1920
  "schedule",
1894
1921
  "requireAuth",
1895
1922
  "requireRole",
@@ -1897,6 +1924,8 @@ const FUNCTION_SPEC_FIELDS = new Set([
1897
1924
  "capabilities",
1898
1925
  ]);
1899
1926
  const FUNCTION_CONFIG_FIELDS = new Set(["timeoutSeconds", "memoryMb"]);
1927
+ const FUNCTION_TRIGGER_FIELDS = new Set(["id", "type", "cron", "timezone", "misfire_policy", "overlap_policy", "mailbox", "events", "run"]);
1928
+ const FUNCTION_TRIGGER_RUN_FIELDS = new Set(["event_type", "payload", "retry", "expires_after_seconds"]);
1900
1929
  const SITE_SPEC_FIELDS = new Set(["replace", "patch", "public_paths"]);
1901
1930
  const SITE_PATCH_FIELDS = new Set(["put", "delete"]);
1902
1931
  const SITE_PUBLIC_PATHS_FIELDS = new Set(["mode", "replace"]);
@@ -2029,6 +2058,81 @@ function validateFunctionMap(value, resource) {
2029
2058
  if (entry.files !== undefined) {
2030
2059
  requireObject(entry.files, `${resource}.${name}.files`);
2031
2060
  }
2061
+ validateFunctionTriggers(entry.triggers, `${resource}.${name}.triggers`);
2062
+ }
2063
+ }
2064
+ function validateFunctionTriggers(value, resource) {
2065
+ if (value === undefined)
2066
+ return;
2067
+ if (!Array.isArray(value))
2068
+ throw invalidSpec(`ReleaseSpec.${resource} must be an array`, resource);
2069
+ const seen = new Set();
2070
+ for (const [index, trigger] of value.entries()) {
2071
+ const path = `${resource}.${index}`;
2072
+ const obj = requireObject(trigger, path);
2073
+ validateKnownFields(obj, path, FUNCTION_TRIGGER_FIELDS, {
2074
+ delivery: "Remove delivery; schedule triggers always start durable function runs.",
2075
+ });
2076
+ if (typeof obj.id !== "string" || obj.id.trim() === "") {
2077
+ throw invalidSpec(`ReleaseSpec.${path}.id is required for function triggers`, `${path}.id`);
2078
+ }
2079
+ if (seen.has(obj.id))
2080
+ throw invalidSpec(`ReleaseSpec.${path}.id duplicates ${JSON.stringify(obj.id)}`, `${path}.id`);
2081
+ seen.add(obj.id);
2082
+ if (obj.type === "schedule") {
2083
+ if (obj.mailbox !== undefined || obj.events !== undefined) {
2084
+ throw invalidSpec(`ReleaseSpec.${path} schedule triggers cannot include mailbox or events`, path);
2085
+ }
2086
+ if (typeof obj.cron !== "string" || obj.cron.trim().split(/\s+/).length !== 5) {
2087
+ throw invalidSpec(`ReleaseSpec.${path}.cron must be a 5-field cron expression`, `${path}.cron`);
2088
+ }
2089
+ if (obj.timezone !== undefined && typeof obj.timezone !== "string") {
2090
+ throw invalidSpec(`ReleaseSpec.${path}.timezone must be a string`, `${path}.timezone`);
2091
+ }
2092
+ if (obj.misfire_policy !== undefined && obj.misfire_policy !== "skip") {
2093
+ throw invalidSpec(`ReleaseSpec.${path}.misfire_policy must be "skip"`, `${path}.misfire_policy`);
2094
+ }
2095
+ if (obj.overlap_policy !== undefined && obj.overlap_policy !== "allow") {
2096
+ throw invalidSpec(`ReleaseSpec.${path}.overlap_policy must be "allow"`, `${path}.overlap_policy`);
2097
+ }
2098
+ }
2099
+ else if (obj.type === "email") {
2100
+ if (obj.cron !== undefined ||
2101
+ obj.timezone !== undefined ||
2102
+ obj.misfire_policy !== undefined ||
2103
+ obj.overlap_policy !== undefined) {
2104
+ throw invalidSpec(`ReleaseSpec.${path} email triggers cannot include schedule fields`, path);
2105
+ }
2106
+ if (typeof obj.mailbox !== "string" || obj.mailbox.trim() === "") {
2107
+ throw invalidSpec(`ReleaseSpec.${path}.mailbox is required`, `${path}.mailbox`);
2108
+ }
2109
+ if (!Array.isArray(obj.events) || obj.events.length === 0) {
2110
+ throw invalidSpec(`ReleaseSpec.${path}.events must be a non-empty array`, `${path}.events`);
2111
+ }
2112
+ for (const [eventIndex, event] of obj.events.entries()) {
2113
+ if (typeof event !== "string" || !EMAIL_TRIGGER_EVENTS.has(event)) {
2114
+ throw invalidSpec(`ReleaseSpec.${path}.events.${eventIndex} must be one of reply_received, delivery, bounced, complained`, `${path}.events.${eventIndex}`);
2115
+ }
2116
+ }
2117
+ }
2118
+ else {
2119
+ throw invalidSpec(`ReleaseSpec.${path}.type must be "schedule" or "email"`, `${path}.type`);
2120
+ }
2121
+ const run = requireObject(obj.run, `${path}.run`);
2122
+ validateKnownFields(run, `${path}.run`, FUNCTION_TRIGGER_RUN_FIELDS);
2123
+ if (typeof run.event_type !== "string" || run.event_type.trim() === "") {
2124
+ throw invalidSpec(`ReleaseSpec.${path}.run.event_type is required`, `${path}.run.event_type`);
2125
+ }
2126
+ if (run.payload !== undefined)
2127
+ requireObject(run.payload, `${path}.run.payload`);
2128
+ if (run.retry !== undefined)
2129
+ requireObject(run.retry, `${path}.run.retry`);
2130
+ if (run.expires_after_seconds !== undefined &&
2131
+ (typeof run.expires_after_seconds !== "number" ||
2132
+ !Number.isSafeInteger(run.expires_after_seconds) ||
2133
+ run.expires_after_seconds <= 0)) {
2134
+ throw invalidSpec(`ReleaseSpec.${path}.run.expires_after_seconds must be a positive integer`, `${path}.run.expires_after_seconds`);
2135
+ }
2032
2136
  }
2033
2137
  }
2034
2138
  /**
@@ -2934,6 +3038,8 @@ async function normalizeFunction(fn, remember) {
2934
3038
  out.config = fn.config;
2935
3039
  if (fn.deps !== undefined)
2936
3040
  out.deps = fn.deps;
3041
+ if (fn.triggers !== undefined)
3042
+ out.triggers = normalizeFunctionTriggers(fn.triggers);
2937
3043
  if (fn.schedule !== undefined)
2938
3044
  out.schedule = fn.schedule;
2939
3045
  if (fn.entrypoint)
@@ -2955,6 +3061,38 @@ async function normalizeFunction(fn, remember) {
2955
3061
  }
2956
3062
  return out;
2957
3063
  }
3064
+ function normalizeFunctionTriggers(triggers) {
3065
+ return triggers
3066
+ .map((trigger) => {
3067
+ const run = {
3068
+ event_type: trigger.run.event_type,
3069
+ payload: trigger.run.payload ?? {},
3070
+ ...(trigger.run.retry !== undefined ? { retry: { ...trigger.run.retry } } : {}),
3071
+ ...(trigger.run.expires_after_seconds !== undefined
3072
+ ? { expires_after_seconds: trigger.run.expires_after_seconds }
3073
+ : {}),
3074
+ };
3075
+ if (trigger.type === "email") {
3076
+ return {
3077
+ id: trigger.id,
3078
+ type: "email",
3079
+ mailbox: trigger.mailbox,
3080
+ events: [...trigger.events].sort(),
3081
+ run,
3082
+ };
3083
+ }
3084
+ return {
3085
+ id: trigger.id,
3086
+ type: "schedule",
3087
+ cron: trigger.cron,
3088
+ timezone: trigger.timezone ?? "UTC",
3089
+ misfire_policy: trigger.misfire_policy ?? "skip",
3090
+ overlap_policy: trigger.overlap_policy ?? "allow",
3091
+ run,
3092
+ };
3093
+ })
3094
+ .sort((a, b) => a.id.localeCompare(b.id));
3095
+ }
2958
3096
  async function normalizeFileSet(set, remember) {
2959
3097
  // `dir(path)` produces a LocalDirRef sentinel that is documented as a
2960
3098
  // valid site.replace / site.patch.put input. Expand it to a plain FileSet