@agent-native/core 0.141.3 → 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-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 +11 -3
- 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/routes.d.ts +1 -1
- 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 +5 -5
- 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/secrets/routes.d.ts +9 -9
- 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 +39 -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 +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/vite/client.js +20 -14
- package/package.json +2 -1
|
@@ -50,3 +50,15 @@ export function shouldDisableRecurringJobsRuntime(env = process.env) {
|
|
|
50
50
|
}
|
|
51
51
|
return isLocalRuntime;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Hosted Netlify deploys get a durable scheduled sweep emitted by the build.
|
|
55
|
+
* The in-process timer must stay off there: a scale-to-zero recycle destroys
|
|
56
|
+
* that timer, which is exactly the failure mode the emitted sweep fixes.
|
|
57
|
+
*/
|
|
58
|
+
export function isNetlifyRecurringJobsRuntime(env = process.env) {
|
|
59
|
+
if (env.NETLIFY_LOCAL === "true")
|
|
60
|
+
return false;
|
|
61
|
+
if (env.NETLIFY === "false")
|
|
62
|
+
return false;
|
|
63
|
+
return Boolean((env.NETLIFY && env.NETLIFY !== "false") || env.SITE_ID);
|
|
64
|
+
}
|
|
@@ -8,7 +8,7 @@ import { _agentChatPromptSectionsForTests } from "./agent-chat/framework-prompts
|
|
|
8
8
|
import { type AgentChatPluginOptions, type NitroPluginDef } from "./agent-chat/plugin-options.js";
|
|
9
9
|
import { finalizeClaimedAgentChatProcessRunFailure } from "./agent-chat/process-run-failure.js";
|
|
10
10
|
import { loadResourcesForPrompt } from "./agent-chat/prompt-resources.js";
|
|
11
|
-
import { shouldDisableRecurringJobsRuntime } from "./agent-chat/recurring-jobs-runtime.js";
|
|
11
|
+
import { isNetlifyRecurringJobsRuntime, shouldDisableRecurringJobsRuntime } from "./agent-chat/recurring-jobs-runtime.js";
|
|
12
12
|
import { shouldBlockInProductCodeEditingSurface } from "./agent-chat/request-surface.js";
|
|
13
13
|
import { loadRunCodeToolEntries } from "./agent-chat/run-code-tools.js";
|
|
14
14
|
export { loadResourcesForPrompt };
|
|
@@ -22,6 +22,7 @@ export { createA2AEngineToolSurface };
|
|
|
22
22
|
export { DEFAULT_DELEGATED_MAX_ITERATIONS, DEFAULT_DELEGATED_MAX_RUN_INPUT_TOKENS, DEFAULT_DELEGATED_MAX_TOOL_RESULT_CHARS, };
|
|
23
23
|
export { shouldBlockInProductCodeEditingSurface };
|
|
24
24
|
export { loadRunCodeToolEntries };
|
|
25
|
+
export { isNetlifyRecurringJobsRuntime };
|
|
25
26
|
export { shouldDisableRecurringJobsRuntime };
|
|
26
27
|
export { finalizeClaimedAgentChatProcessRunFailure };
|
|
27
28
|
export declare function resolveInteractiveAgentRunOptions(options?: Pick<AgentChatPluginOptions, "runSoftTimeoutMs" | "runNoProgressTimeoutMs" | "durableBackgroundRuns">): {
|
|
@@ -25,6 +25,7 @@ import { isCheckpointRestorePath } from "../checkpoints/route-match.js";
|
|
|
25
25
|
import { createDbAdminAgentTools } from "../db-admin/agent-tools.js";
|
|
26
26
|
import { isTransientDatabaseError } from "../db/client.js";
|
|
27
27
|
import { verifyInternalToken, extractBearerToken, } from "../integrations/internal-token.js";
|
|
28
|
+
import { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "../jobs/scheduler-dispatch.js";
|
|
28
29
|
import { McpClientManager, loadMcpConfig, autoDetectMcpConfig, mcpToolsToActionEntries, syncMcpActionEntries, mountMcpServersRoutes, mountMcpHubRoutes, buildMergedConfig, startMcpConfigRefresh, getHubStatus, isHubServeEnabled, } from "../mcp-client/index.js";
|
|
29
30
|
import { setProgressPreListHook } from "../progress/store.js";
|
|
30
31
|
import { getSkillNameFromPath } from "../resources/metadata.js";
|
|
@@ -109,7 +110,7 @@ import { _agentChatPromptSectionsForTests, buildFrameworkPrompts, buildSchemaBlo
|
|
|
109
110
|
import { resolveA2AAgentDelegationEnabled, } from "./agent-chat/plugin-options.js";
|
|
110
111
|
import { finalizeClaimedAgentChatProcessRunFailure } from "./agent-chat/process-run-failure.js";
|
|
111
112
|
import { loadResourcesForPrompt, promptResourceManifestSections, resourceScopeForOwner, } from "./agent-chat/prompt-resources.js";
|
|
112
|
-
import { shouldDisableRecurringJobsRuntime } from "./agent-chat/recurring-jobs-runtime.js";
|
|
113
|
+
import { isNetlifyRecurringJobsRuntime, shouldDisableRecurringJobsRuntime, } from "./agent-chat/recurring-jobs-runtime.js";
|
|
113
114
|
import { isLocalhost, shouldBlockInProductCodeEditingSurface, } from "./agent-chat/request-surface.js";
|
|
114
115
|
import { loadRunCodeToolEntries } from "./agent-chat/run-code-tools.js";
|
|
115
116
|
import { createAgentEngineScriptEntries, createAgentLoopSettingsScriptEntries, createCallAgentScriptEntry, createChatScriptEntries, createDbScriptEntries, createDocsScriptEntries, createResourceScriptEntries, } from "./agent-chat/script-entries.js";
|
|
@@ -124,6 +125,7 @@ export { createA2AEngineToolSurface };
|
|
|
124
125
|
export { DEFAULT_DELEGATED_MAX_ITERATIONS, DEFAULT_DELEGATED_MAX_RUN_INPUT_TOKENS, DEFAULT_DELEGATED_MAX_TOOL_RESULT_CHARS, };
|
|
125
126
|
export { shouldBlockInProductCodeEditingSurface };
|
|
126
127
|
export { loadRunCodeToolEntries };
|
|
128
|
+
export { isNetlifyRecurringJobsRuntime };
|
|
127
129
|
export { shouldDisableRecurringJobsRuntime };
|
|
128
130
|
export { finalizeClaimedAgentChatProcessRunFailure };
|
|
129
131
|
export function resolveInteractiveAgentRunOptions(options) {
|
|
@@ -4697,11 +4699,47 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4697
4699
|
appId: options?.appId,
|
|
4698
4700
|
};
|
|
4699
4701
|
runNowSchedulerDeps = schedulerDeps;
|
|
4702
|
+
// Platform schedulers use the existing durable background function as
|
|
4703
|
+
// the long-lived worker. Keeping the sweep behind a signed, fixed
|
|
4704
|
+
// route prevents a public request from choosing an owner or job.
|
|
4705
|
+
getH3App(nitroApp).use(RECURRING_JOBS_SWEEP_PATH, defineEventHandler(async (event) => {
|
|
4706
|
+
if (getMethod(event) !== "POST") {
|
|
4707
|
+
setResponseStatus(event, 405);
|
|
4708
|
+
return { error: "Method not allowed" };
|
|
4709
|
+
}
|
|
4710
|
+
const token = extractBearerToken(getHeader(event, "authorization"));
|
|
4711
|
+
if (!token ||
|
|
4712
|
+
!verifyInternalToken(RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, token)) {
|
|
4713
|
+
setResponseStatus(event, 401);
|
|
4714
|
+
return { error: "Invalid or expired internal token" };
|
|
4715
|
+
}
|
|
4716
|
+
if (isNetlifyRecurringJobsRuntime() &&
|
|
4717
|
+
!isInBackgroundFunctionRuntime()) {
|
|
4718
|
+
setResponseStatus(event, 503);
|
|
4719
|
+
return {
|
|
4720
|
+
error: "Recurring-job sweep reached the synchronous server instead of the durable background worker.",
|
|
4721
|
+
};
|
|
4722
|
+
}
|
|
4723
|
+
try {
|
|
4724
|
+
await processRecurringJobs(schedulerDeps);
|
|
4725
|
+
return { ok: true };
|
|
4726
|
+
}
|
|
4727
|
+
catch (error) {
|
|
4728
|
+
console.error("[recurring-jobs] Sweep route failed:", error);
|
|
4729
|
+
setResponseStatus(event, 500);
|
|
4730
|
+
return { error: "Recurring-job sweep failed" };
|
|
4731
|
+
}
|
|
4732
|
+
}));
|
|
4700
4733
|
if (disableRecurringJobsRuntime) {
|
|
4701
4734
|
if (process.env.DEBUG) {
|
|
4702
4735
|
console.log("[recurring-jobs] Scheduler disabled for local development");
|
|
4703
4736
|
}
|
|
4704
4737
|
}
|
|
4738
|
+
else if (isNetlifyRecurringJobsRuntime()) {
|
|
4739
|
+
if (process.env.DEBUG) {
|
|
4740
|
+
console.log("[recurring-jobs] Using the durable Netlify scheduled sweep");
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4705
4743
|
else {
|
|
4706
4744
|
// Start after a 10-second delay to let the server fully initialize
|
|
4707
4745
|
setTimeout(() => {
|
|
@@ -18,6 +18,7 @@ import { TEMPLATES } from "../cli/templates-meta.js";
|
|
|
18
18
|
import { getDbExec, isPostgres } from "../db/client.js";
|
|
19
19
|
import { getDialect, getDatabaseUrl, getDatabaseAuthToken, closePgliteClients, getPgliteClient, isPgliteUrl, loadPgliteDrizzle, pgPoolOptions, neonPoolMax, attachNeonPoolErrorLogger, sharedDbPool, onSharedDbPoolsClosed, onSharedDbPoolReplaced, } from "../db/client.js";
|
|
20
20
|
import { ensureTableExists } from "../db/ddl-guard.js";
|
|
21
|
+
import { CORE_RESET_PASSWORD_EMAIL_ID, CORE_VERIFY_SIGNUP_EMAIL_ID, } from "../email-catalog/system-emails.js";
|
|
21
22
|
import { saveOAuthTokens } from "../oauth-tokens/store.js";
|
|
22
23
|
import { acceptPendingInvitationsForEmail } from "../org/accept-pending.js";
|
|
23
24
|
import { getAuthEmailForUserId, getRequiredAuthProviderForEmail, } from "../org/auth-policy.js";
|
|
@@ -918,7 +919,14 @@ async function createBetterAuthInstance(config) {
|
|
|
918
919
|
email: user.email,
|
|
919
920
|
resetUrl,
|
|
920
921
|
});
|
|
921
|
-
await sendEmail({
|
|
922
|
+
await sendEmail({
|
|
923
|
+
to: user.email,
|
|
924
|
+
subject,
|
|
925
|
+
html,
|
|
926
|
+
text,
|
|
927
|
+
appSender,
|
|
928
|
+
templateId: CORE_RESET_PASSWORD_EMAIL_ID,
|
|
929
|
+
});
|
|
922
930
|
},
|
|
923
931
|
},
|
|
924
932
|
emailVerification: {
|
|
@@ -942,7 +950,14 @@ async function createBetterAuthInstance(config) {
|
|
|
942
950
|
email: user.email,
|
|
943
951
|
verifyUrl,
|
|
944
952
|
});
|
|
945
|
-
await sendEmail({
|
|
953
|
+
await sendEmail({
|
|
954
|
+
to: user.email,
|
|
955
|
+
subject,
|
|
956
|
+
html,
|
|
957
|
+
text,
|
|
958
|
+
appSender,
|
|
959
|
+
templateId: CORE_VERIFY_SIGNUP_EMAIL_ID,
|
|
960
|
+
});
|
|
946
961
|
},
|
|
947
962
|
},
|
|
948
963
|
socialProviders,
|
|
@@ -77,6 +77,11 @@ export interface UrlExtractionResult {
|
|
|
77
77
|
googleFonts?: string[];
|
|
78
78
|
ogImage?: string;
|
|
79
79
|
favicon?: string;
|
|
80
|
+
stylesheetUrls?: string[];
|
|
81
|
+
stylesheetFailures?: {
|
|
82
|
+
url: string;
|
|
83
|
+
error: string;
|
|
84
|
+
}[];
|
|
80
85
|
}
|
|
81
86
|
export interface GitHubFetchOptions {
|
|
82
87
|
token?: string | null;
|
|
@@ -145,5 +150,5 @@ export declare function extractDocumentFonts(text: string): string[];
|
|
|
145
150
|
export declare function classifyFile(fileType: string): ContentType;
|
|
146
151
|
/** Return per-type suggestions for how to use a document for design extraction. */
|
|
147
152
|
export declare function suggestionsForType(contentType: ContentType, hasText: boolean): string[];
|
|
148
|
-
/** Fetch and extract design tokens from a URL's HTML. */
|
|
153
|
+
/** Fetch and extract design tokens from a URL's HTML and linked CSS. */
|
|
149
154
|
export declare function extractDesignTokensFromUrl(rawUrl: string): Promise<UrlExtractionResult>;
|
|
@@ -663,91 +663,284 @@ export function suggestionsForType(contentType, hasText) {
|
|
|
663
663
|
// ---------------------------------------------------------------------------
|
|
664
664
|
// URL scraping helpers (import-from-url)
|
|
665
665
|
// ---------------------------------------------------------------------------
|
|
666
|
-
|
|
666
|
+
const URL_FETCH_TIMEOUT = 10_000;
|
|
667
|
+
const MAX_URL_HTML_CHARS = 1_000_000;
|
|
668
|
+
const MAX_URL_STYLESHEETS = 10;
|
|
669
|
+
const MAX_URL_STYLESHEET_CHARS = 256 * 1024;
|
|
670
|
+
const MAX_URL_STYLESHEET_TOTAL_CHARS = 512 * 1024;
|
|
671
|
+
function normalizeUrlInput(rawUrl) {
|
|
672
|
+
const trimmed = rawUrl.trim();
|
|
673
|
+
if (!trimmed)
|
|
674
|
+
throw new Error("Website URL is required");
|
|
675
|
+
const candidate = /^[a-z][a-z\d+.-]*:/i.test(trimmed)
|
|
676
|
+
? trimmed
|
|
677
|
+
: `https://${trimmed}`;
|
|
678
|
+
return new URL(candidate).href;
|
|
679
|
+
}
|
|
680
|
+
function decodeHtmlAttribute(value) {
|
|
681
|
+
return value
|
|
682
|
+
.replace(/&/gi, "&")
|
|
683
|
+
.replace(/"/gi, '"')
|
|
684
|
+
.replace(/'|'/gi, "'");
|
|
685
|
+
}
|
|
686
|
+
function readHtmlAttribute(tag, attribute) {
|
|
687
|
+
const match = tag.match(new RegExp(`\\b${attribute}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i"));
|
|
688
|
+
const value = match?.[1] ?? match?.[2] ?? match?.[3];
|
|
689
|
+
return value === undefined ? undefined : decodeHtmlAttribute(value.trim());
|
|
690
|
+
}
|
|
691
|
+
function resolveHttpUrl(value, baseUrl) {
|
|
692
|
+
if (!value)
|
|
693
|
+
return { kind: "missing" };
|
|
694
|
+
try {
|
|
695
|
+
const parsed = new URL(value, baseUrl);
|
|
696
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
697
|
+
return { kind: "missing" };
|
|
698
|
+
}
|
|
699
|
+
return { kind: "resolved", url: parsed.href };
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
return { kind: "invalid", value };
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function readMetaContent(html, attribute, value) {
|
|
706
|
+
for (const match of html.matchAll(/<meta\b[^>]*>/gi)) {
|
|
707
|
+
const tag = match[0];
|
|
708
|
+
if (readHtmlAttribute(tag, attribute)?.toLowerCase() !== value)
|
|
709
|
+
continue;
|
|
710
|
+
const content = readHtmlAttribute(tag, "content");
|
|
711
|
+
if (content !== undefined)
|
|
712
|
+
return content;
|
|
713
|
+
}
|
|
714
|
+
return undefined;
|
|
715
|
+
}
|
|
716
|
+
function extractStylesheetUrls(html, baseUrl) {
|
|
717
|
+
const urls = new Set();
|
|
718
|
+
for (const match of html.matchAll(/<link\b[^>]*>/gi)) {
|
|
719
|
+
const tag = match[0];
|
|
720
|
+
const rel = readHtmlAttribute(tag, "rel");
|
|
721
|
+
if (!rel?.split(/\s+/).some((token) => token.toLowerCase() === "stylesheet")) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const resolved = resolveHttpUrl(readHtmlAttribute(tag, "href"), baseUrl);
|
|
725
|
+
if (resolved.kind === "resolved")
|
|
726
|
+
urls.add(resolved.url);
|
|
727
|
+
}
|
|
728
|
+
return [...urls];
|
|
729
|
+
}
|
|
730
|
+
function extractInlineCss(html) {
|
|
731
|
+
const parts = [];
|
|
732
|
+
for (const match of html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)) {
|
|
733
|
+
parts.push(match[1]);
|
|
734
|
+
}
|
|
735
|
+
for (const match of html.matchAll(/<[a-z][^>]*>/gi)) {
|
|
736
|
+
const style = readHtmlAttribute(match[0], "style");
|
|
737
|
+
if (style)
|
|
738
|
+
parts.push(style);
|
|
739
|
+
}
|
|
740
|
+
return parts.join("\n");
|
|
741
|
+
}
|
|
742
|
+
function extractFontFaces(content) {
|
|
743
|
+
const fonts = [];
|
|
744
|
+
for (const match of content.matchAll(/@font-face\s*\{([^}]+)\}/g)) {
|
|
745
|
+
const block = match[1];
|
|
746
|
+
const familyMatch = block.match(/font-family\s*:\s*["']?([^"';]+)/);
|
|
747
|
+
const srcMatch = block.match(/src\s*:\s*([^;]+)/);
|
|
748
|
+
fonts.push({
|
|
749
|
+
family: familyMatch?.[1]?.trim(),
|
|
750
|
+
src: srcMatch?.[1]?.trim()?.slice(0, 200),
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
return fonts;
|
|
754
|
+
}
|
|
755
|
+
function extractGoogleFontUrls(content) {
|
|
756
|
+
return [
|
|
757
|
+
...content.matchAll(/fonts\.googleapis\.com\/css2?\?[^"'>\s)]+/g),
|
|
758
|
+
].map((match) => match[0]);
|
|
759
|
+
}
|
|
760
|
+
function addColorMatches(content, colors) {
|
|
761
|
+
for (const match of content.matchAll(/#[0-9a-fA-F]{3,8}\b/g)) {
|
|
762
|
+
colors.add(match[0]);
|
|
763
|
+
}
|
|
764
|
+
for (const match of content.matchAll(/\b(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\([^)]*\)/gi)) {
|
|
765
|
+
colors.add(match[0]);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async function readBoundedResponseText(response, maxChars, label) {
|
|
769
|
+
const contentLength = Number.parseInt(response.headers.get("content-length") ?? "", 10);
|
|
770
|
+
if (Number.isFinite(contentLength) && contentLength > maxChars) {
|
|
771
|
+
throw new Error(`${label} exceeds the ${maxChars}-character limit`);
|
|
772
|
+
}
|
|
773
|
+
if (!response.body) {
|
|
774
|
+
const text = await response.text();
|
|
775
|
+
if (text.length > maxChars) {
|
|
776
|
+
throw new Error(`${label} exceeds the ${maxChars}-character limit`);
|
|
777
|
+
}
|
|
778
|
+
return text;
|
|
779
|
+
}
|
|
780
|
+
const reader = response.body.getReader();
|
|
781
|
+
const decoder = new TextDecoder();
|
|
782
|
+
let text = "";
|
|
783
|
+
try {
|
|
784
|
+
while (true) {
|
|
785
|
+
const chunk = await reader.read();
|
|
786
|
+
if (chunk.done) {
|
|
787
|
+
text += decoder.decode();
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
791
|
+
if (text.length > maxChars) {
|
|
792
|
+
await reader.cancel().catch(() => undefined);
|
|
793
|
+
throw new Error(`${label} exceeds the ${maxChars}-character limit`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
finally {
|
|
798
|
+
reader.releaseLock();
|
|
799
|
+
}
|
|
800
|
+
return text;
|
|
801
|
+
}
|
|
802
|
+
async function fetchStylesheets(pageUrl, html) {
|
|
803
|
+
const discovered = extractStylesheetUrls(html, pageUrl);
|
|
804
|
+
const fetched = [];
|
|
805
|
+
const urls = [];
|
|
806
|
+
const failures = [];
|
|
807
|
+
let totalChars = 0;
|
|
808
|
+
for (const stylesheetUrl of discovered.slice(0, MAX_URL_STYLESHEETS)) {
|
|
809
|
+
if (totalChars >= MAX_URL_STYLESHEET_TOTAL_CHARS) {
|
|
810
|
+
failures.push({
|
|
811
|
+
url: stylesheetUrl,
|
|
812
|
+
error: `Skipped because the ${MAX_URL_STYLESHEET_TOTAL_CHARS}-character stylesheet budget was reached`,
|
|
813
|
+
});
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
const response = await ssrfSafeFetch(stylesheetUrl, {
|
|
818
|
+
headers: {
|
|
819
|
+
"User-Agent": "Mozilla/5.0 (compatible; AgentNative/1.0; +https://agent-native.com)",
|
|
820
|
+
},
|
|
821
|
+
signal: AbortSignal.timeout(URL_FETCH_TIMEOUT),
|
|
822
|
+
}, { maxRedirects: 3 });
|
|
823
|
+
if (!response.ok) {
|
|
824
|
+
throw new Error(`HTTP ${response.status}`);
|
|
825
|
+
}
|
|
826
|
+
const remainingChars = MAX_URL_STYLESHEET_TOTAL_CHARS - totalChars;
|
|
827
|
+
const content = await readBoundedResponseText(response, Math.min(MAX_URL_STYLESHEET_CHARS, remainingChars), `Stylesheet ${stylesheetUrl}`);
|
|
828
|
+
totalChars += content.length;
|
|
829
|
+
fetched.push({ url: stylesheetUrl, content });
|
|
830
|
+
urls.push(stylesheetUrl);
|
|
831
|
+
}
|
|
832
|
+
catch (error) {
|
|
833
|
+
failures.push({
|
|
834
|
+
url: stylesheetUrl,
|
|
835
|
+
error: error instanceof Error ? error.message : String(error),
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (discovered.length > MAX_URL_STYLESHEETS) {
|
|
840
|
+
failures.push({
|
|
841
|
+
url: pageUrl,
|
|
842
|
+
error: `Skipped ${discovered.length - MAX_URL_STYLESHEETS} stylesheet(s) after the ${MAX_URL_STYLESHEETS}-stylesheet limit`,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
return { fetched, urls, failures };
|
|
846
|
+
}
|
|
847
|
+
/** Fetch and extract design tokens from a URL's HTML and linked CSS. */
|
|
667
848
|
export async function extractDesignTokensFromUrl(rawUrl) {
|
|
668
|
-
const url = rawUrl
|
|
849
|
+
const url = normalizeUrlInput(rawUrl);
|
|
669
850
|
validateUrl(url);
|
|
670
851
|
const response = await ssrfSafeFetch(url, {
|
|
671
852
|
headers: {
|
|
672
853
|
"User-Agent": "Mozilla/5.0 (compatible; AgentNative/1.0; +https://agent-native.com)",
|
|
673
854
|
},
|
|
674
|
-
signal: AbortSignal.timeout(
|
|
675
|
-
});
|
|
676
|
-
|
|
855
|
+
signal: AbortSignal.timeout(URL_FETCH_TIMEOUT),
|
|
856
|
+
}, { maxRedirects: 3 });
|
|
857
|
+
if (!response.ok) {
|
|
858
|
+
throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
|
|
859
|
+
}
|
|
860
|
+
const html = await readBoundedResponseText(response, MAX_URL_HTML_CHARS, `Website ${url}`);
|
|
861
|
+
const pageUrl = response.url || url;
|
|
677
862
|
const result = { url };
|
|
678
863
|
// Title
|
|
679
|
-
const titleMatch = html.match(/<title[^>]*>([
|
|
864
|
+
const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
680
865
|
if (titleMatch) {
|
|
681
866
|
result.pageTitle = titleMatch[1].trim();
|
|
682
867
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
result.metaDescription = descMatch[1];
|
|
868
|
+
const metaDescription = readMetaContent(html, "name", "description");
|
|
869
|
+
if (metaDescription !== undefined) {
|
|
870
|
+
result.metaDescription = metaDescription;
|
|
687
871
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
result.themeColor = themeColorMatch[1];
|
|
872
|
+
const themeColor = readMetaContent(html, "name", "theme-color");
|
|
873
|
+
if (themeColor !== undefined) {
|
|
874
|
+
result.themeColor = themeColor;
|
|
692
875
|
}
|
|
693
|
-
|
|
694
|
-
const
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
876
|
+
const inlineCss = extractInlineCss(html);
|
|
877
|
+
const pageCss = parseCss(inlineCss);
|
|
878
|
+
const stylesheetResult = await fetchStylesheets(pageUrl, html);
|
|
879
|
+
const stylesheetCssVars = {};
|
|
880
|
+
const fonts = extractFontFaces(inlineCss);
|
|
881
|
+
const googleFonts = new Set(extractGoogleFontUrls(html));
|
|
882
|
+
const colorMatches = new Set();
|
|
883
|
+
addColorMatches(html, colorMatches);
|
|
884
|
+
for (const stylesheet of stylesheetResult.fetched) {
|
|
885
|
+
const parsed = parseCss(stylesheet.content);
|
|
886
|
+
Object.assign(stylesheetCssVars, parsed.cssCustomProperties);
|
|
887
|
+
fonts.push(...extractFontFaces(stylesheet.content));
|
|
888
|
+
for (const fontUrl of extractGoogleFontUrls(stylesheet.content)) {
|
|
889
|
+
googleFonts.add(fontUrl);
|
|
890
|
+
}
|
|
891
|
+
addColorMatches(stylesheet.content, colorMatches);
|
|
698
892
|
}
|
|
893
|
+
const cssVars = {
|
|
894
|
+
...stylesheetCssVars,
|
|
895
|
+
...(pageCss.cssCustomProperties ?? {}),
|
|
896
|
+
};
|
|
699
897
|
if (Object.keys(cssVars).length > 0) {
|
|
700
898
|
const entries = Object.entries(cssVars).slice(0, 50);
|
|
701
899
|
result.cssCustomProperties = Object.fromEntries(entries);
|
|
702
900
|
}
|
|
703
|
-
// Inline colors (hex, rgb)
|
|
704
|
-
const colorMatches = new Set();
|
|
705
|
-
const hexPattern = /#[0-9a-fA-F]{3,8}\b/g;
|
|
706
|
-
let hexMatch;
|
|
707
|
-
while ((hexMatch = hexPattern.exec(html)) !== null) {
|
|
708
|
-
colorMatches.add(hexMatch[0]);
|
|
709
|
-
}
|
|
710
|
-
const rgbPattern = /rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\s*,\s*[\d.]+)?\s*\)/g;
|
|
711
|
-
let rgbMatch;
|
|
712
|
-
while ((rgbMatch = rgbPattern.exec(html)) !== null) {
|
|
713
|
-
colorMatches.add(rgbMatch[0]);
|
|
714
|
-
}
|
|
715
901
|
if (colorMatches.size > 0) {
|
|
716
902
|
result.colors = [...colorMatches].slice(0, 30);
|
|
717
903
|
}
|
|
718
|
-
// @font-face
|
|
719
|
-
const fontFaceMatches = html.matchAll(/@font-face\s*\{([^}]+)\}/g);
|
|
720
|
-
const fonts = [];
|
|
721
|
-
for (const match of fontFaceMatches) {
|
|
722
|
-
const block = match[1];
|
|
723
|
-
const familyMatch = block.match(/font-family\s*:\s*["']?([^"';]+)["']?/);
|
|
724
|
-
const srcMatch = block.match(/src\s*:\s*([^;]+)/);
|
|
725
|
-
fonts.push({
|
|
726
|
-
family: familyMatch?.[1]?.trim(),
|
|
727
|
-
src: srcMatch?.[1]?.trim()?.slice(0, 200),
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
904
|
if (fonts.length > 0) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
905
|
+
const seenFonts = new Set();
|
|
906
|
+
result.fontFaces = fonts
|
|
907
|
+
.filter((font) => {
|
|
908
|
+
const key = `${font.family ?? ""}\u0000${font.src ?? ""}`;
|
|
909
|
+
if (seenFonts.has(key))
|
|
910
|
+
return false;
|
|
911
|
+
seenFonts.add(key);
|
|
912
|
+
return true;
|
|
913
|
+
})
|
|
914
|
+
.slice(0, 20);
|
|
915
|
+
}
|
|
916
|
+
if (googleFonts.size > 0) {
|
|
917
|
+
result.googleFonts = [...googleFonts];
|
|
741
918
|
}
|
|
742
919
|
// OG image
|
|
743
|
-
const
|
|
744
|
-
if (
|
|
745
|
-
|
|
920
|
+
const ogImage = readMetaContent(html, "property", "og:image");
|
|
921
|
+
if (ogImage) {
|
|
922
|
+
const resolved = resolveHttpUrl(ogImage, pageUrl);
|
|
923
|
+
result.ogImage = resolved.kind === "resolved" ? resolved.url : ogImage;
|
|
746
924
|
}
|
|
747
925
|
// Favicon
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
926
|
+
for (const match of html.matchAll(/<link\b[^>]*>/gi)) {
|
|
927
|
+
const tag = match[0];
|
|
928
|
+
const rel = readHtmlAttribute(tag, "rel");
|
|
929
|
+
if (!rel?.split(/\s+/).some((token) => token.toLowerCase() === "icon")) {
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
const href = readHtmlAttribute(tag, "href");
|
|
933
|
+
if (href && !/^data:/i.test(href)) {
|
|
934
|
+
const resolved = resolveHttpUrl(href, pageUrl);
|
|
935
|
+
result.favicon = resolved.kind === "resolved" ? resolved.url : href;
|
|
936
|
+
}
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
if (stylesheetResult.urls.length > 0) {
|
|
940
|
+
result.stylesheetUrls = stylesheetResult.urls;
|
|
941
|
+
}
|
|
942
|
+
if (stylesheetResult.failures.length > 0) {
|
|
943
|
+
result.stylesheetFailures = stylesheetResult.failures;
|
|
751
944
|
}
|
|
752
945
|
return result;
|
|
753
946
|
}
|
package/dist/server/email.d.ts
CHANGED
|
@@ -58,6 +58,17 @@ export interface SendEmailArgs {
|
|
|
58
58
|
references?: string;
|
|
59
59
|
attachments?: EmailAttachment[];
|
|
60
60
|
timeoutMs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Registered transactional email id (see `defineTransactionalEmail`), e.g.
|
|
63
|
+
* `calendar.booking-confirmed`. Tags the message at the provider so delivery
|
|
64
|
+
* and open metrics attribute to one email instead of to the whole account,
|
|
65
|
+
* and keys the row written to `email_log`. Omit for genuinely one-off sends.
|
|
66
|
+
*/
|
|
67
|
+
templateId?: string;
|
|
68
|
+
/** App slug that owns the send. Defaults to the running app. */
|
|
69
|
+
app?: string;
|
|
70
|
+
/** Organization that owns the send. Defaults to the current request org. */
|
|
71
|
+
orgId?: string;
|
|
61
72
|
}
|
|
62
73
|
export declare function isEmailConfigured(): Promise<boolean>;
|
|
63
74
|
/**
|
package/dist/server/email.js
CHANGED
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
* so the reset-password flow still works end-to-end for local development.
|
|
11
11
|
*/
|
|
12
12
|
import { FAVICON_PNG_BASE64 } from "../assets/branding/favicon-base64.js";
|
|
13
|
+
import { getScopedEmailProviderCategory, recordEmailSend, } from "../email-catalog/log.js";
|
|
14
|
+
import { getAppSlug } from "./app-name.js";
|
|
13
15
|
import { resolveSecret } from "./credential-provider.js";
|
|
14
16
|
import { AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID } from "./email-template.js";
|
|
17
|
+
import { getRequestOrgId } from "./request-context.js";
|
|
15
18
|
let cachedAgentNativeLogo;
|
|
16
19
|
function getAgentNativeLogoAttachment() {
|
|
17
20
|
cachedAgentNativeLogo ??= Buffer.from(FAVICON_PNG_BASE64, "base64");
|
|
@@ -154,7 +157,7 @@ function resolveAppSender(configuredFrom, appSender) {
|
|
|
154
157
|
replyTo: appSender.replyTo,
|
|
155
158
|
};
|
|
156
159
|
}
|
|
157
|
-
async function
|
|
160
|
+
async function deliverEmail(args, signal) {
|
|
158
161
|
const config = await resolveEmailTransport();
|
|
159
162
|
signal?.throwIfAborted();
|
|
160
163
|
const provider = config.provider;
|
|
@@ -206,7 +209,7 @@ async function sendEmailWithSignal(args, signal) {
|
|
|
206
209
|
const body = await res.text().catch(() => "");
|
|
207
210
|
throw new Error(`Resend error ${res.status}: ${body}`);
|
|
208
211
|
}
|
|
209
|
-
return;
|
|
212
|
+
return { provider, from };
|
|
210
213
|
}
|
|
211
214
|
if (provider === "sendgrid") {
|
|
212
215
|
const personalization = {
|
|
@@ -227,6 +230,19 @@ async function sendEmailWithSignal(args, signal) {
|
|
|
227
230
|
};
|
|
228
231
|
if (replyTo)
|
|
229
232
|
sgPayload.reply_to = parseSendGridFrom(replyTo);
|
|
233
|
+
// Categories are how per-email delivery/open stats are attributed. Without
|
|
234
|
+
// them every send lands in one undifferentiated account-wide bucket, which
|
|
235
|
+
// is indistinguishable from an email that never sent.
|
|
236
|
+
const orgId = args.orgId ?? getRequestOrgId();
|
|
237
|
+
const categories = [
|
|
238
|
+
args.templateId,
|
|
239
|
+
args.app ?? getAppSlug(),
|
|
240
|
+
args.templateId && orgId
|
|
241
|
+
? getScopedEmailProviderCategory(args.templateId, orgId)
|
|
242
|
+
: undefined,
|
|
243
|
+
].filter((value) => Boolean(value));
|
|
244
|
+
if (categories.length)
|
|
245
|
+
sgPayload.categories = categories;
|
|
230
246
|
const sgHeaders = {};
|
|
231
247
|
if (args.inReplyTo)
|
|
232
248
|
sgHeaders["In-Reply-To"] = args.inReplyTo;
|
|
@@ -258,7 +274,7 @@ async function sendEmailWithSignal(args, signal) {
|
|
|
258
274
|
const body = await res.text().catch(() => "");
|
|
259
275
|
throw new Error(`SendGrid error ${res.status}: ${body}`);
|
|
260
276
|
}
|
|
261
|
-
return;
|
|
277
|
+
return { provider, from };
|
|
262
278
|
}
|
|
263
279
|
// Dev fallback — no provider configured. Logging the full body exposes
|
|
264
280
|
// reset tokens, so only do it outside production. In production, refuse
|
|
@@ -270,6 +286,41 @@ async function sendEmailWithSignal(args, signal) {
|
|
|
270
286
|
`Save RESEND_API_KEY or SENDGRID_API_KEY in settings to send real emails.\n` +
|
|
271
287
|
`---\nTo: ${args.to}\nFrom: ${from}\nSubject: ${args.subject}\n\n` +
|
|
272
288
|
`${args.text || stripHtml(args.html)}\n---\n`);
|
|
289
|
+
return { provider, from };
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Deliver, then record the attempt. Recording lives here rather than in each
|
|
293
|
+
* provider branch so a new transport cannot be added without being logged.
|
|
294
|
+
*/
|
|
295
|
+
async function sendEmailWithSignal(args, signal) {
|
|
296
|
+
let outcome;
|
|
297
|
+
try {
|
|
298
|
+
outcome = await deliverEmail(args, signal);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
await recordEmailSend({
|
|
302
|
+
templateId: args.templateId,
|
|
303
|
+
app: args.app ?? getAppSlug() ?? "unknown",
|
|
304
|
+
orgId: args.orgId ?? getRequestOrgId(),
|
|
305
|
+
recipient: args.to,
|
|
306
|
+
sender: outcome?.from ?? args.from ?? "unknown",
|
|
307
|
+
subject: args.subject,
|
|
308
|
+
status: "failed",
|
|
309
|
+
error: error instanceof Error ? error.message : String(error),
|
|
310
|
+
provider: outcome?.provider ?? "unknown",
|
|
311
|
+
});
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
await recordEmailSend({
|
|
315
|
+
templateId: args.templateId,
|
|
316
|
+
app: args.app ?? getAppSlug() ?? "unknown",
|
|
317
|
+
orgId: args.orgId ?? getRequestOrgId(),
|
|
318
|
+
recipient: args.to,
|
|
319
|
+
sender: outcome.from,
|
|
320
|
+
subject: args.subject,
|
|
321
|
+
status: "sent",
|
|
322
|
+
provider: outcome.provider,
|
|
323
|
+
});
|
|
273
324
|
}
|
|
274
325
|
export async function sendEmail(args) {
|
|
275
326
|
const requestedTimeoutMs = Number(args.timeoutMs);
|
package/dist/server/index.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export { builderDesignSystemUrl, builderProjectBranchUrl, buildBuilderDesignSyst
|
|
|
66
66
|
export { getBuilderBranchProjectId, isBuilderBranchingEnabled, requestBuilderBrowserConnection, resolveBuilderBranchProjectId, resolveIsBuilderBranchingEnabled, runBuilderAgent, type RunBuilderAgentResult, } from "./builder-browser.js";
|
|
67
67
|
export { ensureFusionContainer, sendFusionBranchMessage, pushFusionBranch, reserveFusionHostingSlug, deployFusionProject, getFusionDeploys, getFusionBranchEditorUrl, getFusionHostingUrl, type FusionBranchRef, type EnsureFusionContainerResult, type SendFusionMessageResult, } from "./fusion-app.js";
|
|
68
68
|
export { sendEmail, isEmailConfigured, getEmailReadiness, getEmailProvider, type EmailAttachment, type EmailReadiness, type EmailProvider, type SendEmailArgs, } from "./email.js";
|
|
69
|
+
export { defineTransactionalEmail, listTransactionalEmails, getTransactionalEmail, renderTransactionalEmailPreview, type TransactionalEmailDefinition, type RegisteredTransactionalEmail, } from "../email-catalog/registry.js";
|
|
69
70
|
export { notifyActivity, runActivityNotification, resolveActivityRecipients, type ActivityDeliveryFailure, type ActivityNotificationResult, type ActivityNotificationStatus, type NotifyActivityInput, type ResolveActivityRecipientsInput, } from "./activity-notifications.js";
|
|
70
71
|
export { renderEmail, emailStrong, emailLink, type RenderEmailArgs, type RenderedEmail, type EmailCta, } from "./email-template.js";
|
|
71
72
|
export { getAppProductionUrl, getFirstPartyProdUrl } from "./app-url.js";
|
package/dist/server/index.js
CHANGED
|
@@ -67,6 +67,7 @@ export { builderDesignSystemUrl, builderProjectBranchUrl, buildBuilderDesignSyst
|
|
|
67
67
|
export { getBuilderBranchProjectId, isBuilderBranchingEnabled, requestBuilderBrowserConnection, resolveBuilderBranchProjectId, resolveIsBuilderBranchingEnabled, runBuilderAgent, } from "./builder-browser.js";
|
|
68
68
|
export { ensureFusionContainer, sendFusionBranchMessage, pushFusionBranch, reserveFusionHostingSlug, deployFusionProject, getFusionDeploys, getFusionBranchEditorUrl, getFusionHostingUrl, } from "./fusion-app.js";
|
|
69
69
|
export { sendEmail, isEmailConfigured, getEmailReadiness, getEmailProvider, } from "./email.js";
|
|
70
|
+
export { defineTransactionalEmail, listTransactionalEmails, getTransactionalEmail, renderTransactionalEmailPreview, } from "../email-catalog/registry.js";
|
|
70
71
|
export { notifyActivity, runActivityNotification, resolveActivityRecipients, } from "./activity-notifications.js";
|
|
71
72
|
export { renderEmail, emailStrong, emailLink, } from "./email-template.js";
|
|
72
73
|
export { getAppProductionUrl, getFirstPartyProdUrl } from "./app-url.js";
|
|
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
|
|
|
26
26
|
expiresAt?: undefined;
|
|
27
27
|
ttlSeconds?: undefined;
|
|
28
28
|
} | {
|
|
29
|
+
error?: undefined;
|
|
29
30
|
token: string;
|
|
30
31
|
expiresAt: string;
|
|
31
32
|
ttlSeconds: number;
|
|
32
|
-
error?: undefined;
|
|
33
33
|
}>>;
|