handoff-app 2.0.0-9 → 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 +813 -406
- 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/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 +26 -2
- package/dist/config/resolve-env.d.ts +2 -1
- package/dist/config/resolve-env.js +28 -12
- package/dist/config/runtime.js +1 -1
- 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/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +17 -17
- package/dist/transformers/preview/component/css.d.ts +2 -1
- package/dist/transformers/preview/component/css.js +8 -5
- 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/utils/build.d.ts +5 -0
- package/dist/transformers/utils/build.js +21 -4
- package/dist/types/config.d.ts +129 -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/Preview.tsx +5 -2
- 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/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 +25 -3
- package/src/config/resolve-env.ts +28 -15
- package/src/config/runtime.ts +1 -1
- 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/preview/component/builder.ts +19 -25
- package/src/transformers/preview/component/css.ts +9 -4
- package/src/transformers/preview/component/html.ts +6 -2
- package/src/transformers/preview/component/javascript.ts +14 -4
- package/src/transformers/utils/build.ts +16 -3
- package/src/types/config.ts +142 -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,97 @@
|
|
|
1
|
+
import { aiCredentialKind, parseAiModelId, type AiConnectionSettings } from '@handoff/ai/connections';
|
|
2
|
+
import type { AiCredentialKind } from '@handoff/types/config';
|
|
3
|
+
import { getServerRuntimeConfig } from '../docs-api/runtime-config';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-request AI connection resolution.
|
|
7
|
+
*
|
|
8
|
+
* Reports what the config declared and whether a usable key exists for the current reader. The key
|
|
9
|
+
* value itself never leaves the server: the browser only ever learns `configured`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** One connection as the browser sees it. */
|
|
13
|
+
export interface ResolvedAiConnection {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
/** Absent for a connection served by a `defineAiProvider()` module rather than a base URL. */
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
models?: string[];
|
|
19
|
+
credential: AiCredentialKind;
|
|
20
|
+
/**
|
|
21
|
+
* Whether this reader can use the connection right now: the deployment's key is set, the endpoint
|
|
22
|
+
* needs none, or the reader has saved their own.
|
|
23
|
+
*/
|
|
24
|
+
configured: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Find a declared connection by id. Ids only ever come from the resolved list, never from a reader. */
|
|
28
|
+
export const findAiConnection = (connectionId: string): AiConnectionSettings | undefined =>
|
|
29
|
+
getServerRuntimeConfig().ai.connections.find((connection) => connection.id === connectionId);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read this reader's saved connection ids. Registry mode only — a workspace has one user, whose
|
|
33
|
+
* config file and `.env` are their user layer, so it has no per-user key store.
|
|
34
|
+
*
|
|
35
|
+
* The registry data layer is imported lazily so the Drizzle/Postgres dependencies stay out of the
|
|
36
|
+
* workspace path, the same way the rest of the registry runtime is reached.
|
|
37
|
+
*/
|
|
38
|
+
const readSavedConnections = async (userId: string | null): Promise<Set<string>> => {
|
|
39
|
+
if (!userId || getServerRuntimeConfig().mode !== 'registry') return new Set();
|
|
40
|
+
const [{ getRegistryConnection }, { listRegistryAiKeyConnections }] = await Promise.all([
|
|
41
|
+
import('../registry-connection'),
|
|
42
|
+
import('@handoff/registry/auth'),
|
|
43
|
+
]);
|
|
44
|
+
const { db } = await getRegistryConnection();
|
|
45
|
+
return new Set(await listRegistryAiKeyConnections(db, userId));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Whether a connection is usable for a reader, given the keys that reader has saved. */
|
|
49
|
+
const isConfigured = (connection: AiConnectionSettings, saved: ReadonlySet<string>): boolean => {
|
|
50
|
+
switch (aiCredentialKind(connection)) {
|
|
51
|
+
case 'service':
|
|
52
|
+
return Boolean(connection.apiKeyEnv && process.env[connection.apiKeyEnv]?.trim());
|
|
53
|
+
case 'user':
|
|
54
|
+
return saved.has(connection.id);
|
|
55
|
+
default:
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Every declared connection, each marked with whether this reader can use it. */
|
|
61
|
+
export const describeAiConnections = async (userId: string | null): Promise<ResolvedAiConnection[]> => {
|
|
62
|
+
const { ai } = getServerRuntimeConfig();
|
|
63
|
+
const saved = await readSavedConnections(userId);
|
|
64
|
+
return ai.connections.map((connection) => ({
|
|
65
|
+
id: connection.id,
|
|
66
|
+
label: connection.label,
|
|
67
|
+
...(connection.baseUrl ? { baseUrl: connection.baseUrl } : {}),
|
|
68
|
+
...(connection.models?.length ? { models: connection.models } : {}),
|
|
69
|
+
credential: aiCredentialKind(connection),
|
|
70
|
+
configured: isConfigured(connection, saved),
|
|
71
|
+
}));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** The connections a reader may add a key for. Empty when the config declares none. */
|
|
75
|
+
export const describeAiUserConnections = async (userId: string | null): Promise<ResolvedAiConnection[]> =>
|
|
76
|
+
(await describeAiConnections(userId)).filter((connection) => connection.credential === 'user');
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The model to run, given what the reader asked for. A requested model must name a declared
|
|
80
|
+
* connection and one of that connection's declared models, so a reader can never reach a model the
|
|
81
|
+
* deployment did not list. Falls back to `defaultModel`, then to the first usable model.
|
|
82
|
+
*/
|
|
83
|
+
export const selectAiModel = (
|
|
84
|
+
connections: readonly ResolvedAiConnection[],
|
|
85
|
+
requested: string | undefined
|
|
86
|
+
): { connection: ResolvedAiConnection; model: string } | null => {
|
|
87
|
+
const usable = connections.filter((connection) => connection.configured && connection.models?.length);
|
|
88
|
+
const pick = (value: string | undefined) => {
|
|
89
|
+
const parsed = value ? parseAiModelId(value) : null;
|
|
90
|
+
if (!parsed) return null;
|
|
91
|
+
const connection = usable.find((candidate) => candidate.id === parsed.connectionId);
|
|
92
|
+
return connection?.models?.includes(parsed.model) ? { connection, model: parsed.model } : null;
|
|
93
|
+
};
|
|
94
|
+
const { ai } = getServerRuntimeConfig();
|
|
95
|
+
const first = usable[0];
|
|
96
|
+
return pick(requested) ?? pick(ai.defaultModel) ?? (first?.models?.[0] ? { connection: first, model: first.models[0] } : null);
|
|
97
|
+
};
|
|
@@ -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() {
|