@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.
Files changed (157) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +1 -1
  3. package/corpus/templates/analytics/actions/gong-calls.ts +56 -18
  4. package/corpus/templates/analytics/changelog/2026-08-05-analytics-queries-keep-every-table-tenant-scoped.md +6 -0
  5. package/corpus/templates/analytics/changelog/2026-08-05-durable-a2a-workers-avoid-startup-migration-contention.md +6 -0
  6. package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
  7. package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
  8. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  9. package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
  10. package/corpus/templates/analytics/server/lib/emails.ts +51 -0
  11. package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
  12. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
  13. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
  14. package/corpus/templates/analytics/server/lib/gong.ts +132 -33
  15. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  16. package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
  17. package/corpus/templates/analytics/server/plugins/db.ts +35 -10
  18. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
  19. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
  20. package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
  21. package/corpus/templates/calendar/server/lib/emails.ts +167 -0
  22. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
  23. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
  24. package/corpus/templates/clips/actions/invite-member.ts +39 -16
  25. package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
  26. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  27. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
  28. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  29. package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
  30. package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
  31. package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
  32. package/corpus/templates/clips/desktop/src/styles.css +0 -3
  33. package/corpus/templates/clips/server/lib/emails.ts +256 -0
  34. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
  35. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
  36. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
  37. package/corpus/templates/clips/shared/email-preview.ts +61 -0
  38. package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
  39. package/corpus/templates/content/server/lib/emails.ts +70 -0
  40. package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
  41. package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
  42. package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
  43. package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
  44. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
  45. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
  46. package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
  47. package/corpus/templates/factory/AGENTS.md +18 -17
  48. package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
  49. package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
  50. package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
  51. package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
  52. package/corpus/templates/factory/app/routes/factory.tsx +376 -206
  53. package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
  54. package/corpus/templates/factory/server/db/schema.ts +6 -0
  55. package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
  56. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
  57. package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
  58. package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
  59. package/corpus/templates/forms/server/lib/emails.ts +52 -0
  60. package/corpus/templates/forms/server/lib/response-email.ts +2 -0
  61. package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
  62. package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
  63. package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
  64. package/corpus/templates/plan/server/lib/emails.ts +67 -0
  65. package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
  66. package/corpus/templates/slides/actions/import-from-url.ts +1 -0
  67. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
  68. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
  69. package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
  70. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
  71. package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
  72. package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
  73. package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
  74. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
  75. package/corpus/templates/slides/app/pages/Index.tsx +127 -137
  76. package/corpus/templates/slides/app/root.tsx +8 -0
  77. package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
  79. package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
  80. package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
  81. package/corpus/templates/slides/server/lib/emails.ts +40 -0
  82. package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
  83. package/dist/brand-kit/index.d.ts +1 -0
  84. package/dist/brand-kit/index.js +1 -0
  85. package/dist/brand-kit/tokens.d.ts +91 -0
  86. package/dist/brand-kit/tokens.js +279 -0
  87. package/dist/brand-kit/types.d.ts +25 -0
  88. package/dist/client/AssistantChat.js +7 -7
  89. package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
  90. package/dist/client/onboarding/first-run-registry.d.ts +25 -0
  91. package/dist/client/onboarding/first-run-registry.js +18 -0
  92. package/dist/client/onboarding/index.d.ts +2 -0
  93. package/dist/client/onboarding/index.js +1 -0
  94. package/dist/collab/awareness.d.ts +2 -2
  95. package/dist/collab/routes.d.ts +1 -1
  96. package/dist/collab/struct-routes.d.ts +1 -1
  97. package/dist/db/index.d.ts +1 -1
  98. package/dist/db/index.js +1 -1
  99. package/dist/db/migrations.d.ts +13 -3
  100. package/dist/db/migrations.js +19 -4
  101. package/dist/deploy/build.d.ts +8 -0
  102. package/dist/deploy/build.js +92 -3
  103. package/dist/deploy/workspace-deploy.js +86 -3
  104. package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
  105. package/dist/email-catalog/actions/list-email-activity.js +19 -0
  106. package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
  107. package/dist/email-catalog/actions/list-email-engagement.js +15 -0
  108. package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
  109. package/dist/email-catalog/actions/list-email-log.js +23 -0
  110. package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
  111. package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
  112. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
  113. package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
  114. package/dist/email-catalog/authorize.d.ts +1 -0
  115. package/dist/email-catalog/authorize.js +9 -0
  116. package/dist/email-catalog/log.d.ts +60 -0
  117. package/dist/email-catalog/log.js +142 -0
  118. package/dist/email-catalog/provider-metrics.d.ts +34 -0
  119. package/dist/email-catalog/provider-metrics.js +182 -0
  120. package/dist/email-catalog/registry.d.ts +78 -0
  121. package/dist/email-catalog/registry.js +70 -0
  122. package/dist/email-catalog/schema.d.ts +231 -0
  123. package/dist/email-catalog/schema.js +53 -0
  124. package/dist/email-catalog/system-emails.d.ts +13 -0
  125. package/dist/email-catalog/system-emails.js +67 -0
  126. package/dist/jobs/index.d.ts +2 -0
  127. package/dist/jobs/index.js +2 -0
  128. package/dist/jobs/run-history.js +48 -1
  129. package/dist/jobs/scheduler-dispatch.d.ts +7 -0
  130. package/dist/jobs/scheduler-dispatch.js +7 -0
  131. package/dist/jobs/scheduler-health.d.ts +22 -0
  132. package/dist/jobs/scheduler-health.js +172 -0
  133. package/dist/jobs/scheduler.js +67 -2
  134. package/dist/localization/default-messages.d.ts +45 -0
  135. package/dist/localization/default-messages.js +45 -0
  136. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  137. package/dist/notifications/routes.d.ts +3 -3
  138. package/dist/observability/routes.d.ts +3 -3
  139. package/dist/org/handlers.js +9 -1
  140. package/dist/progress/routes.d.ts +1 -1
  141. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  142. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  143. package/dist/server/action-discovery.js +22 -0
  144. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
  145. package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
  146. package/dist/server/agent-chat-plugin.d.ts +2 -1
  147. package/dist/server/agent-chat-plugin.js +69 -22
  148. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  149. package/dist/server/better-auth-instance.js +17 -2
  150. package/dist/server/design-token-utils.d.ts +6 -1
  151. package/dist/server/design-token-utils.js +252 -59
  152. package/dist/server/email.d.ts +11 -0
  153. package/dist/server/email.js +54 -3
  154. package/dist/server/index.d.ts +3 -2
  155. package/dist/server/index.js +3 -2
  156. package/dist/vite/client.js +20 -14
  157. package/package.json +2 -1
@@ -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) {
@@ -4661,7 +4663,8 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4661
4663
  }
4662
4664
  return invokeAgentChatHandler(event);
4663
4665
  }));
4664
- const disableRecurringJobsRuntime = shouldDisableRecurringJobsRuntime();
4666
+ const isBackgroundRuntime = isInBackgroundFunctionRuntime();
4667
+ const disableRecurringJobsRuntime = isBackgroundRuntime || shouldDisableRecurringJobsRuntime();
4665
4668
  // ─── Recurring Jobs Scheduler ──────────────────────────────────────
4666
4669
  // Poll every 60 seconds for due recurring jobs and execute them.
4667
4670
  // Uses setInterval so it works in all deployment environments without
@@ -4696,11 +4699,47 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4696
4699
  appId: options?.appId,
4697
4700
  };
4698
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
+ }));
4699
4733
  if (disableRecurringJobsRuntime) {
4700
4734
  if (process.env.DEBUG) {
4701
4735
  console.log("[recurring-jobs] Scheduler disabled for local development");
4702
4736
  }
4703
4737
  }
4738
+ else if (isNetlifyRecurringJobsRuntime()) {
4739
+ if (process.env.DEBUG) {
4740
+ console.log("[recurring-jobs] Using the durable Netlify scheduled sweep");
4741
+ }
4742
+ }
4704
4743
  else {
4705
4744
  // Start after a 10-second delay to let the server fully initialize
4706
4745
  setTimeout(() => {
@@ -4720,26 +4759,28 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4720
4759
  // A non-Netlify self-dispatch only waits for the request to leave the
4721
4760
  // current invocation. Keep the durable history row as a retryable queue
4722
4761
  // so a frozen serverless handoff is recovered on the next sweep.
4723
- (() => {
4724
- let inFlight = false;
4725
- const sweep = async () => {
4726
- if (inFlight)
4727
- return;
4728
- inFlight = true;
4729
- try {
4730
- const { redispatchUnclaimedAutomationRuns } = await import("../jobs/run-now.js");
4731
- await redispatchUnclaimedAutomationRuns();
4732
- }
4733
- catch (error) {
4734
- console.warn("[automations] queued-run sweep failed; retrying next tick:", error);
4735
- }
4736
- finally {
4737
- inFlight = false;
4738
- }
4739
- };
4740
- setTimeout(() => void sweep(), 15_000);
4741
- setInterval(() => void sweep(), 30_000);
4742
- })();
4762
+ if (!isBackgroundRuntime) {
4763
+ (() => {
4764
+ let inFlight = false;
4765
+ const sweep = async () => {
4766
+ if (inFlight)
4767
+ return;
4768
+ inFlight = true;
4769
+ try {
4770
+ const { redispatchUnclaimedAutomationRuns } = await import("../jobs/run-now.js");
4771
+ await redispatchUnclaimedAutomationRuns();
4772
+ }
4773
+ catch (error) {
4774
+ console.warn("[automations] queued-run sweep failed; retrying next tick:", error);
4775
+ }
4776
+ finally {
4777
+ inFlight = false;
4778
+ }
4779
+ };
4780
+ setTimeout(() => void sweep(), 15_000);
4781
+ setInterval(() => void sweep(), 30_000);
4782
+ })();
4783
+ }
4743
4784
  mcpInitializationPromise = initializeMcpManager().catch((err) => {
4744
4785
  console.warn(`[mcp-client] deferred initialization failed: ${err?.message ?? err}`);
4745
4786
  });
@@ -4750,6 +4791,8 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4750
4791
  // per instance; cheap (one indexed query when no active tasks are found).
4751
4792
  // Throttled by the same per-owner interval guard inside reconcileAgentTeamRunsForOwner.
4752
4793
  (() => {
4794
+ if (isBackgroundRuntime)
4795
+ return;
4753
4796
  // Track when this instance last ran the sweep so only one sweep fires
4754
4797
  // per 2-min window even if multiple timers fire in overlapping invocations.
4755
4798
  let lastSweep = 0;
@@ -4897,6 +4940,8 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4897
4940
  // comment above for why this exists and the timing budget in
4898
4941
  // run-store.ts's `UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS` doc comment.
4899
4942
  (() => {
4943
+ if (isBackgroundRuntime)
4944
+ return;
4900
4945
  setTimeout(() => {
4901
4946
  (async () => {
4902
4947
  const { UNCLAIMED_BACKGROUND_RUN_FAST_SWEEP_MS } = await import("../agent/run-store.js");
@@ -4942,6 +4987,8 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4942
4987
  // still fails loud, and it survives the fast sweep having missed a row
4943
4988
  // entirely (e.g. a restart landed between fast-sweep ticks).
4944
4989
  (() => {
4990
+ if (isBackgroundRuntime)
4991
+ return;
4945
4992
  let lastSweep = 0;
4946
4993
  const SWEEP_INTERVAL_MS = 2 * 60 * 1000;
4947
4994
  setTimeout(() => {
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
- error?: undefined;
31
30
  ok: boolean;
32
31
  key: string;
33
32
  baseUrlKey?: string;
34
33
  scope: AgentEngineApiKeyScope;
34
+ error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -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({ to: user.email, subject, html, text, appSender });
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({ to: user.email, subject, html, text, appSender });
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
- /** Fetch and extract design tokens from a URL's HTML. */
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(/&amp;/gi, "&")
683
+ .replace(/&quot;/gi, '"')
684
+ .replace(/&#39;|&apos;/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.startsWith("http") ? rawUrl : `https://${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(10000),
675
- });
676
- const html = await response.text();
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[^>]*>([^<]+)<\/title>/i);
864
+ const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
680
865
  if (titleMatch) {
681
866
  result.pageTitle = titleMatch[1].trim();
682
867
  }
683
- // Meta description
684
- const descMatch = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i);
685
- if (descMatch) {
686
- result.metaDescription = descMatch[1];
868
+ const metaDescription = readMetaContent(html, "name", "description");
869
+ if (metaDescription !== undefined) {
870
+ result.metaDescription = metaDescription;
687
871
  }
688
- // Meta theme-color
689
- const themeColorMatch = html.match(/<meta[^>]*name=["']theme-color["'][^>]*content=["']([^"']+)["']/i);
690
- if (themeColorMatch) {
691
- result.themeColor = themeColorMatch[1];
872
+ const themeColor = readMetaContent(html, "name", "theme-color");
873
+ if (themeColor !== undefined) {
874
+ result.themeColor = themeColor;
692
875
  }
693
- // CSS custom properties
694
- const cssVarMatches = html.matchAll(/--([\w-]+)\s*:\s*([^;}\n]+)/g);
695
- const cssVars = {};
696
- for (const match of cssVarMatches) {
697
- cssVars[`--${match[1]}`] = match[2].trim();
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
- result.fontFaces = fonts.slice(0, 20);
732
- }
733
- // Google Fonts
734
- const googleFontMatches = html.matchAll(/fonts\.googleapis\.com\/css2?\?[^"'>\s]+/g);
735
- const googleFonts = [];
736
- for (const match of googleFontMatches) {
737
- googleFonts.push(match[0]);
738
- }
739
- if (googleFonts.length > 0) {
740
- result.googleFonts = googleFonts;
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 ogImageMatch = html.match(/<meta[^>]*property=["']og:image["'][^>]*content=["']([^"']+)["']/i);
744
- if (ogImageMatch) {
745
- result.ogImage = ogImageMatch[1];
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 faviconMatch = html.match(/<link[^>]*rel=["'](?:icon|shortcut icon)["'][^>]*href=["']([^"']+)["']/i);
749
- if (faviconMatch) {
750
- result.favicon = faviconMatch[1];
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
  }
@@ -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
  /**