@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
|
@@ -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
|
|
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
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
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({
|
|
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
|
/**
|