@jami-studio/core 0.92.11 → 0.92.13

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.
@@ -139,6 +139,38 @@ export const CLOUDFLARE_WORKER_STUB_MODULES = {
139
139
  "@excalidraw/excalidraw": "export default {}; export const MainMenu = {}; export const WelcomeScreen = {};\n",
140
140
  "@excalidraw/mermaid-to-excalidraw": "export default async () => ({ elements: [], files: {} });\n",
141
141
  };
142
+ /**
143
+ * Optional AI SDK packages that ai-sdk-engine imports with LITERAL dynamic
144
+ * import() specifiers (so bundlers include provider packages when present).
145
+ * They are optional peer dependencies — when an app hasn't installed one,
146
+ * esbuild fails the worker bundle with "Could not resolve". Stub every
147
+ * uninstalled one with a module that throws at import time, which makes the
148
+ * dynamic import() reject exactly like the missing package does under Node.
149
+ */
150
+ export const OPTIONAL_AI_SDK_MODULES = [
151
+ "ai",
152
+ "@ai-sdk/anthropic",
153
+ "@ai-sdk/openai",
154
+ "@ai-sdk/google",
155
+ "@ai-sdk/groq",
156
+ "@ai-sdk/mistral",
157
+ "@ai-sdk/cohere",
158
+ "@openrouter/ai-sdk-provider",
159
+ "ai-sdk-ollama",
160
+ ];
161
+ export function uninstalledOptionalAiSdkStubs(appDir) {
162
+ const appRequire = createRequire(path.join(appDir, "package.json"));
163
+ const stubs = {};
164
+ for (const mod of OPTIONAL_AI_SDK_MODULES) {
165
+ try {
166
+ appRequire.resolve(mod);
167
+ }
168
+ catch {
169
+ stubs[mod] = `throw new Error(${JSON.stringify(`${mod} is not installed in this deployment`)});\n`;
170
+ }
171
+ }
172
+ return stubs;
173
+ }
142
174
  function cloudflareNodeBuiltinStubSource(moduleName, namedExports, overrides = []) {
143
175
  const overridden = new Set(overrides.flatMap((source) => Array.from(source.matchAll(/\bexport const ([A-Za-z_$][\w$]*)/g)).map((match) => match[1])));
144
176
  const exports = Array.from(new Set(namedExports))
@@ -328,19 +360,19 @@ export const CLOUDFLARE_WORKER_NODE_BUILTIN_STUB_MODULES = {
328
360
  "export const createRequire = () => globalThis.require ?? ((specifier) => { throw new Error('Cannot require: ' + specifier); });",
329
361
  ]),
330
362
  net: cloudflareNodeBuiltinStubSource("net", ["Socket", "connect", "createConnection", "createServer", "isIP"], [
331
- `export const isIP = (value) => {
332
- const input = String(value ?? "");
333
- const ipv4Parts = input.split(".");
334
- if (
335
- ipv4Parts.length === 4 &&
336
- ipv4Parts.every((part) => /^\\d{1,3}$/.test(part) && Number(part) >= 0 && Number(part) <= 255)
337
- ) {
338
- return 4;
339
- }
340
- if (input.includes(":") && /^[0-9A-Fa-f:.]+$/.test(input)) {
341
- return 6;
342
- }
343
- return 0;
363
+ `export const isIP = (value) => {
364
+ const input = String(value ?? "");
365
+ const ipv4Parts = input.split(".");
366
+ if (
367
+ ipv4Parts.length === 4 &&
368
+ ipv4Parts.every((part) => /^\\d{1,3}$/.test(part) && Number(part) >= 0 && Number(part) <= 255)
369
+ ) {
370
+ return 4;
371
+ }
372
+ if (input.includes(":") && /^[0-9A-Fa-f:.]+$/.test(input)) {
373
+ return 6;
374
+ }
375
+ return 0;
344
376
  };`,
345
377
  ]),
346
378
  os: cloudflareNodeBuiltinStubSource("os", [
@@ -443,16 +475,16 @@ function isNodeOnlyPlugin(filePath) {
443
475
  }
444
476
  export function generateProvidedPluginsNitroPluginSource(pluginStems) {
445
477
  const stems = [...new Set(pluginStems.filter(Boolean))].sort();
446
- return `// AUTO-GENERATED by @agent-native/core deploy build
447
- import { markDefaultPluginProvided } from "${EDGE_SERVER_ENTRYPOINT}";
448
-
449
- const pluginStems = ${JSON.stringify(stems)};
450
-
451
- export default function markBuildDiscoveredPlugins(nitroApp) {
452
- for (const stem of pluginStems) {
453
- markDefaultPluginProvided(nitroApp, stem);
454
- }
455
- }
478
+ return `// AUTO-GENERATED by @agent-native/core deploy build
479
+ import { markDefaultPluginProvided } from "${EDGE_SERVER_ENTRYPOINT}";
480
+
481
+ const pluginStems = ${JSON.stringify(stems)};
482
+
483
+ export default function markBuildDiscoveredPlugins(nitroApp) {
484
+ for (const stem of pluginStems) {
485
+ markDefaultPluginProvided(nitroApp, stem);
486
+ }
487
+ }
456
488
  `;
457
489
  }
458
490
  async function writeProvidedPluginsNitroPlugin() {
@@ -504,20 +536,20 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
504
536
  routeImports.push(`import ${varName} from ${JSON.stringify(r.absPath)};`);
505
537
  routeRegistrations.push(` app.on(${JSON.stringify(r.method.toUpperCase())}, ${JSON.stringify(r.route)}, ${varName});`);
506
538
  if (r.method.toLowerCase() === "get") {
507
- routeRegistrations.push(` app.on("HEAD", ${JSON.stringify(r.route)}, defineEventHandler(async (event) => {
508
- const originalReq = event.req;
509
- event.req = requestWithMethod(event.req, "GET");
510
- try {
511
- const result = await ${varName}(event);
512
- const response = result instanceof Response ? result : toResponse(result, event);
513
- return new Response(null, {
514
- status: response.status,
515
- statusText: response.statusText,
516
- headers: response.headers,
517
- });
518
- } finally {
519
- event.req = originalReq;
520
- }
539
+ routeRegistrations.push(` app.on("HEAD", ${JSON.stringify(r.route)}, defineEventHandler(async (event) => {
540
+ const originalReq = event.req;
541
+ event.req = requestWithMethod(event.req, "GET");
542
+ try {
543
+ const result = await ${varName}(event);
544
+ const response = result instanceof Response ? result : toResponse(result, event);
545
+ return new Response(null, {
546
+ status: response.status,
547
+ statusText: response.statusText,
548
+ headers: response.headers,
549
+ });
550
+ } finally {
551
+ event.req = originalReq;
552
+ }
521
553
  }));`);
522
554
  }
523
555
  }
@@ -530,15 +562,15 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
530
562
  actionImports.push(`import ${varName} from ${JSON.stringify(a.absPath)};`);
531
563
  // Mirror the runtime mount (action-routes.ts): `path = http?.path ?? name`.
532
564
  const routePath = `/_agent-native/actions/${a.path ?? a.name}`;
533
- actionRegistrations.push(` app.on(${JSON.stringify(a.method.toUpperCase())}, ${JSON.stringify(routePath)}, defineEventHandler(async (event) => {
534
- const params = ${a.method === "get" ? "parseActionSearchParams(event.url.searchParams)" : "(await readBody(event)) ?? {}"};
535
- try {
536
- const result = await ${varName}.run(params, { caller: "http" });
537
- if (typeof result === "string") { try { return JSON.parse(result); } catch { return result; } }
538
- return result;
539
- } catch (err) {
540
- return new Response(JSON.stringify({ error: err?.message || "Action failed" }), { status: err?.message?.startsWith("Invalid action parameters") ? 400 : 500, headers: { "Content-Type": "application/json" } });
541
- }
565
+ actionRegistrations.push(` app.on(${JSON.stringify(a.method.toUpperCase())}, ${JSON.stringify(routePath)}, defineEventHandler(async (event) => {
566
+ const params = ${a.method === "get" ? "parseActionSearchParams(event.url.searchParams)" : "(await readBody(event)) ?? {}"};
567
+ try {
568
+ const result = await ${varName}.run(params, { caller: "http" });
569
+ if (typeof result === "string") { try { return JSON.parse(result); } catch { return result; } }
570
+ return result;
571
+ } catch (err) {
572
+ return new Response(JSON.stringify({ error: err?.message || "Action failed" }), { status: err?.message?.startsWith("Invalid action parameters") ? 400 : 500, headers: { "Content-Type": "application/json" } });
573
+ }
542
574
  }));`);
543
575
  }
544
576
  // Filter out Node-only plugins
@@ -550,8 +582,8 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
550
582
  const varName = `plugin_${i}`;
551
583
  providedPluginStems.add(path.basename(edgePlugins[i], path.extname(edgePlugins[i])));
552
584
  pluginImports.push(`import ${varName} from ${JSON.stringify(edgePlugins[i])};`);
553
- pluginCalls.push(` if (typeof ${varName} === "function") {
554
- await ${varName}(nitroApp);
585
+ pluginCalls.push(` if (typeof ${varName} === "function") {
586
+ await ${varName}(nitroApp);
555
587
  }`);
556
588
  }
557
589
  // Auto-mounted default plugins (for slots the template doesn't override
@@ -574,8 +606,8 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
574
606
  continue;
575
607
  pluginImports.push(`import { ${defaultExportName} as ${varName} } from "${EDGE_SERVER_ENTRYPOINT}";`);
576
608
  }
577
- pluginCalls.push(` if (typeof ${varName} === "function") {
578
- await ${varName}(nitroApp);
609
+ pluginCalls.push(` if (typeof ${varName} === "function") {
610
+ await ${varName}(nitroApp);
579
611
  }`);
580
612
  }
581
613
  const generatedPluginMarks = providedPluginStems.size > 0
@@ -589,509 +621,509 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
589
621
  if (generatedPluginMarks.length > 0) {
590
622
  pluginImports.unshift(`import { markDefaultPluginProvided as markGeneratedPluginProvided } from "${EDGE_SERVER_ENTRYPOINT}";`);
591
623
  }
592
- return `
593
- // Auto-generated worker entry point for ${preset}
594
- import { H3, defineEventHandler, readBody, toResponse } from "h3";
595
- ${includeReactRouterSsr ? 'import { createRequestHandler } from "react-router";' : ""}
596
- ${includeReactRouterSsr ? 'import * as serverBuild from "./server-build.js";' : ""}
597
-
598
- function normalizeAppBasePath(value) {
599
- if (!value || value === "/") return "";
600
- const trimmed = String(value).trim();
601
- if (!trimmed || trimmed === "/") return "";
602
- return "/" + trimmed.replace(/^\\/+/, "").replace(/\\/+$/, "");
603
- }
604
-
605
- function getAppBasePath() {
606
- const builtAppBasePath = ${JSON.stringify(builtAppBasePath)};
607
- return normalizeAppBasePath(
608
- globalThis.process?.env?.VITE_APP_BASE_PATH ||
609
- globalThis.process?.env?.APP_BASE_PATH ||
610
- builtAppBasePath,
611
- );
612
- }
613
-
614
- function stripAppBasePath(pathname) {
615
- const basePath = getAppBasePath();
616
- if (!basePath) return pathname;
617
- if (pathname === basePath) return "/";
618
- if (pathname.startsWith(basePath + "/")) {
619
- return pathname.slice(basePath.length) || "/";
620
- }
621
- return pathname;
622
- }
623
-
624
- function parseActionSearchParams(searchParams) {
625
- const params = {};
626
- for (const [rawKey, value] of searchParams.entries()) {
627
- const isArrayKey = rawKey.endsWith("[]");
628
- // The core client serializes arrays as key[]=value so one-item arrays
629
- // survive GET action parsing in generated worker deployments.
630
- const key = isArrayKey ? rawKey.slice(0, -2) : rawKey;
631
- const current = params[key];
632
- if (current === undefined) {
633
- params[key] = isArrayKey ? [value] : value;
634
- } else if (Array.isArray(current)) {
635
- current.push(value);
636
- } else {
637
- params[key] = [current, value];
638
- }
639
- }
640
- return params;
641
- }
642
-
643
- function isApiPath(pathname) {
644
- return pathname === "/api" || pathname.startsWith("/api/");
645
- }
646
-
647
- function isFrameworkPath(pathname) {
648
- return (
649
- pathname === "/_agent-native" || pathname.startsWith("/_agent-native/")
650
- );
651
- }
652
-
653
- function requestWithMountedApiPrefixStripped(request) {
654
- const basePath = getAppBasePath();
655
- if (!basePath) return request;
656
- const url = new URL(request.url);
657
- const strippedPathname = stripAppBasePath(url.pathname);
658
- if (strippedPathname === url.pathname) {
659
- return request;
660
- }
661
- if (!isApiPath(strippedPathname) && !isFrameworkPath(strippedPathname)) {
662
- return request;
663
- }
664
- url.pathname = strippedPathname;
665
- return new Request(url, request);
666
- }
667
-
668
- function prefixMountedPath(path, basePath) {
669
- if (!basePath || !path.startsWith("/") || path.startsWith("//")) return path;
670
- if (path === basePath || path.startsWith(basePath + "/")) return path;
671
- return basePath + path;
672
- }
673
-
674
- function prefixMountedHtml(html, basePath) {
675
- if (!basePath) return html;
676
- return html
677
- .replace(
678
- /\\b(href|src|action|formaction|poster)=(["'])(\\/(?!\\/)[^"']*)\\2/g,
679
- (_match, attr, quote, path) =>
680
- attr + "=" + quote + prefixMountedPath(path, basePath) + quote,
681
- )
682
- .replace(/url\\((["']?)(\\/(?!\\/)[^)'" ]+)\\1\\)/g, (_match, quote, path) => {
683
- const q = quote || "";
684
- return "url(" + q + prefixMountedPath(path, basePath) + q + ")";
685
- });
686
- }
687
-
688
- function firstNonEmpty() {
689
- for (const value of arguments) {
690
- const trimmed = typeof value === "string" ? value.trim() : "";
691
- if (trimmed) return trimmed;
692
- }
693
- }
694
-
695
- function getSentryClientConfigScript() {
696
- const env = globalThis.process?.env || {};
697
- const key = firstNonEmpty(env.SENTRY_CLIENT_KEY, env.VITE_SENTRY_CLIENT_KEY);
698
- const projectId = firstNonEmpty(
699
- env.SENTRY_PROJECT_ID,
700
- env.VITE_SENTRY_PROJECT_ID,
701
- );
702
- const host = firstNonEmpty(
703
- env.SENTRY_INGEST_HOST,
704
- env.VITE_SENTRY_INGEST_HOST,
705
- );
706
- const dsn =
707
- firstNonEmpty(
708
- env.SENTRY_CLIENT_DSN,
709
- env.VITE_SENTRY_CLIENT_DSN,
710
- env.VITE_SENTRY_DSN,
711
- env.SENTRY_DSN,
712
- ) || (key && projectId && host ? "https://" + key + "@" + host + "/" + projectId : undefined);
713
- if (!dsn) return null;
714
- const config = {
715
- sentryDsn: dsn,
716
- sentryEnvironment:
717
- firstNonEmpty(
718
- env.SENTRY_ENVIRONMENT,
719
- env.NETLIFY_CONTEXT,
720
- env.VERCEL_ENV,
721
- env.NODE_ENV,
722
- ) || "production",
723
- };
724
- return (
725
- '<script data-agent-native-sentry-config>' +
726
- 'window.__AGENT_NATIVE_CONFIG__=Object.assign({},window.__AGENT_NATIVE_CONFIG__,' +
727
- JSON.stringify(config) +
728
- ");</script>"
729
- );
730
- }
731
-
732
- function injectHeadScript(html, script) {
733
- if (!script) return html;
734
- const headCloseIdx = html.indexOf("</head>");
735
- if (headCloseIdx === -1) return html;
736
- return html.slice(0, headCloseIdx) + script + html.slice(headCloseIdx);
737
- }
738
-
739
- const DEFAULT_SSR_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_CACHE_CONTROL)};
740
- const DEFAULT_SSR_CDN_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_CDN_CACHE_CONTROL)};
741
- const DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL)};
742
- const DEFAULT_SPECULATION_RULES_PATH = ${JSON.stringify(DEFAULT_SPECULATION_RULES_PATH)};
743
- const IMMUTABLE_ASSET_CACHE_CONTROL = ${JSON.stringify(IMMUTABLE_ASSET_CACHE_CONTROL)};
744
- const IMMUTABLE_ASSET_PATHS = new Set(${JSON.stringify([...new Set(immutableAssetPaths)].sort())});
745
- const AGENT_NATIVE_SOCIAL_IMAGE_PATH = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_PATH)};
746
- const AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER)};
747
- const AGENT_NATIVE_SOCIAL_IMAGE_ALT = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_ALT)};
748
- const AGENT_NATIVE_SOCIAL_IMAGE_TYPE = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_TYPE)};
749
- const AGENT_NATIVE_SOCIAL_IMAGE_WIDTH = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_WIDTH)};
750
- const AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT)};
751
- const OG_IMAGE_META_RE = /<meta\\b(?=[^>]*\\bproperty=(["'])og:image\\1)[^>]*>/i;
752
- const TWITTER_CARD_META_RE = /<meta\\b(?=[^>]*\\bname=(["'])twitter:card\\1)[^>]*>/i;
753
- const TWITTER_IMAGE_META_RE = /<meta\\b(?=[^>]*\\bname=(["'])twitter:image\\1)[^>]*>/i;
754
-
755
- function withAgentNativeSocialImageCacheBuster(image) {
756
- const separator = image.includes("?") ? "&" : "?";
757
- return image + separator + "v=" + encodeURIComponent(AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER);
758
- }
759
-
760
- function defaultSocialImageUrl(request, basePath) {
761
- return withAgentNativeSocialImageCacheBuster(
762
- new URL(prefixMountedPath(AGENT_NATIVE_SOCIAL_IMAGE_PATH, basePath), request.url).toString()
763
- );
764
- }
765
-
766
- function injectDefaultSocialImageMeta(html, imageUrl) {
767
- const headCloseIdx = html.indexOf("</head>");
768
- if (headCloseIdx === -1) return html;
769
-
770
- const hasAnySocialImage =
771
- OG_IMAGE_META_RE.test(html) || TWITTER_IMAGE_META_RE.test(html);
772
- const tags = [];
773
-
774
- if (!hasAnySocialImage) {
775
- tags.push('<meta property="og:image" content="' + imageUrl + '">');
776
- tags.push('<meta property="og:image:secure_url" content="' + imageUrl + '">');
777
- tags.push('<meta property="og:image:type" content="' + AGENT_NATIVE_SOCIAL_IMAGE_TYPE + '">');
778
- tags.push('<meta property="og:image:width" content="' + AGENT_NATIVE_SOCIAL_IMAGE_WIDTH + '">');
779
- tags.push('<meta property="og:image:height" content="' + AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT + '">');
780
- tags.push('<meta property="og:image:alt" content="' + AGENT_NATIVE_SOCIAL_IMAGE_ALT + '">');
781
- }
782
- if (!TWITTER_CARD_META_RE.test(html)) {
783
- tags.push('<meta name="twitter:card" content="summary_large_image">');
784
- }
785
- if (!hasAnySocialImage) {
786
- tags.push('<meta name="twitter:image" content="' + imageUrl + '">');
787
- tags.push('<meta name="twitter:image:alt" content="' + AGENT_NATIVE_SOCIAL_IMAGE_ALT + '">');
788
- }
789
-
790
- if (tags.length === 0) return html;
791
- return html.slice(0, headCloseIdx) + tags.join("") + html.slice(headCloseIdx);
792
- }
793
-
794
- function isSsrHtmlOrDataResponse(headers, status, pathname) {
795
- if (status < 200 || status >= 400) return false;
796
- const contentType = (headers.get("content-type") || "").toLowerCase();
797
- if (contentType.includes("text/html")) return true;
798
- return pathname.endsWith(".data") && contentType.includes("text/x-script");
799
- }
800
-
801
- /**
802
- * Apply the SSR cache policy to the response headers.
803
- *
804
- * SSR IS A PUBLIC, HARD-CDN-CACHED SHELL — SERVED IDENTICALLY TO EVERYONE.
805
- * Every SSR HTML / React Router .data response gets the same public
806
- * stale-while-revalidate policy for ALL visitors, authenticated or not. The SSR
807
- * output is impersonal (the handler never reads the request's session/cookies),
808
- * so it is safe to hard-cache one shared copy at the edge. Do NOT reintroduce
809
- * per-user / cookie-based cache variation here (no private, no no-store, no
810
- * "authenticated then don't cache" branch) — that makes every logged-in
811
- * visitor's pages uncacheable. Per-user state is resolved client-side instead.
812
- */
813
- function applyDefaultSsrCacheHeader(headers, status, pathname) {
814
- if (!isSsrHtmlOrDataResponse(headers, status, pathname)) return;
815
-
816
- headers.set("cache-control", DEFAULT_SSR_CACHE_CONTROL);
817
- headers.set("cdn-cache-control", DEFAULT_SSR_CDN_CACHE_CONTROL);
818
- // Netlify function responses are dynamic by default and can otherwise show
819
- // Cache-Status fwd=bypass even with Cache-Control: public. Keep this
820
- // Netlify-specific header so SSR HTML/.data are served from the shared
821
- // durable CDN cache instead of stampeding origin — for every visitor.
822
- headers.set("netlify-cdn-cache-control", DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL);
823
- }
824
-
825
- function applyDefaultSpeculationRulesHeader(headers, status, basePath) {
826
- if (status < 200 || status >= 400) return;
827
- if (headers.has("speculation-rules")) return;
828
-
829
- const contentType = (headers.get("content-type") || "").toLowerCase();
830
- if (!contentType.includes("text/html")) return;
831
-
832
- // Cloudflare Speed Brain injects Speculation-Rules when origin omits this
833
- // header. Those browser prefetches carry Sec-Purpose: prefetch and
834
- // Cloudflare can return 503 before the request reaches origin. Publish an
835
- // explicit no-op ruleset by default; apps can still provide their own header.
836
- headers.set("speculation-rules", '"' + prefixMountedPath(DEFAULT_SPECULATION_RULES_PATH, basePath) + '"');
837
- }
838
- function isImmutableAssetRequest(request) {
839
- const pathname = stripAppBasePath(new URL(request.url).pathname);
840
- return IMMUTABLE_ASSET_PATHS.has(pathname);
841
- }
842
-
843
- function applyImmutableAssetCacheHeaders(response, request) {
844
- if (!isImmutableAssetRequest(request)) return response;
845
- if (!((response.status >= 200 && response.status < 300) || response.status === 304)) {
846
- return response;
847
- }
848
- const headers = new Headers(response.headers);
849
- headers.set("Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL);
850
- headers.set("CDN-Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL);
851
- headers.set("Netlify-CDN-Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL);
852
- return new Response(response.body, {
853
- status: response.status,
854
- statusText: response.statusText,
855
- headers,
856
- });
857
- }
858
-
859
- async function rewriteMountedResponse(response, basePath, pathname, request) {
860
- const sentryClientConfigScript = getSentryClientConfigScript();
861
- const headers = new Headers(response.headers);
862
- applyDefaultSsrCacheHeader(headers, response.status, pathname);
863
- applyDefaultSpeculationRulesHeader(headers, response.status, basePath);
864
-
865
- const location = headers.get("location");
866
- if (location?.startsWith("/") && !location.startsWith("//")) {
867
- headers.set("location", prefixMountedPath(location, basePath));
868
- }
869
-
870
- const contentType = headers.get("content-type") || "";
871
- if (!contentType.toLowerCase().includes("text/html") || !response.body) {
872
- return new Response(response.body, {
873
- status: response.status,
874
- statusText: response.statusText,
875
- headers,
876
- });
877
- }
878
-
879
- const html = await response.text();
880
- headers.delete("content-length");
881
- return new Response(
882
- injectHeadScript(
883
- injectDefaultSocialImageMeta(
884
- prefixMountedHtml(html, basePath),
885
- defaultSocialImageUrl(request, basePath),
886
- ),
887
- sentryClientConfigScript,
888
- ),
889
- {
890
- status: response.status,
891
- statusText: response.statusText,
892
- headers,
893
- },
894
- );
895
- }
896
-
897
- function requestWithMethod(request, method) {
898
- return new Request(request.url, {
899
- method,
900
- headers: request.headers,
901
- signal: request.signal,
902
- });
903
- }
904
-
905
- function requestWithPathname(request, pathname) {
906
- const url = new URL(request.url);
907
- if (url.pathname === pathname) return request;
908
- url.pathname = pathname;
909
- return new Request(url, request);
910
- }
911
-
912
- function isStaticAppShellRequest(request) {
913
- if (request.method !== "GET" && request.method !== "HEAD") return false;
914
- const p = stripAppBasePath(new URL(request.url).pathname);
915
- if (
916
- p.startsWith("/.well-known/") ||
917
- p.startsWith("/_agent-native/") ||
918
- isApiPath(p) ||
919
- p === "/favicon.ico" ||
920
- p === "/favicon.png" ||
921
- /\\.\\w+$/.test(p)
922
- ) {
923
- return false;
924
- }
925
- return true;
926
- }
927
-
928
- async function fetchStaticAppShell(request, env) {
929
- if (!env?.ASSETS || !isStaticAppShellRequest(request)) return null;
930
- const basePath = getAppBasePath();
931
- const p = stripAppBasePath(new URL(request.url).pathname);
932
- const shellRequest = requestWithPathname(
933
- requestWithMethod(request, "GET"),
934
- "/index.html",
935
- );
936
- let response;
937
- try {
938
- response = await env.ASSETS.fetch(shellRequest);
939
- } catch {
940
- return null;
941
- }
942
- if (response.status === 404) return null;
943
- if (request.method === "HEAD") {
944
- return rewriteMountedResponse(
945
- new Response(null, {
946
- status: response.status,
947
- statusText: response.statusText,
948
- headers: response.headers,
949
- }),
950
- basePath,
951
- p,
952
- request,
953
- );
954
- }
955
- return rewriteMountedResponse(response, basePath, p, request);
956
- }
957
-
958
- // API route handlers
959
- ${routeImports.join("\n")}
960
-
961
- // Action handlers (auto-discovered from actions/)
962
- ${actionImports.join("\n")}
963
-
964
- // Server plugins
965
- ${pluginImports.join("\n")}
966
-
967
- let _handler;
968
-
969
- async function getHandler() {
970
- if (_handler) return _handler;
971
-
972
- const app = new H3();
973
-
974
- // Build a fake nitroApp surface so framework plugins (which expect
975
- // \`nitroApp.h3["~middleware"]\`) can register routes via getH3App().
976
- const noop = () => {};
977
- const nitroApp = {
978
- h3: app,
979
- hooks: { hook: noop, callHook: noop, hookOnce: noop },
980
- captureError: noop,
981
- };
982
-
983
- // CORS — applied as global middleware via .use(handler)
984
- app.use(defineEventHandler((event) => {
985
- if (event.req.method === "OPTIONS") {
986
- return new Response(null, {
987
- status: 204,
988
- headers: {
989
- "Access-Control-Allow-Origin": "*",
990
- "Access-Control-Allow-Methods": "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS",
991
- "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Embed-Target",
992
- },
993
- });
994
- }
995
- }));
996
-
997
- // Run plugins — they call getH3App(nitroApp).use(path, handler) which
998
- // pushes path-prefix middleware onto app["~middleware"].
999
- // Pre-mark every build-time plugin slot before any plugin awaits the runtime
1000
- // default bootstrap. Bundled serverless workers often lack server/plugins/
1001
- // on disk, so runtime discovery would otherwise auto-mount duplicate
1002
- // framework defaults before later custom plugins get a chance to mark
1003
- // themselves as provided.
1004
- ${generatedPluginMarks.map((stem) => ` markGeneratedPluginProvided(nitroApp, ${JSON.stringify(stem)});`).join("\n")}
1005
- ${pluginCalls.join("\n")}
1006
-
1007
- // Register API routes
1008
- ${routeRegistrations.join("\n")}
1009
-
1010
- // Register action routes (/_agent-native/actions/*)
1011
- ${actionRegistrations.join("\n")}
1012
-
624
+ return `
625
+ // Auto-generated worker entry point for ${preset}
626
+ import { H3, defineEventHandler, readBody, toResponse } from "h3";
627
+ ${includeReactRouterSsr ? 'import { createRequestHandler } from "react-router";' : ""}
628
+ ${includeReactRouterSsr ? 'import * as serverBuild from "./server-build.js";' : ""}
629
+
630
+ function normalizeAppBasePath(value) {
631
+ if (!value || value === "/") return "";
632
+ const trimmed = String(value).trim();
633
+ if (!trimmed || trimmed === "/") return "";
634
+ return "/" + trimmed.replace(/^\\/+/, "").replace(/\\/+$/, "");
635
+ }
636
+
637
+ function getAppBasePath() {
638
+ const builtAppBasePath = ${JSON.stringify(builtAppBasePath)};
639
+ return normalizeAppBasePath(
640
+ globalThis.process?.env?.VITE_APP_BASE_PATH ||
641
+ globalThis.process?.env?.APP_BASE_PATH ||
642
+ builtAppBasePath,
643
+ );
644
+ }
645
+
646
+ function stripAppBasePath(pathname) {
647
+ const basePath = getAppBasePath();
648
+ if (!basePath) return pathname;
649
+ if (pathname === basePath) return "/";
650
+ if (pathname.startsWith(basePath + "/")) {
651
+ return pathname.slice(basePath.length) || "/";
652
+ }
653
+ return pathname;
654
+ }
655
+
656
+ function parseActionSearchParams(searchParams) {
657
+ const params = {};
658
+ for (const [rawKey, value] of searchParams.entries()) {
659
+ const isArrayKey = rawKey.endsWith("[]");
660
+ // The core client serializes arrays as key[]=value so one-item arrays
661
+ // survive GET action parsing in generated worker deployments.
662
+ const key = isArrayKey ? rawKey.slice(0, -2) : rawKey;
663
+ const current = params[key];
664
+ if (current === undefined) {
665
+ params[key] = isArrayKey ? [value] : value;
666
+ } else if (Array.isArray(current)) {
667
+ current.push(value);
668
+ } else {
669
+ params[key] = [current, value];
670
+ }
671
+ }
672
+ return params;
673
+ }
674
+
675
+ function isApiPath(pathname) {
676
+ return pathname === "/api" || pathname.startsWith("/api/");
677
+ }
678
+
679
+ function isFrameworkPath(pathname) {
680
+ return (
681
+ pathname === "/_agent-native" || pathname.startsWith("/_agent-native/")
682
+ );
683
+ }
684
+
685
+ function requestWithMountedApiPrefixStripped(request) {
686
+ const basePath = getAppBasePath();
687
+ if (!basePath) return request;
688
+ const url = new URL(request.url);
689
+ const strippedPathname = stripAppBasePath(url.pathname);
690
+ if (strippedPathname === url.pathname) {
691
+ return request;
692
+ }
693
+ if (!isApiPath(strippedPathname) && !isFrameworkPath(strippedPathname)) {
694
+ return request;
695
+ }
696
+ url.pathname = strippedPathname;
697
+ return new Request(url, request);
698
+ }
699
+
700
+ function prefixMountedPath(path, basePath) {
701
+ if (!basePath || !path.startsWith("/") || path.startsWith("//")) return path;
702
+ if (path === basePath || path.startsWith(basePath + "/")) return path;
703
+ return basePath + path;
704
+ }
705
+
706
+ function prefixMountedHtml(html, basePath) {
707
+ if (!basePath) return html;
708
+ return html
709
+ .replace(
710
+ /\\b(href|src|action|formaction|poster)=(["'])(\\/(?!\\/)[^"']*)\\2/g,
711
+ (_match, attr, quote, path) =>
712
+ attr + "=" + quote + prefixMountedPath(path, basePath) + quote,
713
+ )
714
+ .replace(/url\\((["']?)(\\/(?!\\/)[^)'" ]+)\\1\\)/g, (_match, quote, path) => {
715
+ const q = quote || "";
716
+ return "url(" + q + prefixMountedPath(path, basePath) + q + ")";
717
+ });
718
+ }
719
+
720
+ function firstNonEmpty() {
721
+ for (const value of arguments) {
722
+ const trimmed = typeof value === "string" ? value.trim() : "";
723
+ if (trimmed) return trimmed;
724
+ }
725
+ }
726
+
727
+ function getSentryClientConfigScript() {
728
+ const env = globalThis.process?.env || {};
729
+ const key = firstNonEmpty(env.SENTRY_CLIENT_KEY, env.VITE_SENTRY_CLIENT_KEY);
730
+ const projectId = firstNonEmpty(
731
+ env.SENTRY_PROJECT_ID,
732
+ env.VITE_SENTRY_PROJECT_ID,
733
+ );
734
+ const host = firstNonEmpty(
735
+ env.SENTRY_INGEST_HOST,
736
+ env.VITE_SENTRY_INGEST_HOST,
737
+ );
738
+ const dsn =
739
+ firstNonEmpty(
740
+ env.SENTRY_CLIENT_DSN,
741
+ env.VITE_SENTRY_CLIENT_DSN,
742
+ env.VITE_SENTRY_DSN,
743
+ env.SENTRY_DSN,
744
+ ) || (key && projectId && host ? "https://" + key + "@" + host + "/" + projectId : undefined);
745
+ if (!dsn) return null;
746
+ const config = {
747
+ sentryDsn: dsn,
748
+ sentryEnvironment:
749
+ firstNonEmpty(
750
+ env.SENTRY_ENVIRONMENT,
751
+ env.NETLIFY_CONTEXT,
752
+ env.VERCEL_ENV,
753
+ env.NODE_ENV,
754
+ ) || "production",
755
+ };
756
+ return (
757
+ '<script data-agent-native-sentry-config>' +
758
+ 'window.__AGENT_NATIVE_CONFIG__=Object.assign({},window.__AGENT_NATIVE_CONFIG__,' +
759
+ JSON.stringify(config) +
760
+ ");</script>"
761
+ );
762
+ }
763
+
764
+ function injectHeadScript(html, script) {
765
+ if (!script) return html;
766
+ const headCloseIdx = html.indexOf("</head>");
767
+ if (headCloseIdx === -1) return html;
768
+ return html.slice(0, headCloseIdx) + script + html.slice(headCloseIdx);
769
+ }
770
+
771
+ const DEFAULT_SSR_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_CACHE_CONTROL)};
772
+ const DEFAULT_SSR_CDN_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_CDN_CACHE_CONTROL)};
773
+ const DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL)};
774
+ const DEFAULT_SPECULATION_RULES_PATH = ${JSON.stringify(DEFAULT_SPECULATION_RULES_PATH)};
775
+ const IMMUTABLE_ASSET_CACHE_CONTROL = ${JSON.stringify(IMMUTABLE_ASSET_CACHE_CONTROL)};
776
+ const IMMUTABLE_ASSET_PATHS = new Set(${JSON.stringify([...new Set(immutableAssetPaths)].sort())});
777
+ const AGENT_NATIVE_SOCIAL_IMAGE_PATH = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_PATH)};
778
+ const AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER)};
779
+ const AGENT_NATIVE_SOCIAL_IMAGE_ALT = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_ALT)};
780
+ const AGENT_NATIVE_SOCIAL_IMAGE_TYPE = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_TYPE)};
781
+ const AGENT_NATIVE_SOCIAL_IMAGE_WIDTH = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_WIDTH)};
782
+ const AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT)};
783
+ const OG_IMAGE_META_RE = /<meta\\b(?=[^>]*\\bproperty=(["'])og:image\\1)[^>]*>/i;
784
+ const TWITTER_CARD_META_RE = /<meta\\b(?=[^>]*\\bname=(["'])twitter:card\\1)[^>]*>/i;
785
+ const TWITTER_IMAGE_META_RE = /<meta\\b(?=[^>]*\\bname=(["'])twitter:image\\1)[^>]*>/i;
786
+
787
+ function withAgentNativeSocialImageCacheBuster(image) {
788
+ const separator = image.includes("?") ? "&" : "?";
789
+ return image + separator + "v=" + encodeURIComponent(AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER);
790
+ }
791
+
792
+ function defaultSocialImageUrl(request, basePath) {
793
+ return withAgentNativeSocialImageCacheBuster(
794
+ new URL(prefixMountedPath(AGENT_NATIVE_SOCIAL_IMAGE_PATH, basePath), request.url).toString()
795
+ );
796
+ }
797
+
798
+ function injectDefaultSocialImageMeta(html, imageUrl) {
799
+ const headCloseIdx = html.indexOf("</head>");
800
+ if (headCloseIdx === -1) return html;
801
+
802
+ const hasAnySocialImage =
803
+ OG_IMAGE_META_RE.test(html) || TWITTER_IMAGE_META_RE.test(html);
804
+ const tags = [];
805
+
806
+ if (!hasAnySocialImage) {
807
+ tags.push('<meta property="og:image" content="' + imageUrl + '">');
808
+ tags.push('<meta property="og:image:secure_url" content="' + imageUrl + '">');
809
+ tags.push('<meta property="og:image:type" content="' + AGENT_NATIVE_SOCIAL_IMAGE_TYPE + '">');
810
+ tags.push('<meta property="og:image:width" content="' + AGENT_NATIVE_SOCIAL_IMAGE_WIDTH + '">');
811
+ tags.push('<meta property="og:image:height" content="' + AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT + '">');
812
+ tags.push('<meta property="og:image:alt" content="' + AGENT_NATIVE_SOCIAL_IMAGE_ALT + '">');
813
+ }
814
+ if (!TWITTER_CARD_META_RE.test(html)) {
815
+ tags.push('<meta name="twitter:card" content="summary_large_image">');
816
+ }
817
+ if (!hasAnySocialImage) {
818
+ tags.push('<meta name="twitter:image" content="' + imageUrl + '">');
819
+ tags.push('<meta name="twitter:image:alt" content="' + AGENT_NATIVE_SOCIAL_IMAGE_ALT + '">');
820
+ }
821
+
822
+ if (tags.length === 0) return html;
823
+ return html.slice(0, headCloseIdx) + tags.join("") + html.slice(headCloseIdx);
824
+ }
825
+
826
+ function isSsrHtmlOrDataResponse(headers, status, pathname) {
827
+ if (status < 200 || status >= 400) return false;
828
+ const contentType = (headers.get("content-type") || "").toLowerCase();
829
+ if (contentType.includes("text/html")) return true;
830
+ return pathname.endsWith(".data") && contentType.includes("text/x-script");
831
+ }
832
+
833
+ /**
834
+ * Apply the SSR cache policy to the response headers.
835
+ *
836
+ * SSR IS A PUBLIC, HARD-CDN-CACHED SHELL — SERVED IDENTICALLY TO EVERYONE.
837
+ * Every SSR HTML / React Router .data response gets the same public
838
+ * stale-while-revalidate policy for ALL visitors, authenticated or not. The SSR
839
+ * output is impersonal (the handler never reads the request's session/cookies),
840
+ * so it is safe to hard-cache one shared copy at the edge. Do NOT reintroduce
841
+ * per-user / cookie-based cache variation here (no private, no no-store, no
842
+ * "authenticated then don't cache" branch) — that makes every logged-in
843
+ * visitor's pages uncacheable. Per-user state is resolved client-side instead.
844
+ */
845
+ function applyDefaultSsrCacheHeader(headers, status, pathname) {
846
+ if (!isSsrHtmlOrDataResponse(headers, status, pathname)) return;
847
+
848
+ headers.set("cache-control", DEFAULT_SSR_CACHE_CONTROL);
849
+ headers.set("cdn-cache-control", DEFAULT_SSR_CDN_CACHE_CONTROL);
850
+ // Netlify function responses are dynamic by default and can otherwise show
851
+ // Cache-Status fwd=bypass even with Cache-Control: public. Keep this
852
+ // Netlify-specific header so SSR HTML/.data are served from the shared
853
+ // durable CDN cache instead of stampeding origin — for every visitor.
854
+ headers.set("netlify-cdn-cache-control", DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL);
855
+ }
856
+
857
+ function applyDefaultSpeculationRulesHeader(headers, status, basePath) {
858
+ if (status < 200 || status >= 400) return;
859
+ if (headers.has("speculation-rules")) return;
860
+
861
+ const contentType = (headers.get("content-type") || "").toLowerCase();
862
+ if (!contentType.includes("text/html")) return;
863
+
864
+ // Cloudflare Speed Brain injects Speculation-Rules when origin omits this
865
+ // header. Those browser prefetches carry Sec-Purpose: prefetch and
866
+ // Cloudflare can return 503 before the request reaches origin. Publish an
867
+ // explicit no-op ruleset by default; apps can still provide their own header.
868
+ headers.set("speculation-rules", '"' + prefixMountedPath(DEFAULT_SPECULATION_RULES_PATH, basePath) + '"');
869
+ }
870
+ function isImmutableAssetRequest(request) {
871
+ const pathname = stripAppBasePath(new URL(request.url).pathname);
872
+ return IMMUTABLE_ASSET_PATHS.has(pathname);
873
+ }
874
+
875
+ function applyImmutableAssetCacheHeaders(response, request) {
876
+ if (!isImmutableAssetRequest(request)) return response;
877
+ if (!((response.status >= 200 && response.status < 300) || response.status === 304)) {
878
+ return response;
879
+ }
880
+ const headers = new Headers(response.headers);
881
+ headers.set("Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL);
882
+ headers.set("CDN-Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL);
883
+ headers.set("Netlify-CDN-Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL);
884
+ return new Response(response.body, {
885
+ status: response.status,
886
+ statusText: response.statusText,
887
+ headers,
888
+ });
889
+ }
890
+
891
+ async function rewriteMountedResponse(response, basePath, pathname, request) {
892
+ const sentryClientConfigScript = getSentryClientConfigScript();
893
+ const headers = new Headers(response.headers);
894
+ applyDefaultSsrCacheHeader(headers, response.status, pathname);
895
+ applyDefaultSpeculationRulesHeader(headers, response.status, basePath);
896
+
897
+ const location = headers.get("location");
898
+ if (location?.startsWith("/") && !location.startsWith("//")) {
899
+ headers.set("location", prefixMountedPath(location, basePath));
900
+ }
901
+
902
+ const contentType = headers.get("content-type") || "";
903
+ if (!contentType.toLowerCase().includes("text/html") || !response.body) {
904
+ return new Response(response.body, {
905
+ status: response.status,
906
+ statusText: response.statusText,
907
+ headers,
908
+ });
909
+ }
910
+
911
+ const html = await response.text();
912
+ headers.delete("content-length");
913
+ return new Response(
914
+ injectHeadScript(
915
+ injectDefaultSocialImageMeta(
916
+ prefixMountedHtml(html, basePath),
917
+ defaultSocialImageUrl(request, basePath),
918
+ ),
919
+ sentryClientConfigScript,
920
+ ),
921
+ {
922
+ status: response.status,
923
+ statusText: response.statusText,
924
+ headers,
925
+ },
926
+ );
927
+ }
928
+
929
+ function requestWithMethod(request, method) {
930
+ return new Request(request.url, {
931
+ method,
932
+ headers: request.headers,
933
+ signal: request.signal,
934
+ });
935
+ }
936
+
937
+ function requestWithPathname(request, pathname) {
938
+ const url = new URL(request.url);
939
+ if (url.pathname === pathname) return request;
940
+ url.pathname = pathname;
941
+ return new Request(url, request);
942
+ }
943
+
944
+ function isStaticAppShellRequest(request) {
945
+ if (request.method !== "GET" && request.method !== "HEAD") return false;
946
+ const p = stripAppBasePath(new URL(request.url).pathname);
947
+ if (
948
+ p.startsWith("/.well-known/") ||
949
+ p.startsWith("/_agent-native/") ||
950
+ isApiPath(p) ||
951
+ p === "/favicon.ico" ||
952
+ p === "/favicon.png" ||
953
+ /\\.\\w+$/.test(p)
954
+ ) {
955
+ return false;
956
+ }
957
+ return true;
958
+ }
959
+
960
+ async function fetchStaticAppShell(request, env) {
961
+ if (!env?.ASSETS || !isStaticAppShellRequest(request)) return null;
962
+ const basePath = getAppBasePath();
963
+ const p = stripAppBasePath(new URL(request.url).pathname);
964
+ const shellRequest = requestWithPathname(
965
+ requestWithMethod(request, "GET"),
966
+ "/index.html",
967
+ );
968
+ let response;
969
+ try {
970
+ response = await env.ASSETS.fetch(shellRequest);
971
+ } catch {
972
+ return null;
973
+ }
974
+ if (response.status === 404) return null;
975
+ if (request.method === "HEAD") {
976
+ return rewriteMountedResponse(
977
+ new Response(null, {
978
+ status: response.status,
979
+ statusText: response.statusText,
980
+ headers: response.headers,
981
+ }),
982
+ basePath,
983
+ p,
984
+ request,
985
+ );
986
+ }
987
+ return rewriteMountedResponse(response, basePath, p, request);
988
+ }
989
+
990
+ // API route handlers
991
+ ${routeImports.join("\n")}
992
+
993
+ // Action handlers (auto-discovered from actions/)
994
+ ${actionImports.join("\n")}
995
+
996
+ // Server plugins
997
+ ${pluginImports.join("\n")}
998
+
999
+ let _handler;
1000
+
1001
+ async function getHandler() {
1002
+ if (_handler) return _handler;
1003
+
1004
+ const app = new H3();
1005
+
1006
+ // Build a fake nitroApp surface so framework plugins (which expect
1007
+ // \`nitroApp.h3["~middleware"]\`) can register routes via getH3App().
1008
+ const noop = () => {};
1009
+ const nitroApp = {
1010
+ h3: app,
1011
+ hooks: { hook: noop, callHook: noop, hookOnce: noop },
1012
+ captureError: noop,
1013
+ };
1014
+
1015
+ // CORS — applied as global middleware via .use(handler)
1016
+ app.use(defineEventHandler((event) => {
1017
+ if (event.req.method === "OPTIONS") {
1018
+ return new Response(null, {
1019
+ status: 204,
1020
+ headers: {
1021
+ "Access-Control-Allow-Origin": "*",
1022
+ "Access-Control-Allow-Methods": "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS",
1023
+ "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Tool-Bridge,X-Agent-Native-Tool-Id,X-Agent-Native-Frontend,X-Agent-Native-Embed-Target",
1024
+ },
1025
+ });
1026
+ }
1027
+ }));
1028
+
1029
+ // Run plugins — they call getH3App(nitroApp).use(path, handler) which
1030
+ // pushes path-prefix middleware onto app["~middleware"].
1031
+ // Pre-mark every build-time plugin slot before any plugin awaits the runtime
1032
+ // default bootstrap. Bundled serverless workers often lack server/plugins/
1033
+ // on disk, so runtime discovery would otherwise auto-mount duplicate
1034
+ // framework defaults before later custom plugins get a chance to mark
1035
+ // themselves as provided.
1036
+ ${generatedPluginMarks.map((stem) => ` markGeneratedPluginProvided(nitroApp, ${JSON.stringify(stem)});`).join("\n")}
1037
+ ${pluginCalls.join("\n")}
1038
+
1039
+ // Register API routes
1040
+ ${routeRegistrations.join("\n")}
1041
+
1042
+ // Register action routes (/_agent-native/actions/*)
1043
+ ${actionRegistrations.join("\n")}
1044
+
1013
1045
  ${includeReactRouterSsr
1014
- ? ` // SSR catch-all for React Router
1015
- const rrHandler = createRequestHandler(() => serverBuild);
1016
- app.all("/**", defineEventHandler(async (event) => {
1017
- const basePath = getAppBasePath();
1018
- const p = stripAppBasePath(new URL(event.req.url).pathname);
1019
- if (
1020
- p.startsWith("/.well-known/") ||
1021
- p.startsWith("/_agent-native/") ||
1022
- isApiPath(p) ||
1023
- p === "/favicon.ico" ||
1024
- p === "/favicon.png" ||
1025
- (/\\.\\w+$/.test(p) && !p.endsWith(".data"))
1026
- ) {
1027
- return new Response(null, { status: 404 });
1028
- }
1029
- const request = requestWithPathname(event.req, p);
1030
- if (event.req.method === "HEAD") {
1031
- const getRequest = requestWithMethod(request, "GET");
1032
- const response = await rrHandler(getRequest);
1033
- return rewriteMountedResponse(
1034
- new Response(null, {
1035
- status: response.status,
1036
- statusText: response.statusText,
1037
- headers: response.headers,
1038
- }),
1039
- basePath,
1040
- p,
1041
- getRequest
1042
- );
1043
- }
1044
- return rewriteMountedResponse(await rrHandler(request), basePath, p, request);
1046
+ ? ` // SSR catch-all for React Router
1047
+ const rrHandler = createRequestHandler(() => serverBuild);
1048
+ app.all("/**", defineEventHandler(async (event) => {
1049
+ const basePath = getAppBasePath();
1050
+ const p = stripAppBasePath(new URL(event.req.url).pathname);
1051
+ if (
1052
+ p.startsWith("/.well-known/") ||
1053
+ p.startsWith("/_agent-native/") ||
1054
+ isApiPath(p) ||
1055
+ p === "/favicon.ico" ||
1056
+ p === "/favicon.png" ||
1057
+ (/\\.\\w+$/.test(p) && !p.endsWith(".data"))
1058
+ ) {
1059
+ return new Response(null, { status: 404 });
1060
+ }
1061
+ const request = requestWithPathname(event.req, p);
1062
+ if (event.req.method === "HEAD") {
1063
+ const getRequest = requestWithMethod(request, "GET");
1064
+ const response = await rrHandler(getRequest);
1065
+ return rewriteMountedResponse(
1066
+ new Response(null, {
1067
+ status: response.status,
1068
+ statusText: response.statusText,
1069
+ headers: response.headers,
1070
+ }),
1071
+ basePath,
1072
+ p,
1073
+ getRequest
1074
+ );
1075
+ }
1076
+ return rewriteMountedResponse(await rrHandler(request), basePath, p, request);
1045
1077
  }));`
1046
- : ""}
1047
-
1048
- _handler = app.fetch.bind(app);
1049
- return _handler;
1050
- }
1051
-
1052
- export default {
1053
- async fetch(request, env, ctx) {
1054
- // Expose env and ctx bindings globally for compatibility
1055
- if (ctx) globalThis.__cf_ctx = ctx;
1056
- if (env) {
1057
- globalThis.process = globalThis.process || { env: {} };
1058
- globalThis.process.env = globalThis.process.env || {};
1059
- // Expose D1/KV/R2 bindings on globalThis.__cf_env for the db layer
1060
- globalThis.__cf_env = env;
1061
- for (const [key, value] of Object.entries(env)) {
1062
- if (typeof value === "string") {
1063
- globalThis.process.env[key] = value;
1064
- }
1065
- }
1066
- }
1067
-
1068
- // Try serving static assets first (CF Pages advanced mode).
1069
- // Only attempt this for GET/HEAD — the ASSETS binding is a static file
1070
- // server and returns 405 for any other method, which would short-circuit
1071
- // API calls (PUT/POST/DELETE to /_agent-native/*) before they reach our
1072
- // h3 middleware.
1073
- if (env?.ASSETS && (request.method === "GET" || request.method === "HEAD")) {
1074
- try {
1075
- const assetResponse = await env.ASSETS.fetch(request);
1076
- if (assetResponse.status !== 404) {
1077
- return applyImmutableAssetCacheHeaders(assetResponse, request);
1078
- }
1079
- } catch {
1080
- // Asset fetch failed — fall through to SSR
1081
- }
1082
- }
1083
-
1084
- const handler = await getHandler();
1085
- const response = await handler(requestWithMountedApiPrefixStripped(request));
1078
+ : ""}
1079
+
1080
+ _handler = app.fetch.bind(app);
1081
+ return _handler;
1082
+ }
1083
+
1084
+ export default {
1085
+ async fetch(request, env, ctx) {
1086
+ // Expose env and ctx bindings globally for compatibility
1087
+ if (ctx) globalThis.__cf_ctx = ctx;
1088
+ if (env) {
1089
+ globalThis.process = globalThis.process || { env: {} };
1090
+ globalThis.process.env = globalThis.process.env || {};
1091
+ // Expose D1/KV/R2 bindings on globalThis.__cf_env for the db layer
1092
+ globalThis.__cf_env = env;
1093
+ for (const [key, value] of Object.entries(env)) {
1094
+ if (typeof value === "string") {
1095
+ globalThis.process.env[key] = value;
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ // Try serving static assets first (CF Pages advanced mode).
1101
+ // Only attempt this for GET/HEAD — the ASSETS binding is a static file
1102
+ // server and returns 405 for any other method, which would short-circuit
1103
+ // API calls (PUT/POST/DELETE to /_agent-native/*) before they reach our
1104
+ // h3 middleware.
1105
+ if (env?.ASSETS && (request.method === "GET" || request.method === "HEAD")) {
1106
+ try {
1107
+ const assetResponse = await env.ASSETS.fetch(request);
1108
+ if (assetResponse.status !== 404) {
1109
+ return applyImmutableAssetCacheHeaders(assetResponse, request);
1110
+ }
1111
+ } catch {
1112
+ // Asset fetch failed — fall through to SSR
1113
+ }
1114
+ }
1115
+
1116
+ const handler = await getHandler();
1117
+ const response = await handler(requestWithMountedApiPrefixStripped(request));
1086
1118
  ${includeReactRouterSsr
1087
1119
  ? " return response;"
1088
- : ` if (response.status === 404) {
1089
- const shellResponse = await fetchStaticAppShell(request, env);
1090
- if (shellResponse) return shellResponse;
1091
- }
1092
- return response;`}
1093
- }
1094
- };
1120
+ : ` if (response.status === 404) {
1121
+ const shellResponse = await fetchStaticAppShell(request, env);
1122
+ if (shellResponse) return shellResponse;
1123
+ }
1124
+ return response;`}
1125
+ }
1126
+ };
1095
1127
  `;
1096
1128
  }
1097
1129
  function escapeHtmlAttribute(value) {
@@ -1193,35 +1225,35 @@ function writeCloudflarePagesStaticShell({ serverDir, distDir, tmpDir, }) {
1193
1225
  const outFile = path.join(distDir, "index.html");
1194
1226
  const renderScript = path.join(tmpDir, "render-cloudflare-static-shell.mjs");
1195
1227
  const basePath = normalizeConfiguredAppBasePath();
1196
- fs.writeFileSync(renderScript, `
1197
- import fs from "node:fs";
1198
- import { createRequire } from "node:module";
1199
- import { pathToFileURL } from "node:url";
1200
-
1201
- const cwd = ${JSON.stringify(cwd)};
1202
- const serverEntry = ${JSON.stringify(serverEntry)};
1203
- const outFile = ${JSON.stringify(outFile)};
1204
- const basePath = ${JSON.stringify(basePath)};
1205
-
1206
- const requireFromApp = createRequire(cwd + "/package.json");
1207
- const reactRouterEntry = requireFromApp.resolve("react-router");
1208
- const { createRequestHandler } = await import(pathToFileURL(reactRouterEntry).href);
1209
- const serverBuild = await import(pathToFileURL(serverEntry).href);
1210
- const handler = createRequestHandler(serverBuild, "production");
1211
- const pathname = basePath ? basePath + "/" : "/";
1212
- const response = await handler(
1213
- new Request(new URL(pathname, "https://agent-native.local"), {
1214
- headers: { "X-React-Router-SPA-Mode": "yes" },
1215
- }),
1216
- );
1217
- const html = await response.text();
1218
-
1219
- if (!html || !html.includes("__reactRouterContext") || !html.includes("entry.client")) {
1220
- throw new Error("React Router did not render a usable Cloudflare Pages static shell");
1221
- }
1222
-
1223
- fs.writeFileSync(outFile, html);
1224
- process.exit(0);
1228
+ fs.writeFileSync(renderScript, `
1229
+ import fs from "node:fs";
1230
+ import { createRequire } from "node:module";
1231
+ import { pathToFileURL } from "node:url";
1232
+
1233
+ const cwd = ${JSON.stringify(cwd)};
1234
+ const serverEntry = ${JSON.stringify(serverEntry)};
1235
+ const outFile = ${JSON.stringify(outFile)};
1236
+ const basePath = ${JSON.stringify(basePath)};
1237
+
1238
+ const requireFromApp = createRequire(cwd + "/package.json");
1239
+ const reactRouterEntry = requireFromApp.resolve("react-router");
1240
+ const { createRequestHandler } = await import(pathToFileURL(reactRouterEntry).href);
1241
+ const serverBuild = await import(pathToFileURL(serverEntry).href);
1242
+ const handler = createRequestHandler(serverBuild, "production");
1243
+ const pathname = basePath ? basePath + "/" : "/";
1244
+ const response = await handler(
1245
+ new Request(new URL(pathname, "https://agent-native.local"), {
1246
+ headers: { "X-React-Router-SPA-Mode": "yes" },
1247
+ }),
1248
+ );
1249
+ const html = await response.text();
1250
+
1251
+ if (!html || !html.includes("__reactRouterContext") || !html.includes("entry.client")) {
1252
+ throw new Error("React Router did not render a usable Cloudflare Pages static shell");
1253
+ }
1254
+
1255
+ fs.writeFileSync(outFile, html);
1256
+ process.exit(0);
1225
1257
  `);
1226
1258
  try {
1227
1259
  execFileSync(process.execPath, [renderScript], {
@@ -1333,15 +1365,21 @@ async function buildCloudflarePages() {
1333
1365
  ].join("\n"));
1334
1366
  // Create stub modules for native/Node-only deps that can't run on Workers.
1335
1367
  // These get resolved by esbuild instead of the real modules, avoiding bundling
1336
- // native code that would fail on the Workers runtime.
1368
+ // native code that would fail on the Workers runtime. Uninstalled optional
1369
+ // AI SDK provider packages are stubbed the same way so their literal dynamic
1370
+ // import() sites in ai-sdk-engine don't fail the bundle.
1371
+ const workerStubModules = {
1372
+ ...CLOUDFLARE_WORKER_STUB_MODULES,
1373
+ ...uninstalledOptionalAiSdkStubs(cwd),
1374
+ };
1337
1375
  const stubDir = path.join(tmpDir, "node_modules");
1338
- for (const [mod, source] of Object.entries(CLOUDFLARE_WORKER_STUB_MODULES)) {
1376
+ for (const [mod, source] of Object.entries(workerStubModules)) {
1339
1377
  const modDir = path.join(stubDir, mod);
1340
1378
  fs.mkdirSync(modDir, { recursive: true });
1341
1379
  fs.writeFileSync(path.join(modDir, "index.js"), source);
1342
1380
  fs.writeFileSync(path.join(modDir, "package.json"), JSON.stringify({ name: mod, main: "index.js", type: "module" }));
1343
1381
  }
1344
- const stubAliases = Object.keys(CLOUDFLARE_WORKER_STUB_MODULES).map((mod) => `--alias:${mod}=${path.join(stubDir, mod, "index.js")}`);
1382
+ const stubAliases = Object.keys(workerStubModules).map((mod) => `--alias:${mod}=${path.join(stubDir, mod, "index.js")}`);
1345
1383
  const nodeBuiltinStubDir = path.join(tmpDir, "node-builtin-stubs");
1346
1384
  fs.mkdirSync(nodeBuiltinStubDir, { recursive: true });
1347
1385
  const nodeBuiltinStubAliases = [];
@@ -1968,76 +2006,76 @@ export function emitSingleTemplateNetlifyBackgroundFunction(projectCwd) {
1968
2006
  // copied bundle does NOT re-register the catch-all `config.path`.
1969
2007
  fs.rmSync(path.join(dest, "server.mjs"), { force: true });
1970
2008
  const processRunPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
1971
- const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
1972
- // bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
1973
- // in this function. The deployed Lambda name is NOT guaranteed to end in
1974
- // "-background" (Netlify may mangle/prefix it), so we cannot depend on
1975
- // AWS_LAMBDA_FUNCTION_NAME alone. A globalThis flag (NOT process.env) avoids the
1976
- // no-env-mutation guard and carries no cross-request state — it is a static,
1977
- // set-once isolate marker read back by isInBackgroundFunctionRuntime().
1978
- globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
1979
-
1980
- // The framework route the Nitro router dispatches to (the _process-run plugin).
1981
- const PROCESS_RUN_PATH = ${processRunPath};
1982
-
1983
- let cachedHandler;
1984
-
1985
- // Netlify v2 invokes this as (request, context). The Nitro netlify handler is a
1986
- // Web-standard \`async (Request) => Response\` (see nitro/presets/netlify/runtime).
1987
- // This function declares NO custom \`config.path\`, so it is reached at its
1988
- // DEFAULT url (/.netlify/functions/${backgroundName}). The Nitro router only
1989
- // knows the framework route, so we REWRITE the incoming pathname to
1990
- // PROCESS_RUN_PATH before delegating. Method, ALL headers (the HMAC
1991
- // Authorization: Bearer MUST survive — the plugin verifies it) and the body are
1992
- // preserved by cloning the incoming Request with only its URL pathname set.
1993
- export default async function handler(request, context) {
1994
- try {
1995
- cachedHandler ??= (await import("./main.mjs")).default;
1996
- const url = new URL(request.url);
1997
- url.pathname = PROCESS_RUN_PATH;
1998
- // Read the body once and pass it through. GET/HEAD have no body.
1999
- const method = request.method || "POST";
2000
- const hasBody = method !== "GET" && method !== "HEAD";
2001
- const body = hasBody ? await request.text() : undefined;
2002
- const rewritten = new Request(url.toString(), {
2003
- method,
2004
- headers: request.headers,
2005
- body,
2006
- });
2007
- // Netlify Functions v2 invokes the handler as (request, context); the Nitro
2008
- // netlify handler accepts (request[, context]). Pass context through so a
2009
- // handler that uses it (e.g. waitUntil) does not trip over an undefined arg
2010
- // before it ever routes the request.
2011
- return await cachedHandler(rewritten, context);
2012
- } catch (err) {
2013
- // Netlify already returned 202 for this background invocation and DISCARDS
2014
- // this return, so a throw here is otherwise INVISIBLE — it would only surface
2015
- // downstream as the reaper's "worker never claimed the run". Log it loudly
2016
- // for the function log; the FOREGROUND circuit-breaker (production-agent.ts)
2017
- // is what recovers the run by executing it inline when no worker claims.
2018
- console.error(
2019
- "[agent-background] wrapper failed before reaching the route:",
2020
- (err && err.stack) || err,
2021
- );
2022
- throw err;
2023
- }
2024
- }
2025
-
2026
- export const config = {
2027
- name: "agent background handler",
2028
- generator: "agent-native build",
2029
- // background: true makes Netlify invoke this ASYNCHRONOUSLY (immediate HTTP
2030
- // 202 ack) with the 15-minute budget (Netlify docs:
2031
- // build/functions/background-functions + build/functions/api). We declare NO
2032
- // custom path, so the function keeps its DEFAULT url
2033
- // /.netlify/functions/${backgroundName}; the Nitro \`server\` /* catch-all
2034
- // already excludes /.netlify/* so that default url is never shadowed by the
2035
- // synchronous function. The foreground dispatches to that default url.
2036
- background: true,
2037
- nodeBundler: "none",
2038
- includedFiles: ["**"],
2039
- preferStatic: false,
2040
- };
2009
+ const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
2010
+ // bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
2011
+ // in this function. The deployed Lambda name is NOT guaranteed to end in
2012
+ // "-background" (Netlify may mangle/prefix it), so we cannot depend on
2013
+ // AWS_LAMBDA_FUNCTION_NAME alone. A globalThis flag (NOT process.env) avoids the
2014
+ // no-env-mutation guard and carries no cross-request state — it is a static,
2015
+ // set-once isolate marker read back by isInBackgroundFunctionRuntime().
2016
+ globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
2017
+
2018
+ // The framework route the Nitro router dispatches to (the _process-run plugin).
2019
+ const PROCESS_RUN_PATH = ${processRunPath};
2020
+
2021
+ let cachedHandler;
2022
+
2023
+ // Netlify v2 invokes this as (request, context). The Nitro netlify handler is a
2024
+ // Web-standard \`async (Request) => Response\` (see nitro/presets/netlify/runtime).
2025
+ // This function declares NO custom \`config.path\`, so it is reached at its
2026
+ // DEFAULT url (/.netlify/functions/${backgroundName}). The Nitro router only
2027
+ // knows the framework route, so we REWRITE the incoming pathname to
2028
+ // PROCESS_RUN_PATH before delegating. Method, ALL headers (the HMAC
2029
+ // Authorization: Bearer MUST survive — the plugin verifies it) and the body are
2030
+ // preserved by cloning the incoming Request with only its URL pathname set.
2031
+ export default async function handler(request, context) {
2032
+ try {
2033
+ cachedHandler ??= (await import("./main.mjs")).default;
2034
+ const url = new URL(request.url);
2035
+ url.pathname = PROCESS_RUN_PATH;
2036
+ // Read the body once and pass it through. GET/HEAD have no body.
2037
+ const method = request.method || "POST";
2038
+ const hasBody = method !== "GET" && method !== "HEAD";
2039
+ const body = hasBody ? await request.text() : undefined;
2040
+ const rewritten = new Request(url.toString(), {
2041
+ method,
2042
+ headers: request.headers,
2043
+ body,
2044
+ });
2045
+ // Netlify Functions v2 invokes the handler as (request, context); the Nitro
2046
+ // netlify handler accepts (request[, context]). Pass context through so a
2047
+ // handler that uses it (e.g. waitUntil) does not trip over an undefined arg
2048
+ // before it ever routes the request.
2049
+ return await cachedHandler(rewritten, context);
2050
+ } catch (err) {
2051
+ // Netlify already returned 202 for this background invocation and DISCARDS
2052
+ // this return, so a throw here is otherwise INVISIBLE — it would only surface
2053
+ // downstream as the reaper's "worker never claimed the run". Log it loudly
2054
+ // for the function log; the FOREGROUND circuit-breaker (production-agent.ts)
2055
+ // is what recovers the run by executing it inline when no worker claims.
2056
+ console.error(
2057
+ "[agent-background] wrapper failed before reaching the route:",
2058
+ (err && err.stack) || err,
2059
+ );
2060
+ throw err;
2061
+ }
2062
+ }
2063
+
2064
+ export const config = {
2065
+ name: "agent background handler",
2066
+ generator: "agent-native build",
2067
+ // background: true makes Netlify invoke this ASYNCHRONOUSLY (immediate HTTP
2068
+ // 202 ack) with the 15-minute budget (Netlify docs:
2069
+ // build/functions/background-functions + build/functions/api). We declare NO
2070
+ // custom path, so the function keeps its DEFAULT url
2071
+ // /.netlify/functions/${backgroundName}; the Nitro \`server\` /* catch-all
2072
+ // already excludes /.netlify/* so that default url is never shadowed by the
2073
+ // synchronous function. The foreground dispatches to that default url.
2074
+ background: true,
2075
+ nodeBundler: "none",
2076
+ includedFiles: ["**"],
2077
+ preferStatic: false,
2078
+ };
2041
2079
  `;
2042
2080
  fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), entry);
2043
2081
  console.log(`[build] Emitted durable-background function "${backgroundName}" into the ` +
@@ -2672,11 +2710,11 @@ async function buildWithNitro() {
2672
2710
  : null;
2673
2711
  const agentsBundleModuleSource = () => {
2674
2712
  const bundle = readAgentsBundleFromFs(cwd, nitroWorkspaceSource);
2675
- return `// AUTO-GENERATED by @agent-native/core deploy build (Nitro virtual)
2676
- // Contains the inlined AGENTS.md + .agents/skills/ content from the template,
2677
- // merged with the workspace core's AGENTS.md + skills/ when present.
2678
- const bundle = ${JSON.stringify(bundle)};
2679
- export default bundle;
2713
+ return `// AUTO-GENERATED by @agent-native/core deploy build (Nitro virtual)
2714
+ // Contains the inlined AGENTS.md + .agents/skills/ content from the template,
2715
+ // merged with the workspace core's AGENTS.md + skills/ when present.
2716
+ const bundle = ${JSON.stringify(bundle)};
2717
+ export default bundle;
2680
2718
  `;
2681
2719
  };
2682
2720
  // Path aliases used by templates (mirrors tsconfig + Vite config). Nitro