@agent-native/core 0.141.2 → 0.141.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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +1 -1
- package/corpus/templates/analytics/actions/gong-calls.ts +56 -18
- package/corpus/templates/analytics/changelog/2026-08-05-analytics-queries-keep-every-table-tenant-scoped.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-durable-a2a-workers-avoid-startup-migration-contention.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +1 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
- package/corpus/templates/analytics/server/lib/emails.ts +51 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
- package/corpus/templates/analytics/server/lib/gong.ts +132 -33
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
- package/corpus/templates/analytics/server/plugins/db.ts +35 -10
- package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
- package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
- package/corpus/templates/calendar/server/lib/emails.ts +167 -0
- package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
- package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/clips/actions/invite-member.ts +39 -16
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
- package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
- package/corpus/templates/clips/desktop/src/styles.css +0 -3
- package/corpus/templates/clips/server/lib/emails.ts +256 -0
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
- package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
- package/corpus/templates/clips/shared/email-preview.ts +61 -0
- package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
- package/corpus/templates/content/server/lib/emails.ts +70 -0
- package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
- package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
- package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
- package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
- package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
- package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
- package/corpus/templates/factory/AGENTS.md +18 -17
- package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
- package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
- package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
- package/corpus/templates/factory/app/routes/factory.tsx +376 -206
- package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
- package/corpus/templates/factory/server/db/schema.ts +6 -0
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
- package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
- package/corpus/templates/forms/server/lib/emails.ts +52 -0
- package/corpus/templates/forms/server/lib/response-email.ts +2 -0
- package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
- package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
- package/corpus/templates/plan/server/lib/emails.ts +67 -0
- package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/slides/actions/import-from-url.ts +1 -0
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
- package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
- package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
- package/corpus/templates/slides/app/pages/Index.tsx +127 -137
- package/corpus/templates/slides/app/root.tsx +8 -0
- package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
- package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
- package/corpus/templates/slides/server/lib/emails.ts +40 -0
- package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
- package/dist/brand-kit/index.d.ts +1 -0
- package/dist/brand-kit/index.js +1 -0
- package/dist/brand-kit/tokens.d.ts +91 -0
- package/dist/brand-kit/tokens.js +279 -0
- package/dist/brand-kit/types.d.ts +25 -0
- package/dist/client/AssistantChat.js +7 -7
- package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
- package/dist/client/onboarding/first-run-registry.d.ts +25 -0
- package/dist/client/onboarding/first-run-registry.js +18 -0
- package/dist/client/onboarding/index.d.ts +2 -0
- package/dist/client/onboarding/index.js +1 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/migrations.d.ts +13 -3
- package/dist/db/migrations.js +19 -4
- package/dist/deploy/build.d.ts +8 -0
- package/dist/deploy/build.js +92 -3
- package/dist/deploy/workspace-deploy.js +86 -3
- package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
- package/dist/email-catalog/actions/list-email-activity.js +19 -0
- package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
- package/dist/email-catalog/actions/list-email-engagement.js +15 -0
- package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
- package/dist/email-catalog/actions/list-email-log.js +23 -0
- package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
- package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
- package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
- package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
- package/dist/email-catalog/authorize.d.ts +1 -0
- package/dist/email-catalog/authorize.js +9 -0
- package/dist/email-catalog/log.d.ts +60 -0
- package/dist/email-catalog/log.js +142 -0
- package/dist/email-catalog/provider-metrics.d.ts +34 -0
- package/dist/email-catalog/provider-metrics.js +182 -0
- package/dist/email-catalog/registry.d.ts +78 -0
- package/dist/email-catalog/registry.js +70 -0
- package/dist/email-catalog/schema.d.ts +231 -0
- package/dist/email-catalog/schema.js +53 -0
- package/dist/email-catalog/system-emails.d.ts +13 -0
- package/dist/email-catalog/system-emails.js +67 -0
- package/dist/jobs/index.d.ts +2 -0
- package/dist/jobs/index.js +2 -0
- package/dist/jobs/run-history.js +48 -1
- package/dist/jobs/scheduler-dispatch.d.ts +7 -0
- package/dist/jobs/scheduler-dispatch.js +7 -0
- package/dist/jobs/scheduler-health.d.ts +22 -0
- package/dist/jobs/scheduler-health.js +172 -0
- package/dist/jobs/scheduler.js +67 -2
- package/dist/localization/default-messages.d.ts +45 -0
- package/dist/localization/default-messages.js +45 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/handlers.js +9 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/server/action-discovery.js +22 -0
- package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
- package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
- package/dist/server/agent-chat-plugin.d.ts +2 -1
- package/dist/server/agent-chat-plugin.js +69 -22
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/better-auth-instance.js +17 -2
- package/dist/server/design-token-utils.d.ts +6 -1
- package/dist/server/design-token-utils.js +252 -59
- package/dist/server/email.d.ts +11 -0
- package/dist/server/email.js +54 -3
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.js +3 -2
- package/dist/vite/client.js +20 -14
- package/package.json +2 -1
package/dist/db/migrations.js
CHANGED
|
@@ -154,6 +154,15 @@ function splitSqlStatements(sql) {
|
|
|
154
154
|
out.push(tail);
|
|
155
155
|
return out;
|
|
156
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* A run-only migration can defer its work without recording itself as applied.
|
|
159
|
+
* This is for serialized backfills where another instance owns the advisory
|
|
160
|
+
* lock; throwing would turn an expected loser into a startup retry storm.
|
|
161
|
+
*/
|
|
162
|
+
export const MIGRATION_DEFERRED = Symbol("migration-deferred");
|
|
163
|
+
export function deferMigration() {
|
|
164
|
+
return MIGRATION_DEFERRED;
|
|
165
|
+
}
|
|
157
166
|
function resolveMigrationSql(sql, pg) {
|
|
158
167
|
if (typeof sql === "string")
|
|
159
168
|
return sql;
|
|
@@ -272,8 +281,11 @@ export function runMigrations(migrations, options) {
|
|
|
272
281
|
try {
|
|
273
282
|
// D1 is SQLite-compatible
|
|
274
283
|
const raw = resolveMigrationSql(m.sql, false);
|
|
275
|
-
|
|
276
|
-
|
|
284
|
+
const runResult = m.run ? await m.run() : undefined;
|
|
285
|
+
if (runResult === MIGRATION_DEFERRED) {
|
|
286
|
+
console.info(`[db] Deferred migration ${m.name ? `"${m.name}" ` : ""}v${m.version}; it remains pending for a later boot`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
277
289
|
const recordStatements = [
|
|
278
290
|
m.name
|
|
279
291
|
? d1
|
|
@@ -482,8 +494,11 @@ export function runMigrations(migrations, options) {
|
|
|
482
494
|
// A throw here escapes to the outer handler with nothing recorded,
|
|
483
495
|
// so the entry is retried on the next boot rather than being marked
|
|
484
496
|
// applied against work that never happened.
|
|
485
|
-
|
|
486
|
-
|
|
497
|
+
const runResult = m.run ? await m.run() : undefined;
|
|
498
|
+
if (runResult === MIGRATION_DEFERRED) {
|
|
499
|
+
console.info(`[db] Deferred migration ${label}; it remains pending for a later boot`);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
487
502
|
if (raw == null) {
|
|
488
503
|
// Dialect-gated migration with no SQL for this dialect; still mark
|
|
489
504
|
// as applied so we don't retry forever.
|
package/dist/deploy/build.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ export declare function findInstalledResvgPackages(nodeModulesRoots: string[]):
|
|
|
90
90
|
export declare function isDurableBackgroundDeployEnabled(): boolean;
|
|
91
91
|
export declare const NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME = "server-integration-recovery";
|
|
92
92
|
export declare function isIntegrationDurableDispatchDeployEnabled(): boolean;
|
|
93
|
+
export declare const NETLIFY_RECURRING_JOBS_FUNCTION_NAME = "agent-native-recurring-jobs";
|
|
94
|
+
export declare function isRecurringJobsDeployEnabled(): boolean;
|
|
93
95
|
/**
|
|
94
96
|
* Emit a site-local Netlify Scheduled Function that wakes the public server
|
|
95
97
|
* function and its database every minute. GitHub Actions schedules can be
|
|
@@ -97,6 +99,12 @@ export declare function isIntegrationDurableDispatchDeployEnabled(): boolean;
|
|
|
97
99
|
* autosuspend window and leaves the next visitor to pay the cold-start cost.
|
|
98
100
|
*/
|
|
99
101
|
export declare function emitSingleTemplateNetlifyKeepWarmFunction(projectCwd: string): void;
|
|
102
|
+
/**
|
|
103
|
+
* Emit the durable recurring-job trigger. Netlify's scheduled function only
|
|
104
|
+
* hands off work; the existing `-background` function owns the long sweep so
|
|
105
|
+
* a model run is not constrained by the synchronous scheduled-function wall.
|
|
106
|
+
*/
|
|
107
|
+
export declare function emitSingleTemplateNetlifyRecurringJobsFunction(projectCwd: string): void;
|
|
100
108
|
/**
|
|
101
109
|
* Single-template Netlify build: emit an async (background) function INSIDE the
|
|
102
110
|
* scanned functions dir so the chat `_process-run` worker runs on Netlify's
|
package/dist/deploy/build.js
CHANGED
|
@@ -20,6 +20,7 @@ import path from "path";
|
|
|
20
20
|
import { fileURLToPath } from "url";
|
|
21
21
|
import { AGENT_BACKGROUND_FUNCTION_NAME, AGENT_BACKGROUND_FUNCTION_URL_PATH, AGENT_BACKGROUND_PROCESSOR_A2A, AGENT_BACKGROUND_PROCESSOR_FIELD, AGENT_BACKGROUND_PROCESSOR_INTEGRATION, AGENT_BACKGROUND_PROCESSOR_ROUTE, AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, isDurableBackgroundFlagExplicitlyDisabled, } from "../agent/durable-background.js";
|
|
22
22
|
import { INTEGRATION_RECOVERY_RUNTIME_MARKER, INTEGRATION_RETRY_SWEEP_PATH, INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT, isIntegrationDurableDispatchConfigured, } from "../integrations/integration-durable-dispatch-config.js";
|
|
23
|
+
import { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "../jobs/scheduler-dispatch.js";
|
|
23
24
|
import { normalizeAppBasePath } from "../server/app-base-path.js";
|
|
24
25
|
import { DEFAULT_SPECULATION_RULES_PATH, resolveSsrCacheHeaders, } from "../shared/cache-control.js";
|
|
25
26
|
import { mcpEmbedStaticAssetRouteRules } from "../shared/mcp-embed-headers.js";
|
|
@@ -2090,13 +2091,19 @@ export function isDurableBackgroundDeployEnabled() {
|
|
|
2090
2091
|
*/
|
|
2091
2092
|
function isDurableBackgroundEmitRequired() {
|
|
2092
2093
|
return (isDurableBackgroundDeployEnabled() ||
|
|
2093
|
-
isIntegrationDurableDispatchDeployEnabled()
|
|
2094
|
+
isIntegrationDurableDispatchDeployEnabled() ||
|
|
2095
|
+
isRecurringJobsDeployEnabled());
|
|
2094
2096
|
}
|
|
2095
2097
|
export const NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME = "server-integration-recovery";
|
|
2096
2098
|
export function isIntegrationDurableDispatchDeployEnabled() {
|
|
2097
2099
|
return isIntegrationDurableDispatchConfigured();
|
|
2098
2100
|
}
|
|
2099
2101
|
const NETLIFY_KEEP_WARM_FUNCTION_NAME = "agent-native-keep-warm";
|
|
2102
|
+
export const NETLIFY_RECURRING_JOBS_FUNCTION_NAME = "agent-native-recurring-jobs";
|
|
2103
|
+
export function isRecurringJobsDeployEnabled() {
|
|
2104
|
+
const value = process.env.AGENT_NATIVE_DISABLE_RECURRING_JOBS?.trim();
|
|
2105
|
+
return !value || !["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
2106
|
+
}
|
|
2100
2107
|
/**
|
|
2101
2108
|
* Emit a site-local Netlify Scheduled Function that wakes the public server
|
|
2102
2109
|
* function and its database every minute. GitHub Actions schedules can be
|
|
@@ -2170,7 +2177,7 @@ export default async function handler(request) {
|
|
|
2170
2177
|
}
|
|
2171
2178
|
|
|
2172
2179
|
if (!response.ok) {
|
|
2173
|
-
const body = await response.text()
|
|
2180
|
+
const body = await response.text();
|
|
2174
2181
|
throw new Error(
|
|
2175
2182
|
"[agent-native-keep-warm] Health request failed with " +
|
|
2176
2183
|
response.status +
|
|
@@ -2194,6 +2201,84 @@ export const config = {
|
|
|
2194
2201
|
fs.writeFileSync(path.join(dest, `${NETLIFY_KEEP_WARM_FUNCTION_NAME}.mjs`), entry);
|
|
2195
2202
|
console.log(`[build] Emitted Netlify scheduled keep-warm function "${NETLIFY_KEEP_WARM_FUNCTION_NAME}".`);
|
|
2196
2203
|
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Emit the durable recurring-job trigger. Netlify's scheduled function only
|
|
2206
|
+
* hands off work; the existing `-background` function owns the long sweep so
|
|
2207
|
+
* a model run is not constrained by the synchronous scheduled-function wall.
|
|
2208
|
+
*/
|
|
2209
|
+
export function emitSingleTemplateNetlifyRecurringJobsFunction(projectCwd) {
|
|
2210
|
+
if (!isRecurringJobsDeployEnabled())
|
|
2211
|
+
return;
|
|
2212
|
+
const internalDir = path.join(projectCwd, ".netlify", "functions-internal");
|
|
2213
|
+
const backgroundEntry = path.join(internalDir, AGENT_BACKGROUND_FUNCTION_NAME, `${AGENT_BACKGROUND_FUNCTION_NAME}.mjs`);
|
|
2214
|
+
if (!fs.existsSync(backgroundEntry)) {
|
|
2215
|
+
throw new Error("[build] Recurring-job trigger cannot be emitted without the durable background function.");
|
|
2216
|
+
}
|
|
2217
|
+
const functionName = NETLIFY_RECURRING_JOBS_FUNCTION_NAME;
|
|
2218
|
+
const dest = path.join(internalDir, functionName);
|
|
2219
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
2220
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
2221
|
+
const entry = `import { createHmac } from "node:crypto";
|
|
2222
|
+
|
|
2223
|
+
const BACKGROUND_PATH = ${JSON.stringify(AGENT_BACKGROUND_FUNCTION_URL_PATH)};
|
|
2224
|
+
const SWEEP_PATH = ${JSON.stringify(RECURRING_JOBS_SWEEP_PATH)};
|
|
2225
|
+
const TOKEN_SUBJECT = ${JSON.stringify(RECURRING_JOBS_SWEEP_TOKEN_SUBJECT)};
|
|
2226
|
+
const PROCESSOR_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_FIELD)};
|
|
2227
|
+
const PROCESSOR_ROUTE = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE)};
|
|
2228
|
+
const PROCESSOR_ROUTE_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD)};
|
|
2229
|
+
|
|
2230
|
+
function siteOrigin(request) {
|
|
2231
|
+
const configured = process.env.URL || process.env.DEPLOY_URL;
|
|
2232
|
+
if (configured) return configured;
|
|
2233
|
+
return new URL(request.url).origin;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
function token(secret) {
|
|
2237
|
+
const timestamp = Date.now();
|
|
2238
|
+
const signature = createHmac("sha256", secret)
|
|
2239
|
+
.update(\`\${TOKEN_SUBJECT}:\${timestamp}\`)
|
|
2240
|
+
.digest("hex");
|
|
2241
|
+
return \`\${timestamp}.\${signature}\`;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
export default async function handler(request) {
|
|
2245
|
+
const secret = process.env.A2A_SECRET;
|
|
2246
|
+
if (!secret) {
|
|
2247
|
+
throw new Error("[recurring-jobs] A2A_SECRET is required for the scheduled sweep");
|
|
2248
|
+
}
|
|
2249
|
+
const url = new URL(BACKGROUND_PATH, siteOrigin(request));
|
|
2250
|
+
const response = await fetch(url, {
|
|
2251
|
+
method: "POST",
|
|
2252
|
+
headers: {
|
|
2253
|
+
Authorization: \`Bearer \${token(secret)}\`,
|
|
2254
|
+
"Content-Type": "application/json",
|
|
2255
|
+
"user-agent": "agent-native-recurring-jobs",
|
|
2256
|
+
},
|
|
2257
|
+
body: JSON.stringify({
|
|
2258
|
+
[PROCESSOR_FIELD]: PROCESSOR_ROUTE,
|
|
2259
|
+
[PROCESSOR_ROUTE_FIELD]: SWEEP_PATH,
|
|
2260
|
+
}),
|
|
2261
|
+
});
|
|
2262
|
+
if (!response.ok) {
|
|
2263
|
+
const body = await response.text();
|
|
2264
|
+
throw new Error(
|
|
2265
|
+
\`[recurring-jobs] Durable sweep handoff failed (\${response.status}): \${body.slice(0, 500)}\`,
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
console.log("[recurring-jobs] Durable sweep handed off", url.toString());
|
|
2269
|
+
return new Response(null, { status: 204 });
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
export const config = {
|
|
2273
|
+
name: "agent-native recurring jobs",
|
|
2274
|
+
generator: "agent-native build",
|
|
2275
|
+
schedule: "* * * * *",
|
|
2276
|
+
nodeBundler: "none",
|
|
2277
|
+
};
|
|
2278
|
+
`;
|
|
2279
|
+
fs.writeFileSync(path.join(dest, `${functionName}.mjs`), entry);
|
|
2280
|
+
console.log(`[build] Emitted Netlify scheduled recurring-job function "${functionName}".`);
|
|
2281
|
+
}
|
|
2197
2282
|
/**
|
|
2198
2283
|
* Single-template Netlify build: emit an async (background) function INSIDE the
|
|
2199
2284
|
* scanned functions dir so the chat `_process-run` worker runs on Netlify's
|
|
@@ -2289,6 +2374,7 @@ export function emitSingleTemplateNetlifyBackgroundFunction(projectCwd) {
|
|
|
2289
2374
|
const backgroundProcessorIntegration = JSON.stringify(AGENT_BACKGROUND_PROCESSOR_INTEGRATION);
|
|
2290
2375
|
const backgroundProcessorRoute = JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE);
|
|
2291
2376
|
const backgroundProcessorRouteField = JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD);
|
|
2377
|
+
const recurringJobsSweepPath = JSON.stringify(RECURRING_JOBS_SWEEP_PATH);
|
|
2292
2378
|
const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
|
|
2293
2379
|
// bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
|
|
2294
2380
|
// in this function. The deployed Lambda name is NOT guaranteed to end in
|
|
@@ -2307,6 +2393,7 @@ const BACKGROUND_PROCESSOR_A2A = ${backgroundProcessorA2A};
|
|
|
2307
2393
|
const BACKGROUND_PROCESSOR_INTEGRATION = ${backgroundProcessorIntegration};
|
|
2308
2394
|
const BACKGROUND_PROCESSOR_ROUTE = ${backgroundProcessorRoute};
|
|
2309
2395
|
const BACKGROUND_PROCESSOR_ROUTE_FIELD = ${backgroundProcessorRouteField};
|
|
2396
|
+
const RECURRING_JOBS_SWEEP_PATH = ${recurringJobsSweepPath};
|
|
2310
2397
|
|
|
2311
2398
|
function processorPathFromBody(body) {
|
|
2312
2399
|
if (!body) return null;
|
|
@@ -2326,7 +2413,8 @@ function processorPathFromBody(body) {
|
|
|
2326
2413
|
parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_ROUTE &&
|
|
2327
2414
|
typeof route === "string" &&
|
|
2328
2415
|
route.startsWith("/") &&
|
|
2329
|
-
route
|
|
2416
|
+
(route === RECURRING_JOBS_SWEEP_PATH ||
|
|
2417
|
+
route.includes("/api/_agent-native-background/")) &&
|
|
2330
2418
|
!route.includes("?") &&
|
|
2331
2419
|
!route.includes("#")
|
|
2332
2420
|
) {
|
|
@@ -3367,6 +3455,7 @@ export default bundle;
|
|
|
3367
3455
|
if (isDurableBackgroundEmitRequired()) {
|
|
3368
3456
|
emitSingleTemplateNetlifyBackgroundFunction(cwd);
|
|
3369
3457
|
}
|
|
3458
|
+
emitSingleTemplateNetlifyRecurringJobsFunction(cwd);
|
|
3370
3459
|
// Emit keep-warm after the background artifact so it only pings a function
|
|
3371
3460
|
// that this build actually produced.
|
|
3372
3461
|
emitSingleTemplateNetlifyKeepWarmFunction(cwd);
|
|
@@ -19,10 +19,11 @@ import fs from "fs";
|
|
|
19
19
|
import path from "path";
|
|
20
20
|
import { AGENT_BACKGROUND_PROCESSOR_A2A, AGENT_BACKGROUND_PROCESSOR_FIELD, AGENT_BACKGROUND_PROCESSOR_INTEGRATION, AGENT_BACKGROUND_PROCESSOR_ROUTE, AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, isDurableBackgroundFlagExplicitlyDisabled, } from "../agent/durable-background.js";
|
|
21
21
|
import { INTEGRATION_RECOVERY_RUNTIME_MARKER, INTEGRATION_RETRY_SWEEP_PATH, INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT, isIntegrationDurableDispatchConfigured, } from "../integrations/integration-durable-dispatch-config.js";
|
|
22
|
+
import { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "../jobs/scheduler-dispatch.js";
|
|
22
23
|
import { findWorkspaceRoot } from "../scripts/utils.js";
|
|
23
24
|
import { DEFAULT_WORKSPACE_APP_AUDIENCE, normalizeWorkspaceAppAudience, normalizeWorkspaceAppPathList, workspaceAppAudienceFromPackageJson, workspaceAppRouteAccessFromPackageJson, } from "../shared/workspace-app-audience.js";
|
|
24
25
|
import { DISPATCH_WORKSPACE_ROOT_REDIRECTS } from "../shared/workspace-app-id.js";
|
|
25
|
-
import { assertEmittedBackgroundFunctionOnDisk } from "./build.js";
|
|
26
|
+
import { assertEmittedBackgroundFunctionOnDisk, isRecurringJobsDeployEnabled, } from "./build.js";
|
|
26
27
|
import { cloneServerBundleForFunction } from "./function-bundle.js";
|
|
27
28
|
import { collectImmutableAssetPaths, IMMUTABLE_ASSET_CACHE_HEADERS, } from "./immutable-assets.js";
|
|
28
29
|
const NETLIFY_WORKSPACE_STATIC_DIR = "_workspace_static";
|
|
@@ -464,14 +465,93 @@ function copyNetlifyFunctionIntoWorkspace(workspaceRoot, app, workspaceApps, sta
|
|
|
464
465
|
// Durable background agent runs. Additive ONLY: when explicitly opted out
|
|
465
466
|
// this emits nothing and the single-function deploy is unchanged.
|
|
466
467
|
const integrationDurableDispatch = app === "dispatch" && isIntegrationDurableDispatchConfigured();
|
|
468
|
+
const recurringJobs = isRecurringJobsDeployEnabled();
|
|
467
469
|
if (isDurableBackgroundWorkspaceDeployEnabled() ||
|
|
468
|
-
integrationDurableDispatch
|
|
470
|
+
integrationDurableDispatch ||
|
|
471
|
+
recurringJobs) {
|
|
469
472
|
emitNetlifyBackgroundFunction(workspaceRoot, app, src, workspaceApps);
|
|
470
473
|
}
|
|
474
|
+
if (recurringJobs) {
|
|
475
|
+
emitNetlifyRecurringJobsFunction(workspaceRoot, app);
|
|
476
|
+
}
|
|
471
477
|
if (integrationDurableDispatch) {
|
|
472
478
|
emitNetlifyIntegrationRecoveryFunction(workspaceRoot, app, src, workspaceApps);
|
|
473
479
|
}
|
|
474
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Emit the per-app Netlify Scheduled Function that hands one recurring-job
|
|
483
|
+
* sweep to that app's durable background worker. The worker owns the actual
|
|
484
|
+
* scan so scheduled-function timeouts cannot strand the automation runner.
|
|
485
|
+
*/
|
|
486
|
+
function emitNetlifyRecurringJobsFunction(workspaceRoot, app) {
|
|
487
|
+
const functionName = `${app}-agent-recurring-jobs`;
|
|
488
|
+
const dest = path.join(netlifyFunctionsDir(workspaceRoot), functionName);
|
|
489
|
+
const backgroundName = `${app}-agent-background`;
|
|
490
|
+
const backgroundPath = `/.netlify/functions/${backgroundName}`;
|
|
491
|
+
const sweepPath = `/${app}${RECURRING_JOBS_SWEEP_PATH}`;
|
|
492
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
493
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
494
|
+
const entry = `import { createHmac } from "node:crypto";
|
|
495
|
+
|
|
496
|
+
const BACKGROUND_PATH = ${JSON.stringify(backgroundPath)};
|
|
497
|
+
const SWEEP_PATH = ${JSON.stringify(sweepPath)};
|
|
498
|
+
const TOKEN_SUBJECT = ${JSON.stringify(RECURRING_JOBS_SWEEP_TOKEN_SUBJECT)};
|
|
499
|
+
const PROCESSOR_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_FIELD)};
|
|
500
|
+
const PROCESSOR_ROUTE = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE)};
|
|
501
|
+
const PROCESSOR_ROUTE_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD)};
|
|
502
|
+
|
|
503
|
+
function siteOrigin(request) {
|
|
504
|
+
const configured = process.env.URL || process.env.DEPLOY_URL;
|
|
505
|
+
if (configured) return configured;
|
|
506
|
+
return new URL(request.url).origin;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function token(secret) {
|
|
510
|
+
const timestamp = Date.now();
|
|
511
|
+
const signature = createHmac("sha256", secret)
|
|
512
|
+
.update(\`${RECURRING_JOBS_SWEEP_TOKEN_SUBJECT}:\${timestamp}\`)
|
|
513
|
+
.digest("hex");
|
|
514
|
+
return \`\${timestamp}.\${signature}\`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export default async function handler(request) {
|
|
518
|
+
const secret = process.env.A2A_SECRET;
|
|
519
|
+
if (!secret) {
|
|
520
|
+
throw new Error("[recurring-jobs] A2A_SECRET is required for the scheduled sweep");
|
|
521
|
+
}
|
|
522
|
+
const url = new URL(BACKGROUND_PATH, siteOrigin(request));
|
|
523
|
+
const response = await fetch(url, {
|
|
524
|
+
method: "POST",
|
|
525
|
+
headers: {
|
|
526
|
+
Authorization: \`Bearer \${token(secret)}\`,
|
|
527
|
+
"Content-Type": "application/json",
|
|
528
|
+
"user-agent": "agent-native-recurring-jobs",
|
|
529
|
+
},
|
|
530
|
+
body: JSON.stringify({
|
|
531
|
+
[PROCESSOR_FIELD]: PROCESSOR_ROUTE,
|
|
532
|
+
[PROCESSOR_ROUTE_FIELD]: SWEEP_PATH,
|
|
533
|
+
}),
|
|
534
|
+
});
|
|
535
|
+
if (!response.ok) {
|
|
536
|
+
const body = await response.text();
|
|
537
|
+
throw new Error(
|
|
538
|
+
\`[recurring-jobs] Durable sweep handoff failed (\${response.status}): \${body.slice(0, 500)}\`,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
console.log("[recurring-jobs] Durable sweep handed off", url.toString());
|
|
542
|
+
return new Response(null, { status: 204 });
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export const config = {
|
|
546
|
+
name: ${JSON.stringify(`${app} agent-native recurring jobs`)},
|
|
547
|
+
generator: "agent-native workspace deploy",
|
|
548
|
+
schedule: "* * * * *",
|
|
549
|
+
nodeBundler: "none",
|
|
550
|
+
};
|
|
551
|
+
`;
|
|
552
|
+
fs.writeFileSync(path.join(dest, `${functionName}.mjs`), entry);
|
|
553
|
+
console.log(`[workspace-deploy] Emitted Netlify scheduled recurring-job function "${functionName}" for app "${app}".`);
|
|
554
|
+
}
|
|
475
555
|
/**
|
|
476
556
|
* Deploy-time gate for emitting the per-app `-background` Netlify function.
|
|
477
557
|
*
|
|
@@ -534,6 +614,7 @@ function emitNetlifyBackgroundFunction(workspaceRoot, app, srcServerDir, workspa
|
|
|
534
614
|
const processRunPath = `${basePath}${AGENT_CHAT_PROCESS_RUN_PATH}`;
|
|
535
615
|
const a2aProcessTaskPath = `${basePath}/_agent-native/a2a/_process-task`;
|
|
536
616
|
const integrationProcessTaskPath = `${basePath}/_agent-native/integrations/process-task`;
|
|
617
|
+
const recurringJobsSweepPath = `${basePath}${RECURRING_JOBS_SWEEP_PATH}`;
|
|
537
618
|
const server = `// Mark this isolate as the durable background runtime BEFORE the handler bundle
|
|
538
619
|
// is imported, so isInBackgroundFunctionRuntime() reliably returns true in this
|
|
539
620
|
// function (the deployed Lambda name is not guaranteed to end in -background). A
|
|
@@ -546,6 +627,7 @@ const basePath = ${JSON.stringify(basePath)};
|
|
|
546
627
|
const PROCESS_RUN_PATH = ${JSON.stringify(processRunPath)};
|
|
547
628
|
const A2A_PROCESS_TASK_PATH = ${JSON.stringify(a2aProcessTaskPath)};
|
|
548
629
|
const INTEGRATION_PROCESS_TASK_PATH = ${JSON.stringify(integrationProcessTaskPath)};
|
|
630
|
+
const RECURRING_JOBS_SWEEP_PATH = ${JSON.stringify(recurringJobsSweepPath)};
|
|
549
631
|
const BACKGROUND_PROCESSOR_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_FIELD)};
|
|
550
632
|
const BACKGROUND_PROCESSOR_A2A = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_A2A)};
|
|
551
633
|
const BACKGROUND_PROCESSOR_INTEGRATION = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_INTEGRATION)};
|
|
@@ -569,7 +651,8 @@ function processorPathFromBody(body) {
|
|
|
569
651
|
if (
|
|
570
652
|
parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_ROUTE &&
|
|
571
653
|
typeof route === "string" &&
|
|
572
|
-
route
|
|
654
|
+
(route === RECURRING_JOBS_SWEEP_PATH ||
|
|
655
|
+
route.startsWith(basePath + "/api/_agent-native-background/")) &&
|
|
573
656
|
!route.includes("?") &&
|
|
574
657
|
!route.includes("#")
|
|
575
658
|
) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineAction } from "../../action.js";
|
|
3
|
+
import { getRequestOrgId } from "../../server/request-context.js";
|
|
4
|
+
import { authorizeTransactionalEmailRead } from "../authorize.js";
|
|
5
|
+
import { fetchEmailActivity } from "../provider-metrics.js";
|
|
6
|
+
export default defineAction({
|
|
7
|
+
description: "List recent provider activity for one registered transactional email in this app. Organization admin only.",
|
|
8
|
+
schema: z.object({
|
|
9
|
+
templateId: z.string().min(1),
|
|
10
|
+
limit: z.coerce.number().int().min(1).max(1000).default(50),
|
|
11
|
+
}),
|
|
12
|
+
http: { method: "GET" },
|
|
13
|
+
authorize: ({ templateId }) => authorizeTransactionalEmailRead([templateId]),
|
|
14
|
+
run: async ({ templateId, limit }) => fetchEmailActivity({
|
|
15
|
+
templateId,
|
|
16
|
+
limit,
|
|
17
|
+
orgId: getRequestOrgId(),
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineAction } from "../../action.js";
|
|
3
|
+
import { getRequestOrgId } from "../../server/request-context.js";
|
|
4
|
+
import { authorizeTransactionalEmailRead } from "../authorize.js";
|
|
5
|
+
import { fetchEmailEngagement } from "../provider-metrics.js";
|
|
6
|
+
export default defineAction({
|
|
7
|
+
description: "Read provider engagement metrics for registered transactional emails in this app. Organization admin only.",
|
|
8
|
+
schema: z.object({
|
|
9
|
+
templateIds: z.array(z.string().min(1)).max(100),
|
|
10
|
+
windowDays: z.coerce.number().int().min(1).max(365).default(30),
|
|
11
|
+
}),
|
|
12
|
+
http: { method: "POST" },
|
|
13
|
+
authorize: ({ templateIds }) => authorizeTransactionalEmailRead(templateIds),
|
|
14
|
+
run: async ({ templateIds, windowDays }) => fetchEmailEngagement(templateIds, windowDays, getRequestOrgId()),
|
|
15
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineAction } from "../../action.js";
|
|
3
|
+
import { getAppSlug } from "../../server/app-name.js";
|
|
4
|
+
import { getRequestOrgId } from "../../server/request-context.js";
|
|
5
|
+
import { authorizeTransactionalEmailRead } from "../authorize.js";
|
|
6
|
+
import { listEmailLog } from "../log.js";
|
|
7
|
+
export default defineAction({
|
|
8
|
+
description: "List recent transactional email sends from this app, newest first, optionally filtered to one registered email id.",
|
|
9
|
+
schema: z.object({
|
|
10
|
+
templateId: z.string().optional(),
|
|
11
|
+
limit: z.coerce.number().int().min(1).max(500).default(100),
|
|
12
|
+
}),
|
|
13
|
+
http: { method: "GET" },
|
|
14
|
+
authorize: ({ templateId }) => authorizeTransactionalEmailRead(templateId ? [templateId] : []),
|
|
15
|
+
run: async ({ templateId, limit }) => ({
|
|
16
|
+
entries: await listEmailLog({
|
|
17
|
+
orgId: getRequestOrgId() ?? "",
|
|
18
|
+
app: getAppSlug() ?? "unknown",
|
|
19
|
+
templateId,
|
|
20
|
+
limit,
|
|
21
|
+
}),
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
declare const _default: import("../../action.js").ActionDefinition<{
|
|
2
|
+
windowDays?: unknown;
|
|
3
|
+
}, {
|
|
4
|
+
app: string;
|
|
5
|
+
windowDays: number;
|
|
6
|
+
statsAvailable: boolean;
|
|
7
|
+
statsError: string;
|
|
8
|
+
emails: {
|
|
9
|
+
id: string;
|
|
10
|
+
app: string;
|
|
11
|
+
name: string;
|
|
12
|
+
trigger: string;
|
|
13
|
+
recipient: string;
|
|
14
|
+
recipientLabel: string;
|
|
15
|
+
sender: string;
|
|
16
|
+
senderLabel: string;
|
|
17
|
+
sent: number;
|
|
18
|
+
failed: number;
|
|
19
|
+
lastSentAt: number;
|
|
20
|
+
}[];
|
|
21
|
+
}>;
|
|
22
|
+
export default _default;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineAction } from "../../action.js";
|
|
3
|
+
import { getAppSlug } from "../../server/app-name.js";
|
|
4
|
+
import { getRequestOrgId } from "../../server/request-context.js";
|
|
5
|
+
import { authorizeTransactionalEmailRead } from "../authorize.js";
|
|
6
|
+
import { getEmailSendStats } from "../log.js";
|
|
7
|
+
import { listTransactionalEmails } from "../registry.js";
|
|
8
|
+
import { registerCoreSystemEmails } from "../system-emails.js";
|
|
9
|
+
const DEFAULT_WINDOW_DAYS = 30;
|
|
10
|
+
export default defineAction({
|
|
11
|
+
description: "List the transactional emails this app can send, with the trigger, recipient and sender logic for each, plus local send counts and last-sent. Engagement metrics such as open rate are not included here — they live at the email provider.",
|
|
12
|
+
schema: z.object({
|
|
13
|
+
windowDays: z.coerce
|
|
14
|
+
.number()
|
|
15
|
+
.int()
|
|
16
|
+
.min(1)
|
|
17
|
+
.max(365)
|
|
18
|
+
.default(DEFAULT_WINDOW_DAYS)
|
|
19
|
+
.describe("How many days of send history to summarize."),
|
|
20
|
+
}),
|
|
21
|
+
http: { method: "GET" },
|
|
22
|
+
authorize: () => authorizeTransactionalEmailRead([]),
|
|
23
|
+
run: async ({ windowDays }) => {
|
|
24
|
+
registerCoreSystemEmails();
|
|
25
|
+
const since = Date.now() - windowDays * 24 * 60 * 60 * 1000;
|
|
26
|
+
const app = getAppSlug() ?? "unknown";
|
|
27
|
+
const orgId = getRequestOrgId();
|
|
28
|
+
const definitions = listTransactionalEmails();
|
|
29
|
+
// A failed stats read must not masquerade as "no email ever sent" — the
|
|
30
|
+
// catalog is still worth returning, but the caller has to be able to tell
|
|
31
|
+
// that the numbers are missing rather than zero.
|
|
32
|
+
let statsById = null;
|
|
33
|
+
let statsError = null;
|
|
34
|
+
try {
|
|
35
|
+
if (!orgId) {
|
|
36
|
+
throw new Error("Organization context is required for transactional email stats.");
|
|
37
|
+
}
|
|
38
|
+
const stats = await getEmailSendStats(since, app, orgId);
|
|
39
|
+
statsById = new Map(stats.map((row) => [row.templateId, row]));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
statsError = error instanceof Error ? error.message : String(error);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
app,
|
|
46
|
+
windowDays,
|
|
47
|
+
statsAvailable: statsError === null,
|
|
48
|
+
statsError,
|
|
49
|
+
emails: definitions.map((definition) => {
|
|
50
|
+
const stats = statsById?.get(definition.id);
|
|
51
|
+
return {
|
|
52
|
+
id: definition.id,
|
|
53
|
+
app: definition.app,
|
|
54
|
+
name: definition.name,
|
|
55
|
+
trigger: definition.trigger,
|
|
56
|
+
recipient: definition.recipient,
|
|
57
|
+
recipientLabel: definition.recipientLabel,
|
|
58
|
+
sender: definition.sender,
|
|
59
|
+
senderLabel: definition.senderLabel,
|
|
60
|
+
sent: stats?.sent ?? (statsById ? 0 : null),
|
|
61
|
+
failed: stats?.failed ?? (statsById ? 0 : null),
|
|
62
|
+
lastSentAt: stats?.lastSentAt ?? null,
|
|
63
|
+
};
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineAction } from "../../action.js";
|
|
3
|
+
import { authorizeTransactionalEmailRead } from "../authorize.js";
|
|
4
|
+
import { renderTransactionalEmailPreview } from "../registry.js";
|
|
5
|
+
import { registerCoreSystemEmails } from "../system-emails.js";
|
|
6
|
+
export default defineAction({
|
|
7
|
+
description: "Render one of this app's transactional emails with representative dummy data, returning the subject, HTML and plain-text bodies for preview.",
|
|
8
|
+
schema: z.object({
|
|
9
|
+
id: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Registered email id, e.g. calendar.booking-confirmed."),
|
|
12
|
+
}),
|
|
13
|
+
http: { method: "GET" },
|
|
14
|
+
authorize: ({ id }) => authorizeTransactionalEmailRead([id]),
|
|
15
|
+
run: async ({ id }) => {
|
|
16
|
+
registerCoreSystemEmails();
|
|
17
|
+
const rendered = renderTransactionalEmailPreview(id);
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
subject: rendered.subject,
|
|
21
|
+
html: rendered.html,
|
|
22
|
+
text: rendered.text,
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function authorizeTransactionalEmailRead(templateIds?: string[]): Promise<boolean>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { currentRequestUserIsOrgAdmin } from "../server/org-admin.js";
|
|
2
|
+
import { getTransactionalEmail } from "./registry.js";
|
|
3
|
+
import { registerCoreSystemEmails } from "./system-emails.js";
|
|
4
|
+
export async function authorizeTransactionalEmailRead(templateIds = []) {
|
|
5
|
+
if (!(await currentRequestUserIsOrgAdmin()))
|
|
6
|
+
return false;
|
|
7
|
+
registerCoreSystemEmails();
|
|
8
|
+
return templateIds.every((id) => Boolean(getTransactionalEmail(id)));
|
|
9
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable send log for transactional emails.
|
|
3
|
+
*
|
|
4
|
+
* Written by `sendEmail` on every attempt, successful or not. Read by Dispatch
|
|
5
|
+
* to report per-email send counts and last-sent without depending on the
|
|
6
|
+
* provider's activity retention window.
|
|
7
|
+
*/
|
|
8
|
+
export interface RecordEmailSendArgs {
|
|
9
|
+
orgId?: string | null;
|
|
10
|
+
templateId?: string;
|
|
11
|
+
app?: string;
|
|
12
|
+
recipient: string;
|
|
13
|
+
sender: string;
|
|
14
|
+
subject: string;
|
|
15
|
+
status: "sent" | "failed";
|
|
16
|
+
error?: string;
|
|
17
|
+
provider: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Append one send record.
|
|
21
|
+
*
|
|
22
|
+
* Callers treat logging as best-effort: a logging failure must not turn a
|
|
23
|
+
* delivered email into a thrown send. The failure is surfaced on the console
|
|
24
|
+
* rather than swallowed, so a persistently broken log is visible instead of
|
|
25
|
+
* quietly producing an empty activity view.
|
|
26
|
+
*/
|
|
27
|
+
export declare function recordEmailSend(args: RecordEmailSendArgs): Promise<void>;
|
|
28
|
+
export interface EmailSendStats {
|
|
29
|
+
templateId: string;
|
|
30
|
+
sent: number;
|
|
31
|
+
failed: number;
|
|
32
|
+
lastSentAt: number | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Per-template send counts and last-sent, for sends at or after `since`.
|
|
36
|
+
* Templates with no rows are absent from the result — callers distinguish
|
|
37
|
+
* "never sent" from "sent zero times in window" by that absence.
|
|
38
|
+
*/
|
|
39
|
+
export declare function getEmailSendStats(since: number, app: string, orgId: string): Promise<EmailSendStats[]>;
|
|
40
|
+
export interface EmailLogEntry {
|
|
41
|
+
id: string;
|
|
42
|
+
templateId: string | null;
|
|
43
|
+
app: string | null;
|
|
44
|
+
recipient: string;
|
|
45
|
+
sender: string;
|
|
46
|
+
subject: string;
|
|
47
|
+
status: string;
|
|
48
|
+
error: string | null;
|
|
49
|
+
provider: string;
|
|
50
|
+
createdAt: number;
|
|
51
|
+
}
|
|
52
|
+
/** Most recent sends for one app, newest first, optionally filtered to one template. */
|
|
53
|
+
export declare function listEmailLog(options: {
|
|
54
|
+
orgId: string;
|
|
55
|
+
app: string;
|
|
56
|
+
templateId?: string;
|
|
57
|
+
limit?: number;
|
|
58
|
+
}): Promise<EmailLogEntry[]>;
|
|
59
|
+
/** Provider category that is safe to query for one organization only. */
|
|
60
|
+
export declare function getScopedEmailProviderCategory(templateId: string, orgId: string): string;
|