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.
- package/{Changelog.md → CHANGELOG.md} +84 -0
- package/{License.md → LICENSE.md} +1 -1
- package/README.md +760 -801
- package/RECIPES.md +975 -0
- 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/define.d.ts +1 -1
- package/dist/ai/define.js +1 -1
- package/dist/ai/types.d.ts +1 -1
- package/dist/ai/types.js +1 -1
- package/dist/app-builder/build.js +14 -14
- package/dist/app-builder/client-config.js +1 -1
- package/dist/config/loader.js +1 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- 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/providers.d.ts +10 -0
- package/dist/{define.js → providers.js} +4 -4
- 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 +8 -11
- package/dist/registry/asset-storage/resolve.js +12 -13
- package/dist/registry/asset-storage/types.d.ts +12 -12
- package/dist/registry/asset-storage/types.js +2 -2
- package/dist/registry/db/schema.d.ts +2 -2
- package/dist/registry/db/schema.js +2 -2
- package/dist/registry/email/define.d.ts +1 -1
- package/dist/registry/email/define.js +1 -1
- package/dist/registry/email/resolve.d.ts +7 -8
- package/dist/registry/email/resolve.js +21 -23
- package/dist/registry/email/types.d.ts +1 -1
- package/dist/registry/email/types.js +1 -1
- package/dist/store/registry.d.ts +3 -3
- package/dist/store/registry.js +4 -4
- package/dist/types/config.d.ts +4 -13
- package/docs/cli.md +275 -54
- package/package.json +7 -7
- package/src/ai/define.ts +1 -1
- package/src/ai/types.ts +1 -1
- package/src/app/components/Auth/AccountLayout.tsx +1 -1
- package/src/app/components/Auth/AuthShell.tsx +10 -4
- package/src/app/components/Foundations/ColorGrid.tsx +13 -14
- package/src/app/components/Layout/Main.tsx +2 -2
- package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
- package/src/app/components/Navigation/MobileNav.tsx +0 -2
- package/src/app/components/util/index.ts +0 -11
- package/src/app/lib/asset-storage.ts +25 -25
- package/src/app/lib/auth/email.ts +4 -5
- 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 +8 -8
- package/src/app/lib/registry-api/asset-transfer.ts +25 -25
- package/src/app/lib/server-module.ts +1 -1
- package/src/app/lib/utils.ts +7 -0
- package/src/app/next.config.mjs +2 -2
- package/src/app/pages/[...slug]/index.tsx +2 -1
- package/src/app/pages/account/ai.tsx +1 -1
- package/src/app/pages/account/index.tsx +1 -1
- package/src/app/pages/account/tokens.tsx +1 -1
- package/src/app/pages/account/users.tsx +1 -1
- package/src/app/pages/api/docs/nav.json.ts +1 -1
- package/src/app/pages/api/mcp.ts +6 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
- package/src/app/pages/cli/device.tsx +1 -1
- 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/login.tsx +10 -5
- package/src/app/pages/reset-password.tsx +7 -2
- 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/build.ts +10 -10
- package/src/app-builder/client-config.ts +1 -1
- package/src/config/loader.ts +1 -2
- package/src/index.ts +6 -6
- package/src/nav/index.ts +2 -2
- package/src/nav/registry.ts +3 -3
- package/src/nav/workspace.ts +3 -3
- package/src/{define.ts → providers.ts} +8 -8
- package/src/registry/asset-storage/define.ts +6 -6
- package/src/registry/asset-storage/resolve.ts +16 -20
- package/src/registry/asset-storage/types.ts +12 -12
- package/src/registry/db/schema.ts +3 -3
- package/src/registry/email/define.ts +1 -1
- package/src/registry/email/resolve.ts +23 -23
- package/src/registry/email/types.ts +1 -1
- package/src/store/registry.ts +7 -7
- package/src/types/config.ts +4 -14
- 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/define.d.ts +0 -10
- package/dist/registry/email/adapters/resend.d.ts +0 -7
- package/dist/registry/email/adapters/resend.js +0 -35
- package/src/app/components/old/Header.tsx +0 -89
- 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/registry/email/adapters/resend.ts +0 -22
- /package/dist/registry/email/{adapters → providers}/smtp.d.ts +0 -0
- /package/dist/registry/email/{adapters → providers}/smtp.js +0 -0
- /package/src/registry/email/{adapters → providers}/smtp.ts +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Server-only asset storage contract for custom {@link
|
|
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
|
|
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
|
|
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
|
|
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
|
|
44
|
-
export interface
|
|
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:
|
|
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<
|
|
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
|
|
60
|
-
export interface
|
|
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
|
|
68
|
-
export type
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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 `
|
|
5
|
-
*
|
|
6
|
-
* baked as it is. An environment reference stays `{ $env }` and is
|
|
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 = '
|
|
13
|
+
export type EmailProviderKind = 'smtp' | 'custom';
|
|
13
14
|
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
if (email === undefined) return { options: {} };
|
|
43
|
+
const provider = email.provider as EmailProviderKind;
|
|
47
44
|
if (!PROVIDERS.includes(provider)) {
|
|
48
|
-
fail(
|
|
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
|
|
51
|
+
if (email.options !== undefined && !isPlainRecord(email.options)) {
|
|
51
52
|
fail('"options" must be an object.');
|
|
52
53
|
}
|
|
53
|
-
const options = { ...((email
|
|
54
|
-
const module = typeof email
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
*
|
|
3
|
+
* SMTP is built in. Anything else is a {@link defineEmailProvider} module.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/** One rendered message. */
|
package/src/store/registry.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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.
|
|
425
|
+
if (!row.storageRef || !this.context.resolveStorageProvider) {
|
|
426
426
|
return null;
|
|
427
427
|
}
|
|
428
|
-
const
|
|
429
|
-
if (!
|
|
428
|
+
const storage = await this.context.resolveStorageProvider(row.storageProvider);
|
|
429
|
+
if (!storage) {
|
|
430
430
|
return null;
|
|
431
431
|
}
|
|
432
|
-
const result:
|
|
432
|
+
const result: StorageReadResult = await storage.get(row.storageRef);
|
|
433
433
|
if (result.kind === 'redirect') {
|
|
434
434
|
return { ...base, redirectUrl: result.url };
|
|
435
435
|
}
|
package/src/types/config.ts
CHANGED
|
@@ -129,16 +129,6 @@ interface HandoffEmailBase {
|
|
|
129
129
|
from?: string;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
/** Send through the Resend HTTP API. */
|
|
133
|
-
export interface HandoffResendEmail extends HandoffEmailBase {
|
|
134
|
-
/** @default "resend" */
|
|
135
|
-
provider?: 'resend';
|
|
136
|
-
options?: {
|
|
137
|
-
/** Environment reference to the Resend API key. @default fromEnv('RESEND_API_KEY') */
|
|
138
|
-
apiKey?: RuntimeEnvReference<string>;
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
132
|
/** Send through an SMTP server, such as Amazon SES, SendGrid, Postmark, Mailgun or Microsoft 365. */
|
|
143
133
|
export interface HandoffSmtpEmail extends HandoffEmailBase {
|
|
144
134
|
provider: 'smtp';
|
|
@@ -170,7 +160,7 @@ export interface HandoffCustomEmail extends HandoffEmailBase {
|
|
|
170
160
|
}
|
|
171
161
|
|
|
172
162
|
/** Registry email settings. `provider` selects the shape of `options`. */
|
|
173
|
-
export type HandoffEmailConfig =
|
|
163
|
+
export type HandoffEmailConfig = HandoffSmtpEmail | HandoffCustomEmail;
|
|
174
164
|
|
|
175
165
|
/**
|
|
176
166
|
* One AI connection the deployment declares. Connections are the whole surface: a reader can add a
|
|
@@ -277,13 +267,13 @@ export interface HandoffRuntimeConfig {
|
|
|
277
267
|
* `custom` module for object storage.
|
|
278
268
|
* @default "database"
|
|
279
269
|
*/
|
|
280
|
-
|
|
281
|
-
/** For `
|
|
270
|
+
provider?: 'database' | 'custom';
|
|
271
|
+
/** For `provider: "custom"` - server-only module path default-exporting a `defineStorageProvider()` result. */
|
|
282
272
|
module?: string;
|
|
283
273
|
/** Max bytes kept inline in the database `bytea` column (larger uploads are rejected). @default 4194304 */
|
|
284
274
|
maxInlineBytes?: number;
|
|
285
275
|
/**
|
|
286
|
-
* Options passed to the
|
|
276
|
+
* Options passed to the provider factory. Literals are baked. Environment references are resolved
|
|
287
277
|
* at request time, so secrets must be environment references.
|
|
288
278
|
*/
|
|
289
279
|
options?: Record<string, unknown>;
|
package/.editorconfig
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# EditorConfig is awesome: https://EditorConfig.org
|
|
2
|
-
|
|
3
|
-
# top-most EditorConfig file
|
|
4
|
-
root = true
|
|
5
|
-
|
|
6
|
-
[*.{css,scss,sass}]
|
|
7
|
-
charset = utf-8
|
|
8
|
-
indent_style = tab
|
|
9
|
-
indent_size = unset
|
|
10
|
-
end_of_line = lf
|
|
11
|
-
trim_trailing_whitespace = true
|
|
12
|
-
insert_final_newline = true
|
|
13
|
-
|
|
14
|
-
[*.{js,ts,jsx,tsx}]
|
|
15
|
-
charset = utf-8
|
|
16
|
-
indent_style = space
|
|
17
|
-
end_of_line = lf
|
|
18
|
-
trim_trailing_whitespace = true
|
|
19
|
-
insert_final_newline = true
|
package/.eslintrc.json
DELETED
package/.prettierignore
DELETED
|
File without changes
|
package/.prettierrc.json
DELETED
package/.vscode/settings.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
3
|
-
"[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
|
4
|
-
"editor.formatOnSave": true,
|
|
5
|
-
"editor.codeActionsOnSave": {
|
|
6
|
-
"source.organizeImports": "explicit"
|
|
7
|
-
},
|
|
8
|
-
"[xml]": {
|
|
9
|
-
"editor.defaultFormatter": "redhat.vscode-xml"
|
|
10
|
-
},
|
|
11
|
-
"[typescriptreact]": {
|
|
12
|
-
"editor.defaultFormatter": "vscode.typescript-language-features"
|
|
13
|
-
},
|
|
14
|
-
"[markdown]": {
|
|
15
|
-
"editor.defaultFormatter": "unifiedjs.vscode-remark"
|
|
16
|
-
}
|
|
17
|
-
}
|
package/dist/define.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
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.
|
|
4
|
-
*/
|
|
5
|
-
export { defineAiProvider } from './ai/define';
|
|
6
|
-
export type { AiProvider, AiProviderContext, AiProviderFactory } from './ai/types';
|
|
7
|
-
export { defineAssetStorage } from './registry/asset-storage/define';
|
|
8
|
-
export type { AssetStorage, AssetStorageContext, AssetStorageFactory, AssetStorageInput, AssetStorageReadResult, AssetUpload, AssetUploadInput, } from './registry/asset-storage/types';
|
|
9
|
-
export { defineEmailProvider } from './registry/email/define';
|
|
10
|
-
export type { EmailMessage, EmailProvider, EmailProviderContext, EmailProviderFactory } from './registry/email/types';
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Built-in Resend provider, selected with `runtime.registry.email.provider: "resend"` (the default).
|
|
3
|
-
* A plain `fetch` to the Resend HTTP API, so it adds no dependency.
|
|
4
|
-
*/
|
|
5
|
-
import type { EmailProvider } from '../types';
|
|
6
|
-
/** Build a Resend provider from options whose references are already resolved. */
|
|
7
|
-
export declare const createResendProvider: (options: Record<string, unknown>) => EmailProvider;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Built-in Resend provider, selected with `runtime.registry.email.provider: "resend"` (the default).
|
|
4
|
-
* A plain `fetch` to the Resend HTTP API, so it adds no dependency.
|
|
5
|
-
*/
|
|
6
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
7
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
8
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
9
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
10
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
11
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
12
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
13
|
-
});
|
|
14
|
-
};
|
|
15
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
-
exports.createResendProvider = void 0;
|
|
17
|
-
/** Build a Resend provider from options whose references are already resolved. */
|
|
18
|
-
const createResendProvider = (options) => ({
|
|
19
|
-
send(_a) {
|
|
20
|
-
return __awaiter(this, arguments, void 0, function* ({ from, to, subject, html, text }) {
|
|
21
|
-
const apiKey = typeof options.apiKey === 'string' ? options.apiKey : '';
|
|
22
|
-
if (!apiKey)
|
|
23
|
-
throw new Error('The Resend API key is not set.');
|
|
24
|
-
const response = yield fetch('https://api.resend.com/emails', {
|
|
25
|
-
method: 'POST',
|
|
26
|
-
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
27
|
-
body: JSON.stringify({ from, to: [to], subject, html, text }),
|
|
28
|
-
});
|
|
29
|
-
if (!response.ok) {
|
|
30
|
-
throw new Error(`Resend rejected the email (HTTP ${response.status}): ${yield response.text()}`);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
exports.createResendProvider = createResendProvider;
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { ClientConfig } from '@handoff/types/config';
|
|
2
|
-
import Link from 'next/link';
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import NavLink from '../NavLink';
|
|
5
|
-
import { useNav } from '../context/NavProvider';
|
|
6
|
-
|
|
7
|
-
interface HeaderProps {
|
|
8
|
-
config: ClientConfig;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function Header({ config }: HeaderProps) {
|
|
12
|
-
const { menu } = useNav();
|
|
13
|
-
const [mobile, setMobile] = React.useState('');
|
|
14
|
-
const toggle = () => {
|
|
15
|
-
if (mobile === 'is-active') {
|
|
16
|
-
setMobile('');
|
|
17
|
-
} else {
|
|
18
|
-
setMobile('is-active');
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<>
|
|
24
|
-
<header id="site-header" className="c-site-header">
|
|
25
|
-
<div className="o-container-fluid">
|
|
26
|
-
<div className="c-site-header__wrapper">
|
|
27
|
-
<div>
|
|
28
|
-
<div className="c-site-menu">
|
|
29
|
-
<button className={`c-hamburger-icon c-hamburger-icon--slider ${mobile}`} onClick={toggle}>
|
|
30
|
-
<div className="c-hamburger-icon__box">
|
|
31
|
-
<div className="c-hamburger-icon__inner"></div>
|
|
32
|
-
</div>
|
|
33
|
-
</button>
|
|
34
|
-
</div>
|
|
35
|
-
<div className="c-site-title">
|
|
36
|
-
<Link href="/" className="c-site-logo c-site-logo--basic" title="" rel="home" aria-label="logo">
|
|
37
|
-
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
38
|
-
<img src={`${process.env.HANDOFF_APP_BASE_PATH ?? ''}/logo.svg`} alt={config?.app?.title} />
|
|
39
|
-
</Link>
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
<nav className="c-site-nav">
|
|
43
|
-
<ul>
|
|
44
|
-
{menu.map((item) => (
|
|
45
|
-
<li key={item.path}>
|
|
46
|
-
<NavLink href={`${item.path}`}>{item.title}</NavLink>
|
|
47
|
-
</li>
|
|
48
|
-
))}
|
|
49
|
-
</ul>
|
|
50
|
-
</nav>
|
|
51
|
-
</div>
|
|
52
|
-
<div>
|
|
53
|
-
{
|
|
54
|
-
// TODO: Reimpliment cmd k search
|
|
55
|
-
false && <small>hit cmd+k to search</small>
|
|
56
|
-
}
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
</header>
|
|
61
|
-
<div className={`c-offcanvas c-offcanvas--menu ${mobile}`}>
|
|
62
|
-
<div className="c-offcanvas__inner">
|
|
63
|
-
<nav className="c-mobilenav">
|
|
64
|
-
<ul className="c-mobilenav__menu">
|
|
65
|
-
{menu.map((item) => (
|
|
66
|
-
<li key={`mobile-${item.path}`}>
|
|
67
|
-
<NavLink href={`${item.path}`}>{item.title}</NavLink>
|
|
68
|
-
{item.subSections.length > 0 && (
|
|
69
|
-
<ul className="c-mobilenav__submenu">
|
|
70
|
-
{item.subSections
|
|
71
|
-
.filter((sub) => sub.path)
|
|
72
|
-
.map((sub) => (
|
|
73
|
-
<li key={sub.path}>
|
|
74
|
-
<NavLink href={`${sub.path}`}>{sub.title}</NavLink>
|
|
75
|
-
</li>
|
|
76
|
-
))}
|
|
77
|
-
</ul>
|
|
78
|
-
)}
|
|
79
|
-
</li>
|
|
80
|
-
))}
|
|
81
|
-
</ul>
|
|
82
|
-
</nav>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
</>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export default Header;
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs-extra';
|
|
2
|
-
import uniq from 'lodash/uniq';
|
|
3
|
-
import { FileArchive } from 'lucide-react';
|
|
4
|
-
import type { GetStaticProps } from 'next';
|
|
5
|
-
import Head from 'next/head';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import * as React from 'react';
|
|
8
|
-
import ReactMarkdown from 'react-markdown';
|
|
9
|
-
import rehypeRaw from 'rehype-raw';
|
|
10
|
-
import remarkGfm from 'remark-gfm';
|
|
11
|
-
import Footer from '../../components/Footer';
|
|
12
|
-
import { MarkdownComponents, remarkCodeMeta } from '../../components/Markdown/MarkdownComponents';
|
|
13
|
-
import Header from '../../components/old/Header';
|
|
14
|
-
import { buildTimeFoundationDesign, fetchDocPageMarkdown, FontDocumentationProps, getClientRuntimeConfig } from '../../components/util';
|
|
15
|
-
import { useFoundationTokens } from '../../components/util/useFoundationTokens';
|
|
16
|
-
|
|
17
|
-
const basePath = process.env.HANDOFF_APP_BASE_PATH ?? '';
|
|
18
|
-
const isRegistry = process.env.HANDOFF_RUNTIME_MODE === 'registry';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* This statically renders content from the markdown, creating menu and providing
|
|
22
|
-
* metadata
|
|
23
|
-
*
|
|
24
|
-
* This is all done statically at build time
|
|
25
|
-
* @param context GetStaticProps
|
|
26
|
-
* @returns
|
|
27
|
-
*/
|
|
28
|
-
export const getStaticProps: GetStaticProps = async () => {
|
|
29
|
-
// Workspace/static reads the mirrored font archives; a registry build has none (hydrated at runtime).
|
|
30
|
-
const customFonts: string[] = [];
|
|
31
|
-
try {
|
|
32
|
-
const fontDir = path.resolve(process.env.HANDOFF_MODULE_PATH ?? '', '.handoff', `${process.env.HANDOFF_PROJECT_ID}`, 'public', 'fonts');
|
|
33
|
-
for (const font of fs.readdirSync(fontDir)) {
|
|
34
|
-
if (font.endsWith('.zip')) customFonts.push(font.replace('.zip', ''));
|
|
35
|
-
}
|
|
36
|
-
} catch {
|
|
37
|
-
// No local font archives (e.g. a registry build), so customFonts stays empty.
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
props: {
|
|
42
|
-
...(await fetchDocPageMarkdown('docs/assets/', 'fonts', `/assets`)).props,
|
|
43
|
-
design: buildTimeFoundationDesign(),
|
|
44
|
-
config: getClientRuntimeConfig(),
|
|
45
|
-
customFonts,
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const FontsPage = ({ content, menu, metadata, customFonts, design, config }: FontDocumentationProps) => {
|
|
51
|
-
// Registry hydrates typography tokens (for the family list) + the published font archives at runtime.
|
|
52
|
-
const tokens = useFoundationTokens('typography', { design });
|
|
53
|
-
const [hydratedFonts, setHydratedFonts] = React.useState<string[]>(customFonts);
|
|
54
|
-
React.useEffect(() => {
|
|
55
|
-
if (!isRegistry) return;
|
|
56
|
-
let active = true;
|
|
57
|
-
fetch(`${basePath}/api/docs/assets/fonts`)
|
|
58
|
-
.then((res) => (res.ok ? res.json() : null))
|
|
59
|
-
.then((data) => {
|
|
60
|
-
if (!active || !data?.assets) return;
|
|
61
|
-
const names = (data.assets as { path: string }[])
|
|
62
|
-
.filter((a) => typeof a.path === 'string' && /^fonts\/.+\.zip$/i.test(a.path))
|
|
63
|
-
.map((a) => a.path.slice('fonts/'.length).replace(/\.zip$/i, ''));
|
|
64
|
-
setHydratedFonts(names);
|
|
65
|
-
})
|
|
66
|
-
.catch(() => {});
|
|
67
|
-
return () => {
|
|
68
|
-
active = false;
|
|
69
|
-
};
|
|
70
|
-
}, []);
|
|
71
|
-
|
|
72
|
-
const typography = tokens.design?.typography ?? [];
|
|
73
|
-
const fontFamilies: string[] = uniq(typography.map((type) => type.values.fontFamily));
|
|
74
|
-
const fontLinks: string[] = fontFamilies.map((fontFamily) => {
|
|
75
|
-
const machineName = fontFamily.replace(/\s/g, '');
|
|
76
|
-
const custom = hydratedFonts.find((font) => font === machineName);
|
|
77
|
-
if (custom) {
|
|
78
|
-
return isRegistry ? `${basePath}/api/docs/assets/fonts/fonts/${machineName}.zip` : `/fonts/${machineName}.zip`;
|
|
79
|
-
}
|
|
80
|
-
return `https://fonts.google.com/specimen/${fontFamily}`;
|
|
81
|
-
});
|
|
82
|
-
return (
|
|
83
|
-
<div className="c-page">
|
|
84
|
-
<Head>
|
|
85
|
-
<title>{metadata.metaTitle}</title>
|
|
86
|
-
<meta name="description" content={metadata.metaDescription} />
|
|
87
|
-
</Head>
|
|
88
|
-
<Header config={config} />
|
|
89
|
-
<section className="c-content">
|
|
90
|
-
<div className="o-container-fluid">
|
|
91
|
-
<div className="c-hero">
|
|
92
|
-
<div>
|
|
93
|
-
<h1>{metadata.title}</h1>
|
|
94
|
-
<p>{metadata.description}</p>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
{fontFamilies.map((fontFamily, i) => (
|
|
98
|
-
<React.Fragment key={fontFamily}>
|
|
99
|
-
<div className="o-row u-justify-between">
|
|
100
|
-
<div className="o-col-5@md">
|
|
101
|
-
<h4>{fontFamily}</h4>
|
|
102
|
-
</div>
|
|
103
|
-
<div className="o-col-6@md">
|
|
104
|
-
<div className="c-card">
|
|
105
|
-
<FileArchive />
|
|
106
|
-
<h4>{fontFamily}</h4>
|
|
107
|
-
<p>Font files for installing on a local machine.</p>
|
|
108
|
-
<p>
|
|
109
|
-
<a href={fontLinks[i]}>Download Font</a>
|
|
110
|
-
</p>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
<hr />
|
|
115
|
-
</React.Fragment>
|
|
116
|
-
))}
|
|
117
|
-
|
|
118
|
-
<div className="prose">
|
|
119
|
-
<ReactMarkdown components={MarkdownComponents} remarkPlugins={[remarkGfm, remarkCodeMeta]} rehypePlugins={[rehypeRaw]}>
|
|
120
|
-
{content}
|
|
121
|
-
</ReactMarkdown>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
</section>
|
|
125
|
-
<Footer config={config} />
|
|
126
|
-
</div>
|
|
127
|
-
);
|
|
128
|
-
};
|
|
129
|
-
export default FontsPage;
|