handoff-app 2.0.0-11 → 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 (141) 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 +16 -16
  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 +8 -9
  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/mcp/components.ts +33 -29
  83. package/src/app/lib/mcp/pages.ts +26 -19
  84. package/src/app/lib/mcp/server.ts +33 -9
  85. package/src/app/lib/mcp/tokens.ts +22 -16
  86. package/src/app/lib/mcp/tool.ts +19 -0
  87. package/src/app/lib/registry-api/asset-transfer.ts +25 -25
  88. package/src/app/lib/server-module.ts +1 -1
  89. package/src/app/lib/utils.ts +7 -0
  90. package/src/app/next.config.mjs +2 -2
  91. package/src/app/pages/[...slug]/index.tsx +2 -1
  92. package/src/app/pages/account/ai.tsx +1 -1
  93. package/src/app/pages/account/index.tsx +1 -1
  94. package/src/app/pages/account/tokens.tsx +1 -1
  95. package/src/app/pages/account/users.tsx +1 -1
  96. package/src/app/pages/api/docs/nav.json.ts +1 -1
  97. package/src/app/pages/api/mcp.ts +6 -0
  98. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
  99. package/src/app/pages/cli/device.tsx +1 -1
  100. package/src/app/pages/foundations/colors.tsx +9 -8
  101. package/src/app/pages/foundations/index.tsx +3 -3
  102. package/src/app/pages/index.tsx +2 -2
  103. package/src/app/pages/login.tsx +10 -5
  104. package/src/app/pages/reset-password.tsx +7 -2
  105. package/src/app/pages/system/component/[component]/index.tsx +2 -5
  106. package/src/app/pages/system/index.tsx +11 -11
  107. package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
  108. package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
  109. package/src/app-builder/build.ts +12 -12
  110. package/src/app-builder/client-config.ts +1 -1
  111. package/src/config/loader.ts +1 -2
  112. package/src/index.ts +6 -6
  113. package/src/nav/index.ts +2 -2
  114. package/src/nav/registry.ts +3 -3
  115. package/src/nav/workspace.ts +3 -3
  116. package/src/{define.ts → providers.ts} +8 -8
  117. package/src/registry/asset-storage/define.ts +6 -6
  118. package/src/registry/asset-storage/resolve.ts +16 -20
  119. package/src/registry/asset-storage/types.ts +12 -12
  120. package/src/registry/db/schema.ts +3 -3
  121. package/src/registry/email/define.ts +1 -1
  122. package/src/registry/email/resolve.ts +23 -23
  123. package/src/registry/email/types.ts +1 -1
  124. package/src/store/registry.ts +7 -7
  125. package/src/types/config.ts +4 -14
  126. package/.editorconfig +0 -19
  127. package/.eslintrc.json +0 -4
  128. package/.prettierignore +0 -0
  129. package/.prettierrc.json +0 -8
  130. package/.vscode/settings.json +0 -17
  131. package/dist/define.d.ts +0 -10
  132. package/dist/registry/email/adapters/resend.d.ts +0 -7
  133. package/dist/registry/email/adapters/resend.js +0 -35
  134. package/src/app/components/old/Header.tsx +0 -89
  135. package/src/app/pages/assets/fonts.tsx +0 -129
  136. package/src/app/pages/assets/index.tsx +0 -91
  137. package/src/app/pages/assets/logos.tsx +0 -75
  138. package/src/registry/email/adapters/resend.ts +0 -22
  139. /package/dist/registry/email/{adapters → providers}/smtp.d.ts +0 -0
  140. /package/dist/registry/email/{adapters → providers}/smtp.js +0 -0
  141. /package/src/registry/email/{adapters → providers}/smtp.ts +0 -0
@@ -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.
@@ -582,7 +582,7 @@ containers, custom Node servers, and other non-Vercel hosts.
582
582
  * Resolve the set of npm packages the packaged registry app must be able to `require`/`import` at
583
583
  * runtime, given the configured database driver. `next`/`react`/`react-dom` run the server and React
584
584
  * runtime; `drizzle-orm` backs both the request-time DB client and the migration runner; the MCP SDK
585
- * (and its `zod` peer) is only needed when `/api/mcp/` is enabled; the driver package is
585
+ * is only needed when `/api/mcp/` is enabled; the driver package is
586
586
  * driver-specific (the Neon serverless driver also needs `ws` for its Node WebSocket transport); the
587
587
  * SMTP email provider needs `nodemailer`.
588
588
  */
@@ -592,7 +592,7 @@ const getRequiredRegistryRuntimeModules = (handoff: Handoff): string[] => {
592
592
  // The assistant runs the MCP tools in-process, so it needs the MCP SDK whether or not the HTTP
593
593
  // endpoint is served.
594
594
  if (isMcpEnabled(handoff.config) || ai.enabled) {
595
- base.push('@modelcontextprotocol/sdk', 'zod');
595
+ base.push('@modelcontextprotocol/sdk');
596
596
  }
597
597
  if (ai.enabled) {
598
598
  base.push('ai', '@ai-sdk/openai-compatible');
@@ -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)) {
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Server-only asset storage contract for custom {@link defineAssetStorage} object storage modules.
2
+ * Server-only asset storage contract for custom {@link defineStorageProvider} object storage modules.
3
3
  * The built-in `database` provider is the inline default: bytes live in the `asset_blobs.content`
4
- * bytea column, read/written directly by the registry, so it needs no adapter. Object providers
4
+ * bytea column, read/written directly by the registry, so it needs no module. Object providers
5
5
  * record a `storageRef` + provider id so reads resolve back through whichever provider stored the
6
6
  * blob.
7
7
  */
8
8
 
9
9
  /** Input for storing one content-addressed blob. */
10
- export interface AssetStorageInput {
10
+ export interface StorageInput {
11
11
  /** SHA-256 of the bytes (hex), the content identity, usable as a stable object key. */
12
12
  hash: string;
13
13
  bytes: Buffer;
@@ -19,7 +19,7 @@ export interface AssetStorageInput {
19
19
  * How a provider returns stored content: raw bytes, a readable stream, or a signed/redirect URL the
20
20
  * client can fetch directly (used to avoid routing large payloads through the serverless function).
21
21
  */
22
- export type AssetStorageReadResult =
22
+ export type StorageReadResult =
23
23
  | { kind: 'bytes'; bytes: Buffer; contentType?: string }
24
24
  | { kind: 'stream'; stream: NodeJS.ReadableStream; contentType?: string }
25
25
  | { kind: 'redirect'; url: string };
@@ -40,10 +40,10 @@ export interface AssetUpload {
40
40
  storageRef: string;
41
41
  }
42
42
 
43
- /** The pluggable storage adapter contract for object-backed asset providers. */
44
- export interface AssetStorage {
43
+ /** The pluggable storage contract for object-backed asset providers. */
44
+ export interface StorageProvider {
45
45
  /** Store one blob and return the reference to persist on its `asset_blobs` row. */
46
- put(input: AssetStorageInput): Promise<{ storageRef: string }>;
46
+ put(input: StorageInput): Promise<{ storageRef: string }>;
47
47
  /**
48
48
  * Return a signed URL so the CLI uploads the blob directly to storage instead of through the
49
49
  * registry, which avoids serverless request size limits. The URL must make storage reject bytes
@@ -51,18 +51,18 @@ export interface AssetStorage {
51
51
  */
52
52
  createUpload?(input: AssetUploadInput): Promise<AssetUpload>;
53
53
  /** Resolve stored content by its `storageRef`. */
54
- get(storageRef: string): Promise<AssetStorageReadResult>;
54
+ get(storageRef: string): Promise<StorageReadResult>;
55
55
  /** Delete stored content by its `storageRef`. Should be retryable/idempotent. */
56
56
  delete(storageRef: string): Promise<void>;
57
57
  }
58
58
 
59
- /** Context passed to a custom adapter factory: its config options and the process env. */
60
- export interface AssetStorageContext {
59
+ /** Context passed to a custom provider factory: its config options and the process env. */
60
+ export interface StorageProviderContext {
61
61
  /** `runtime.registry.assetStorage.options`, with each `fromEnv()` reference replaced by its value. */
62
62
  options: Record<string, unknown>;
63
63
  /** The process environment. */
64
64
  env: NodeJS.ProcessEnv;
65
65
  }
66
66
 
67
- /** A custom adapter may export the adapter directly or a factory that builds it from its context. */
68
- export type AssetStorageFactory = (context: AssetStorageContext) => AssetStorage | Promise<AssetStorage>;
67
+ /** A custom provider module may export the provider directly or a factory that builds it from its context. */
68
+ export type StorageProviderFactory = (context: StorageProviderContext) => StorageProvider | Promise<StorageProvider>;
@@ -30,7 +30,7 @@ import type { TokenSetKind } from '../tokens/sets';
30
30
 
31
31
  /**
32
32
  * PostgreSQL `bytea` column. Drizzle pg-core has no built-in binary type, so declare it once here.
33
- * Reads/writes are Node `Buffer`s. The default asset storage adapter keeps small/ordinary asset
33
+ * Reads/writes are Node `Buffer`s. The default asset storage provider keeps small/ordinary asset
34
34
  * bytes inline in this column; larger assets are offloaded to an object-storage `storageRef` instead.
35
35
  */
36
36
  const bytea = customType<{ data: Buffer; driverData: Buffer }>({
@@ -39,7 +39,7 @@ const bytea = customType<{ data: Buffer; driverData: Buffer }>({
39
39
  },
40
40
  });
41
41
 
42
- /** Where an asset blob's bytes live: inline in the database (`database`) or in the custom adapter (`custom`). */
42
+ /** Where an asset blob's bytes live: inline in the database (`database`) or in a custom provider (`custom`). */
43
43
  export type AssetStorageProvider = 'database' | 'custom';
44
44
  import type { ComponentListObject, PageListObject, PatternComponentEntry, PatternListObject } from '../../transformers/preview/types';
45
45
 
@@ -432,7 +432,7 @@ export const assets = pgTable(
432
432
  /**
433
433
  * Content-addressed binary blob store, shared across assets/collections (the `hash` is the blob
434
434
  * identity, so an unchanged file re-references the same row). Exactly one location is active per row:
435
- * inline `content` (bytea, default DB adapter) or an external `storageRef` resolved through the
435
+ * inline `content` (bytea, default `database` provider) or an external `storageRef` resolved through the
436
436
  * recorded `storageProvider`. Both nullable so DB- and object-backed blobs coexist.
437
437
  */
438
438
  export const assetBlobs = pgTable('asset_blobs', {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Typed identity helper for custom email providers (mirrors `defineAssetStorage`): a consumer
2
+ * Typed identity helper for custom email providers (mirrors `defineStorageProvider`): a consumer
3
3
  * default-exports its result from a **server-only** module and points `runtime.registry.email.module`
4
4
  * at it. It must be its own module (not an inline `defineConfig` function) so the build can trace it
5
5
  * into the deployed registry. The default export may be the provider object, or a factory
@@ -1,33 +1,29 @@
1
1
  /**
2
2
  * Registry email settings resolution.
3
3
  *
4
- * `runtime.registry.email.provider` selects the built-in `resend` or `smtp` provider, or a `custom`
5
- * module. The build bakes the provider, the module location, and the options. An option literal is
6
- * baked as it is. An environment reference stays `{ $env }` and is resolved at request time.
4
+ * `runtime.registry.email.provider` selects the built-in `smtp` provider or a `custom` module. Without
5
+ * an email block, email delivery is off. The build bakes the provider, the module location, and the
6
+ * options. An option literal is baked as it is. An environment reference stays `{ $env }` and is
7
+ * resolved at request time.
7
8
  */
8
9
 
9
10
  import { HandoffConfigError } from '../../config/errors';
10
11
  import type { ResolvedConfig } from '../../types/config';
11
12
 
12
- export type EmailProviderKind = 'resend' | 'smtp' | 'custom';
13
+ export type EmailProviderKind = 'smtp' | 'custom';
13
14
 
14
- export const DEFAULT_EMAIL_PROVIDER: EmailProviderKind = 'resend';
15
-
16
- /** Default env-var name holding the Resend API key. */
17
- export const DEFAULT_RESEND_API_KEY_ENV = 'RESEND_API_KEY';
18
-
19
- const PROVIDERS: readonly EmailProviderKind[] = ['resend', 'smtp', 'custom'];
15
+ const PROVIDERS: readonly EmailProviderKind[] = ['smtp', 'custom'];
20
16
 
21
17
  /** The options each built-in provider accepts. A custom provider accepts any. */
22
18
  const BUILT_IN_OPTIONS: Record<Exclude<EmailProviderKind, 'custom'>, readonly string[]> = {
23
- resend: ['apiKey'],
24
19
  smtp: ['host', 'port', 'secure', 'user', 'password'],
25
20
  };
26
21
 
27
22
  /** Email settings as they are baked: literals and `{ $env }` references, never secret values. */
28
23
  export interface EmailSettings {
29
24
  from?: string;
30
- provider: EmailProviderKind;
25
+ /** Unset when the config has no email block. */
26
+ provider?: EmailProviderKind;
31
27
  /** For `custom`: the server-only provider module path. */
32
28
  module?: string;
33
29
  options: Record<string, unknown>;
@@ -38,25 +34,30 @@ const isPlainRecord = (value: unknown): value is Record<string, unknown> =>
38
34
 
39
35
  /** Resolve and validate the email block from a loaded config (CLI/build side). An unusable block fails the build. */
40
36
  export const resolveEmailFromConfig = (config: ResolvedConfig | null | undefined): EmailSettings => {
41
- const email = config?.runtime?.registry?.email as Record<string, unknown> | undefined;
37
+ const email = config?.runtime?.registry?.email as unknown as Record<string, unknown> | undefined;
42
38
  const fail = (problem: string): never => {
43
39
  throw new HandoffConfigError(`Config "runtime.registry.email": ${problem}`);
44
40
  };
45
41
 
46
- const provider = (email?.provider ?? DEFAULT_EMAIL_PROVIDER) as EmailProviderKind;
42
+ if (email === undefined) return { options: {} };
43
+ const provider = email.provider as EmailProviderKind;
47
44
  if (!PROVIDERS.includes(provider)) {
48
- fail(`unknown provider ${JSON.stringify(provider)}. Use "resend", "smtp" or "custom".`);
45
+ fail(
46
+ email.provider === undefined
47
+ ? '"provider" is required. Use "smtp" or "custom".'
48
+ : `unknown provider ${JSON.stringify(provider)}. Use "smtp" or "custom".`
49
+ );
49
50
  }
50
- if (email?.options !== undefined && !isPlainRecord(email.options)) {
51
+ if (email.options !== undefined && !isPlainRecord(email.options)) {
51
52
  fail('"options" must be an object.');
52
53
  }
53
- const options = { ...((email?.options as Record<string, unknown> | undefined) ?? {}) };
54
- const module = typeof email?.module === 'string' ? email.module.trim() : undefined;
54
+ const options = { ...((email.options as Record<string, unknown> | undefined) ?? {}) };
55
+ const module = typeof email.module === 'string' ? email.module.trim() : undefined;
55
56
 
56
57
  if (provider === 'custom') {
57
58
  if (!module) fail('provider "custom" needs a "module" that default-exports a defineEmailProvider() result.');
58
59
  } else {
59
- if (email?.module !== undefined) fail(`"module" is used only by provider "custom", not "${provider}".`);
60
+ if (email.module !== undefined) fail(`"module" is used only by provider "custom", not "${provider}".`);
60
61
  const allowed = BUILT_IN_OPTIONS[provider];
61
62
  const unknown = Object.keys(options).filter((key) => !allowed.includes(key));
62
63
  if (unknown.length > 0) {
@@ -67,9 +68,8 @@ export const resolveEmailFromConfig = (config: ResolvedConfig | null | undefined
67
68
  }
68
69
  }
69
70
  if (provider === 'smtp' && options.host === undefined) fail('provider "smtp" needs "options.host".');
70
- if (provider === 'resend' && options.apiKey === undefined) options.apiKey = { $env: DEFAULT_RESEND_API_KEY_ENV };
71
71
 
72
- const from = typeof email?.from === 'string' ? email.from.trim() || undefined : undefined;
72
+ const from = typeof email.from === 'string' ? email.from.trim() || undefined : undefined;
73
73
  return { from, provider, ...(module ? { module } : {}), options };
74
74
  };
75
75
 
@@ -83,11 +83,11 @@ export const parseEmailSettings = (raw: { from?: unknown; provider?: unknown; mo
83
83
  options = {};
84
84
  }
85
85
  }
86
- const provider = PROVIDERS.find((kind) => kind === raw.provider) ?? DEFAULT_EMAIL_PROVIDER;
86
+ const provider = PROVIDERS.find((kind) => kind === raw.provider);
87
87
  const module = typeof raw.module === 'string' ? raw.module.trim() : '';
88
88
  return {
89
89
  from: typeof raw.from === 'string' ? raw.from.trim() || undefined : undefined,
90
- provider,
90
+ ...(provider ? { provider } : {}),
91
91
  ...(module ? { module } : {}),
92
92
  options: isPlainRecord(options) ? options : {},
93
93
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Server-only email provider contract. Handoff renders every message, and a provider only delivers it.
3
- * Resend and SMTP are built in. Anything else is a {@link defineEmailProvider} module.
3
+ * SMTP is built in. Anything else is a {@link defineEmailProvider} module.
4
4
  */
5
5
 
6
6
  /** One rendered message. */
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { orderPreviews } from '../catalog/previews';
18
18
  import { and, eq, inArray, or, sql, type SQLWrapper } from 'drizzle-orm';
19
- import type { AssetStorage, AssetStorageReadResult } from '../registry/asset-storage/types';
19
+ import type { StorageProvider, StorageReadResult } from '../registry/asset-storage/types';
20
20
  import type { RegistryDatabase } from '../registry/db/client';
21
21
  import {
22
22
  assetBlobs,
@@ -53,11 +53,11 @@ import type {
53
53
  export interface RegistryStoreContext {
54
54
  db: RegistryDatabase;
55
55
  /**
56
- * Resolve the {@link AssetStorage} adapter for a blob's recorded provider (or `null` for the inline
56
+ * Resolve the {@link StorageProvider} for a blob's recorded provider (or `null` for the inline
57
57
  * `database` provider). Injected by the app runtime so `src/store` stays free of app-side imports;
58
58
  * when omitted, only inline DB-backed asset content is resolvable.
59
59
  */
60
- resolveAssetAdapter?: (provider: AssetStorageProvider) => Promise<AssetStorage | null>;
60
+ resolveStorageProvider?: (provider: AssetStorageProvider) => Promise<StorageProvider | null>;
61
61
  }
62
62
 
63
63
  /** A persisted text-file row, as selected from `component_files`/`pattern_files`. */
@@ -422,14 +422,14 @@ export class RegistryAssetStore implements AssetStore {
422
422
  }
423
423
 
424
424
  // Object-backed - resolve through the provider that stored it.
425
- if (!row.storageRef || !this.context.resolveAssetAdapter) {
425
+ if (!row.storageRef || !this.context.resolveStorageProvider) {
426
426
  return null;
427
427
  }
428
- const adapter = await this.context.resolveAssetAdapter(row.storageProvider);
429
- if (!adapter) {
428
+ const storage = await this.context.resolveStorageProvider(row.storageProvider);
429
+ if (!storage) {
430
430
  return null;
431
431
  }
432
- const result: AssetStorageReadResult = await adapter.get(row.storageRef);
432
+ const result: StorageReadResult = await storage.get(row.storageRef);
433
433
  if (result.kind === 'redirect') {
434
434
  return { ...base, redirectUrl: result.url };
435
435
  }