@jami-studio/core 0.92.31 → 0.92.33
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/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/deploy/build.ts +4234 -4127
- package/corpus/core/src/file-upload/types.ts +9 -0
- package/corpus/core/src/shared/global-scope.ts +32 -0
- package/corpus/core/src/shared/workspace-app-audience.ts +14 -3
- package/corpus/templates/analytics/changelog/2026-07-11-tracking-and-session-replay-ingest-endpoints-are-reachable-f.md +6 -0
- package/corpus/templates/analytics/package.json +9 -0
- package/corpus/templates/clips/actions/create-recording.ts +16 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +60 -14
- package/corpus/templates/clips/app/routes/record.tsx +35 -9
- package/corpus/templates/clips/changelog/2026-07-11-uploads-now-stream-straight-to-s3-compatible-storage-r2-s3-m.md +6 -0
- package/corpus/templates/clips/server/lib/s3-upload-provider.ts +256 -0
- package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +9 -3
- package/corpus/templates/clips/server/lib/upload-chunk-limits.ts +19 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +15 -4
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +49 -1
- package/dist/deploy/build.d.ts +37 -1
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +769 -676
- package/dist/deploy/build.js.map +1 -1
- package/dist/file-upload/types.d.ts +9 -0
- package/dist/file-upload/types.d.ts.map +1 -1
- package/dist/file-upload/types.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/shared/global-scope.d.ts +4 -0
- package/dist/shared/global-scope.d.ts.map +1 -1
- package/dist/shared/global-scope.js +27 -0
- package/dist/shared/global-scope.js.map +1 -1
- package/dist/shared/workspace-app-audience.d.ts.map +1 -1
- package/dist/shared/workspace-app-audience.js +12 -3
- package/dist/shared/workspace-app-audience.js.map +1 -1
- package/package.json +1 -1
package/dist/deploy/build.js
CHANGED
|
@@ -421,19 +421,19 @@ export const CLOUDFLARE_WORKER_NODE_BUILTIN_STUB_MODULES = {
|
|
|
421
421
|
"export const createRequire = () => globalThis.require ?? ((specifier) => { throw new Error('Cannot require: ' + specifier); });",
|
|
422
422
|
]),
|
|
423
423
|
net: cloudflareNodeBuiltinStubSource("net", ["Socket", "connect", "createConnection", "createServer", "isIP"], [
|
|
424
|
-
`export const isIP = (value) => {
|
|
425
|
-
const input = String(value ?? "");
|
|
426
|
-
const ipv4Parts = input.split(".");
|
|
427
|
-
if (
|
|
428
|
-
ipv4Parts.length === 4 &&
|
|
429
|
-
ipv4Parts.every((part) => /^\\d{1,3}$/.test(part) && Number(part) >= 0 && Number(part) <= 255)
|
|
430
|
-
) {
|
|
431
|
-
return 4;
|
|
432
|
-
}
|
|
433
|
-
if (input.includes(":") && /^[0-9A-Fa-f:.]+$/.test(input)) {
|
|
434
|
-
return 6;
|
|
435
|
-
}
|
|
436
|
-
return 0;
|
|
424
|
+
`export const isIP = (value) => {
|
|
425
|
+
const input = String(value ?? "");
|
|
426
|
+
const ipv4Parts = input.split(".");
|
|
427
|
+
if (
|
|
428
|
+
ipv4Parts.length === 4 &&
|
|
429
|
+
ipv4Parts.every((part) => /^\\d{1,3}$/.test(part) && Number(part) >= 0 && Number(part) <= 255)
|
|
430
|
+
) {
|
|
431
|
+
return 4;
|
|
432
|
+
}
|
|
433
|
+
if (input.includes(":") && /^[0-9A-Fa-f:.]+$/.test(input)) {
|
|
434
|
+
return 6;
|
|
435
|
+
}
|
|
436
|
+
return 0;
|
|
437
437
|
};`,
|
|
438
438
|
]),
|
|
439
439
|
os: cloudflareNodeBuiltinStubSource("os", [
|
|
@@ -522,15 +522,108 @@ export const CLOUDFLARE_WORKER_NODE_BUILTIN_STUB_MODULES = {
|
|
|
522
522
|
* unified workspace deployments. Must stay dependency-free beyond core's
|
|
523
523
|
* lean `global-scope` subpath so nothing registry-touching evaluates before
|
|
524
524
|
* the scope id is set (ESM evaluates imports depth-first in order).
|
|
525
|
+
*
|
|
526
|
+
* Two kinds of baked env, split deliberately (issue-49 regression lesson):
|
|
527
|
+
*
|
|
528
|
+
* - `envDefaults` — workspace-IDENTICAL keys (the workspace-apps manifest,
|
|
529
|
+
* the workspace flag). These are safe as `process.env` DEFAULTS even
|
|
530
|
+
* though the unified worker shares one `process.env` across all apps,
|
|
531
|
+
* because every app bakes the same value. Runtime bindings still win
|
|
532
|
+
* (defaults only fill absent keys; the per-request bindings copy assigns
|
|
533
|
+
* binding keys on top).
|
|
534
|
+
*
|
|
535
|
+
* - `moduleGraphEnvDefaults` — PER-APP keys (app id, base path, audience,
|
|
536
|
+
* public/protected route lists). These must NEVER touch the shared
|
|
537
|
+
* `process.env`: the first app's scope-init would win and poison every
|
|
538
|
+
* sibling (observed live: all apps stripped paths against dispatch's
|
|
539
|
+
* base path and 401'd framework routes). They are stored per module
|
|
540
|
+
* graph via `setModuleGraphEnvDefaults`, and env readers fall back to
|
|
541
|
+
* `getModuleGraphEnvDefault`.
|
|
542
|
+
*
|
|
543
|
+
* workerd has no filesystem and no ambient build env, so without this the
|
|
544
|
+
* runtime falls back as if the app were standalone — agent discovery
|
|
545
|
+
* targets builtin hosted prod URLs (ask_app "internal error") and per-app
|
|
546
|
+
* route access/audience declarations never apply. The Netlify preset bakes
|
|
547
|
+
* the same keys in its generated function entry (`setBasePathEnv`), where
|
|
548
|
+
* per-function isolation makes plain env safe.
|
|
525
549
|
*/
|
|
526
|
-
export function generateScopeInitSource(appScopeId) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
550
|
+
export function generateScopeInitSource(appScopeId, envDefaults, moduleGraphEnvDefaults) {
|
|
551
|
+
const sharedEntries = Object.entries(envDefaults ?? {}).filter(([, value]) => typeof value === "string" && value !== "");
|
|
552
|
+
const perAppEntries = Object.entries(moduleGraphEnvDefaults ?? {}).filter(([, value]) => typeof value === "string" && value !== "");
|
|
553
|
+
const envBlock = sharedEntries.length
|
|
554
|
+
? `
|
|
555
|
+
// Baked workspace-identical env defaults (runtime bindings still win).
|
|
556
|
+
{
|
|
557
|
+
const processRef = (globalThis.process ??= { env: {} });
|
|
558
|
+
processRef.env ??= {};
|
|
559
|
+
const defaults = ${JSON.stringify(Object.fromEntries(sharedEntries))};
|
|
560
|
+
for (const key of Object.keys(defaults)) {
|
|
561
|
+
if (processRef.env[key] === undefined) processRef.env[key] = defaults[key];
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
`
|
|
565
|
+
: "";
|
|
566
|
+
const moduleGraphBlock = perAppEntries.length
|
|
567
|
+
? `setModuleGraphEnvDefaults(${JSON.stringify(Object.fromEntries(perAppEntries))});
|
|
568
|
+
`
|
|
569
|
+
: "";
|
|
570
|
+
return `// AUTO-GENERATED by @agent-native/core deploy build
|
|
571
|
+
// Scope globalThis-pinned framework registries to this app's module graph.
|
|
572
|
+
// Evaluated FIRST in the worker entry's import graph so the scope is set
|
|
573
|
+
// before any registry module initializes. See core shared/global-scope.
|
|
574
|
+
import { setGlobalScopeId${perAppEntries.length ? ", setModuleGraphEnvDefaults" : ""} } from "@agent-native/core/global-scope";
|
|
575
|
+
setGlobalScopeId(${JSON.stringify(appScopeId)});
|
|
576
|
+
${moduleGraphBlock}${envBlock}`;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Snapshot the workspace-IDENTICAL env keys that must survive into the edge
|
|
580
|
+
* runtime as shared `process.env` defaults. Per-app keys deliberately live
|
|
581
|
+
* in `workspaceAppModuleGraphEnvDefaultsFromBuildEnv` instead — see
|
|
582
|
+
* `generateScopeInitSource`.
|
|
583
|
+
*/
|
|
584
|
+
export function workspaceEnvDefaultsFromBuildEnv(env = process.env) {
|
|
585
|
+
if (env.AGENT_NATIVE_WORKSPACE !== "1")
|
|
586
|
+
return {};
|
|
587
|
+
const keys = [
|
|
588
|
+
"AGENT_NATIVE_WORKSPACE",
|
|
589
|
+
"AGENT_NATIVE_WORKSPACE_APPS_JSON",
|
|
590
|
+
"VITE_AGENT_NATIVE_WORKSPACE",
|
|
591
|
+
"VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON",
|
|
592
|
+
];
|
|
593
|
+
const defaults = {};
|
|
594
|
+
for (const key of keys) {
|
|
595
|
+
const value = env[key];
|
|
596
|
+
if (typeof value === "string" && value !== "")
|
|
597
|
+
defaults[key] = value;
|
|
598
|
+
}
|
|
599
|
+
return defaults;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Snapshot the PER-APP workspace env keys for this app's module graph.
|
|
603
|
+
* Delivered via `setModuleGraphEnvDefaults` (never shared `process.env`).
|
|
604
|
+
*/
|
|
605
|
+
export function workspaceAppModuleGraphEnvDefaultsFromBuildEnv(env = process.env) {
|
|
606
|
+
if (env.AGENT_NATIVE_WORKSPACE !== "1")
|
|
607
|
+
return {};
|
|
608
|
+
const keys = [
|
|
609
|
+
"AGENT_NATIVE_WORKSPACE_APP_ID",
|
|
610
|
+
"APP_BASE_PATH",
|
|
611
|
+
"AGENT_NATIVE_WORKSPACE_APP_AUDIENCE",
|
|
612
|
+
"AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS",
|
|
613
|
+
"AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS",
|
|
614
|
+
"VITE_AGENT_NATIVE_WORKSPACE_APP_ID",
|
|
615
|
+
"VITE_APP_BASE_PATH",
|
|
616
|
+
"VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE",
|
|
617
|
+
"VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS",
|
|
618
|
+
"VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS",
|
|
619
|
+
];
|
|
620
|
+
const defaults = {};
|
|
621
|
+
for (const key of keys) {
|
|
622
|
+
const value = env[key];
|
|
623
|
+
if (typeof value === "string" && value !== "")
|
|
624
|
+
defaults[key] = value;
|
|
625
|
+
}
|
|
626
|
+
return defaults;
|
|
534
627
|
}
|
|
535
628
|
/**
|
|
536
629
|
* Derive the workspace app id from a mounted app base path ("/assets" →
|
|
@@ -562,16 +655,16 @@ function isNodeOnlyPlugin(filePath) {
|
|
|
562
655
|
}
|
|
563
656
|
export function generateProvidedPluginsNitroPluginSource(pluginStems) {
|
|
564
657
|
const stems = [...new Set(pluginStems.filter(Boolean))].sort();
|
|
565
|
-
return `// AUTO-GENERATED by @agent-native/core deploy build
|
|
566
|
-
import { markDefaultPluginProvided } from "${EDGE_SERVER_ENTRYPOINT}";
|
|
567
|
-
|
|
568
|
-
const pluginStems = ${JSON.stringify(stems)};
|
|
569
|
-
|
|
570
|
-
export default function markBuildDiscoveredPlugins(nitroApp) {
|
|
571
|
-
for (const stem of pluginStems) {
|
|
572
|
-
markDefaultPluginProvided(nitroApp, stem);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
658
|
+
return `// AUTO-GENERATED by @agent-native/core deploy build
|
|
659
|
+
import { markDefaultPluginProvided } from "${EDGE_SERVER_ENTRYPOINT}";
|
|
660
|
+
|
|
661
|
+
const pluginStems = ${JSON.stringify(stems)};
|
|
662
|
+
|
|
663
|
+
export default function markBuildDiscoveredPlugins(nitroApp) {
|
|
664
|
+
for (const stem of pluginStems) {
|
|
665
|
+
markDefaultPluginProvided(nitroApp, stem);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
575
668
|
`;
|
|
576
669
|
}
|
|
577
670
|
async function writeProvidedPluginsNitroPlugin() {
|
|
@@ -624,20 +717,20 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
|
|
|
624
717
|
routeImports.push(`import ${varName} from ${JSON.stringify(r.absPath)};`);
|
|
625
718
|
routeRegistrations.push(` app.on(${JSON.stringify(r.method.toUpperCase())}, ${JSON.stringify(r.route)}, ${varName});`);
|
|
626
719
|
if (r.method.toLowerCase() === "get") {
|
|
627
|
-
routeRegistrations.push(` app.on("HEAD", ${JSON.stringify(r.route)}, defineEventHandler(async (event) => {
|
|
628
|
-
const originalReq = event.req;
|
|
629
|
-
event.req = requestWithMethod(event.req, "GET");
|
|
630
|
-
try {
|
|
631
|
-
const result = await ${varName}(event);
|
|
632
|
-
const response = result instanceof Response ? result : toResponse(result, event);
|
|
633
|
-
return new Response(null, {
|
|
634
|
-
status: response.status,
|
|
635
|
-
statusText: response.statusText,
|
|
636
|
-
headers: response.headers,
|
|
637
|
-
});
|
|
638
|
-
} finally {
|
|
639
|
-
event.req = originalReq;
|
|
640
|
-
}
|
|
720
|
+
routeRegistrations.push(` app.on("HEAD", ${JSON.stringify(r.route)}, defineEventHandler(async (event) => {
|
|
721
|
+
const originalReq = event.req;
|
|
722
|
+
event.req = requestWithMethod(event.req, "GET");
|
|
723
|
+
try {
|
|
724
|
+
const result = await ${varName}(event);
|
|
725
|
+
const response = result instanceof Response ? result : toResponse(result, event);
|
|
726
|
+
return new Response(null, {
|
|
727
|
+
status: response.status,
|
|
728
|
+
statusText: response.statusText,
|
|
729
|
+
headers: response.headers,
|
|
730
|
+
});
|
|
731
|
+
} finally {
|
|
732
|
+
event.req = originalReq;
|
|
733
|
+
}
|
|
641
734
|
}));`);
|
|
642
735
|
}
|
|
643
736
|
}
|
|
@@ -650,15 +743,15 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
|
|
|
650
743
|
actionImports.push(`import ${varName} from ${JSON.stringify(a.absPath)};`);
|
|
651
744
|
// Mirror the runtime mount (action-routes.ts): `path = http?.path ?? name`.
|
|
652
745
|
const routePath = `/_agent-native/actions/${a.path ?? a.name}`;
|
|
653
|
-
actionRegistrations.push(` app.on(${JSON.stringify(a.method.toUpperCase())}, ${JSON.stringify(routePath)}, defineEventHandler(async (event) => {
|
|
654
|
-
const params = ${a.method === "get" ? "parseActionSearchParams(event.url.searchParams)" : "(await readBody(event)) ?? {}"};
|
|
655
|
-
try {
|
|
656
|
-
const result = await ${varName}.run(params, { caller: "http" });
|
|
657
|
-
if (typeof result === "string") { try { return JSON.parse(result); } catch { return result; } }
|
|
658
|
-
return result;
|
|
659
|
-
} catch (err) {
|
|
660
|
-
return new Response(JSON.stringify({ error: err?.message || "Action failed" }), { status: err?.message?.startsWith("Invalid action parameters") ? 400 : 500, headers: { "Content-Type": "application/json" } });
|
|
661
|
-
}
|
|
746
|
+
actionRegistrations.push(` app.on(${JSON.stringify(a.method.toUpperCase())}, ${JSON.stringify(routePath)}, defineEventHandler(async (event) => {
|
|
747
|
+
const params = ${a.method === "get" ? "parseActionSearchParams(event.url.searchParams)" : "(await readBody(event)) ?? {}"};
|
|
748
|
+
try {
|
|
749
|
+
const result = await ${varName}.run(params, { caller: "http" });
|
|
750
|
+
if (typeof result === "string") { try { return JSON.parse(result); } catch { return result; } }
|
|
751
|
+
return result;
|
|
752
|
+
} catch (err) {
|
|
753
|
+
return new Response(JSON.stringify({ error: err?.message || "Action failed" }), { status: err?.message?.startsWith("Invalid action parameters") ? 400 : 500, headers: { "Content-Type": "application/json" } });
|
|
754
|
+
}
|
|
662
755
|
}));`);
|
|
663
756
|
}
|
|
664
757
|
// Filter out Node-only plugins
|
|
@@ -670,8 +763,8 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
|
|
|
670
763
|
const varName = `plugin_${i}`;
|
|
671
764
|
providedPluginStems.add(path.basename(edgePlugins[i], path.extname(edgePlugins[i])));
|
|
672
765
|
pluginImports.push(`import ${varName} from ${JSON.stringify(edgePlugins[i])};`);
|
|
673
|
-
pluginCalls.push(` if (typeof ${varName} === "function") {
|
|
674
|
-
await ${varName}(nitroApp);
|
|
766
|
+
pluginCalls.push(` if (typeof ${varName} === "function") {
|
|
767
|
+
await ${varName}(nitroApp);
|
|
675
768
|
}`);
|
|
676
769
|
}
|
|
677
770
|
// Auto-mounted default plugins (for slots the template doesn't override
|
|
@@ -694,8 +787,8 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
|
|
|
694
787
|
continue;
|
|
695
788
|
pluginImports.push(`import { ${defaultExportName} as ${varName} } from "${EDGE_SERVER_ENTRYPOINT}";`);
|
|
696
789
|
}
|
|
697
|
-
pluginCalls.push(` if (typeof ${varName} === "function") {
|
|
698
|
-
await ${varName}(nitroApp);
|
|
790
|
+
pluginCalls.push(` if (typeof ${varName} === "function") {
|
|
791
|
+
await ${varName}(nitroApp);
|
|
699
792
|
}`);
|
|
700
793
|
}
|
|
701
794
|
const generatedPluginMarks = providedPluginStems.size > 0
|
|
@@ -709,523 +802,523 @@ export function generateWorkerEntry(routes, pluginPaths, defaultPluginStems = []
|
|
|
709
802
|
if (generatedPluginMarks.length > 0) {
|
|
710
803
|
pluginImports.unshift(`import { markDefaultPluginProvided as markGeneratedPluginProvided } from "${EDGE_SERVER_ENTRYPOINT}";`);
|
|
711
804
|
}
|
|
712
|
-
return `
|
|
713
|
-
// Auto-generated worker entry point for ${preset}
|
|
714
|
-
${appScopeId ? 'import "./_scope-init.js";\n' : ""}import { H3, defineEventHandler, readBody, toResponse } from "h3";
|
|
715
|
-
${includeReactRouterSsr ? 'import { createRequestHandler } from "react-router";' : ""}
|
|
716
|
-
${includeReactRouterSsr ? 'import * as serverBuild from "./server-build.js";' : ""}
|
|
717
|
-
|
|
718
|
-
function normalizeAppBasePath(value) {
|
|
719
|
-
if (!value || value === "/") return "";
|
|
720
|
-
const trimmed = String(value).trim();
|
|
721
|
-
if (!trimmed || trimmed === "/") return "";
|
|
722
|
-
return "/" + trimmed.replace(/^\\/+/, "").replace(/\\/+$/, "");
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function getAppBasePath() {
|
|
726
|
-
const builtAppBasePath = ${JSON.stringify(builtAppBasePath)};
|
|
727
|
-
return normalizeAppBasePath(
|
|
728
|
-
globalThis.process?.env?.VITE_APP_BASE_PATH ||
|
|
729
|
-
globalThis.process?.env?.APP_BASE_PATH ||
|
|
730
|
-
builtAppBasePath,
|
|
731
|
-
);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function stripAppBasePath(pathname) {
|
|
735
|
-
const basePath = getAppBasePath();
|
|
736
|
-
if (!basePath) return pathname;
|
|
737
|
-
if (pathname === basePath) return "/";
|
|
738
|
-
if (pathname.startsWith(basePath + "/")) {
|
|
739
|
-
return pathname.slice(basePath.length) || "/";
|
|
740
|
-
}
|
|
741
|
-
return pathname;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function parseActionSearchParams(searchParams) {
|
|
745
|
-
const params = {};
|
|
746
|
-
for (const [rawKey, value] of searchParams.entries()) {
|
|
747
|
-
const isArrayKey = rawKey.endsWith("[]");
|
|
748
|
-
// The core client serializes arrays as key[]=value so one-item arrays
|
|
749
|
-
// survive GET action parsing in generated worker deployments.
|
|
750
|
-
const key = isArrayKey ? rawKey.slice(0, -2) : rawKey;
|
|
751
|
-
const current = params[key];
|
|
752
|
-
if (current === undefined) {
|
|
753
|
-
params[key] = isArrayKey ? [value] : value;
|
|
754
|
-
} else if (Array.isArray(current)) {
|
|
755
|
-
current.push(value);
|
|
756
|
-
} else {
|
|
757
|
-
params[key] = [current, value];
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
return params;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function isApiPath(pathname) {
|
|
764
|
-
return pathname === "/api" || pathname.startsWith("/api/");
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function isFrameworkPath(pathname) {
|
|
768
|
-
return (
|
|
769
|
-
pathname === "/_agent-native" || pathname.startsWith("/_agent-native/")
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
function requestWithMountedApiPrefixStripped(request) {
|
|
774
|
-
const basePath = getAppBasePath();
|
|
775
|
-
if (!basePath) return request;
|
|
776
|
-
const url = new URL(request.url);
|
|
777
|
-
const strippedPathname = stripAppBasePath(url.pathname);
|
|
778
|
-
if (strippedPathname === url.pathname) {
|
|
779
|
-
return request;
|
|
780
|
-
}
|
|
781
|
-
if (!isApiPath(strippedPathname) && !isFrameworkPath(strippedPathname)) {
|
|
782
|
-
return request;
|
|
783
|
-
}
|
|
784
|
-
url.pathname = strippedPathname;
|
|
785
|
-
return new Request(url, request);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function prefixMountedPath(path, basePath) {
|
|
789
|
-
if (!basePath || !path.startsWith("/") || path.startsWith("//")) return path;
|
|
790
|
-
if (path === basePath || path.startsWith(basePath + "/")) return path;
|
|
791
|
-
return basePath + path;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function prefixMountedHtml(html, basePath) {
|
|
795
|
-
if (!basePath) return html;
|
|
796
|
-
return html
|
|
797
|
-
.replace(
|
|
798
|
-
/\\b(href|src|action|formaction|poster)=(["'])(\\/(?!\\/)[^"']*)\\2/g,
|
|
799
|
-
(_match, attr, quote, path) =>
|
|
800
|
-
attr + "=" + quote + prefixMountedPath(path, basePath) + quote,
|
|
801
|
-
)
|
|
802
|
-
.replace(/url\\((["']?)(\\/(?!\\/)[^)'" ]+)\\1\\)/g, (_match, quote, path) => {
|
|
803
|
-
const q = quote || "";
|
|
804
|
-
return "url(" + q + prefixMountedPath(path, basePath) + q + ")";
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
function firstNonEmpty() {
|
|
809
|
-
for (const value of arguments) {
|
|
810
|
-
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
811
|
-
if (trimmed) return trimmed;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
function getSentryClientConfigScript() {
|
|
816
|
-
const env = globalThis.process?.env || {};
|
|
817
|
-
const key = firstNonEmpty(env.SENTRY_CLIENT_KEY, env.VITE_SENTRY_CLIENT_KEY);
|
|
818
|
-
const projectId = firstNonEmpty(
|
|
819
|
-
env.SENTRY_PROJECT_ID,
|
|
820
|
-
env.VITE_SENTRY_PROJECT_ID,
|
|
821
|
-
);
|
|
822
|
-
const host = firstNonEmpty(
|
|
823
|
-
env.SENTRY_INGEST_HOST,
|
|
824
|
-
env.VITE_SENTRY_INGEST_HOST,
|
|
825
|
-
);
|
|
826
|
-
const dsn =
|
|
827
|
-
firstNonEmpty(
|
|
828
|
-
env.SENTRY_CLIENT_DSN,
|
|
829
|
-
env.VITE_SENTRY_CLIENT_DSN,
|
|
830
|
-
env.VITE_SENTRY_DSN,
|
|
831
|
-
env.SENTRY_DSN,
|
|
832
|
-
) || (key && projectId && host ? "https://" + key + "@" + host + "/" + projectId : undefined);
|
|
833
|
-
if (!dsn) return null;
|
|
834
|
-
const config = {
|
|
835
|
-
sentryDsn: dsn,
|
|
836
|
-
sentryEnvironment:
|
|
837
|
-
firstNonEmpty(
|
|
838
|
-
env.SENTRY_ENVIRONMENT,
|
|
839
|
-
env.NETLIFY_CONTEXT,
|
|
840
|
-
env.VERCEL_ENV,
|
|
841
|
-
env.NODE_ENV,
|
|
842
|
-
) || "production",
|
|
843
|
-
};
|
|
844
|
-
return (
|
|
845
|
-
'<script data-agent-native-sentry-config>' +
|
|
846
|
-
'window.__AGENT_NATIVE_CONFIG__=Object.assign({},window.__AGENT_NATIVE_CONFIG__,' +
|
|
847
|
-
JSON.stringify(config) +
|
|
848
|
-
");</script>"
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function injectHeadScript(html, script) {
|
|
853
|
-
if (!script) return html;
|
|
854
|
-
const headCloseIdx = html.indexOf("</head>");
|
|
855
|
-
if (headCloseIdx === -1) return html;
|
|
856
|
-
return html.slice(0, headCloseIdx) + script + html.slice(headCloseIdx);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const DEFAULT_SSR_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_CACHE_CONTROL)};
|
|
860
|
-
const DEFAULT_SSR_CDN_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_CDN_CACHE_CONTROL)};
|
|
861
|
-
const DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL)};
|
|
862
|
-
const DEFAULT_SPECULATION_RULES_PATH = ${JSON.stringify(DEFAULT_SPECULATION_RULES_PATH)};
|
|
863
|
-
const IMMUTABLE_ASSET_CACHE_CONTROL = ${JSON.stringify(IMMUTABLE_ASSET_CACHE_CONTROL)};
|
|
864
|
-
const IMMUTABLE_ASSET_PATHS = new Set(${JSON.stringify([...new Set(immutableAssetPaths)].sort())});
|
|
865
|
-
const AGENT_NATIVE_SOCIAL_IMAGE_PATH = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_PATH)};
|
|
866
|
-
const AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER)};
|
|
867
|
-
const AGENT_NATIVE_SOCIAL_IMAGE_ALT = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_ALT)};
|
|
868
|
-
const AGENT_NATIVE_SOCIAL_IMAGE_TYPE = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_TYPE)};
|
|
869
|
-
const AGENT_NATIVE_SOCIAL_IMAGE_WIDTH = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_WIDTH)};
|
|
870
|
-
const AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT)};
|
|
871
|
-
const OG_IMAGE_META_RE = /<meta\\b(?=[^>]*\\bproperty=(["'])og:image\\1)[^>]*>/i;
|
|
872
|
-
const TWITTER_CARD_META_RE = /<meta\\b(?=[^>]*\\bname=(["'])twitter:card\\1)[^>]*>/i;
|
|
873
|
-
const TWITTER_IMAGE_META_RE = /<meta\\b(?=[^>]*\\bname=(["'])twitter:image\\1)[^>]*>/i;
|
|
874
|
-
|
|
875
|
-
function withAgentNativeSocialImageCacheBuster(image) {
|
|
876
|
-
const separator = image.includes("?") ? "&" : "?";
|
|
877
|
-
return image + separator + "v=" + encodeURIComponent(AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
function defaultSocialImageUrl(request, basePath) {
|
|
881
|
-
return withAgentNativeSocialImageCacheBuster(
|
|
882
|
-
new URL(prefixMountedPath(AGENT_NATIVE_SOCIAL_IMAGE_PATH, basePath), request.url).toString()
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function injectDefaultSocialImageMeta(html, imageUrl) {
|
|
887
|
-
const headCloseIdx = html.indexOf("</head>");
|
|
888
|
-
if (headCloseIdx === -1) return html;
|
|
889
|
-
|
|
890
|
-
const hasAnySocialImage =
|
|
891
|
-
OG_IMAGE_META_RE.test(html) || TWITTER_IMAGE_META_RE.test(html);
|
|
892
|
-
const tags = [];
|
|
893
|
-
|
|
894
|
-
if (!hasAnySocialImage) {
|
|
895
|
-
tags.push('<meta property="og:image" content="' + imageUrl + '">');
|
|
896
|
-
tags.push('<meta property="og:image:secure_url" content="' + imageUrl + '">');
|
|
897
|
-
tags.push('<meta property="og:image:type" content="' + AGENT_NATIVE_SOCIAL_IMAGE_TYPE + '">');
|
|
898
|
-
tags.push('<meta property="og:image:width" content="' + AGENT_NATIVE_SOCIAL_IMAGE_WIDTH + '">');
|
|
899
|
-
tags.push('<meta property="og:image:height" content="' + AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT + '">');
|
|
900
|
-
tags.push('<meta property="og:image:alt" content="' + AGENT_NATIVE_SOCIAL_IMAGE_ALT + '">');
|
|
901
|
-
}
|
|
902
|
-
if (!TWITTER_CARD_META_RE.test(html)) {
|
|
903
|
-
tags.push('<meta name="twitter:card" content="summary_large_image">');
|
|
904
|
-
}
|
|
905
|
-
if (!hasAnySocialImage) {
|
|
906
|
-
tags.push('<meta name="twitter:image" content="' + imageUrl + '">');
|
|
907
|
-
tags.push('<meta name="twitter:image:alt" content="' + AGENT_NATIVE_SOCIAL_IMAGE_ALT + '">');
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
if (tags.length === 0) return html;
|
|
911
|
-
return html.slice(0, headCloseIdx) + tags.join("") + html.slice(headCloseIdx);
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
function isSsrHtmlOrDataResponse(headers, status, pathname) {
|
|
915
|
-
if (status < 200 || status >= 400) return false;
|
|
916
|
-
const contentType = (headers.get("content-type") || "").toLowerCase();
|
|
917
|
-
if (contentType.includes("text/html")) return true;
|
|
918
|
-
return pathname.endsWith(".data") && contentType.includes("text/x-script");
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
/**
|
|
922
|
-
* Apply the SSR cache policy to the response headers.
|
|
923
|
-
*
|
|
924
|
-
* SSR IS A PUBLIC, HARD-CDN-CACHED SHELL — SERVED IDENTICALLY TO EVERYONE.
|
|
925
|
-
* Every SSR HTML / React Router .data response gets the same public
|
|
926
|
-
* stale-while-revalidate policy for ALL visitors, authenticated or not. The SSR
|
|
927
|
-
* output is impersonal (the handler never reads the request's session/cookies),
|
|
928
|
-
* so it is safe to hard-cache one shared copy at the edge. Do NOT reintroduce
|
|
929
|
-
* per-user / cookie-based cache variation here (no private, no no-store, no
|
|
930
|
-
* "authenticated then don't cache" branch) — that makes every logged-in
|
|
931
|
-
* visitor's pages uncacheable. Per-user state is resolved client-side instead.
|
|
932
|
-
*/
|
|
933
|
-
function applyDefaultSsrCacheHeader(headers, status, pathname) {
|
|
934
|
-
if (!isSsrHtmlOrDataResponse(headers, status, pathname)) return;
|
|
935
|
-
|
|
936
|
-
headers.set("cache-control", DEFAULT_SSR_CACHE_CONTROL);
|
|
937
|
-
headers.set("cdn-cache-control", DEFAULT_SSR_CDN_CACHE_CONTROL);
|
|
938
|
-
// Netlify function responses are dynamic by default and can otherwise show
|
|
939
|
-
// Cache-Status fwd=bypass even with Cache-Control: public. Keep this
|
|
940
|
-
// Netlify-specific header so SSR HTML/.data are served from the shared
|
|
941
|
-
// durable CDN cache instead of stampeding origin — for every visitor.
|
|
942
|
-
headers.set("netlify-cdn-cache-control", DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
function applyDefaultSpeculationRulesHeader(headers, status, basePath) {
|
|
946
|
-
if (status < 200 || status >= 400) return;
|
|
947
|
-
if (headers.has("speculation-rules")) return;
|
|
948
|
-
|
|
949
|
-
const contentType = (headers.get("content-type") || "").toLowerCase();
|
|
950
|
-
if (!contentType.includes("text/html")) return;
|
|
951
|
-
|
|
952
|
-
// Cloudflare Speed Brain injects Speculation-Rules when origin omits this
|
|
953
|
-
// header. Those browser prefetches carry Sec-Purpose: prefetch and
|
|
954
|
-
// Cloudflare can return 503 before the request reaches origin. Publish an
|
|
955
|
-
// explicit no-op ruleset by default; apps can still provide their own header.
|
|
956
|
-
headers.set("speculation-rules", '"' + prefixMountedPath(DEFAULT_SPECULATION_RULES_PATH, basePath) + '"');
|
|
957
|
-
}
|
|
958
|
-
function isImmutableAssetRequest(request) {
|
|
959
|
-
const pathname = stripAppBasePath(new URL(request.url).pathname);
|
|
960
|
-
return IMMUTABLE_ASSET_PATHS.has(pathname);
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function applyImmutableAssetCacheHeaders(response, request) {
|
|
964
|
-
if (!isImmutableAssetRequest(request)) return response;
|
|
965
|
-
if (!((response.status >= 200 && response.status < 300) || response.status === 304)) {
|
|
966
|
-
return response;
|
|
967
|
-
}
|
|
968
|
-
const headers = new Headers(response.headers);
|
|
969
|
-
headers.set("Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL);
|
|
970
|
-
headers.set("CDN-Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL);
|
|
971
|
-
headers.set("Netlify-CDN-Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL);
|
|
972
|
-
return new Response(response.body, {
|
|
973
|
-
status: response.status,
|
|
974
|
-
statusText: response.statusText,
|
|
975
|
-
headers,
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
async function rewriteMountedResponse(response, basePath, pathname, request) {
|
|
980
|
-
const sentryClientConfigScript = getSentryClientConfigScript();
|
|
981
|
-
const headers = new Headers(response.headers);
|
|
982
|
-
applyDefaultSsrCacheHeader(headers, response.status, pathname);
|
|
983
|
-
applyDefaultSpeculationRulesHeader(headers, response.status, basePath);
|
|
984
|
-
|
|
985
|
-
const location = headers.get("location");
|
|
986
|
-
if (location?.startsWith("/") && !location.startsWith("//")) {
|
|
987
|
-
headers.set("location", prefixMountedPath(location, basePath));
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
const contentType = headers.get("content-type") || "";
|
|
991
|
-
if (!contentType.toLowerCase().includes("text/html") || !response.body) {
|
|
992
|
-
return new Response(response.body, {
|
|
993
|
-
status: response.status,
|
|
994
|
-
statusText: response.statusText,
|
|
995
|
-
headers,
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
const html = await response.text();
|
|
1000
|
-
headers.delete("content-length");
|
|
1001
|
-
return new Response(
|
|
1002
|
-
injectHeadScript(
|
|
1003
|
-
injectDefaultSocialImageMeta(
|
|
1004
|
-
prefixMountedHtml(html, basePath),
|
|
1005
|
-
defaultSocialImageUrl(request, basePath),
|
|
1006
|
-
),
|
|
1007
|
-
sentryClientConfigScript,
|
|
1008
|
-
),
|
|
1009
|
-
{
|
|
1010
|
-
status: response.status,
|
|
1011
|
-
statusText: response.statusText,
|
|
1012
|
-
headers,
|
|
1013
|
-
},
|
|
1014
|
-
);
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
function requestWithMethod(request, method) {
|
|
1018
|
-
return new Request(request.url, {
|
|
1019
|
-
method,
|
|
1020
|
-
headers: request.headers,
|
|
1021
|
-
signal: request.signal,
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
function requestWithPathname(request, pathname) {
|
|
1026
|
-
const url = new URL(request.url);
|
|
1027
|
-
if (url.pathname === pathname) return request;
|
|
1028
|
-
url.pathname = pathname;
|
|
1029
|
-
return new Request(url, request);
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
function isStaticAppShellRequest(request) {
|
|
1033
|
-
if (request.method !== "GET" && request.method !== "HEAD") return false;
|
|
1034
|
-
const p = stripAppBasePath(new URL(request.url).pathname);
|
|
1035
|
-
if (
|
|
1036
|
-
p.startsWith("/.well-known/") ||
|
|
1037
|
-
p.startsWith("/_agent-native/") ||
|
|
1038
|
-
isApiPath(p) ||
|
|
1039
|
-
p === "/favicon.ico" ||
|
|
1040
|
-
p === "/favicon.png" ||
|
|
1041
|
-
/\\.\\w+$/.test(p)
|
|
1042
|
-
) {
|
|
1043
|
-
return false;
|
|
1044
|
-
}
|
|
1045
|
-
return true;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
async function fetchStaticAppShell(request, env) {
|
|
1049
|
-
if (!env?.ASSETS || !isStaticAppShellRequest(request)) return null;
|
|
1050
|
-
const basePath = getAppBasePath();
|
|
1051
|
-
const p = stripAppBasePath(new URL(request.url).pathname);
|
|
1052
|
-
// Workspace deployments serve each app's static files under its base path
|
|
1053
|
-
// (/dispatch/index.html on the unified origin) — an unprefixed
|
|
1054
|
-
// "/index.html" 404s against the shared ASSETS binding and authed deep
|
|
1055
|
-
// links (e.g. /dispatch/overview) leaked h3's JSON 404 instead of the SPA
|
|
1056
|
-
// shell. Try the base-path-prefixed shell first, fall back to the root
|
|
1057
|
-
// shell for single-app deployments.
|
|
1058
|
-
let response;
|
|
1059
|
-
try {
|
|
1060
|
-
if (basePath) {
|
|
1061
|
-
response = await env.ASSETS.fetch(
|
|
1062
|
-
requestWithPathname(
|
|
1063
|
-
requestWithMethod(request, "GET"),
|
|
1064
|
-
basePath + "/index.html",
|
|
1065
|
-
),
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
if (!response || response.status === 404) {
|
|
1069
|
-
response = await env.ASSETS.fetch(
|
|
1070
|
-
requestWithPathname(requestWithMethod(request, "GET"), "/index.html"),
|
|
1071
|
-
);
|
|
1072
|
-
}
|
|
1073
|
-
} catch {
|
|
1074
|
-
return null;
|
|
1075
|
-
}
|
|
1076
|
-
if (response.status === 404) return null;
|
|
1077
|
-
if (request.method === "HEAD") {
|
|
1078
|
-
return rewriteMountedResponse(
|
|
1079
|
-
new Response(null, {
|
|
1080
|
-
status: response.status,
|
|
1081
|
-
statusText: response.statusText,
|
|
1082
|
-
headers: response.headers,
|
|
1083
|
-
}),
|
|
1084
|
-
basePath,
|
|
1085
|
-
p,
|
|
1086
|
-
request,
|
|
1087
|
-
);
|
|
1088
|
-
}
|
|
1089
|
-
return rewriteMountedResponse(response, basePath, p, request);
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
// API route handlers
|
|
1093
|
-
${routeImports.join("\n")}
|
|
1094
|
-
|
|
1095
|
-
// Action handlers (auto-discovered from actions/)
|
|
1096
|
-
${actionImports.join("\n")}
|
|
1097
|
-
|
|
1098
|
-
// Server plugins
|
|
1099
|
-
${pluginImports.join("\n")}
|
|
1100
|
-
|
|
1101
|
-
let _handler;
|
|
1102
|
-
|
|
1103
|
-
async function getHandler() {
|
|
1104
|
-
if (_handler) return _handler;
|
|
1105
|
-
|
|
1106
|
-
const app = new H3();
|
|
1107
|
-
|
|
1108
|
-
// Build a fake nitroApp surface so framework plugins (which expect
|
|
1109
|
-
// \`nitroApp.h3["~middleware"]\`) can register routes via getH3App().
|
|
1110
|
-
const noop = () => {};
|
|
1111
|
-
const nitroApp = {
|
|
1112
|
-
h3: app,
|
|
1113
|
-
hooks: { hook: noop, callHook: noop, hookOnce: noop },
|
|
1114
|
-
captureError: noop,
|
|
1115
|
-
};
|
|
1116
|
-
|
|
1117
|
-
// CORS — applied as global middleware via .use(handler)
|
|
1118
|
-
app.use(defineEventHandler((event) => {
|
|
1119
|
-
if (event.req.method === "OPTIONS") {
|
|
1120
|
-
return new Response(null, {
|
|
1121
|
-
status: 204,
|
|
1122
|
-
headers: {
|
|
1123
|
-
"Access-Control-Allow-Origin": "*",
|
|
1124
|
-
"Access-Control-Allow-Methods": "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS",
|
|
1125
|
-
"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",
|
|
1126
|
-
},
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
}));
|
|
1130
|
-
|
|
1131
|
-
// Run plugins — they call getH3App(nitroApp).use(path, handler) which
|
|
1132
|
-
// pushes path-prefix middleware onto app["~middleware"].
|
|
1133
|
-
// Pre-mark every build-time plugin slot before any plugin awaits the runtime
|
|
1134
|
-
// default bootstrap. Bundled serverless workers often lack server/plugins/
|
|
1135
|
-
// on disk, so runtime discovery would otherwise auto-mount duplicate
|
|
1136
|
-
// framework defaults before later custom plugins get a chance to mark
|
|
1137
|
-
// themselves as provided.
|
|
1138
|
-
${generatedPluginMarks.map((stem) => ` markGeneratedPluginProvided(nitroApp, ${JSON.stringify(stem)});`).join("\n")}
|
|
1139
|
-
${pluginCalls.join("\n")}
|
|
1140
|
-
|
|
1141
|
-
// Register API routes
|
|
1142
|
-
${routeRegistrations.join("\n")}
|
|
1143
|
-
|
|
1144
|
-
// Register action routes (/_agent-native/actions/*)
|
|
1145
|
-
${actionRegistrations.join("\n")}
|
|
1146
|
-
|
|
805
|
+
return `
|
|
806
|
+
// Auto-generated worker entry point for ${preset}
|
|
807
|
+
${appScopeId ? 'import "./_scope-init.js";\n' : ""}import { H3, defineEventHandler, readBody, toResponse } from "h3";
|
|
808
|
+
${includeReactRouterSsr ? 'import { createRequestHandler } from "react-router";' : ""}
|
|
809
|
+
${includeReactRouterSsr ? 'import * as serverBuild from "./server-build.js";' : ""}
|
|
810
|
+
|
|
811
|
+
function normalizeAppBasePath(value) {
|
|
812
|
+
if (!value || value === "/") return "";
|
|
813
|
+
const trimmed = String(value).trim();
|
|
814
|
+
if (!trimmed || trimmed === "/") return "";
|
|
815
|
+
return "/" + trimmed.replace(/^\\/+/, "").replace(/\\/+$/, "");
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function getAppBasePath() {
|
|
819
|
+
const builtAppBasePath = ${JSON.stringify(builtAppBasePath)};
|
|
820
|
+
return normalizeAppBasePath(
|
|
821
|
+
globalThis.process?.env?.VITE_APP_BASE_PATH ||
|
|
822
|
+
globalThis.process?.env?.APP_BASE_PATH ||
|
|
823
|
+
builtAppBasePath,
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function stripAppBasePath(pathname) {
|
|
828
|
+
const basePath = getAppBasePath();
|
|
829
|
+
if (!basePath) return pathname;
|
|
830
|
+
if (pathname === basePath) return "/";
|
|
831
|
+
if (pathname.startsWith(basePath + "/")) {
|
|
832
|
+
return pathname.slice(basePath.length) || "/";
|
|
833
|
+
}
|
|
834
|
+
return pathname;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function parseActionSearchParams(searchParams) {
|
|
838
|
+
const params = {};
|
|
839
|
+
for (const [rawKey, value] of searchParams.entries()) {
|
|
840
|
+
const isArrayKey = rawKey.endsWith("[]");
|
|
841
|
+
// The core client serializes arrays as key[]=value so one-item arrays
|
|
842
|
+
// survive GET action parsing in generated worker deployments.
|
|
843
|
+
const key = isArrayKey ? rawKey.slice(0, -2) : rawKey;
|
|
844
|
+
const current = params[key];
|
|
845
|
+
if (current === undefined) {
|
|
846
|
+
params[key] = isArrayKey ? [value] : value;
|
|
847
|
+
} else if (Array.isArray(current)) {
|
|
848
|
+
current.push(value);
|
|
849
|
+
} else {
|
|
850
|
+
params[key] = [current, value];
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return params;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function isApiPath(pathname) {
|
|
857
|
+
return pathname === "/api" || pathname.startsWith("/api/");
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function isFrameworkPath(pathname) {
|
|
861
|
+
return (
|
|
862
|
+
pathname === "/_agent-native" || pathname.startsWith("/_agent-native/")
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function requestWithMountedApiPrefixStripped(request) {
|
|
867
|
+
const basePath = getAppBasePath();
|
|
868
|
+
if (!basePath) return request;
|
|
869
|
+
const url = new URL(request.url);
|
|
870
|
+
const strippedPathname = stripAppBasePath(url.pathname);
|
|
871
|
+
if (strippedPathname === url.pathname) {
|
|
872
|
+
return request;
|
|
873
|
+
}
|
|
874
|
+
if (!isApiPath(strippedPathname) && !isFrameworkPath(strippedPathname)) {
|
|
875
|
+
return request;
|
|
876
|
+
}
|
|
877
|
+
url.pathname = strippedPathname;
|
|
878
|
+
return new Request(url, request);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function prefixMountedPath(path, basePath) {
|
|
882
|
+
if (!basePath || !path.startsWith("/") || path.startsWith("//")) return path;
|
|
883
|
+
if (path === basePath || path.startsWith(basePath + "/")) return path;
|
|
884
|
+
return basePath + path;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function prefixMountedHtml(html, basePath) {
|
|
888
|
+
if (!basePath) return html;
|
|
889
|
+
return html
|
|
890
|
+
.replace(
|
|
891
|
+
/\\b(href|src|action|formaction|poster)=(["'])(\\/(?!\\/)[^"']*)\\2/g,
|
|
892
|
+
(_match, attr, quote, path) =>
|
|
893
|
+
attr + "=" + quote + prefixMountedPath(path, basePath) + quote,
|
|
894
|
+
)
|
|
895
|
+
.replace(/url\\((["']?)(\\/(?!\\/)[^)'" ]+)\\1\\)/g, (_match, quote, path) => {
|
|
896
|
+
const q = quote || "";
|
|
897
|
+
return "url(" + q + prefixMountedPath(path, basePath) + q + ")";
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function firstNonEmpty() {
|
|
902
|
+
for (const value of arguments) {
|
|
903
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
904
|
+
if (trimmed) return trimmed;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function getSentryClientConfigScript() {
|
|
909
|
+
const env = globalThis.process?.env || {};
|
|
910
|
+
const key = firstNonEmpty(env.SENTRY_CLIENT_KEY, env.VITE_SENTRY_CLIENT_KEY);
|
|
911
|
+
const projectId = firstNonEmpty(
|
|
912
|
+
env.SENTRY_PROJECT_ID,
|
|
913
|
+
env.VITE_SENTRY_PROJECT_ID,
|
|
914
|
+
);
|
|
915
|
+
const host = firstNonEmpty(
|
|
916
|
+
env.SENTRY_INGEST_HOST,
|
|
917
|
+
env.VITE_SENTRY_INGEST_HOST,
|
|
918
|
+
);
|
|
919
|
+
const dsn =
|
|
920
|
+
firstNonEmpty(
|
|
921
|
+
env.SENTRY_CLIENT_DSN,
|
|
922
|
+
env.VITE_SENTRY_CLIENT_DSN,
|
|
923
|
+
env.VITE_SENTRY_DSN,
|
|
924
|
+
env.SENTRY_DSN,
|
|
925
|
+
) || (key && projectId && host ? "https://" + key + "@" + host + "/" + projectId : undefined);
|
|
926
|
+
if (!dsn) return null;
|
|
927
|
+
const config = {
|
|
928
|
+
sentryDsn: dsn,
|
|
929
|
+
sentryEnvironment:
|
|
930
|
+
firstNonEmpty(
|
|
931
|
+
env.SENTRY_ENVIRONMENT,
|
|
932
|
+
env.NETLIFY_CONTEXT,
|
|
933
|
+
env.VERCEL_ENV,
|
|
934
|
+
env.NODE_ENV,
|
|
935
|
+
) || "production",
|
|
936
|
+
};
|
|
937
|
+
return (
|
|
938
|
+
'<script data-agent-native-sentry-config>' +
|
|
939
|
+
'window.__AGENT_NATIVE_CONFIG__=Object.assign({},window.__AGENT_NATIVE_CONFIG__,' +
|
|
940
|
+
JSON.stringify(config) +
|
|
941
|
+
");</script>"
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function injectHeadScript(html, script) {
|
|
946
|
+
if (!script) return html;
|
|
947
|
+
const headCloseIdx = html.indexOf("</head>");
|
|
948
|
+
if (headCloseIdx === -1) return html;
|
|
949
|
+
return html.slice(0, headCloseIdx) + script + html.slice(headCloseIdx);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const DEFAULT_SSR_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_CACHE_CONTROL)};
|
|
953
|
+
const DEFAULT_SSR_CDN_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_CDN_CACHE_CONTROL)};
|
|
954
|
+
const DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL = ${JSON.stringify(DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL)};
|
|
955
|
+
const DEFAULT_SPECULATION_RULES_PATH = ${JSON.stringify(DEFAULT_SPECULATION_RULES_PATH)};
|
|
956
|
+
const IMMUTABLE_ASSET_CACHE_CONTROL = ${JSON.stringify(IMMUTABLE_ASSET_CACHE_CONTROL)};
|
|
957
|
+
const IMMUTABLE_ASSET_PATHS = new Set(${JSON.stringify([...new Set(immutableAssetPaths)].sort())});
|
|
958
|
+
const AGENT_NATIVE_SOCIAL_IMAGE_PATH = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_PATH)};
|
|
959
|
+
const AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER)};
|
|
960
|
+
const AGENT_NATIVE_SOCIAL_IMAGE_ALT = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_ALT)};
|
|
961
|
+
const AGENT_NATIVE_SOCIAL_IMAGE_TYPE = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_TYPE)};
|
|
962
|
+
const AGENT_NATIVE_SOCIAL_IMAGE_WIDTH = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_WIDTH)};
|
|
963
|
+
const AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT = ${JSON.stringify(AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT)};
|
|
964
|
+
const OG_IMAGE_META_RE = /<meta\\b(?=[^>]*\\bproperty=(["'])og:image\\1)[^>]*>/i;
|
|
965
|
+
const TWITTER_CARD_META_RE = /<meta\\b(?=[^>]*\\bname=(["'])twitter:card\\1)[^>]*>/i;
|
|
966
|
+
const TWITTER_IMAGE_META_RE = /<meta\\b(?=[^>]*\\bname=(["'])twitter:image\\1)[^>]*>/i;
|
|
967
|
+
|
|
968
|
+
function withAgentNativeSocialImageCacheBuster(image) {
|
|
969
|
+
const separator = image.includes("?") ? "&" : "?";
|
|
970
|
+
return image + separator + "v=" + encodeURIComponent(AGENT_NATIVE_SOCIAL_IMAGE_CACHE_BUSTER);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function defaultSocialImageUrl(request, basePath) {
|
|
974
|
+
return withAgentNativeSocialImageCacheBuster(
|
|
975
|
+
new URL(prefixMountedPath(AGENT_NATIVE_SOCIAL_IMAGE_PATH, basePath), request.url).toString()
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function injectDefaultSocialImageMeta(html, imageUrl) {
|
|
980
|
+
const headCloseIdx = html.indexOf("</head>");
|
|
981
|
+
if (headCloseIdx === -1) return html;
|
|
982
|
+
|
|
983
|
+
const hasAnySocialImage =
|
|
984
|
+
OG_IMAGE_META_RE.test(html) || TWITTER_IMAGE_META_RE.test(html);
|
|
985
|
+
const tags = [];
|
|
986
|
+
|
|
987
|
+
if (!hasAnySocialImage) {
|
|
988
|
+
tags.push('<meta property="og:image" content="' + imageUrl + '">');
|
|
989
|
+
tags.push('<meta property="og:image:secure_url" content="' + imageUrl + '">');
|
|
990
|
+
tags.push('<meta property="og:image:type" content="' + AGENT_NATIVE_SOCIAL_IMAGE_TYPE + '">');
|
|
991
|
+
tags.push('<meta property="og:image:width" content="' + AGENT_NATIVE_SOCIAL_IMAGE_WIDTH + '">');
|
|
992
|
+
tags.push('<meta property="og:image:height" content="' + AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT + '">');
|
|
993
|
+
tags.push('<meta property="og:image:alt" content="' + AGENT_NATIVE_SOCIAL_IMAGE_ALT + '">');
|
|
994
|
+
}
|
|
995
|
+
if (!TWITTER_CARD_META_RE.test(html)) {
|
|
996
|
+
tags.push('<meta name="twitter:card" content="summary_large_image">');
|
|
997
|
+
}
|
|
998
|
+
if (!hasAnySocialImage) {
|
|
999
|
+
tags.push('<meta name="twitter:image" content="' + imageUrl + '">');
|
|
1000
|
+
tags.push('<meta name="twitter:image:alt" content="' + AGENT_NATIVE_SOCIAL_IMAGE_ALT + '">');
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (tags.length === 0) return html;
|
|
1004
|
+
return html.slice(0, headCloseIdx) + tags.join("") + html.slice(headCloseIdx);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function isSsrHtmlOrDataResponse(headers, status, pathname) {
|
|
1008
|
+
if (status < 200 || status >= 400) return false;
|
|
1009
|
+
const contentType = (headers.get("content-type") || "").toLowerCase();
|
|
1010
|
+
if (contentType.includes("text/html")) return true;
|
|
1011
|
+
return pathname.endsWith(".data") && contentType.includes("text/x-script");
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Apply the SSR cache policy to the response headers.
|
|
1016
|
+
*
|
|
1017
|
+
* SSR IS A PUBLIC, HARD-CDN-CACHED SHELL — SERVED IDENTICALLY TO EVERYONE.
|
|
1018
|
+
* Every SSR HTML / React Router .data response gets the same public
|
|
1019
|
+
* stale-while-revalidate policy for ALL visitors, authenticated or not. The SSR
|
|
1020
|
+
* output is impersonal (the handler never reads the request's session/cookies),
|
|
1021
|
+
* so it is safe to hard-cache one shared copy at the edge. Do NOT reintroduce
|
|
1022
|
+
* per-user / cookie-based cache variation here (no private, no no-store, no
|
|
1023
|
+
* "authenticated then don't cache" branch) — that makes every logged-in
|
|
1024
|
+
* visitor's pages uncacheable. Per-user state is resolved client-side instead.
|
|
1025
|
+
*/
|
|
1026
|
+
function applyDefaultSsrCacheHeader(headers, status, pathname) {
|
|
1027
|
+
if (!isSsrHtmlOrDataResponse(headers, status, pathname)) return;
|
|
1028
|
+
|
|
1029
|
+
headers.set("cache-control", DEFAULT_SSR_CACHE_CONTROL);
|
|
1030
|
+
headers.set("cdn-cache-control", DEFAULT_SSR_CDN_CACHE_CONTROL);
|
|
1031
|
+
// Netlify function responses are dynamic by default and can otherwise show
|
|
1032
|
+
// Cache-Status fwd=bypass even with Cache-Control: public. Keep this
|
|
1033
|
+
// Netlify-specific header so SSR HTML/.data are served from the shared
|
|
1034
|
+
// durable CDN cache instead of stampeding origin — for every visitor.
|
|
1035
|
+
headers.set("netlify-cdn-cache-control", DEFAULT_SSR_NETLIFY_CDN_CACHE_CONTROL);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function applyDefaultSpeculationRulesHeader(headers, status, basePath) {
|
|
1039
|
+
if (status < 200 || status >= 400) return;
|
|
1040
|
+
if (headers.has("speculation-rules")) return;
|
|
1041
|
+
|
|
1042
|
+
const contentType = (headers.get("content-type") || "").toLowerCase();
|
|
1043
|
+
if (!contentType.includes("text/html")) return;
|
|
1044
|
+
|
|
1045
|
+
// Cloudflare Speed Brain injects Speculation-Rules when origin omits this
|
|
1046
|
+
// header. Those browser prefetches carry Sec-Purpose: prefetch and
|
|
1047
|
+
// Cloudflare can return 503 before the request reaches origin. Publish an
|
|
1048
|
+
// explicit no-op ruleset by default; apps can still provide their own header.
|
|
1049
|
+
headers.set("speculation-rules", '"' + prefixMountedPath(DEFAULT_SPECULATION_RULES_PATH, basePath) + '"');
|
|
1050
|
+
}
|
|
1051
|
+
function isImmutableAssetRequest(request) {
|
|
1052
|
+
const pathname = stripAppBasePath(new URL(request.url).pathname);
|
|
1053
|
+
return IMMUTABLE_ASSET_PATHS.has(pathname);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function applyImmutableAssetCacheHeaders(response, request) {
|
|
1057
|
+
if (!isImmutableAssetRequest(request)) return response;
|
|
1058
|
+
if (!((response.status >= 200 && response.status < 300) || response.status === 304)) {
|
|
1059
|
+
return response;
|
|
1060
|
+
}
|
|
1061
|
+
const headers = new Headers(response.headers);
|
|
1062
|
+
headers.set("Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL);
|
|
1063
|
+
headers.set("CDN-Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL);
|
|
1064
|
+
headers.set("Netlify-CDN-Cache-Control", IMMUTABLE_ASSET_CACHE_CONTROL);
|
|
1065
|
+
return new Response(response.body, {
|
|
1066
|
+
status: response.status,
|
|
1067
|
+
statusText: response.statusText,
|
|
1068
|
+
headers,
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
async function rewriteMountedResponse(response, basePath, pathname, request) {
|
|
1073
|
+
const sentryClientConfigScript = getSentryClientConfigScript();
|
|
1074
|
+
const headers = new Headers(response.headers);
|
|
1075
|
+
applyDefaultSsrCacheHeader(headers, response.status, pathname);
|
|
1076
|
+
applyDefaultSpeculationRulesHeader(headers, response.status, basePath);
|
|
1077
|
+
|
|
1078
|
+
const location = headers.get("location");
|
|
1079
|
+
if (location?.startsWith("/") && !location.startsWith("//")) {
|
|
1080
|
+
headers.set("location", prefixMountedPath(location, basePath));
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const contentType = headers.get("content-type") || "";
|
|
1084
|
+
if (!contentType.toLowerCase().includes("text/html") || !response.body) {
|
|
1085
|
+
return new Response(response.body, {
|
|
1086
|
+
status: response.status,
|
|
1087
|
+
statusText: response.statusText,
|
|
1088
|
+
headers,
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const html = await response.text();
|
|
1093
|
+
headers.delete("content-length");
|
|
1094
|
+
return new Response(
|
|
1095
|
+
injectHeadScript(
|
|
1096
|
+
injectDefaultSocialImageMeta(
|
|
1097
|
+
prefixMountedHtml(html, basePath),
|
|
1098
|
+
defaultSocialImageUrl(request, basePath),
|
|
1099
|
+
),
|
|
1100
|
+
sentryClientConfigScript,
|
|
1101
|
+
),
|
|
1102
|
+
{
|
|
1103
|
+
status: response.status,
|
|
1104
|
+
statusText: response.statusText,
|
|
1105
|
+
headers,
|
|
1106
|
+
},
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function requestWithMethod(request, method) {
|
|
1111
|
+
return new Request(request.url, {
|
|
1112
|
+
method,
|
|
1113
|
+
headers: request.headers,
|
|
1114
|
+
signal: request.signal,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function requestWithPathname(request, pathname) {
|
|
1119
|
+
const url = new URL(request.url);
|
|
1120
|
+
if (url.pathname === pathname) return request;
|
|
1121
|
+
url.pathname = pathname;
|
|
1122
|
+
return new Request(url, request);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function isStaticAppShellRequest(request) {
|
|
1126
|
+
if (request.method !== "GET" && request.method !== "HEAD") return false;
|
|
1127
|
+
const p = stripAppBasePath(new URL(request.url).pathname);
|
|
1128
|
+
if (
|
|
1129
|
+
p.startsWith("/.well-known/") ||
|
|
1130
|
+
p.startsWith("/_agent-native/") ||
|
|
1131
|
+
isApiPath(p) ||
|
|
1132
|
+
p === "/favicon.ico" ||
|
|
1133
|
+
p === "/favicon.png" ||
|
|
1134
|
+
/\\.\\w+$/.test(p)
|
|
1135
|
+
) {
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
async function fetchStaticAppShell(request, env) {
|
|
1142
|
+
if (!env?.ASSETS || !isStaticAppShellRequest(request)) return null;
|
|
1143
|
+
const basePath = getAppBasePath();
|
|
1144
|
+
const p = stripAppBasePath(new URL(request.url).pathname);
|
|
1145
|
+
// Workspace deployments serve each app's static files under its base path
|
|
1146
|
+
// (/dispatch/index.html on the unified origin) — an unprefixed
|
|
1147
|
+
// "/index.html" 404s against the shared ASSETS binding and authed deep
|
|
1148
|
+
// links (e.g. /dispatch/overview) leaked h3's JSON 404 instead of the SPA
|
|
1149
|
+
// shell. Try the base-path-prefixed shell first, fall back to the root
|
|
1150
|
+
// shell for single-app deployments.
|
|
1151
|
+
let response;
|
|
1152
|
+
try {
|
|
1153
|
+
if (basePath) {
|
|
1154
|
+
response = await env.ASSETS.fetch(
|
|
1155
|
+
requestWithPathname(
|
|
1156
|
+
requestWithMethod(request, "GET"),
|
|
1157
|
+
basePath + "/index.html",
|
|
1158
|
+
),
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
if (!response || response.status === 404) {
|
|
1162
|
+
response = await env.ASSETS.fetch(
|
|
1163
|
+
requestWithPathname(requestWithMethod(request, "GET"), "/index.html"),
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
} catch {
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
if (response.status === 404) return null;
|
|
1170
|
+
if (request.method === "HEAD") {
|
|
1171
|
+
return rewriteMountedResponse(
|
|
1172
|
+
new Response(null, {
|
|
1173
|
+
status: response.status,
|
|
1174
|
+
statusText: response.statusText,
|
|
1175
|
+
headers: response.headers,
|
|
1176
|
+
}),
|
|
1177
|
+
basePath,
|
|
1178
|
+
p,
|
|
1179
|
+
request,
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
return rewriteMountedResponse(response, basePath, p, request);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// API route handlers
|
|
1186
|
+
${routeImports.join("\n")}
|
|
1187
|
+
|
|
1188
|
+
// Action handlers (auto-discovered from actions/)
|
|
1189
|
+
${actionImports.join("\n")}
|
|
1190
|
+
|
|
1191
|
+
// Server plugins
|
|
1192
|
+
${pluginImports.join("\n")}
|
|
1193
|
+
|
|
1194
|
+
let _handler;
|
|
1195
|
+
|
|
1196
|
+
async function getHandler() {
|
|
1197
|
+
if (_handler) return _handler;
|
|
1198
|
+
|
|
1199
|
+
const app = new H3();
|
|
1200
|
+
|
|
1201
|
+
// Build a fake nitroApp surface so framework plugins (which expect
|
|
1202
|
+
// \`nitroApp.h3["~middleware"]\`) can register routes via getH3App().
|
|
1203
|
+
const noop = () => {};
|
|
1204
|
+
const nitroApp = {
|
|
1205
|
+
h3: app,
|
|
1206
|
+
hooks: { hook: noop, callHook: noop, hookOnce: noop },
|
|
1207
|
+
captureError: noop,
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
// CORS — applied as global middleware via .use(handler)
|
|
1211
|
+
app.use(defineEventHandler((event) => {
|
|
1212
|
+
if (event.req.method === "OPTIONS") {
|
|
1213
|
+
return new Response(null, {
|
|
1214
|
+
status: 204,
|
|
1215
|
+
headers: {
|
|
1216
|
+
"Access-Control-Allow-Origin": "*",
|
|
1217
|
+
"Access-Control-Allow-Methods": "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS",
|
|
1218
|
+
"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",
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}));
|
|
1223
|
+
|
|
1224
|
+
// Run plugins — they call getH3App(nitroApp).use(path, handler) which
|
|
1225
|
+
// pushes path-prefix middleware onto app["~middleware"].
|
|
1226
|
+
// Pre-mark every build-time plugin slot before any plugin awaits the runtime
|
|
1227
|
+
// default bootstrap. Bundled serverless workers often lack server/plugins/
|
|
1228
|
+
// on disk, so runtime discovery would otherwise auto-mount duplicate
|
|
1229
|
+
// framework defaults before later custom plugins get a chance to mark
|
|
1230
|
+
// themselves as provided.
|
|
1231
|
+
${generatedPluginMarks.map((stem) => ` markGeneratedPluginProvided(nitroApp, ${JSON.stringify(stem)});`).join("\n")}
|
|
1232
|
+
${pluginCalls.join("\n")}
|
|
1233
|
+
|
|
1234
|
+
// Register API routes
|
|
1235
|
+
${routeRegistrations.join("\n")}
|
|
1236
|
+
|
|
1237
|
+
// Register action routes (/_agent-native/actions/*)
|
|
1238
|
+
${actionRegistrations.join("\n")}
|
|
1239
|
+
|
|
1147
1240
|
${includeReactRouterSsr
|
|
1148
|
-
? ` // SSR catch-all for React Router
|
|
1149
|
-
const rrHandler = createRequestHandler(() => serverBuild);
|
|
1150
|
-
app.all("/**", defineEventHandler(async (event) => {
|
|
1151
|
-
const basePath = getAppBasePath();
|
|
1152
|
-
const p = stripAppBasePath(new URL(event.req.url).pathname);
|
|
1153
|
-
if (
|
|
1154
|
-
p.startsWith("/.well-known/") ||
|
|
1155
|
-
p.startsWith("/_agent-native/") ||
|
|
1156
|
-
isApiPath(p) ||
|
|
1157
|
-
p === "/favicon.ico" ||
|
|
1158
|
-
p === "/favicon.png" ||
|
|
1159
|
-
(/\\.\\w+$/.test(p) && !p.endsWith(".data"))
|
|
1160
|
-
) {
|
|
1161
|
-
return new Response(null, { status: 404 });
|
|
1162
|
-
}
|
|
1163
|
-
const request = requestWithPathname(event.req, p);
|
|
1164
|
-
if (event.req.method === "HEAD") {
|
|
1165
|
-
const getRequest = requestWithMethod(request, "GET");
|
|
1166
|
-
const response = await rrHandler(getRequest);
|
|
1167
|
-
return rewriteMountedResponse(
|
|
1168
|
-
new Response(null, {
|
|
1169
|
-
status: response.status,
|
|
1170
|
-
statusText: response.statusText,
|
|
1171
|
-
headers: response.headers,
|
|
1172
|
-
}),
|
|
1173
|
-
basePath,
|
|
1174
|
-
p,
|
|
1175
|
-
getRequest
|
|
1176
|
-
);
|
|
1177
|
-
}
|
|
1178
|
-
return rewriteMountedResponse(await rrHandler(request), basePath, p, request);
|
|
1241
|
+
? ` // SSR catch-all for React Router
|
|
1242
|
+
const rrHandler = createRequestHandler(() => serverBuild);
|
|
1243
|
+
app.all("/**", defineEventHandler(async (event) => {
|
|
1244
|
+
const basePath = getAppBasePath();
|
|
1245
|
+
const p = stripAppBasePath(new URL(event.req.url).pathname);
|
|
1246
|
+
if (
|
|
1247
|
+
p.startsWith("/.well-known/") ||
|
|
1248
|
+
p.startsWith("/_agent-native/") ||
|
|
1249
|
+
isApiPath(p) ||
|
|
1250
|
+
p === "/favicon.ico" ||
|
|
1251
|
+
p === "/favicon.png" ||
|
|
1252
|
+
(/\\.\\w+$/.test(p) && !p.endsWith(".data"))
|
|
1253
|
+
) {
|
|
1254
|
+
return new Response(null, { status: 404 });
|
|
1255
|
+
}
|
|
1256
|
+
const request = requestWithPathname(event.req, p);
|
|
1257
|
+
if (event.req.method === "HEAD") {
|
|
1258
|
+
const getRequest = requestWithMethod(request, "GET");
|
|
1259
|
+
const response = await rrHandler(getRequest);
|
|
1260
|
+
return rewriteMountedResponse(
|
|
1261
|
+
new Response(null, {
|
|
1262
|
+
status: response.status,
|
|
1263
|
+
statusText: response.statusText,
|
|
1264
|
+
headers: response.headers,
|
|
1265
|
+
}),
|
|
1266
|
+
basePath,
|
|
1267
|
+
p,
|
|
1268
|
+
getRequest
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
return rewriteMountedResponse(await rrHandler(request), basePath, p, request);
|
|
1179
1272
|
}));`
|
|
1180
|
-
: ""}
|
|
1181
|
-
|
|
1182
|
-
_handler = app.fetch.bind(app);
|
|
1183
|
-
return _handler;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
export default {
|
|
1187
|
-
async fetch(request, env, ctx) {
|
|
1188
|
-
// Expose env and ctx bindings globally for compatibility
|
|
1189
|
-
if (ctx) globalThis.__cf_ctx = ctx;
|
|
1190
|
-
if (env) {
|
|
1191
|
-
globalThis.process = globalThis.process || { env: {} };
|
|
1192
|
-
globalThis.process.env = globalThis.process.env || {};
|
|
1193
|
-
// Expose D1/KV/R2 bindings on globalThis.__cf_env for the db layer
|
|
1194
|
-
globalThis.__cf_env = env;
|
|
1195
|
-
for (const [key, value] of Object.entries(env)) {
|
|
1196
|
-
if (typeof value === "string") {
|
|
1197
|
-
globalThis.process.env[key] = value;
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
// Try serving static assets first (CF Pages advanced mode).
|
|
1203
|
-
// Only attempt this for GET/HEAD — the ASSETS binding is a static file
|
|
1204
|
-
// server and returns 405 for any other method, which would short-circuit
|
|
1205
|
-
// API calls (PUT/POST/DELETE to /_agent-native/*) before they reach our
|
|
1206
|
-
// h3 middleware.
|
|
1207
|
-
if (env?.ASSETS && (request.method === "GET" || request.method === "HEAD")) {
|
|
1208
|
-
try {
|
|
1209
|
-
const assetResponse = await env.ASSETS.fetch(request);
|
|
1210
|
-
if (assetResponse.status !== 404) {
|
|
1211
|
-
return applyImmutableAssetCacheHeaders(assetResponse, request);
|
|
1212
|
-
}
|
|
1213
|
-
} catch {
|
|
1214
|
-
// Asset fetch failed — fall through to SSR
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
const handler = await getHandler();
|
|
1219
|
-
const response = await handler(requestWithMountedApiPrefixStripped(request));
|
|
1273
|
+
: ""}
|
|
1274
|
+
|
|
1275
|
+
_handler = app.fetch.bind(app);
|
|
1276
|
+
return _handler;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
export default {
|
|
1280
|
+
async fetch(request, env, ctx) {
|
|
1281
|
+
// Expose env and ctx bindings globally for compatibility
|
|
1282
|
+
if (ctx) globalThis.__cf_ctx = ctx;
|
|
1283
|
+
if (env) {
|
|
1284
|
+
globalThis.process = globalThis.process || { env: {} };
|
|
1285
|
+
globalThis.process.env = globalThis.process.env || {};
|
|
1286
|
+
// Expose D1/KV/R2 bindings on globalThis.__cf_env for the db layer
|
|
1287
|
+
globalThis.__cf_env = env;
|
|
1288
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1289
|
+
if (typeof value === "string") {
|
|
1290
|
+
globalThis.process.env[key] = value;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Try serving static assets first (CF Pages advanced mode).
|
|
1296
|
+
// Only attempt this for GET/HEAD — the ASSETS binding is a static file
|
|
1297
|
+
// server and returns 405 for any other method, which would short-circuit
|
|
1298
|
+
// API calls (PUT/POST/DELETE to /_agent-native/*) before they reach our
|
|
1299
|
+
// h3 middleware.
|
|
1300
|
+
if (env?.ASSETS && (request.method === "GET" || request.method === "HEAD")) {
|
|
1301
|
+
try {
|
|
1302
|
+
const assetResponse = await env.ASSETS.fetch(request);
|
|
1303
|
+
if (assetResponse.status !== 404) {
|
|
1304
|
+
return applyImmutableAssetCacheHeaders(assetResponse, request);
|
|
1305
|
+
}
|
|
1306
|
+
} catch {
|
|
1307
|
+
// Asset fetch failed — fall through to SSR
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const handler = await getHandler();
|
|
1312
|
+
const response = await handler(requestWithMountedApiPrefixStripped(request));
|
|
1220
1313
|
${includeReactRouterSsr
|
|
1221
1314
|
? " return response;"
|
|
1222
|
-
: ` if (response.status === 404) {
|
|
1223
|
-
const shellResponse = await fetchStaticAppShell(request, env);
|
|
1224
|
-
if (shellResponse) return shellResponse;
|
|
1225
|
-
}
|
|
1226
|
-
return response;`}
|
|
1227
|
-
}
|
|
1228
|
-
};
|
|
1315
|
+
: ` if (response.status === 404) {
|
|
1316
|
+
const shellResponse = await fetchStaticAppShell(request, env);
|
|
1317
|
+
if (shellResponse) return shellResponse;
|
|
1318
|
+
}
|
|
1319
|
+
return response;`}
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1229
1322
|
`;
|
|
1230
1323
|
}
|
|
1231
1324
|
function escapeHtmlAttribute(value) {
|
|
@@ -1377,35 +1470,35 @@ function writeCloudflarePagesStaticShell({ serverDir, distDir, tmpDir, }) {
|
|
|
1377
1470
|
const outFile = path.join(distDir, "index.html");
|
|
1378
1471
|
const renderScript = path.join(tmpDir, "render-cloudflare-static-shell.mjs");
|
|
1379
1472
|
const basePath = normalizeConfiguredAppBasePath();
|
|
1380
|
-
fs.writeFileSync(renderScript, `
|
|
1381
|
-
import fs from "node:fs";
|
|
1382
|
-
import { createRequire } from "node:module";
|
|
1383
|
-
import { pathToFileURL } from "node:url";
|
|
1384
|
-
|
|
1385
|
-
const cwd = ${JSON.stringify(cwd)};
|
|
1386
|
-
const serverEntry = ${JSON.stringify(serverEntry)};
|
|
1387
|
-
const outFile = ${JSON.stringify(outFile)};
|
|
1388
|
-
const basePath = ${JSON.stringify(basePath)};
|
|
1389
|
-
|
|
1390
|
-
const requireFromApp = createRequire(cwd + "/package.json");
|
|
1391
|
-
const reactRouterEntry = requireFromApp.resolve("react-router");
|
|
1392
|
-
const { createRequestHandler } = await import(pathToFileURL(reactRouterEntry).href);
|
|
1393
|
-
const serverBuild = await import(pathToFileURL(serverEntry).href);
|
|
1394
|
-
const handler = createRequestHandler(serverBuild, "production");
|
|
1395
|
-
const pathname = basePath ? basePath + "/" : "/";
|
|
1396
|
-
const response = await handler(
|
|
1397
|
-
new Request(new URL(pathname, "https://agent-native.local"), {
|
|
1398
|
-
headers: { "X-React-Router-SPA-Mode": "yes" },
|
|
1399
|
-
}),
|
|
1400
|
-
);
|
|
1401
|
-
const html = await response.text();
|
|
1402
|
-
|
|
1403
|
-
if (!html || !html.includes("__reactRouterContext") || !html.includes("entry.client")) {
|
|
1404
|
-
throw new Error("React Router did not render a usable Cloudflare Pages static shell");
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
fs.writeFileSync(outFile, html);
|
|
1408
|
-
process.exit(0);
|
|
1473
|
+
fs.writeFileSync(renderScript, `
|
|
1474
|
+
import fs from "node:fs";
|
|
1475
|
+
import { createRequire } from "node:module";
|
|
1476
|
+
import { pathToFileURL } from "node:url";
|
|
1477
|
+
|
|
1478
|
+
const cwd = ${JSON.stringify(cwd)};
|
|
1479
|
+
const serverEntry = ${JSON.stringify(serverEntry)};
|
|
1480
|
+
const outFile = ${JSON.stringify(outFile)};
|
|
1481
|
+
const basePath = ${JSON.stringify(basePath)};
|
|
1482
|
+
|
|
1483
|
+
const requireFromApp = createRequire(cwd + "/package.json");
|
|
1484
|
+
const reactRouterEntry = requireFromApp.resolve("react-router");
|
|
1485
|
+
const { createRequestHandler } = await import(pathToFileURL(reactRouterEntry).href);
|
|
1486
|
+
const serverBuild = await import(pathToFileURL(serverEntry).href);
|
|
1487
|
+
const handler = createRequestHandler(serverBuild, "production");
|
|
1488
|
+
const pathname = basePath ? basePath + "/" : "/";
|
|
1489
|
+
const response = await handler(
|
|
1490
|
+
new Request(new URL(pathname, "https://agent-native.local"), {
|
|
1491
|
+
headers: { "X-React-Router-SPA-Mode": "yes" },
|
|
1492
|
+
}),
|
|
1493
|
+
);
|
|
1494
|
+
const html = await response.text();
|
|
1495
|
+
|
|
1496
|
+
if (!html || !html.includes("__reactRouterContext") || !html.includes("entry.client")) {
|
|
1497
|
+
throw new Error("React Router did not render a usable Cloudflare Pages static shell");
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
fs.writeFileSync(outFile, html);
|
|
1501
|
+
process.exit(0);
|
|
1409
1502
|
`);
|
|
1410
1503
|
try {
|
|
1411
1504
|
execFileSync(process.execPath, [renderScript], {
|
|
@@ -1506,7 +1599,7 @@ async function buildCloudflarePages() {
|
|
|
1506
1599
|
const tmpEntry = path.join(tmpDir, "index.js");
|
|
1507
1600
|
fs.writeFileSync(tmpEntry, adjustedEntry);
|
|
1508
1601
|
if (appScopeId) {
|
|
1509
|
-
fs.writeFileSync(path.join(tmpDir, "_scope-init.js"), generateScopeInitSource(appScopeId));
|
|
1602
|
+
fs.writeFileSync(path.join(tmpDir, "_scope-init.js"), generateScopeInitSource(appScopeId, workspaceEnvDefaultsFromBuildEnv(), workspaceAppModuleGraphEnvDefaultsFromBuildEnv()));
|
|
1510
1603
|
}
|
|
1511
1604
|
if (includeReactRouterSsr) {
|
|
1512
1605
|
copyDir(serverDir, path.join(tmpDir, "server"));
|
|
@@ -2191,76 +2284,76 @@ export function emitSingleTemplateNetlifyBackgroundFunction(projectCwd) {
|
|
|
2191
2284
|
// copied bundle does NOT re-register the catch-all `config.path`.
|
|
2192
2285
|
fs.rmSync(path.join(dest, "server.mjs"), { force: true });
|
|
2193
2286
|
const processRunPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
|
|
2194
|
-
const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
|
|
2195
|
-
// bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
|
|
2196
|
-
// in this function. The deployed Lambda name is NOT guaranteed to end in
|
|
2197
|
-
// "-background" (Netlify may mangle/prefix it), so we cannot depend on
|
|
2198
|
-
// AWS_LAMBDA_FUNCTION_NAME alone. A globalThis flag (NOT process.env) avoids the
|
|
2199
|
-
// no-env-mutation guard and carries no cross-request state — it is a static,
|
|
2200
|
-
// set-once isolate marker read back by isInBackgroundFunctionRuntime().
|
|
2201
|
-
globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
|
|
2202
|
-
|
|
2203
|
-
// The framework route the Nitro router dispatches to (the _process-run plugin).
|
|
2204
|
-
const PROCESS_RUN_PATH = ${processRunPath};
|
|
2205
|
-
|
|
2206
|
-
let cachedHandler;
|
|
2207
|
-
|
|
2208
|
-
// Netlify v2 invokes this as (request, context). The Nitro netlify handler is a
|
|
2209
|
-
// Web-standard \`async (Request) => Response\` (see nitro/presets/netlify/runtime).
|
|
2210
|
-
// This function declares NO custom \`config.path\`, so it is reached at its
|
|
2211
|
-
// DEFAULT url (/.netlify/functions/${backgroundName}). The Nitro router only
|
|
2212
|
-
// knows the framework route, so we REWRITE the incoming pathname to
|
|
2213
|
-
// PROCESS_RUN_PATH before delegating. Method, ALL headers (the HMAC
|
|
2214
|
-
// Authorization: Bearer MUST survive — the plugin verifies it) and the body are
|
|
2215
|
-
// preserved by cloning the incoming Request with only its URL pathname set.
|
|
2216
|
-
export default async function handler(request, context) {
|
|
2217
|
-
try {
|
|
2218
|
-
cachedHandler ??= (await import("./main.mjs")).default;
|
|
2219
|
-
const url = new URL(request.url);
|
|
2220
|
-
url.pathname = PROCESS_RUN_PATH;
|
|
2221
|
-
// Read the body once and pass it through. GET/HEAD have no body.
|
|
2222
|
-
const method = request.method || "POST";
|
|
2223
|
-
const hasBody = method !== "GET" && method !== "HEAD";
|
|
2224
|
-
const body = hasBody ? await request.text() : undefined;
|
|
2225
|
-
const rewritten = new Request(url.toString(), {
|
|
2226
|
-
method,
|
|
2227
|
-
headers: request.headers,
|
|
2228
|
-
body,
|
|
2229
|
-
});
|
|
2230
|
-
// Netlify Functions v2 invokes the handler as (request, context); the Nitro
|
|
2231
|
-
// netlify handler accepts (request[, context]). Pass context through so a
|
|
2232
|
-
// handler that uses it (e.g. waitUntil) does not trip over an undefined arg
|
|
2233
|
-
// before it ever routes the request.
|
|
2234
|
-
return await cachedHandler(rewritten, context);
|
|
2235
|
-
} catch (err) {
|
|
2236
|
-
// Netlify already returned 202 for this background invocation and DISCARDS
|
|
2237
|
-
// this return, so a throw here is otherwise INVISIBLE — it would only surface
|
|
2238
|
-
// downstream as the reaper's "worker never claimed the run". Log it loudly
|
|
2239
|
-
// for the function log; the FOREGROUND circuit-breaker (production-agent.ts)
|
|
2240
|
-
// is what recovers the run by executing it inline when no worker claims.
|
|
2241
|
-
console.error(
|
|
2242
|
-
"[agent-background] wrapper failed before reaching the route:",
|
|
2243
|
-
(err && err.stack) || err,
|
|
2244
|
-
);
|
|
2245
|
-
throw err;
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
export const config = {
|
|
2250
|
-
name: "agent background handler",
|
|
2251
|
-
generator: "agent-native build",
|
|
2252
|
-
// background: true makes Netlify invoke this ASYNCHRONOUSLY (immediate HTTP
|
|
2253
|
-
// 202 ack) with the 15-minute budget (Netlify docs:
|
|
2254
|
-
// build/functions/background-functions + build/functions/api). We declare NO
|
|
2255
|
-
// custom path, so the function keeps its DEFAULT url
|
|
2256
|
-
// /.netlify/functions/${backgroundName}; the Nitro \`server\` /* catch-all
|
|
2257
|
-
// already excludes /.netlify/* so that default url is never shadowed by the
|
|
2258
|
-
// synchronous function. The foreground dispatches to that default url.
|
|
2259
|
-
background: true,
|
|
2260
|
-
nodeBundler: "none",
|
|
2261
|
-
includedFiles: ["**"],
|
|
2262
|
-
preferStatic: false,
|
|
2263
|
-
};
|
|
2287
|
+
const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
|
|
2288
|
+
// bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
|
|
2289
|
+
// in this function. The deployed Lambda name is NOT guaranteed to end in
|
|
2290
|
+
// "-background" (Netlify may mangle/prefix it), so we cannot depend on
|
|
2291
|
+
// AWS_LAMBDA_FUNCTION_NAME alone. A globalThis flag (NOT process.env) avoids the
|
|
2292
|
+
// no-env-mutation guard and carries no cross-request state — it is a static,
|
|
2293
|
+
// set-once isolate marker read back by isInBackgroundFunctionRuntime().
|
|
2294
|
+
globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
|
|
2295
|
+
|
|
2296
|
+
// The framework route the Nitro router dispatches to (the _process-run plugin).
|
|
2297
|
+
const PROCESS_RUN_PATH = ${processRunPath};
|
|
2298
|
+
|
|
2299
|
+
let cachedHandler;
|
|
2300
|
+
|
|
2301
|
+
// Netlify v2 invokes this as (request, context). The Nitro netlify handler is a
|
|
2302
|
+
// Web-standard \`async (Request) => Response\` (see nitro/presets/netlify/runtime).
|
|
2303
|
+
// This function declares NO custom \`config.path\`, so it is reached at its
|
|
2304
|
+
// DEFAULT url (/.netlify/functions/${backgroundName}). The Nitro router only
|
|
2305
|
+
// knows the framework route, so we REWRITE the incoming pathname to
|
|
2306
|
+
// PROCESS_RUN_PATH before delegating. Method, ALL headers (the HMAC
|
|
2307
|
+
// Authorization: Bearer MUST survive — the plugin verifies it) and the body are
|
|
2308
|
+
// preserved by cloning the incoming Request with only its URL pathname set.
|
|
2309
|
+
export default async function handler(request, context) {
|
|
2310
|
+
try {
|
|
2311
|
+
cachedHandler ??= (await import("./main.mjs")).default;
|
|
2312
|
+
const url = new URL(request.url);
|
|
2313
|
+
url.pathname = PROCESS_RUN_PATH;
|
|
2314
|
+
// Read the body once and pass it through. GET/HEAD have no body.
|
|
2315
|
+
const method = request.method || "POST";
|
|
2316
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
2317
|
+
const body = hasBody ? await request.text() : undefined;
|
|
2318
|
+
const rewritten = new Request(url.toString(), {
|
|
2319
|
+
method,
|
|
2320
|
+
headers: request.headers,
|
|
2321
|
+
body,
|
|
2322
|
+
});
|
|
2323
|
+
// Netlify Functions v2 invokes the handler as (request, context); the Nitro
|
|
2324
|
+
// netlify handler accepts (request[, context]). Pass context through so a
|
|
2325
|
+
// handler that uses it (e.g. waitUntil) does not trip over an undefined arg
|
|
2326
|
+
// before it ever routes the request.
|
|
2327
|
+
return await cachedHandler(rewritten, context);
|
|
2328
|
+
} catch (err) {
|
|
2329
|
+
// Netlify already returned 202 for this background invocation and DISCARDS
|
|
2330
|
+
// this return, so a throw here is otherwise INVISIBLE — it would only surface
|
|
2331
|
+
// downstream as the reaper's "worker never claimed the run". Log it loudly
|
|
2332
|
+
// for the function log; the FOREGROUND circuit-breaker (production-agent.ts)
|
|
2333
|
+
// is what recovers the run by executing it inline when no worker claims.
|
|
2334
|
+
console.error(
|
|
2335
|
+
"[agent-background] wrapper failed before reaching the route:",
|
|
2336
|
+
(err && err.stack) || err,
|
|
2337
|
+
);
|
|
2338
|
+
throw err;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
export const config = {
|
|
2343
|
+
name: "agent background handler",
|
|
2344
|
+
generator: "agent-native build",
|
|
2345
|
+
// background: true makes Netlify invoke this ASYNCHRONOUSLY (immediate HTTP
|
|
2346
|
+
// 202 ack) with the 15-minute budget (Netlify docs:
|
|
2347
|
+
// build/functions/background-functions + build/functions/api). We declare NO
|
|
2348
|
+
// custom path, so the function keeps its DEFAULT url
|
|
2349
|
+
// /.netlify/functions/${backgroundName}; the Nitro \`server\` /* catch-all
|
|
2350
|
+
// already excludes /.netlify/* so that default url is never shadowed by the
|
|
2351
|
+
// synchronous function. The foreground dispatches to that default url.
|
|
2352
|
+
background: true,
|
|
2353
|
+
nodeBundler: "none",
|
|
2354
|
+
includedFiles: ["**"],
|
|
2355
|
+
preferStatic: false,
|
|
2356
|
+
};
|
|
2264
2357
|
`;
|
|
2265
2358
|
fs.writeFileSync(path.join(dest, `${backgroundName}.mjs`), entry);
|
|
2266
2359
|
console.log(`[build] Emitted durable-background function "${backgroundName}" into the ` +
|
|
@@ -2984,11 +3077,11 @@ async function buildWithNitro() {
|
|
|
2984
3077
|
: null;
|
|
2985
3078
|
const agentsBundleModuleSource = () => {
|
|
2986
3079
|
const bundle = readAgentsBundleFromFs(cwd, nitroWorkspaceSource);
|
|
2987
|
-
return `// AUTO-GENERATED by @agent-native/core deploy build (Nitro virtual)
|
|
2988
|
-
// Contains the inlined AGENTS.md + .agents/skills/ content from the template,
|
|
2989
|
-
// merged with the workspace core's AGENTS.md + skills/ when present.
|
|
2990
|
-
const bundle = ${JSON.stringify(bundle)};
|
|
2991
|
-
export default bundle;
|
|
3080
|
+
return `// AUTO-GENERATED by @agent-native/core deploy build (Nitro virtual)
|
|
3081
|
+
// Contains the inlined AGENTS.md + .agents/skills/ content from the template,
|
|
3082
|
+
// merged with the workspace core's AGENTS.md + skills/ when present.
|
|
3083
|
+
const bundle = ${JSON.stringify(bundle)};
|
|
3084
|
+
export default bundle;
|
|
2992
3085
|
`;
|
|
2993
3086
|
};
|
|
2994
3087
|
// Path aliases used by templates (mirrors tsconfig + Vite config). Nitro
|