handoff-app 2.0.0-8 → 2.0.0
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/{Changelog.md → CHANGELOG.md} +85 -1
- package/{License.md → LICENSE.md} +1 -1
- package/README.md +816 -393
- package/RECIPES.md +975 -0
- package/UPGRADE.md +57 -9
- package/components.json +20 -20
- package/config/docs/foundations/colors/guidelines.md +0 -1
- package/config/docs/foundations/colors.md +0 -1
- package/config/docs/foundations/effects/guidelines.md +0 -1
- package/config/docs/foundations/effects/resources.md +0 -1
- package/config/docs/foundations/effects.md +0 -1
- package/config/docs/foundations/fonts.md +0 -1
- package/config/docs/foundations/grid.md +0 -1
- package/config/docs/foundations/icon.md +0 -1
- package/config/docs/foundations/icons/guidelines.md +0 -1
- package/config/docs/foundations/icons.md +0 -1
- package/config/docs/foundations/images/guidelines.md +0 -1
- package/config/docs/foundations/images/resources.md +0 -1
- package/config/docs/foundations/images.md +0 -1
- package/config/docs/foundations/logo/resources.md +0 -1
- package/config/docs/foundations/logo.md +0 -1
- package/config/docs/foundations/typography/guidelines.md +0 -1
- package/config/docs/foundations/typography/resources.md +0 -1
- package/config/docs/foundations/typography.md +0 -1
- package/config/docs/foundations.md +0 -1
- package/config/docs/guidelines.md +0 -1
- package/config/docs/index.md +0 -1
- package/config/docs/system/tokens/foundations/colors.md +0 -4
- package/config/docs/system/tokens/foundations/effects.md +0 -4
- package/config/docs/system/tokens/foundations/typography.md +0 -2
- package/config/docs/system.md +0 -2
- package/config/templates/page.md +2 -1
- package/config/templates/page.mdx +2 -1
- package/dist/ai/connections.d.ts +66 -0
- package/dist/ai/connections.js +149 -0
- package/dist/ai/define.d.ts +10 -0
- package/dist/ai/define.js +13 -0
- package/dist/ai/types.d.ts +26 -0
- package/dist/ai/types.js +10 -0
- package/dist/app-builder/build.d.ts +14 -7
- package/dist/app-builder/build.js +195 -66
- package/dist/app-builder/client-config.js +3 -2
- package/dist/app-builder/paths.d.ts +0 -12
- package/dist/app-builder/paths.js +3 -20
- package/dist/app-builder/static-export.js +1 -1
- package/dist/app-builder/vercel-output.d.ts +1 -1
- package/dist/app-builder/vercel-output.js +5 -4
- package/dist/cache/build-cache.d.ts +27 -4
- package/dist/cache/build-cache.js +114 -47
- package/dist/cache/build-inputs.d.ts +13 -0
- package/dist/cache/build-inputs.js +19 -0
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.js +5 -1
- package/dist/catalog/normalize.js +3 -0
- package/dist/catalog/previews.js +5 -4
- package/dist/catalog/types.d.ts +8 -0
- package/dist/cli/create.js +1 -2
- package/dist/commands/build/index.d.ts +1 -1
- package/dist/commands/build/index.js +1 -1
- package/dist/commands/utils.d.ts +3 -3
- package/dist/config/defaults.js +7 -4
- package/dist/config/from-env.d.ts +4 -0
- package/dist/config/from-env.js +38 -1
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +0 -1
- package/dist/config/index.js +1 -4
- package/dist/config/loader.js +28 -2
- package/dist/config/resolve-env.d.ts +2 -1
- package/dist/config/resolve-env.js +28 -12
- package/dist/config/runtime.js +9 -11
- package/dist/handlebars.d.ts +10 -2
- package/dist/index.d.ts +12 -7
- package/dist/index.js +18 -22
- package/dist/nav/index.d.ts +2 -2
- package/dist/nav/registry.d.ts +3 -3
- package/dist/nav/registry.js +1 -1
- package/dist/nav/workspace.d.ts +3 -3
- package/dist/nav/workspace.js +1 -1
- package/dist/pipeline/figma.d.ts +13 -2
- package/dist/pipeline/figma.js +86 -46
- package/dist/pipeline/styles.js +1 -1
- package/dist/pipeline/validation.d.ts +1 -1
- package/dist/pipeline/validation.js +3 -3
- package/dist/providers.d.ts +10 -0
- package/dist/providers.js +13 -0
- package/dist/registry/asset-storage/define.d.ts +5 -5
- package/dist/registry/asset-storage/define.js +4 -4
- package/dist/registry/asset-storage/resolve.d.ts +9 -19
- package/dist/registry/asset-storage/resolve.js +33 -21
- package/dist/registry/asset-storage/types.d.ts +37 -17
- package/dist/registry/asset-storage/types.js +5 -5
- package/dist/registry/auth/ai-keys.d.ts +32 -0
- package/dist/registry/auth/ai-keys.js +69 -0
- package/dist/registry/auth/crypto.d.ts +17 -0
- package/dist/registry/auth/crypto.js +61 -1
- package/dist/registry/auth/index.d.ts +2 -1
- package/dist/registry/auth/index.js +11 -2
- package/dist/registry/checkout/index.d.ts +2 -1
- package/dist/registry/checkout/index.js +57 -127
- package/dist/registry/client.js +53 -17
- package/dist/registry/db/driver.js +3 -3
- package/dist/registry/db/schema.d.ts +261 -3
- package/dist/registry/db/schema.js +29 -4
- package/dist/registry/email/define.d.ts +10 -0
- package/dist/registry/email/define.js +13 -0
- package/dist/registry/email/providers/smtp.d.ts +10 -0
- package/dist/registry/email/providers/smtp.js +73 -0
- package/dist/registry/email/resolve.d.ts +28 -0
- package/dist/registry/email/resolve.js +74 -0
- package/dist/registry/email/types.d.ts +24 -0
- package/dist/registry/email/types.js +6 -0
- package/dist/registry/path.d.ts +2 -0
- package/dist/registry/path.js +11 -1
- package/dist/registry/publish/package.js +8 -0
- package/dist/registry/transfer.d.ts +4 -0
- package/dist/store/registry.d.ts +3 -3
- package/dist/store/registry.js +4 -4
- package/dist/transformers/plugins/ssr-render.js +40 -15
- package/dist/transformers/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +17 -17
- package/dist/transformers/preview/component/css.d.ts +3 -1
- package/dist/transformers/preview/component/css.js +92 -51
- package/dist/transformers/preview/component/html.d.ts +3 -1
- package/dist/transformers/preview/component/html.js +6 -2
- package/dist/transformers/preview/component/javascript.d.ts +2 -1
- package/dist/transformers/preview/component/javascript.js +10 -4
- package/dist/transformers/preview/pattern/html.js +3 -1
- package/dist/transformers/preview/types.d.ts +13 -0
- package/dist/transformers/utils/build.d.ts +24 -0
- package/dist/transformers/utils/build.js +100 -1
- package/dist/transformers/utils/module.js +2 -1
- package/dist/types/config.d.ts +135 -18
- package/dist/utils/path.d.ts +1 -5
- package/dist/utils/path.js +6 -23
- package/docs/api.md +6 -4
- package/docs/cli.md +275 -54
- package/drizzle/0007_add_ai_keys.sql +11 -0
- package/drizzle/0008_add_source_dir.sql +2 -0
- package/drizzle/meta/0007_snapshot.json +2013 -0
- package/drizzle/meta/0008_snapshot.json +2025 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +19 -8
- package/src/ai/README.md +42 -0
- package/src/ai/connections.ts +189 -0
- package/src/ai/define.ts +15 -0
- package/src/ai/types.ts +30 -0
- package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
- package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
- package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
- package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
- package/src/app/components/AiAssistant/dockState.ts +86 -0
- package/src/app/components/AiAssistant/toolLabel.ts +24 -0
- package/src/app/components/AiAssistant/useAiChat.ts +147 -0
- package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
- package/src/app/components/AiAssistant/usePageContext.ts +63 -0
- package/src/app/components/Auth/AccountLayout.tsx +58 -19
- package/src/app/components/Auth/AuthShell.tsx +27 -33
- package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
- package/src/app/components/Auth/installStatus.ts +29 -0
- package/src/app/components/Component/PageSliceResolver.tsx +1 -0
- package/src/app/components/Component/Preview.tsx +383 -189
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
- package/src/app/components/Foundations/ColorGrid.tsx +13 -14
- package/src/app/components/Layout/Header.tsx +21 -15
- package/src/app/components/Layout/Main.tsx +2 -2
- package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
- package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
- package/src/app/components/Navigation/MobileNav.tsx +2 -2
- package/src/app/components/Navigation/SideNav.tsx +1 -15
- package/src/app/components/context/NavProvider.tsx +7 -3
- package/src/app/components/ui/attachment.tsx +50 -0
- package/src/app/components/ui/bubble.tsx +57 -0
- package/src/app/components/ui/button.tsx +3 -1
- package/src/app/components/ui/confirm-dialog.tsx +87 -0
- package/src/app/components/ui/dialog.tsx +7 -2
- package/src/app/components/ui/marker.tsx +56 -0
- package/src/app/components/ui/sidebar.tsx +3 -1
- package/src/app/components/util/index.ts +3 -12
- package/src/app/css/index.css +64 -0
- package/src/app/lib/ai/auth.ts +33 -0
- package/src/app/lib/ai/context.ts +99 -0
- package/src/app/lib/ai/model.ts +79 -0
- package/src/app/lib/ai/prompt.ts +36 -0
- package/src/app/lib/ai/resolve.ts +97 -0
- package/src/app/lib/ai/tools.ts +120 -0
- package/src/app/lib/api/methods.ts +13 -0
- package/src/app/lib/asset-storage.ts +35 -36
- package/src/app/lib/auth/api.ts +4 -12
- package/src/app/lib/auth/email.ts +96 -23
- package/src/app/lib/docs-api/page-rendering.ts +1 -1
- package/src/app/lib/docs-api/registry.ts +2 -3
- package/src/app/lib/docs-api/runtime-config.ts +47 -21
- package/src/app/lib/mcp/components.ts +523 -0
- package/src/app/lib/mcp/page-url.ts +24 -0
- package/src/app/lib/mcp/pages.ts +84 -0
- package/src/app/lib/mcp/result.ts +33 -0
- package/src/app/lib/mcp/server.ts +39 -318
- package/src/app/lib/mcp/tokens.ts +334 -0
- package/src/app/lib/mcp/tool.ts +19 -0
- package/src/app/lib/registry-api/asset-transfer.ts +119 -11
- package/src/app/lib/registry-api/errors.ts +2 -0
- package/src/app/lib/registry-api/handler.ts +7 -1
- package/src/app/lib/registry-api/transfer.ts +28 -6
- package/src/app/lib/server-module.ts +34 -0
- package/src/app/lib/utils.ts +20 -0
- package/src/app/next.config.mjs +26 -24
- package/src/app/pages/[...slug]/index.tsx +2 -1
- package/src/app/pages/_app.tsx +6 -2
- package/src/app/pages/_document.tsx +6 -0
- package/src/app/pages/account/ai.tsx +183 -0
- package/src/app/pages/account/index.tsx +68 -68
- package/src/app/pages/account/tokens.tsx +37 -7
- package/src/app/pages/account/users.tsx +75 -15
- package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
- package/src/app/pages/api/account/ai/keys/index.ts +20 -0
- package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
- package/src/app/pages/api/admin/users/index.ts +2 -2
- package/src/app/pages/api/ai/chat.ts +115 -0
- package/src/app/pages/api/ai/connections.ts +23 -0
- package/src/app/pages/api/auth/request-reset.ts +14 -16
- package/src/app/pages/api/docs/nav.json.ts +1 -1
- package/src/app/pages/api/install.ts +3 -3
- package/src/app/pages/api/mcp.ts +6 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
- package/src/app/pages/cli/device.tsx +10 -5
- package/src/app/pages/foundations/colors.tsx +9 -8
- package/src/app/pages/foundations/index.tsx +3 -3
- package/src/app/pages/index.tsx +2 -2
- package/src/app/pages/install.tsx +7 -2
- package/src/app/pages/login.tsx +32 -27
- package/src/app/pages/reset-password.tsx +21 -20
- package/src/app/pages/system/component/[component]/index.tsx +2 -5
- package/src/app/pages/system/index.tsx +11 -11
- package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
- package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
- package/src/app-builder/README.md +2 -1
- package/src/app-builder/build.ts +218 -73
- package/src/app-builder/client-config.ts +3 -2
- package/src/app-builder/paths.ts +3 -22
- package/src/app-builder/static-export.ts +1 -1
- package/src/app-builder/vercel-output.ts +5 -4
- package/src/cache/README.md +4 -1
- package/src/cache/build-cache.ts +128 -43
- package/src/cache/build-inputs.ts +28 -0
- package/src/cache/index.ts +4 -0
- package/src/catalog/README.md +3 -0
- package/src/catalog/normalize.ts +2 -0
- package/src/catalog/previews.ts +6 -3
- package/src/catalog/types.ts +8 -0
- package/src/cli/create.ts +1 -2
- package/src/commands/build/index.ts +1 -1
- package/src/config/README.md +10 -9
- package/src/config/defaults.ts +7 -4
- package/src/config/from-env.ts +33 -0
- package/src/config/helpers.ts +1 -3
- package/src/config/index.ts +0 -3
- package/src/config/loader.ts +27 -3
- package/src/config/resolve-env.ts +28 -15
- package/src/config/runtime.ts +10 -5
- package/src/handlebars.ts +8 -2
- package/src/index.ts +33 -29
- package/src/nav/index.ts +2 -2
- package/src/nav/registry.ts +3 -3
- package/src/nav/workspace.ts +3 -3
- package/src/pipeline/README.md +1 -1
- package/src/pipeline/figma.ts +98 -49
- package/src/pipeline/styles.ts +1 -1
- package/src/pipeline/validation.ts +3 -3
- package/src/providers.ts +18 -0
- package/src/registry/asset-storage/define.ts +6 -6
- package/src/registry/asset-storage/resolve.ts +38 -37
- package/src/registry/asset-storage/types.ts +39 -17
- package/src/registry/auth/ai-keys.ts +62 -0
- package/src/registry/auth/crypto.ts +63 -1
- package/src/registry/auth/index.ts +11 -1
- package/src/registry/checkout/index.ts +60 -138
- package/src/registry/client.ts +60 -20
- package/src/registry/db/driver.ts +2 -2
- package/src/registry/db/schema.ts +49 -22
- package/src/registry/email/define.ts +15 -0
- package/src/registry/email/providers/smtp.ts +39 -0
- package/src/registry/email/resolve.ts +94 -0
- package/src/registry/email/types.ts +28 -0
- package/src/registry/path.ts +11 -0
- package/src/registry/publish/package.ts +9 -0
- package/src/registry/transfer.ts +4 -0
- package/src/store/registry.ts +7 -7
- package/src/transformers/plugins/ssr-render.ts +53 -16
- package/src/transformers/preview/component/builder.ts +19 -25
- package/src/transformers/preview/component/css.ts +101 -53
- package/src/transformers/preview/component/html.ts +6 -2
- package/src/transformers/preview/component/javascript.ts +14 -4
- package/src/transformers/preview/pattern/html.ts +3 -1
- package/src/transformers/preview/types.ts +14 -0
- package/src/transformers/utils/build.ts +93 -0
- package/src/transformers/utils/module.ts +4 -2
- package/src/types/config.ts +148 -20
- package/src/utils/README.md +1 -1
- package/src/utils/path.ts +6 -29
- package/templates/default/handoff.config.js.tpl +10 -2
- package/templates/default/handoff.config.ts.tpl +10 -2
- package/templates/default/handoff.config.with-examples.js.tpl +10 -2
- package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
- package/templates/default/package.json.tpl +2 -2
- package/.editorconfig +0 -19
- package/.eslintrc.json +0 -4
- package/.prettierignore +0 -0
- package/.prettierrc.json +0 -8
- package/.vscode/settings.json +0 -17
- package/dist/config/validator.d.ts +0 -9
- package/dist/config/validator.js +0 -24
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
- package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
- package/src/app/components/old/Header.tsx +0 -89
- package/src/app/lib/mcp/shape.ts +0 -596
- package/src/app/pages/assets/fonts.tsx +0 -129
- package/src/app/pages/assets/index.tsx +0 -91
- package/src/app/pages/assets/logos.tsx +0 -75
- package/src/config/validator.ts +0 -22
- package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
3
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { dynamicTool, jsonSchema, type ToolSet } from 'ai';
|
|
5
|
+
import { createMcpServer } from '../mcp';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The assistant's tools: the MCP server's own tools, reached in process.
|
|
9
|
+
*
|
|
10
|
+
* The chat route connects a client to {@link createMcpServer} over a linked in-memory transport
|
|
11
|
+
* pair rather than calling `/api/mcp/` over HTTP. In registry mode that endpoint demands a
|
|
12
|
+
* `registry:read` bearer token while the browser holds a session cookie, so the server would have
|
|
13
|
+
* to mint a token to call itself, and the HTTP hop is needless inside one function either way.
|
|
14
|
+
*
|
|
15
|
+
* A tool added to the MCP server therefore reaches the assistant with no further work here.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** A page or component a tool read, surfaced to the reader as a link under the answer. */
|
|
19
|
+
export interface AiSource {
|
|
20
|
+
url: string;
|
|
21
|
+
title: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AiToolSession {
|
|
25
|
+
tools: ToolSet;
|
|
26
|
+
close: () => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Docs route for one component id. Patterns and pages carry their own paths. */
|
|
30
|
+
const componentUrl = (id: string): string => `/system/component/${encodeURIComponent(id)}`;
|
|
31
|
+
|
|
32
|
+
const asRecord = (value: unknown): Record<string, unknown> | null =>
|
|
33
|
+
value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
|
|
34
|
+
|
|
35
|
+
const text = (value: unknown): string | undefined => (typeof value === 'string' && value.trim() ? value.trim() : undefined);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The pages and components behind one tool result, so the answer can link to what it was read from.
|
|
39
|
+
* Only the shapes `./shape` produces are recognized; anything else contributes no source, which
|
|
40
|
+
* costs a link rather than breaking the answer.
|
|
41
|
+
*/
|
|
42
|
+
const sourcesOf = (toolName: string, payload: unknown): AiSource[] => {
|
|
43
|
+
const record = asRecord(payload);
|
|
44
|
+
if (!record) return [];
|
|
45
|
+
|
|
46
|
+
if (toolName === 'handoff_get_component' || toolName === 'handoff_get_component_source') {
|
|
47
|
+
const id = text(record.id);
|
|
48
|
+
return id ? [{ url: componentUrl(id), title: text(record.title) ?? id }] : [];
|
|
49
|
+
}
|
|
50
|
+
// A preview links to the component's docs page, which is where a reader sees it rendered.
|
|
51
|
+
if (toolName === 'handoff_get_component_preview') {
|
|
52
|
+
const component = asRecord(record.component);
|
|
53
|
+
const id = component && text(component.id);
|
|
54
|
+
return id ? [{ url: componentUrl(id), title: text(component.title) ?? id }] : [];
|
|
55
|
+
}
|
|
56
|
+
if (toolName === 'handoff_get_page') {
|
|
57
|
+
const url = text(record.url);
|
|
58
|
+
return url ? [{ url, title: text(record.title) ?? url }] : [];
|
|
59
|
+
}
|
|
60
|
+
if (toolName === 'handoff_search_pages') {
|
|
61
|
+
const results = Array.isArray(record.results) ? record.results : [];
|
|
62
|
+
return results.flatMap((entry) => {
|
|
63
|
+
const page = asRecord(entry);
|
|
64
|
+
const url = page && text(page.url);
|
|
65
|
+
return url ? [{ url, title: text(page.title) ?? url }] : [];
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return [];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Parse the single JSON text block an MCP tool returns. */
|
|
72
|
+
const parseToolPayload = (result: CallToolResult): unknown => {
|
|
73
|
+
const block = result.content?.find((entry) => entry.type === 'text');
|
|
74
|
+
if (!block || block.type !== 'text') return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(block.text);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build the tool set for one request. Tool descriptions and input schemas come from the MCP server
|
|
84
|
+
* itself, so the agent sees what any other MCP client would.
|
|
85
|
+
*
|
|
86
|
+
* @param onSources - Called with the pages and components each tool read, as they are read.
|
|
87
|
+
*/
|
|
88
|
+
export const createAiToolSession = async (onSources: (sources: AiSource[]) => void): Promise<AiToolSession> => {
|
|
89
|
+
const server = createMcpServer();
|
|
90
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
91
|
+
const client = new Client({ name: 'handoff-docs-assistant', version: '1.0.0' });
|
|
92
|
+
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
|
|
93
|
+
|
|
94
|
+
const { tools: definitions } = await client.listTools();
|
|
95
|
+
const tools: ToolSet = Object.fromEntries(
|
|
96
|
+
definitions.map((definition) => [
|
|
97
|
+
definition.name,
|
|
98
|
+
dynamicTool({
|
|
99
|
+
description: definition.description,
|
|
100
|
+
inputSchema: jsonSchema(definition.inputSchema as Parameters<typeof jsonSchema>[0]),
|
|
101
|
+
execute: async (input) => {
|
|
102
|
+
const result = (await client.callTool({
|
|
103
|
+
name: definition.name,
|
|
104
|
+
arguments: (asRecord(input) ?? {}) as Record<string, unknown>,
|
|
105
|
+
})) as CallToolResult;
|
|
106
|
+
if (!result.isError) onSources(sourcesOf(definition.name, parseToolPayload(result)));
|
|
107
|
+
return result.content;
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
])
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
tools,
|
|
115
|
+
close: async () => {
|
|
116
|
+
await client.close();
|
|
117
|
+
await server.close();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Method guard shared by every API route. It carries no registry dependency, so routes that also
|
|
5
|
+
* serve workspace mode can use it without pulling the registry auth/database layer into that path.
|
|
6
|
+
*/
|
|
7
|
+
export const allowApiMethods = (req: NextApiRequest, res: NextApiResponse, methods: string[]): string | null => {
|
|
8
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
9
|
+
if (methods.includes(method)) return method;
|
|
10
|
+
res.setHeader('Allow', methods.join(', '));
|
|
11
|
+
res.status(405).json({ error: `Method ${method} is not allowed.` });
|
|
12
|
+
return null;
|
|
13
|
+
};
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-only asset storage runtime. Resolves the active provider from the baked server runtime
|
|
3
|
-
* config and lazily builds
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* inline default with no adapter (bytes live directly in `asset_blobs.content`).
|
|
3
|
+
* config and lazily builds and memoizes it. Writes use the active provider. Reads resolve by each
|
|
4
|
+
* blob's recorded provider, so `database` blobs (inline bytea) stay readable when a `custom` provider
|
|
5
|
+
* is active. `database` has no module: its bytes live directly in `asset_blobs.content`.
|
|
7
6
|
*
|
|
8
7
|
* Imported exclusively by registry API route handlers and the registry store.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
|
-
import { createVercelBlobStorage } from '@handoff/registry/asset-storage/adapters/vercel-blob';
|
|
12
10
|
import { resolveAssetStorageSettings, type ResolvedAssetStorage } from '@handoff/registry/asset-storage/resolve';
|
|
13
|
-
import type {
|
|
11
|
+
import type { StorageProvider } from '@handoff/registry/asset-storage/types';
|
|
14
12
|
import type { AssetStorageProvider } from '@handoff/registry/db/schema';
|
|
15
13
|
import { getServerRuntimeConfig } from './docs-api/runtime-config';
|
|
14
|
+
import { loadServerModule } from './server-module';
|
|
16
15
|
|
|
17
16
|
let activeCache: ResolvedAssetStorage | null = null;
|
|
18
|
-
const
|
|
17
|
+
const providerCache = new Map<string, StorageProvider | null>();
|
|
19
18
|
|
|
20
19
|
/** The active, fully-resolved asset storage settings for new uploads. */
|
|
21
20
|
export const getActiveAssetStorage = (): ResolvedAssetStorage => {
|
|
@@ -25,57 +24,57 @@ export const getActiveAssetStorage = (): ResolvedAssetStorage => {
|
|
|
25
24
|
return activeCache;
|
|
26
25
|
};
|
|
27
26
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
27
|
+
/** A storage configuration error whose message is safe to show to a client. */
|
|
28
|
+
export class AssetStorageError extends Error {
|
|
29
|
+
constructor(message: string) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'AssetStorageError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Load a custom provider module and coerce its default export (provider object or factory) to a provider. */
|
|
36
|
+
const loadCustomProvider = async (active: ResolvedAssetStorage): Promise<StorageProvider> => {
|
|
30
37
|
if (!active.module) {
|
|
31
|
-
throw new
|
|
38
|
+
throw new AssetStorageError('A custom asset storage provider is selected but no module path is configured.');
|
|
32
39
|
}
|
|
33
|
-
|
|
34
|
-
const mod: any = await import(/* webpackIgnore: true */ active.module);
|
|
35
|
-
const exported = mod?.default ?? mod;
|
|
36
|
-
const adapter: unknown = typeof exported === 'function' ? await (exported as AssetStorageFactory)({ options: active.options, env: process.env }) : exported;
|
|
37
|
-
const candidate = adapter as Partial<AssetStorage> | null;
|
|
40
|
+
const candidate = (await loadServerModule(active.module, { options: active.options })) as Partial<StorageProvider> | null;
|
|
38
41
|
if (!candidate || typeof candidate.put !== 'function' || typeof candidate.get !== 'function' || typeof candidate.delete !== 'function') {
|
|
39
|
-
throw new
|
|
42
|
+
throw new AssetStorageError(`Custom asset storage module "${active.module}" must default-export a defineStorageProvider() result.`);
|
|
40
43
|
}
|
|
41
|
-
return candidate as
|
|
44
|
+
return candidate as StorageProvider;
|
|
42
45
|
};
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
|
-
* Resolve the {@link
|
|
46
|
-
* inline `database` default (there is no
|
|
48
|
+
* Resolve the {@link StorageProvider} for a recorded provider, or `null` when the provider is the
|
|
49
|
+
* inline `database` default (there is no module; content lives in the DB row). Reading a blob whose
|
|
47
50
|
* provider is no longer configured throws an actionable error rather than silently failing.
|
|
48
51
|
*/
|
|
49
|
-
export const
|
|
52
|
+
export const getStorageProvider = async (provider: AssetStorageProvider): Promise<StorageProvider | null> => {
|
|
50
53
|
if (provider === 'database') {
|
|
51
54
|
return null;
|
|
52
55
|
}
|
|
53
|
-
if (
|
|
54
|
-
return
|
|
56
|
+
if (providerCache.has(provider)) {
|
|
57
|
+
return providerCache.get(provider) ?? null;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
const active = getActiveAssetStorage();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
adapter = await loadCustomAdapter(active);
|
|
63
|
-
} else {
|
|
64
|
-
throw new Error(
|
|
65
|
-
`No asset storage adapter is configured for provider "${provider}". A blob was stored by that ` +
|
|
66
|
-
'provider but it is no longer selected. Restore its configuration (or migrate its objects) to read it.'
|
|
61
|
+
if (active.provider !== provider) {
|
|
62
|
+
throw new AssetStorageError(
|
|
63
|
+
`No asset storage module is configured for provider "${provider}". A blob was stored by that ` +
|
|
64
|
+
'provider but it is no longer selected. Restore its configuration to read it.'
|
|
67
65
|
);
|
|
68
66
|
}
|
|
67
|
+
const storage = await loadCustomProvider(active);
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
return
|
|
69
|
+
providerCache.set(provider, storage);
|
|
70
|
+
return storage;
|
|
72
71
|
};
|
|
73
72
|
|
|
74
|
-
/** The
|
|
75
|
-
export const
|
|
73
|
+
/** The active provider (used for new uploads), or `null` for the inline database default. */
|
|
74
|
+
export const getActiveStorageProvider = (): Promise<StorageProvider | null> => getStorageProvider(getActiveAssetStorage().provider);
|
|
76
75
|
|
|
77
76
|
/** Reset memoized state (test seam only). */
|
|
78
77
|
export const __resetAssetStorageCache = (): void => {
|
|
79
78
|
activeCache = null;
|
|
80
|
-
|
|
79
|
+
providerCache.clear();
|
|
81
80
|
};
|
package/src/app/lib/auth/api.ts
CHANGED
|
@@ -3,21 +3,13 @@ import type { RegistryDatabase } from '@handoff/registry/db/client';
|
|
|
3
3
|
import { getRegistryInstallationState, type RegistryUser } from '@handoff/registry/auth';
|
|
4
4
|
import { getServerRuntimeConfig } from '../docs-api/runtime-config';
|
|
5
5
|
import { getRegistryConnection, RegistryConnectionError } from '../registry-connection';
|
|
6
|
+
import { allowApiMethods } from '../api/methods';
|
|
6
7
|
import { canonicalRegistryUrl, getRegistrySessionUser } from './config';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (methods.includes(method)) return method;
|
|
11
|
-
res.setHeader('Allow', methods.join(', '));
|
|
12
|
-
res.status(405).json({ error: `Method ${method} is not allowed.` });
|
|
13
|
-
return null;
|
|
14
|
-
};
|
|
9
|
+
// Re-exported so the registry routes keep reaching it here, next to the authorization they pair it with.
|
|
10
|
+
export { allowApiMethods };
|
|
15
11
|
|
|
16
|
-
export const registryPageUrl = (
|
|
17
|
-
path: string,
|
|
18
|
-
query?: Record<string, string>,
|
|
19
|
-
fragment?: Record<string, string>
|
|
20
|
-
): string | null => {
|
|
12
|
+
export const registryPageUrl = (path: string, query?: Record<string, string>, fragment?: Record<string, string>): string | null => {
|
|
21
13
|
const canonical = canonicalRegistryUrl();
|
|
22
14
|
if (!canonical) return null;
|
|
23
15
|
const base = canonical.pathname.replace(/\/+$/, '');
|
|
@@ -1,34 +1,107 @@
|
|
|
1
|
-
|
|
1
|
+
import { isEnvReference, resolveEnvReferences } from '@handoff/config/from-env';
|
|
2
|
+
import { createSmtpProvider } from '@handoff/registry/email/providers/smtp';
|
|
3
|
+
import type { EmailProviderKind, EmailSettings } from '@handoff/registry/email/resolve';
|
|
4
|
+
import type { EmailProvider } from '@handoff/registry/email/types';
|
|
5
|
+
import { getServerRuntimeConfig } from '../docs-api/runtime-config';
|
|
6
|
+
import { loadServerModule } from '../server-module';
|
|
7
|
+
|
|
8
|
+
/** Options a provider needs before email counts as configured. */
|
|
9
|
+
const REQUIRED_OPTIONS: Record<EmailProviderKind, readonly string[]> = { smtp: ['host'], custom: [] };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The settings that still need a value, named as the administrator sets them: the config key, or the
|
|
13
|
+
* env var that an option references.
|
|
14
|
+
*/
|
|
15
|
+
export const missingRegistryEmailSettings = (): string[] => {
|
|
16
|
+
const settings = getServerRuntimeConfig().email;
|
|
17
|
+
const options = resolveEnvReferences(settings.options, process.env);
|
|
18
|
+
const missing = settings.from ? [] : ['runtime.registry.email.from'];
|
|
19
|
+
if (!settings.provider) return [...missing, 'runtime.registry.email.provider'];
|
|
20
|
+
if (settings.provider === 'custom' && !settings.module) missing.push('runtime.registry.email.module');
|
|
21
|
+
for (const key of REQUIRED_OPTIONS[settings.provider]) {
|
|
22
|
+
if (options[key] !== undefined) continue;
|
|
23
|
+
const authored = settings.options[key];
|
|
24
|
+
missing.push(isEnvReference(authored) ? authored.$env : `runtime.registry.email.options.${key}`);
|
|
25
|
+
}
|
|
26
|
+
return missing;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const registryEmailIsConfigured = (): boolean => missingRegistryEmailSettings().length === 0;
|
|
30
|
+
|
|
31
|
+
/** The provider name for messages shown to an administrator. */
|
|
32
|
+
export const registryEmailProviderName = (): string => {
|
|
33
|
+
const { provider } = getServerRuntimeConfig().email;
|
|
34
|
+
return provider === 'smtp' ? 'SMTP' : 'the custom email provider';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let customProvider: Promise<EmailProvider> | null = null;
|
|
38
|
+
|
|
39
|
+
/** Load the custom module once and coerce its default export (provider object or factory) to a provider. */
|
|
40
|
+
const loadCustomProvider = async (settings: EmailSettings, options: Record<string, unknown>): Promise<EmailProvider> => {
|
|
41
|
+
const provider = await loadServerModule(settings.module!, { options });
|
|
42
|
+
if (typeof (provider as Partial<EmailProvider> | null)?.send !== 'function') {
|
|
43
|
+
throw new Error(`Custom email provider module "${settings.module}" must default-export a defineEmailProvider() provider.`);
|
|
44
|
+
}
|
|
45
|
+
return provider as EmailProvider;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const getEmailProvider = (settings: EmailSettings, options: Record<string, unknown>): Promise<EmailProvider> => {
|
|
49
|
+
if (settings.provider === 'smtp') return Promise.resolve(createSmtpProvider(options));
|
|
50
|
+
customProvider ??= loadCustomProvider(settings, options).catch((error) => {
|
|
51
|
+
customProvider = null;
|
|
52
|
+
throw error;
|
|
53
|
+
});
|
|
54
|
+
return customProvider;
|
|
55
|
+
};
|
|
2
56
|
|
|
3
57
|
const escapeHtml = (value: string): string =>
|
|
4
58
|
value.replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]!);
|
|
5
59
|
|
|
6
|
-
|
|
60
|
+
const FOOTNOTE = 'If you did not expect this email, you can ignore it.';
|
|
61
|
+
|
|
62
|
+
interface AuthEmail {
|
|
7
63
|
to: string;
|
|
8
64
|
subject: string;
|
|
9
65
|
heading: string;
|
|
10
66
|
message: string;
|
|
11
67
|
actionLabel: string;
|
|
12
68
|
actionUrl: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const renderHtml = ({ heading, message, actionLabel, actionUrl }: AuthEmail): string => {
|
|
72
|
+
const url = escapeHtml(actionUrl);
|
|
73
|
+
return `<!DOCTYPE html>
|
|
74
|
+
<html lang="en">
|
|
75
|
+
<body style="margin:0;padding:32px 16px;background:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#18181b;">
|
|
76
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td align="center">
|
|
77
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:480px;background:#ffffff;border-radius:8px;">
|
|
78
|
+
<tr><td style="padding:32px;">
|
|
79
|
+
<h1 style="margin:0 0 16px;font-size:20px;">${escapeHtml(heading)}</h1>
|
|
80
|
+
<p style="margin:0 0 24px;font-size:15px;line-height:1.5;">${escapeHtml(message)}</p>
|
|
81
|
+
<a href="${url}" style="display:inline-block;padding:12px 20px;border-radius:6px;background:#18181b;color:#ffffff;font-size:15px;text-decoration:none;">${escapeHtml(actionLabel)}</a>
|
|
82
|
+
<p style="margin:24px 0 0;font-size:13px;line-height:1.5;color:#71717a;">If the button does not work, open this link:<br><a href="${url}" style="color:#71717a;word-break:break-all;">${url}</a></p>
|
|
83
|
+
<p style="margin:16px 0 0;font-size:13px;color:#71717a;">${FOOTNOTE}</p>
|
|
84
|
+
</td></tr></table>
|
|
85
|
+
</td></tr></table>
|
|
86
|
+
</body>
|
|
87
|
+
</html>`;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const renderText = ({ heading, message, actionLabel, actionUrl }: AuthEmail): string =>
|
|
91
|
+
`${heading}\n\n${message}\n\n${actionLabel}: ${actionUrl}\n\n${FOOTNOTE}\n`;
|
|
92
|
+
|
|
93
|
+
/** Send through the configured provider. Returns false, and logs the failure, when email is not configured or delivery fails. */
|
|
94
|
+
export const sendRegistryAuthEmail = async (email: AuthEmail): Promise<boolean> => {
|
|
95
|
+
if (!registryEmailIsConfigured()) return false;
|
|
96
|
+
const settings = getServerRuntimeConfig().email;
|
|
97
|
+
const options = resolveEnvReferences(settings.options, process.env);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const provider = await getEmailProvider(settings, options);
|
|
101
|
+
await provider.send({ from: settings.from!, to: email.to, subject: email.subject, html: renderHtml(email), text: renderText(email) });
|
|
102
|
+
return true;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`Email delivery through ${registryEmailProviderName()} failed.`, error);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
34
107
|
};
|
|
@@ -31,7 +31,7 @@ export const documentationMetadata = (source: Record<string, unknown>) => {
|
|
|
31
31
|
return {
|
|
32
32
|
title,
|
|
33
33
|
description,
|
|
34
|
-
metaTitle: value('metaTitle')
|
|
34
|
+
metaTitle: value('metaTitle'),
|
|
35
35
|
metaDescription: value('metaDescription') || description,
|
|
36
36
|
};
|
|
37
37
|
};
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import { defaultPageDetail, type BakedDefaultPage } from './page-rendering';
|
|
22
22
|
import { MAX_SEARCH_BODY_LENGTH, MAX_SEARCH_CANDIDATES, truncateSearchBody, type SearchResponse } from './search';
|
|
23
23
|
import { getRegistryConnection } from '../registry-connection';
|
|
24
|
-
import {
|
|
24
|
+
import { getStorageProvider } from '../asset-storage';
|
|
25
25
|
// A static import includes package defaults in each registry serverless bundle. A deployed function
|
|
26
26
|
// cannot read the build machine's `config/docs` directory.
|
|
27
27
|
import defaultPages from '../../generated/default-pages.json';
|
|
@@ -148,8 +148,7 @@ const searchRegistryPages = async (db: RegistryDatabase, request: PageSearchRequ
|
|
|
148
148
|
export const createRegistryDocsBackend = async (): Promise<DocsBackend> => {
|
|
149
149
|
const connection = await getRegistryConnection();
|
|
150
150
|
const { db } = connection;
|
|
151
|
-
|
|
152
|
-
const store = createRegistryStore({ db, resolveAssetAdapter: getAssetStorageAdapter });
|
|
151
|
+
const store = createRegistryStore({ db, resolveStorageProvider: getStorageProvider });
|
|
153
152
|
|
|
154
153
|
return {
|
|
155
154
|
async listComponents() {
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import type { RuntimeMode } from '@handoff/types/config';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from '@handoff/registry/db/driver';
|
|
9
|
-
import { DEFAULT_ASSET_STORAGE_ADAPTER, type AssetStorageSettings } from '@handoff/registry/asset-storage/resolve';
|
|
4
|
+
import { DEFAULT_DATABASE_URL_ENV, DEFAULT_REGISTRY_DRIVER, type RegistryDatabaseDriver } from '@handoff/registry/db/driver';
|
|
5
|
+
import { DEFAULT_ASSET_STORAGE_PROVIDER, type AssetStorageSettings } from '@handoff/registry/asset-storage/resolve';
|
|
6
|
+
import { mergeAiConnections, parseAiConnections, type AiSettings } from '@handoff/ai/connections';
|
|
7
|
+
import { parseEmailSettings, type EmailSettings } from '@handoff/registry/email/resolve';
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
10
|
* Server-side runtime resolution for the docs read API.
|
|
@@ -35,12 +33,20 @@ export interface ServerRuntimeConfig {
|
|
|
35
33
|
mode: RuntimeMode;
|
|
36
34
|
/** Whether this build serves `/api/mcp/`. Config-only, baked at build time. */
|
|
37
35
|
mcp: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* The AI assistant: whether this build serves it, and the connections it can reach. `enabled` is
|
|
38
|
+
* baked; the connection list is resolved per request so a deployment can change it without a
|
|
39
|
+
* rebuild.
|
|
40
|
+
*/
|
|
41
|
+
ai: AiSettings;
|
|
38
42
|
registry: ServerRegistryRuntimeConfig;
|
|
39
43
|
/**
|
|
40
|
-
* Asset storage
|
|
41
|
-
*
|
|
44
|
+
* Asset storage provider, module, and options for registry mode. Options hold `{ $env }` references,
|
|
45
|
+
* never secret values.
|
|
42
46
|
*/
|
|
43
47
|
assetStorage: AssetStorageSettings;
|
|
48
|
+
/** Registry email provider, sender, and options. Options hold `{ $env }` references, never secret values. */
|
|
49
|
+
email: EmailSettings;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
let cached: ServerRuntimeConfig | null = null;
|
|
@@ -49,16 +55,18 @@ let cached: ServerRuntimeConfig | null = null;
|
|
|
49
55
|
const defaults = (): ServerRuntimeConfig => ({
|
|
50
56
|
mode: 'workspace',
|
|
51
57
|
mcp: true,
|
|
58
|
+
ai: { enabled: false, connections: [] },
|
|
52
59
|
registry: {
|
|
53
60
|
driver: DEFAULT_REGISTRY_DRIVER,
|
|
54
61
|
databaseUrlEnv: DEFAULT_DATABASE_URL_ENV,
|
|
55
62
|
},
|
|
56
|
-
assetStorage: {
|
|
63
|
+
assetStorage: { provider: DEFAULT_ASSET_STORAGE_PROVIDER },
|
|
64
|
+
email: { options: {} },
|
|
57
65
|
});
|
|
58
66
|
|
|
59
67
|
/** Parse the baked asset-storage selection from env (names/selectors only; JSON options tolerated). */
|
|
60
68
|
const assetStorageFromEnv = (): AssetStorageSettings => {
|
|
61
|
-
const
|
|
69
|
+
const provider = process.env.HANDOFF_ASSET_STORAGE_PROVIDER?.trim();
|
|
62
70
|
let options: Record<string, unknown> | undefined;
|
|
63
71
|
const rawOptions = process.env.HANDOFF_ASSET_STORAGE_OPTIONS?.trim();
|
|
64
72
|
if (rawOptions) {
|
|
@@ -71,22 +79,33 @@ const assetStorageFromEnv = (): AssetStorageSettings => {
|
|
|
71
79
|
}
|
|
72
80
|
const maxInline = Number(process.env.HANDOFF_ASSET_STORAGE_MAX_INLINE_BYTES);
|
|
73
81
|
return {
|
|
74
|
-
|
|
82
|
+
provider: provider === 'custom' ? provider : DEFAULT_ASSET_STORAGE_PROVIDER,
|
|
75
83
|
module: process.env.HANDOFF_ASSET_STORAGE_MODULE?.trim() || undefined,
|
|
76
|
-
tokenEnv: process.env.HANDOFF_ASSET_STORAGE_TOKEN_ENV?.trim() || undefined,
|
|
77
84
|
maxInlineBytes: Number.isFinite(maxInline) && maxInline > 0 ? maxInline : undefined,
|
|
78
85
|
options,
|
|
79
86
|
};
|
|
80
87
|
};
|
|
81
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the AI settings from the baked flag and connection list, then merge the
|
|
91
|
+
* deployment-supplied `HANDOFF_AI_CONNECTIONS` over it by id.
|
|
92
|
+
*
|
|
93
|
+
* `HANDOFF_AI_CONNECTIONS` is read dynamically rather than as a literal `process.env` reference: the
|
|
94
|
+
* baked values in `next.config.mjs` are inlined at build time, so a name in that block can never
|
|
95
|
+
* carry a deploy-time value. That is why the baked list uses its own `HANDOFF_AI_BAKED_CONNECTIONS`.
|
|
96
|
+
*/
|
|
97
|
+
const aiFromEnv = (): AiSettings => ({
|
|
98
|
+
enabled: process.env.HANDOFF_AI_ENABLED?.trim() === 'true',
|
|
99
|
+
connections: mergeAiConnections(
|
|
100
|
+
parseAiConnections(process.env.HANDOFF_AI_BAKED_CONNECTIONS),
|
|
101
|
+
parseAiConnections(process.env['HANDOFF_AI_CONNECTIONS'])
|
|
102
|
+
),
|
|
103
|
+
defaultModel: process.env.HANDOFF_AI_DEFAULT_MODEL?.trim() || undefined,
|
|
104
|
+
});
|
|
105
|
+
|
|
82
106
|
/** Absolute path of the server-only runtime config persisted next to `client.config.json`. */
|
|
83
107
|
const serverRuntimeConfigPath = (): string =>
|
|
84
|
-
path.resolve(
|
|
85
|
-
process.env.HANDOFF_MODULE_PATH ?? '',
|
|
86
|
-
'.handoff',
|
|
87
|
-
process.env.HANDOFF_PROJECT_ID ?? '',
|
|
88
|
-
'runtime.server.json'
|
|
89
|
-
);
|
|
108
|
+
path.resolve(process.env.HANDOFF_MODULE_PATH ?? '', '.handoff', process.env.HANDOFF_PROJECT_ID ?? '', 'runtime.server.json');
|
|
90
109
|
|
|
91
110
|
/**
|
|
92
111
|
* Resolve the server-side runtime config from the values baked into the bundle at build time
|
|
@@ -107,11 +126,18 @@ const fromEnv = (): ServerRuntimeConfig | null => {
|
|
|
107
126
|
return {
|
|
108
127
|
mode: mode === 'registry' ? 'registry' : 'workspace',
|
|
109
128
|
mcp: process.env.HANDOFF_MCP_ENABLED?.trim() !== 'false',
|
|
129
|
+
ai: aiFromEnv(),
|
|
110
130
|
registry: {
|
|
111
131
|
driver: process.env.HANDOFF_REGISTRY_DRIVER?.trim() === 'neon' ? 'neon' : 'pg',
|
|
112
132
|
databaseUrlEnv,
|
|
113
133
|
},
|
|
114
134
|
assetStorage: assetStorageFromEnv(),
|
|
135
|
+
email: parseEmailSettings({
|
|
136
|
+
from: process.env.HANDOFF_EMAIL_FROM,
|
|
137
|
+
provider: process.env.HANDOFF_EMAIL_PROVIDER?.trim(),
|
|
138
|
+
module: process.env.HANDOFF_EMAIL_MODULE,
|
|
139
|
+
options: process.env.HANDOFF_EMAIL_OPTIONS,
|
|
140
|
+
}),
|
|
115
141
|
};
|
|
116
142
|
};
|
|
117
143
|
|
|
@@ -141,17 +167,17 @@ export const getServerRuntimeConfig = (): ServerRuntimeConfig => {
|
|
|
141
167
|
? parsed.registry.databaseUrlEnv.trim()
|
|
142
168
|
: DEFAULT_DATABASE_URL_ENV;
|
|
143
169
|
const assetStorage: AssetStorageSettings =
|
|
144
|
-
parsed?.assetStorage && typeof parsed.assetStorage === 'object'
|
|
145
|
-
? parsed.assetStorage
|
|
146
|
-
: { adapter: DEFAULT_ASSET_STORAGE_ADAPTER };
|
|
170
|
+
parsed?.assetStorage && typeof parsed.assetStorage === 'object' ? parsed.assetStorage : { provider: DEFAULT_ASSET_STORAGE_PROVIDER };
|
|
147
171
|
cached = {
|
|
148
172
|
mode: parsed?.mode === 'registry' ? 'registry' : 'workspace',
|
|
149
173
|
mcp: parsed?.mcp !== false,
|
|
174
|
+
ai: aiFromEnv(),
|
|
150
175
|
registry: {
|
|
151
176
|
driver: parsed?.registry?.driver === 'neon' ? 'neon' : 'pg',
|
|
152
177
|
databaseUrlEnv,
|
|
153
178
|
},
|
|
154
179
|
assetStorage,
|
|
180
|
+
email: parseEmailSettings(parsed?.email ?? {}),
|
|
155
181
|
};
|
|
156
182
|
return cached;
|
|
157
183
|
}
|