handoff-app 2.0.0-12 → 2.0.0-13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/{Changelog.md → CHANGELOG.md} +84 -0
  2. package/{License.md → LICENSE.md} +1 -1
  3. package/README.md +760 -801
  4. package/RECIPES.md +975 -0
  5. package/components.json +20 -20
  6. package/config/docs/foundations/colors/guidelines.md +0 -1
  7. package/config/docs/foundations/colors.md +0 -1
  8. package/config/docs/foundations/effects/guidelines.md +0 -1
  9. package/config/docs/foundations/effects/resources.md +0 -1
  10. package/config/docs/foundations/effects.md +0 -1
  11. package/config/docs/foundations/fonts.md +0 -1
  12. package/config/docs/foundations/grid.md +0 -1
  13. package/config/docs/foundations/icon.md +0 -1
  14. package/config/docs/foundations/icons/guidelines.md +0 -1
  15. package/config/docs/foundations/icons.md +0 -1
  16. package/config/docs/foundations/images/guidelines.md +0 -1
  17. package/config/docs/foundations/images/resources.md +0 -1
  18. package/config/docs/foundations/images.md +0 -1
  19. package/config/docs/foundations/logo/resources.md +0 -1
  20. package/config/docs/foundations/logo.md +0 -1
  21. package/config/docs/foundations/typography/guidelines.md +0 -1
  22. package/config/docs/foundations/typography/resources.md +0 -1
  23. package/config/docs/foundations/typography.md +0 -1
  24. package/config/docs/foundations.md +0 -1
  25. package/config/docs/guidelines.md +0 -1
  26. package/config/docs/index.md +0 -1
  27. package/config/docs/system/tokens/foundations/colors.md +0 -4
  28. package/config/docs/system/tokens/foundations/effects.md +0 -4
  29. package/config/docs/system/tokens/foundations/typography.md +0 -2
  30. package/config/docs/system.md +0 -2
  31. package/config/templates/page.md +2 -1
  32. package/config/templates/page.mdx +2 -1
  33. package/dist/ai/define.d.ts +1 -1
  34. package/dist/ai/define.js +1 -1
  35. package/dist/ai/types.d.ts +1 -1
  36. package/dist/ai/types.js +1 -1
  37. package/dist/app-builder/build.js +14 -14
  38. package/dist/app-builder/client-config.js +1 -1
  39. package/dist/config/loader.js +1 -2
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.js +2 -2
  42. package/dist/nav/index.d.ts +2 -2
  43. package/dist/nav/registry.d.ts +3 -3
  44. package/dist/nav/registry.js +1 -1
  45. package/dist/nav/workspace.d.ts +3 -3
  46. package/dist/nav/workspace.js +1 -1
  47. package/dist/providers.d.ts +10 -0
  48. package/dist/{define.js → providers.js} +4 -4
  49. package/dist/registry/asset-storage/define.d.ts +5 -5
  50. package/dist/registry/asset-storage/define.js +4 -4
  51. package/dist/registry/asset-storage/resolve.d.ts +8 -11
  52. package/dist/registry/asset-storage/resolve.js +12 -13
  53. package/dist/registry/asset-storage/types.d.ts +12 -12
  54. package/dist/registry/asset-storage/types.js +2 -2
  55. package/dist/registry/db/schema.d.ts +2 -2
  56. package/dist/registry/db/schema.js +2 -2
  57. package/dist/registry/email/define.d.ts +1 -1
  58. package/dist/registry/email/define.js +1 -1
  59. package/dist/registry/email/resolve.d.ts +7 -8
  60. package/dist/registry/email/resolve.js +21 -23
  61. package/dist/registry/email/types.d.ts +1 -1
  62. package/dist/registry/email/types.js +1 -1
  63. package/dist/store/registry.d.ts +3 -3
  64. package/dist/store/registry.js +4 -4
  65. package/dist/types/config.d.ts +4 -13
  66. package/docs/cli.md +275 -54
  67. package/package.json +7 -7
  68. package/src/ai/define.ts +1 -1
  69. package/src/ai/types.ts +1 -1
  70. package/src/app/components/Auth/AccountLayout.tsx +1 -1
  71. package/src/app/components/Auth/AuthShell.tsx +10 -4
  72. package/src/app/components/Foundations/ColorGrid.tsx +13 -14
  73. package/src/app/components/Layout/Main.tsx +2 -2
  74. package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
  75. package/src/app/components/Navigation/MobileNav.tsx +0 -2
  76. package/src/app/components/util/index.ts +0 -11
  77. package/src/app/lib/asset-storage.ts +25 -25
  78. package/src/app/lib/auth/email.ts +4 -5
  79. package/src/app/lib/docs-api/page-rendering.ts +1 -1
  80. package/src/app/lib/docs-api/registry.ts +2 -3
  81. package/src/app/lib/docs-api/runtime-config.ts +8 -8
  82. package/src/app/lib/registry-api/asset-transfer.ts +25 -25
  83. package/src/app/lib/server-module.ts +1 -1
  84. package/src/app/lib/utils.ts +7 -0
  85. package/src/app/next.config.mjs +2 -2
  86. package/src/app/pages/[...slug]/index.tsx +2 -1
  87. package/src/app/pages/account/ai.tsx +1 -1
  88. package/src/app/pages/account/index.tsx +1 -1
  89. package/src/app/pages/account/tokens.tsx +1 -1
  90. package/src/app/pages/account/users.tsx +1 -1
  91. package/src/app/pages/api/docs/nav.json.ts +1 -1
  92. package/src/app/pages/api/mcp.ts +6 -0
  93. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
  94. package/src/app/pages/cli/device.tsx +1 -1
  95. package/src/app/pages/foundations/colors.tsx +9 -8
  96. package/src/app/pages/foundations/index.tsx +3 -3
  97. package/src/app/pages/index.tsx +2 -2
  98. package/src/app/pages/login.tsx +10 -5
  99. package/src/app/pages/reset-password.tsx +7 -2
  100. package/src/app/pages/system/component/[component]/index.tsx +2 -5
  101. package/src/app/pages/system/index.tsx +11 -11
  102. package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
  103. package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
  104. package/src/app-builder/build.ts +10 -10
  105. package/src/app-builder/client-config.ts +1 -1
  106. package/src/config/loader.ts +1 -2
  107. package/src/index.ts +6 -6
  108. package/src/nav/index.ts +2 -2
  109. package/src/nav/registry.ts +3 -3
  110. package/src/nav/workspace.ts +3 -3
  111. package/src/{define.ts → providers.ts} +8 -8
  112. package/src/registry/asset-storage/define.ts +6 -6
  113. package/src/registry/asset-storage/resolve.ts +16 -20
  114. package/src/registry/asset-storage/types.ts +12 -12
  115. package/src/registry/db/schema.ts +3 -3
  116. package/src/registry/email/define.ts +1 -1
  117. package/src/registry/email/resolve.ts +23 -23
  118. package/src/registry/email/types.ts +1 -1
  119. package/src/store/registry.ts +7 -7
  120. package/src/types/config.ts +4 -14
  121. package/.editorconfig +0 -19
  122. package/.eslintrc.json +0 -4
  123. package/.prettierignore +0 -0
  124. package/.prettierrc.json +0 -8
  125. package/.vscode/settings.json +0 -17
  126. package/dist/define.d.ts +0 -10
  127. package/dist/registry/email/adapters/resend.d.ts +0 -7
  128. package/dist/registry/email/adapters/resend.js +0 -35
  129. package/src/app/components/old/Header.tsx +0 -89
  130. package/src/app/pages/assets/fonts.tsx +0 -129
  131. package/src/app/pages/assets/index.tsx +0 -91
  132. package/src/app/pages/assets/logos.tsx +0 -75
  133. package/src/registry/email/adapters/resend.ts +0 -22
  134. /package/dist/registry/email/{adapters → providers}/smtp.d.ts +0 -0
  135. /package/dist/registry/email/{adapters → providers}/smtp.js +0 -0
  136. /package/src/registry/email/{adapters → providers}/smtp.ts +0 -0
@@ -20,7 +20,7 @@ export const getStaticProps: GetStaticProps = async () => ({ props: { config: ge
20
20
 
21
21
  export default function DeviceApprovalPage({ config }: { config: ClientConfig }) {
22
22
  if (process.env.HANDOFF_RUNTIME_MODE !== 'registry') {
23
- return <AuthShell title="Authorize CLI">{null}</AuthShell>;
23
+ return <AuthShell config={config} title="Authorize CLI">{null}</AuthShell>;
24
24
  }
25
25
 
26
26
  return <RegistryDeviceApprovalPage config={config} />;
@@ -33,6 +33,10 @@ export const getStaticProps: GetStaticProps = async () => {
33
33
  };
34
34
  };
35
35
 
36
+ // Tokens without a Figma group land under an empty key.
37
+ const groupTitle = (group: string) => upperFirst(group) || 'Other';
38
+ const groupId = (group: string) => group || 'other';
39
+
36
40
  const ColorsPage = ({
37
41
  content,
38
42
  menu,
@@ -82,20 +86,17 @@ const ColorsPage = ({
82
86
  <div className="lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_280px]">
83
87
  <div className="flex flex-col gap-0">
84
88
  {Object.keys(colorGroups).map((group) => (
85
- <ColorGrid
86
- title={upperFirst(group)}
87
- group={group}
88
- description="Colors that are used most frequently across all pages and components."
89
- colors={colorGroups[group]}
90
- key={group}
91
- />
89
+ <ColorGrid title={groupTitle(group)} group={groupId(group)} colors={colorGroups[group]} key={group} />
92
90
  ))}
93
91
  <PrevNextNav previous={null} next={{ title: 'Typography', href: '/foundations/typography' }} />
94
92
  </div>
95
93
 
96
94
  <AnchorNav
97
95
  groups={[
98
- Object.assign({}, ...[...Object.keys(colorGroups).map((group) => ({ [`${group}-colors`]: `${upperFirst(group)} Colors` }))]),
96
+ Object.assign(
97
+ {},
98
+ ...[...Object.keys(colorGroups).map((group) => ({ [`${groupId(group)}-colors`]: `${groupTitle(group)} Colors` }))]
99
+ ),
99
100
  ]}
100
101
  />
101
102
 
@@ -50,7 +50,7 @@ const DesignPage = ({ content, menu, metadata, current, config }: DocumentationP
50
50
  },
51
51
  {
52
52
  title: 'Colors',
53
- description: 'Official logo used for all digital and offline materials.',
53
+ description: 'Official color palette used for all digital products.',
54
54
  icon: Palette,
55
55
  link: '/foundations/colors',
56
56
  cta: 'Explore Colors',
@@ -74,14 +74,14 @@ const DesignPage = ({ content, menu, metadata, current, config }: DocumentationP
74
74
  description: 'Downloadable icon set for use in digital and offline materials.',
75
75
  icon: Shapes,
76
76
  link: '/foundations/icons',
77
- cta: 'View Library',
77
+ cta: 'Explore Icons',
78
78
  },
79
79
  {
80
80
  title: 'Effects',
81
81
  description: 'Shadows, blurs, and other effects used in the design system.',
82
82
  icon: Sun,
83
83
  link: '/foundations/effects',
84
- cta: 'View Effects',
84
+ cta: 'Explore Effects',
85
85
  },
86
86
  ]}
87
87
  />
@@ -57,11 +57,11 @@ export const getStaticProps: GetStaticProps = async (context) => {
57
57
 
58
58
  export const Home = ({ content, menu, metadata, config, current }: DocumentationProps) => {
59
59
  return (
60
- <Layout config={config} menu={menu} current={current} metadata={metadata} fullWidthHero={true}>
60
+ <Layout config={config} menu={menu} current={current} metadata={{ ...metadata, metaTitle: metadata.metaTitle || config?.app?.title }} fullWidthHero={true}>
61
61
  <div className="w-full bg-linear-to-r py-12 dark:from-gray-900 dark:to-gray-800 sm:py-20">
62
62
  <div className="container mx-auto px-8">
63
63
  <HeadersType.H1 className="max-w-4xl text-3xl font-semibold leading-[-0.05px] sm:text-4xl">
64
- {config?.app?.client} Design System
64
+ {config?.app?.title}
65
65
  </HeadersType.H1>
66
66
  {content && (
67
67
  <div className="text-lg max-w-4xl empty:hidden [&>*]:mt-6 [&>p]:font-light [&>h2:first-child]:mt-0 [&>h2:first-child]:font-normal sm:text-xl">
@@ -2,29 +2,34 @@ import { Button } from '../components/ui/button';
2
2
  import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../components/ui/card';
3
3
  import { Input } from '../components/ui/input';
4
4
  import { Label } from '../components/ui/label';
5
+ import type { ClientConfig } from '@handoff/types/config';
6
+ import type { GetStaticProps } from 'next';
5
7
  import Link from 'next/link';
6
8
  import { signIn, useSession } from 'next-auth/react';
7
9
  import { useRouter } from 'next/router';
8
10
  import { FormEvent, useEffect, useState } from 'react';
9
11
  import { AuthShell } from '../components/Auth/AuthShell';
10
12
  import { loadInstallStatus } from '../components/Auth/installStatus';
13
+ import { getClientRuntimeConfig } from '../components/util';
11
14
 
12
15
  const safeCallbackUrl = (value: unknown): string =>
13
16
  typeof value === 'string' && value.startsWith('/') && !value.startsWith('//') && !value.startsWith('/\\') ? value : '/';
14
17
 
15
- export default function LoginPage() {
18
+ export const getStaticProps: GetStaticProps = async () => ({ props: { config: getClientRuntimeConfig() } });
19
+
20
+ export default function LoginPage({ config }: { config: ClientConfig }) {
16
21
  if (process.env.HANDOFF_RUNTIME_MODE !== 'registry') {
17
22
  return (
18
- <AuthShell title="Sign in" centered>
23
+ <AuthShell config={config} title="Sign in" centered>
19
24
  {null}
20
25
  </AuthShell>
21
26
  );
22
27
  }
23
28
 
24
- return <RegistryLoginPage />;
29
+ return <RegistryLoginPage config={config} />;
25
30
  }
26
31
 
27
- function RegistryLoginPage() {
32
+ function RegistryLoginPage({ config }: { config: ClientConfig }) {
28
33
  const router = useRouter();
29
34
  const { data: session, status } = useSession();
30
35
  const [pending, setPending] = useState(false);
@@ -77,7 +82,7 @@ function RegistryLoginPage() {
77
82
  };
78
83
 
79
84
  return (
80
- <AuthShell title="Sign in" centered>
85
+ <AuthShell config={config} title="Sign in" centered>
81
86
  <Card>
82
87
  <CardHeader>
83
88
  <CardTitle>Sign in</CardTitle>
@@ -2,14 +2,19 @@ import { Button } from '../components/ui/button';
2
2
  import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../components/ui/card';
3
3
  import { Input } from '../components/ui/input';
4
4
  import { Label } from '../components/ui/label';
5
+ import type { ClientConfig } from '@handoff/types/config';
6
+ import type { GetStaticProps } from 'next';
5
7
  import Head from 'next/head';
6
8
  import Link from 'next/link';
7
9
  import { useRouter } from 'next/router';
8
10
  import { FormEvent, useEffect, useState } from 'react';
9
11
  import { AuthShell } from '../components/Auth/AuthShell';
10
12
  import { authApiUrl, readApiError } from '../components/Auth/api';
13
+ import { getClientRuntimeConfig } from '../components/util';
11
14
 
12
- export default function ResetPasswordPage() {
15
+ export const getStaticProps: GetStaticProps = async () => ({ props: { config: getClientRuntimeConfig() } });
16
+
17
+ export default function ResetPasswordPage({ config }: { config: ClientConfig }) {
13
18
  const router = useRouter();
14
19
  const [token, setToken] = useState('');
15
20
  const [purpose, setPurpose] = useState<'invite' | 'reset'>('reset');
@@ -87,7 +92,7 @@ export default function ResetPasswordPage() {
87
92
  };
88
93
 
89
94
  return (
90
- <AuthShell title={token ? 'Set a new password' : 'Reset password'} centered>
95
+ <AuthShell config={config} title={token ? 'Set a new password' : 'Reset password'} centered>
91
96
  <Head>
92
97
  <meta name="referrer" content="no-referrer" />
93
98
  </Head>
@@ -96,9 +96,6 @@ export const getStaticProps = async (context) => {
96
96
  const previousComponent = sameGroupComponents[groupIndex - 1] ?? null;
97
97
  const nextComponent = sameGroupComponents[groupIndex + 1] ?? null;
98
98
 
99
- const fallbackTitle = componentData.title || startCase(component as string);
100
- const fallbackMetaTitle = `${fallbackTitle}${config?.app?.client ? ` | ${config.app.client} Design System` : ''}`;
101
-
102
99
  return {
103
100
  props: {
104
101
  id: component,
@@ -110,7 +107,7 @@ export const getStaticProps = async (context) => {
110
107
  ...componentData,
111
108
  title: componentData.title || docs.metadata.title || startCase(component as string),
112
109
  description: componentData.description,
113
- metaTitle: docs.metadata.metaTitle || fallbackMetaTitle,
110
+ metaTitle: docs.metadata.metaTitle ?? '',
114
111
  metaDescription: docs.metadata.metaDescription || componentData.description,
115
112
  image: docs.metadata.image || 'hero-brand-assets',
116
113
  },
@@ -256,7 +253,7 @@ const GenericComponentPage = ({ menu, metadata, current, id, config, componentHo
256
253
  </Button>
257
254
  )}
258
255
  <Drawer direction="right">
259
- <DrawerTrigger>
256
+ <DrawerTrigger asChild>
260
257
  <Button variant="outline" size={'sm'} className="font-normal [&_svg]:size-3!">
261
258
  API Reference
262
259
  </Button>
@@ -107,20 +107,20 @@ const ComponentsPage = ({ content, menu, metadata, current, config }: ComponentP
107
107
  <div className="mt-3 flex flex-row justify-between gap-3">
108
108
  <p className="text-lg leading-relaxed text-gray-600 dark:text-gray-300">{metadata.description}</p>
109
109
  <Drawer direction="right">
110
- <DrawerTrigger>
111
- <TooltipProvider>
112
- <Tooltip>
113
- <TooltipTrigger asChild>
110
+ <TooltipProvider>
111
+ <Tooltip>
112
+ <TooltipTrigger asChild>
113
+ <DrawerTrigger asChild>
114
114
  <Button variant="outline">
115
115
  API <Webhook strokeWidth={1.5} />
116
116
  </Button>
117
- </TooltipTrigger>
118
- <TooltipContent side="left">
119
- <Badge>{apiUrl}</Badge>
120
- </TooltipContent>
121
- </Tooltip>
122
- </TooltipProvider>
123
- </DrawerTrigger>
117
+ </DrawerTrigger>
118
+ </TooltipTrigger>
119
+ <TooltipContent side="left">
120
+ <Badge>{apiUrl}</Badge>
121
+ </TooltipContent>
122
+ </Tooltip>
123
+ </TooltipProvider>
124
124
  <DrawerContent>
125
125
  <div className="mx-5 w-full max-w-lg">
126
126
  <DrawerHeader>
@@ -130,7 +130,7 @@ export const getStaticProps = async (context: { params: IParams }) => {
130
130
  metadata: {
131
131
  title: fallbackTitle,
132
132
  description: patternData?.description || '',
133
- metaTitle: `${fallbackTitle}${config?.app?.client ? ` | ${config.app.client} Design System` : ''}`,
133
+ metaTitle: '',
134
134
  metaDescription: patternData?.description || '',
135
135
  },
136
136
  previousPattern,
@@ -189,7 +189,7 @@ const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nex
189
189
 
190
190
  if (patternNotFound) {
191
191
  return (
192
- <Layout config={config} menu={menu} current={current} metadata={{ ...metadata, title: '404 - Page Not Found', metaTitle: '404 - Page Not Found' }}>
192
+ <Layout config={config} menu={menu} current={current} metadata={{ ...metadata, title: 'Page Not Found', metaTitle: '' }}>
193
193
  <div className="flex min-h-[60vh] flex-col items-center justify-center">
194
194
  <NotFound />
195
195
  </div>
@@ -70,14 +70,10 @@ export const getStaticProps = async (context) => {
70
70
  description = componentData?.description;
71
71
  }
72
72
 
73
- const fallbackTitle = displayName || startCase(componentSlug);
74
- const fallbackMetaTitle = `${fallbackTitle}${config?.app?.client ? ` | ${config.app.client} Design System` : ''}`;
75
-
76
73
  markdownProps.metadata = {
77
74
  ...markdownProps.metadata,
78
75
  title: displayName || markdownProps.metadata.title || startCase(componentSlug),
79
76
  description: markdownProps.metadata.description || description || '',
80
- metaTitle: markdownProps.metadata.metaTitle || fallbackMetaTitle,
81
77
  metaDescription: markdownProps.metadata.metaDescription || description || '',
82
78
  };
83
79
 
@@ -300,14 +300,14 @@ const initializeProjectApp = async (handoff: Handoff, options: InitializeProject
300
300
  const escapedMcpEnabled = escapeForSingleQuotedJsString(String(isMcpEnabled(handoff.config)));
301
301
  const escapedRegistryDriver = escapeForSingleQuotedJsString(resolveRegistryDriver(handoff.config));
302
302
  const escapedDatabaseUrlEnv = escapeForSingleQuotedJsString(resolveDatabaseUrlEnv(handoff.config));
303
- // Asset storage selection baked (adapter + module + options JSON with `{ $env }` references).
303
+ // Asset storage selection baked (provider + module + options JSON with `{ $env }` references).
304
304
  const assetStorage = resolveAssetStorageFromConfig(handoff.config);
305
- const escapedAssetStorageAdapter = escapeForSingleQuotedJsString(assetStorage.adapterKind);
305
+ const escapedAssetStorageProvider = escapeForSingleQuotedJsString(assetStorage.provider);
306
306
  const escapedAssetStorageModule = escapeForSingleQuotedJsString(assetStorage.module ?? '');
307
307
  const escapedAssetStorageMaxInline = escapeForSingleQuotedJsString(String(assetStorage.maxInlineBytes));
308
308
  const escapedAssetStorageOptions = escapeForSingleQuotedJsString(JSON.stringify(assetStorage.options ?? {}));
309
309
  const email = resolveEmailFromConfig(handoff.config);
310
- const escapedEmailProvider = escapeForSingleQuotedJsString(email.provider);
310
+ const escapedEmailProvider = escapeForSingleQuotedJsString(email.provider ?? '');
311
311
  const escapedEmailFrom = escapeForSingleQuotedJsString(email.from ?? '');
312
312
  const escapedEmailModule = escapeForSingleQuotedJsString(email.module ?? '');
313
313
  const escapedEmailOptions = escapeForSingleQuotedJsString(JSON.stringify(email.options));
@@ -330,7 +330,7 @@ const initializeProjectApp = async (handoff: Handoff, options: InitializeProject
330
330
  '%HANDOFF_MCP_ENABLED%': escapedMcpEnabled,
331
331
  '%HANDOFF_REGISTRY_DRIVER%': escapedRegistryDriver,
332
332
  '%HANDOFF_REGISTRY_DATABASE_URL_ENV%': escapedDatabaseUrlEnv,
333
- '%HANDOFF_ASSET_STORAGE_ADAPTER%': escapedAssetStorageAdapter,
333
+ '%HANDOFF_ASSET_STORAGE_PROVIDER%': escapedAssetStorageProvider,
334
334
  '%HANDOFF_ASSET_STORAGE_MODULE%': escapedAssetStorageModule,
335
335
  '%HANDOFF_ASSET_STORAGE_MAX_INLINE_BYTES%': escapedAssetStorageMaxInline,
336
336
  '%HANDOFF_ASSET_STORAGE_OPTIONS%': escapedAssetStorageOptions,
@@ -474,7 +474,7 @@ merged over the list this build baked in, keyed by \`id\`. Use it to add or repo
474
474
  without rebuilding.
475
475
  `;
476
476
 
477
- const assetStorageEnvs = assetStorage.adapterKind === 'custom' ? envReferenceNames(assetStorage.options) : [];
477
+ const assetStorageEnvs = assetStorage.provider === 'custom' ? envReferenceNames(assetStorage.options) : [];
478
478
  const emailEnvs = envReferenceNames(email.options);
479
479
  const emailSection = !email.from
480
480
  ? 'Email delivery is off because `runtime.registry.email.from` is not set. Invitation links are shown once to an administrator for manual delivery.'
@@ -498,7 +498,7 @@ and \`.next/static/\` already copied alongside so the server serves them.
498
498
  - \`${databaseUrlEnv}\` — PostgreSQL/Neon connection string, read at request time.
499
499
  - \`AUTH_SECRET\` — a long, random secret used to sign browser sessions.
500
500
  - \`AUTH_URL\` — the canonical public registry URL, including the configured base path.
501
- ${assetStorageEnvs.map((name) => `- \`${name}\` — read by the custom asset storage adapter at request time.\n`).join('')}
501
+ ${assetStorageEnvs.map((name) => `- \`${name}\` — read by the custom asset storage provider at request time.\n`).join('')}
502
502
  ${emailSection}
503
503
  ${aiSection}
504
504
  ## Database migrations
@@ -569,8 +569,8 @@ For multi-instance self-hosting, front \`/_next/static\` with a CDN and configur
569
569
 
570
570
  ## Deploying to Vercel
571
571
 
572
- Vercel does **not** use \`output: 'standalone'\` — it ignores that setting and builds with its own
573
- adapter, so this pre-built bundle is **not** the artifact you deploy there. For Vercel, deploy the
572
+ Vercel does **not** use \`output: 'standalone'\` — it ignores that setting and runs its
573
+ own build, so this pre-built bundle is **not** the artifact you deploy there. For Vercel, deploy the
574
574
  registry app via Vercel's Git/CLI source build (it produces and serves the dynamic app itself) and
575
575
  supply the same environment variables above as project env vars. Use this standalone bundle for
576
576
  containers, custom Node servers, and other non-Vercel hosts.
@@ -635,7 +635,7 @@ const getCustomServerModules = (handoff: Handoff): string[] => {
635
635
  const assetStorage = resolveAssetStorageFromConfig(handoff.config);
636
636
  const email = resolveEmailFromConfig(handoff.config);
637
637
  const modules = [
638
- assetStorage.adapterKind === 'custom' ? assetStorage.module : undefined,
638
+ assetStorage.provider === 'custom' ? assetStorage.module : undefined,
639
639
  email.provider === 'custom' ? email.module : undefined,
640
640
  ...resolveAiFromConfig(handoff.config).connections.map((connection) => connection.module),
641
641
  ];
@@ -667,7 +667,7 @@ const copyCustomServerModules = async (handoff: Handoff, tracingRoot: string, en
667
667
  }
668
668
  if ([...fileList].some((file) => file.split(path.sep).join('/').endsWith('node_modules/handoff-app/dist/index.js'))) {
669
669
  throw new HandoffBuildError(
670
- `The custom server module "${module}" imports "handoff-app", which loads the whole CLI. Import the define helpers from "handoff-app/define".`
670
+ `The custom server module "${module}" imports "handoff-app", which loads the whole CLI. Import the define helpers from "handoff-app/providers".`
671
671
  );
672
672
  }
673
673
  fileList.forEach((file) => files.add(file));
@@ -64,7 +64,7 @@ const buildServerRuntimeConfig = (config: ResolvedConfig, modeOverride?: Runtime
64
64
  databaseUrlEnv: resolveDatabaseUrlEnv(config),
65
65
  },
66
66
  assetStorage: {
67
- adapter: assetStorage.adapterKind,
67
+ provider: assetStorage.provider,
68
68
  module: assetStorage.module,
69
69
  maxInlineBytes: assetStorage.maxInlineBytes,
70
70
  options: assetStorage.options,
@@ -5,7 +5,6 @@ import mergeWith from 'lodash/mergeWith';
5
5
  import omit from 'lodash/omit';
6
6
  import { createRequire } from 'module';
7
7
  import path from 'path';
8
- import { DEFAULT_EMAIL_PROVIDER } from '../registry/email/resolve';
9
8
  import { Config, HandoffEmailConfig, ResolvedConfig } from '../types/config';
10
9
  import { Logger } from '../utils/logger';
11
10
  import { resolveWorkingPath } from '../utils/path';
@@ -240,7 +239,7 @@ const replaceValues = (destination: unknown, source: unknown) => {
240
239
  * the deep merge never mixes the settings of two providers.
241
240
  */
242
241
  const resetEmailOnProviderChange = (layers: Partial<Config>[]): Partial<Config>[] => {
243
- let provider: string = DEFAULT_EMAIL_PROVIDER;
242
+ let provider: string | undefined;
244
243
  let start = 0;
245
244
  layers.forEach((layer, index) => {
246
245
  const next = layer.runtime?.registry?.email?.provider;
package/src/index.ts CHANGED
@@ -697,13 +697,13 @@ export type {
697
697
  SourceFormat,
698
698
  RendererKind,
699
699
  } from './catalog';
700
- export { defineAssetStorage } from './registry/asset-storage/define';
700
+ export { defineStorageProvider } from './registry/asset-storage/define';
701
701
  export type {
702
- AssetStorage,
703
- AssetStorageContext,
704
- AssetStorageFactory,
705
- AssetStorageInput,
706
- AssetStorageReadResult,
702
+ StorageProvider,
703
+ StorageProviderContext,
704
+ StorageProviderFactory,
705
+ StorageInput,
706
+ StorageReadResult,
707
707
  AssetUpload,
708
708
  AssetUploadInput,
709
709
  } from './registry/asset-storage/types';
package/src/nav/index.ts CHANGED
@@ -89,6 +89,6 @@ export interface NavData {
89
89
  export { getNavData } from './resolver';
90
90
  export type { GetNavDataInput } from './resolver';
91
91
  export { getWorkspaceNavData } from './workspace';
92
- export type { WorkspaceNavAdapterOptions, WorkspaceNavLoaders } from './workspace';
92
+ export type { WorkspaceNavOptions, WorkspaceNavLoaders } from './workspace';
93
93
  export { getRegistryNavData } from './registry';
94
- export type { RegistryNavAdapterOptions, RegistryNavRecords } from './registry';
94
+ export type { RegistryNavOptions, RegistryNavRecords } from './registry';
@@ -9,7 +9,7 @@ export interface RegistryNavRecords {
9
9
  tokenSets: NavTokenSet[];
10
10
  }
11
11
 
12
- export interface RegistryNavAdapterOptions {
12
+ export interface RegistryNavOptions {
13
13
  shell: SectionLink[];
14
14
  load: NavLoad;
15
15
  basePath?: string;
@@ -18,8 +18,8 @@ export interface RegistryNavAdapterOptions {
18
18
 
19
19
  const emptyRecords = (): RegistryNavRecords => ({ components: [], patterns: [], pages: [], tokenSets: [] });
20
20
 
21
- /** Registry adapter: shell-only initial load, or one best-effort runtime-record fetch on refresh. */
22
- export const getRegistryNavData = async (options: RegistryNavAdapterOptions): Promise<NavData> => {
21
+ /** Registry nav data: shell-only initial load, or one best-effort runtime-record fetch on refresh. */
22
+ export const getRegistryNavData = async (options: RegistryNavOptions): Promise<NavData> => {
23
23
  let records = emptyRecords();
24
24
  if (options.load === 'refresh') {
25
25
  try {
@@ -14,7 +14,7 @@ export interface WorkspaceNavLoaders {
14
14
  tokenSets: () => Awaitable<NavTokenSet[]>;
15
15
  }
16
16
 
17
- export interface WorkspaceNavAdapterOptions {
17
+ export interface WorkspaceNavOptions {
18
18
  docRoot: string;
19
19
  workingPagesDir?: string;
20
20
  basePath?: string;
@@ -43,8 +43,8 @@ const markdownTitle = (id: string, docRoot: string, workingPagesDir?: string): s
43
43
  return undefined;
44
44
  };
45
45
 
46
- /** Workspace/static adapter: live filesystem shell plus best-effort local snapshot loaders. */
47
- export const getWorkspaceNavData = async (options: WorkspaceNavAdapterOptions): Promise<NavData> => {
46
+ /** Workspace/static nav data: live filesystem shell plus best-effort local snapshot loaders. */
47
+ export const getWorkspaceNavData = async (options: WorkspaceNavOptions): Promise<NavData> => {
48
48
  const [loadedComponents, patterns, pages, loadedTokenSets] = await Promise.all([
49
49
  safeLoad(options.loaders.components),
50
50
  safeLoad(options.loaders.patterns),
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Define helpers for custom server modules. This entry point has no runtime dependencies, so a module
3
- * that imports it does not pull the CLI into the registry bundle.
2
+ * Define helpers for custom server modules: storage, email and AI providers. This entry point has no
3
+ * runtime dependencies, so a module that imports it does not pull the CLI into the registry bundle.
4
4
  */
5
5
  export { defineAiProvider } from './ai/define';
6
6
  export type { AiProvider, AiProviderContext, AiProviderFactory } from './ai/types';
7
- export { defineAssetStorage } from './registry/asset-storage/define';
7
+ export { defineStorageProvider } from './registry/asset-storage/define';
8
8
  export type {
9
- AssetStorage,
10
- AssetStorageContext,
11
- AssetStorageFactory,
12
- AssetStorageInput,
13
- AssetStorageReadResult,
9
+ StorageProvider,
10
+ StorageProviderContext,
11
+ StorageProviderFactory,
12
+ StorageInput,
13
+ StorageReadResult,
14
14
  AssetUpload,
15
15
  AssetUploadInput,
16
16
  } from './registry/asset-storage/types';
@@ -1,15 +1,15 @@
1
1
  /**
2
- * Typed identity helper for custom asset storage adapters (mirrors `defineCatalogItem`): a consumer
2
+ * Typed identity helper for custom asset storage providers (mirrors `defineCatalogItem`): a consumer
3
3
  * default-exports its result from a **server-only** module and points
4
4
  * `runtime.registry.assetStorage.module` at it. It must be its own module (not an inline
5
5
  * `defineConfig` function) so the build can trace it into the deployed registry. The default export
6
- * may be the adapter object, or a factory `(context) => AssetStorage` when it needs its options/env.
6
+ * may be the provider object, or a factory `(context) => StorageProvider` when it needs its options/env.
7
7
  */
8
8
 
9
- import type { AssetStorage, AssetStorageFactory } from './types';
9
+ import type { StorageProvider, StorageProviderFactory } from './types';
10
10
 
11
- export function defineAssetStorage(adapter: AssetStorage): AssetStorage;
12
- export function defineAssetStorage(factory: AssetStorageFactory): AssetStorageFactory;
13
- export function defineAssetStorage(input: AssetStorage | AssetStorageFactory): AssetStorage | AssetStorageFactory {
11
+ export function defineStorageProvider(provider: StorageProvider): StorageProvider;
12
+ export function defineStorageProvider(factory: StorageProviderFactory): StorageProviderFactory;
13
+ export function defineStorageProvider(input: StorageProvider | StorageProviderFactory): StorageProvider | StorageProviderFactory {
14
14
  return input;
15
15
  }
@@ -2,54 +2,50 @@
2
2
  * Asset storage settings resolution.
3
3
  *
4
4
  * `runtime.registry.assetStorage` selects the active provider for **new** uploads: the built-in
5
- * `database` inline default or a `custom` module. The build bakes the adapter, the module location,
5
+ * `database` inline default or a `custom` module. The build bakes the provider, the module location,
6
6
  * and the options. An option literal is baked as it is. An environment reference stays `{ $env }`
7
- * and is resolved at request time, before the custom adapter factory runs.
7
+ * and is resolved at request time, before the custom provider factory runs.
8
8
  */
9
9
 
10
10
  import { HandoffConfigError } from '../../config/errors';
11
11
  import type { ResolvedConfig } from '../../types/config';
12
12
  import type { AssetStorageProvider } from '../db/schema';
13
13
 
14
- /** Which adapter implementation is active. Recorded on each blob row as its storage provider. */
15
- export type AssetStorageAdapterKind = AssetStorageProvider;
16
-
17
14
  /** The raw, authored settings block (a structural copy of the config type, resolvable from bake). */
18
15
  export interface AssetStorageSettings {
19
- adapter?: AssetStorageAdapterKind;
16
+ provider?: AssetStorageProvider;
20
17
  module?: string;
21
18
  maxInlineBytes?: number;
22
19
  options?: Record<string, unknown>;
23
20
  }
24
21
 
25
- const ADAPTERS: readonly AssetStorageAdapterKind[] = ['database', 'custom'];
22
+ const PROVIDERS: readonly AssetStorageProvider[] = ['database', 'custom'];
26
23
 
27
- /** Default adapter when none is configured. */
28
- export const DEFAULT_ASSET_STORAGE_ADAPTER: AssetStorageAdapterKind = 'database';
24
+ export const DEFAULT_ASSET_STORAGE_PROVIDER: AssetStorageProvider = 'database';
29
25
 
30
26
  /** Default max bytes kept inline in Postgres `bytea` (4 MB, under Vercel's ~4.5 MB function limit). */
31
27
  export const DEFAULT_MAX_INLINE_BYTES = 4 * 1024 * 1024;
32
28
 
33
29
  /** Fully resolved asset storage settings. */
34
30
  export interface ResolvedAssetStorage {
35
- /** The active adapter implementation. */
36
- adapterKind: AssetStorageAdapterKind;
37
- /** For `custom`: the server-only adapter module path. */
31
+ /** Recorded on each new blob row as its storage provider. */
32
+ provider: AssetStorageProvider;
33
+ /** For `custom`: the server-only provider module path. */
38
34
  module?: string;
39
35
  /** For `database`: the inline-content size ceiling. */
40
36
  maxInlineBytes: number;
41
- /** Adapter options: literals and `{ $env }` references, never secret values. */
37
+ /** Provider options: literals and `{ $env }` references, never secret values. */
42
38
  options: Record<string, unknown>;
43
39
  }
44
40
 
45
41
  /** Resolve raw asset storage settings into a fully-defaulted shape. */
46
42
  export const resolveAssetStorageSettings = (settings: AssetStorageSettings | null | undefined): ResolvedAssetStorage => {
47
- const adapterKind = settings?.adapter ?? DEFAULT_ASSET_STORAGE_ADAPTER;
43
+ const provider = settings?.provider ?? DEFAULT_ASSET_STORAGE_PROVIDER;
48
44
  const options = settings?.options ?? {};
49
45
  const maxInlineBytes =
50
46
  typeof settings?.maxInlineBytes === 'number' && settings.maxInlineBytes > 0 ? settings.maxInlineBytes : DEFAULT_MAX_INLINE_BYTES;
51
47
 
52
- return { adapterKind, module: settings?.module?.trim() || undefined, maxInlineBytes, options };
48
+ return { provider, module: settings?.module?.trim() || undefined, maxInlineBytes, options };
53
49
  };
54
50
 
55
51
  /** Resolve and validate the asset storage block from a loaded config (CLI/build side). An unusable block fails the build. */
@@ -59,17 +55,17 @@ export const resolveAssetStorageFromConfig = (config: ResolvedConfig | null | un
59
55
  throw new HandoffConfigError(`Config "runtime.registry.assetStorage": ${problem}`);
60
56
  };
61
57
 
62
- const adapter = settings?.adapter ?? DEFAULT_ASSET_STORAGE_ADAPTER;
63
- if (!ADAPTERS.includes(adapter as AssetStorageAdapterKind)) {
64
- fail(`unknown adapter ${JSON.stringify(adapter)}. Use "database" or "custom".`);
58
+ const provider = settings?.provider ?? DEFAULT_ASSET_STORAGE_PROVIDER;
59
+ if (!PROVIDERS.includes(provider as AssetStorageProvider)) {
60
+ fail(`unknown provider ${JSON.stringify(provider)}. Use "database" or "custom".`);
65
61
  }
66
62
  const options = settings?.options;
67
63
  if (options !== undefined && (options === null || typeof options !== 'object' || Array.isArray(options))) {
68
64
  fail('"options" must be an object.');
69
65
  }
70
66
  const module = settings?.module;
71
- if (adapter === 'custom' && (typeof module !== 'string' || !module.trim())) {
72
- fail('adapter "custom" needs a "module" that default-exports a defineAssetStorage() result.');
67
+ if (provider === 'custom' && (typeof module !== 'string' || !module.trim())) {
68
+ fail('provider "custom" needs a "module" that default-exports a defineStorageProvider() result.');
73
69
  }
74
70
  const maxInlineBytes = settings?.maxInlineBytes;
75
71
  if (maxInlineBytes !== undefined && (typeof maxInlineBytes !== 'number' || !Number.isInteger(maxInlineBytes) || maxInlineBytes <= 0)) {