@elytracms/studio 0.0.56 → 0.0.58

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 (84) hide show
  1. package/convex/auth.ts +7 -0
  2. package/convex/users.ts +41 -14
  3. package/dist/components/ai/ai-chat-panel-lazy.js +3 -3
  4. package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
  5. package/dist/components/ai/ai-chat-panel.d.ts +0 -9
  6. package/dist/components/ai/ai-chat-panel.js +199 -60
  7. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  8. package/dist/components/ai/chat-ui.d.ts +62 -10
  9. package/dist/components/ai/chat-ui.js +139 -23
  10. package/dist/components/ai/chat-ui.js.map +1 -1
  11. package/dist/components/ai/proposal-card.d.ts +30 -7
  12. package/dist/components/ai/proposal-card.js +81 -21
  13. package/dist/components/ai/proposal-card.js.map +1 -1
  14. package/dist/components/cms/detail-editor.js +12 -0
  15. package/dist/components/cms/detail-editor.js.map +1 -1
  16. package/dist/components/cms/document-diff-view.d.ts +12 -1
  17. package/dist/components/cms/document-diff-view.js +119 -42
  18. package/dist/components/cms/document-diff-view.js.map +1 -1
  19. package/dist/components/cms/rich-text-preview.d.ts +15 -0
  20. package/dist/components/cms/rich-text-preview.js +81 -0
  21. package/dist/components/cms/rich-text-preview.js.map +1 -0
  22. package/dist/components/cms/version-compare-text.d.ts +26 -0
  23. package/dist/components/cms/version-compare-text.js +60 -0
  24. package/dist/components/cms/version-compare-text.js.map +1 -1
  25. package/dist/lib/ai/anthropic-provider.d.ts +0 -12
  26. package/dist/lib/ai/anthropic-provider.js +3 -11
  27. package/dist/lib/ai/anthropic-provider.js.map +1 -1
  28. package/dist/lib/ai/apply-content.js +52 -10
  29. package/dist/lib/ai/apply-content.js.map +1 -1
  30. package/dist/lib/ai/chat-context.d.ts +74 -0
  31. package/dist/lib/ai/chat-context.js +100 -0
  32. package/dist/lib/ai/chat-context.js.map +1 -0
  33. package/dist/lib/ai/chat-loop.d.ts +70 -0
  34. package/dist/lib/ai/chat-loop.js +46 -0
  35. package/dist/lib/ai/chat-loop.js.map +1 -0
  36. package/dist/lib/ai/chat-state.d.ts +73 -57
  37. package/dist/lib/ai/chat-state.js +49 -90
  38. package/dist/lib/ai/chat-state.js.map +1 -1
  39. package/dist/lib/ai/fake-provider.d.ts +139 -6
  40. package/dist/lib/ai/fake-provider.js +261 -8
  41. package/dist/lib/ai/fake-provider.js.map +1 -1
  42. package/dist/lib/ai/gateway-provider.d.ts +30 -0
  43. package/dist/lib/ai/gateway-provider.js +73 -0
  44. package/dist/lib/ai/gateway-provider.js.map +1 -0
  45. package/dist/lib/ai/index.d.ts +5 -1
  46. package/dist/lib/ai/index.js +5 -1
  47. package/dist/lib/ai/index.js.map +1 -1
  48. package/dist/lib/ai/node-handler.d.ts +13 -4
  49. package/dist/lib/ai/node-handler.js +33 -3
  50. package/dist/lib/ai/node-handler.js.map +1 -1
  51. package/dist/lib/ai/rich-text-input.d.ts +27 -0
  52. package/dist/lib/ai/rich-text-input.js +25 -0
  53. package/dist/lib/ai/rich-text-input.js.map +1 -0
  54. package/dist/lib/ai/route-provider.d.ts +35 -10
  55. package/dist/lib/ai/route-provider.js +13 -6
  56. package/dist/lib/ai/route-provider.js.map +1 -1
  57. package/dist/lib/ai/route.d.ts +50 -21
  58. package/dist/lib/ai/route.js +69 -40
  59. package/dist/lib/ai/route.js.map +1 -1
  60. package/dist/lib/ai/shape.d.ts +12 -1
  61. package/dist/lib/ai/shape.js +46 -20
  62. package/dist/lib/ai/shape.js.map +1 -1
  63. package/dist/lib/ai/tools.d.ts +242 -37
  64. package/dist/lib/ai/tools.js +548 -63
  65. package/dist/lib/ai/tools.js.map +1 -1
  66. package/dist/lib/ai/types.d.ts +31 -16
  67. package/dist/lib/ai/types.js.map +1 -1
  68. package/dist/lib/ai/update-fields.d.ts +68 -0
  69. package/dist/lib/ai/update-fields.js +296 -0
  70. package/dist/lib/ai/update-fields.js.map +1 -0
  71. package/dist/lib/ai-current-document.d.ts +53 -0
  72. package/dist/lib/ai-current-document.js +54 -0
  73. package/dist/lib/ai-current-document.js.map +1 -0
  74. package/dist/lib/auth/convex-auth-adapter.d.ts +9 -1
  75. package/dist/lib/auth/convex-auth-adapter.js +28 -2
  76. package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
  77. package/dist/lib/cms-views/view-model.d.ts +6 -6
  78. package/dist/lib/i18n/messages.en.js +19 -1
  79. package/dist/lib/i18n/messages.en.js.map +1 -1
  80. package/dist/ui/badge.d.ts +1 -1
  81. package/package.json +4 -3
  82. package/dist/lib/ai/create-flow.d.ts +0 -132
  83. package/dist/lib/ai/create-flow.js +0 -280
  84. package/dist/lib/ai/create-flow.js.map +0 -1
@@ -1,29 +1,10 @@
1
+ import { chatToolDefinitions } from './tools.js';
1
2
  function jsonResponse(value, status) {
2
3
  return new Response(JSON.stringify(value), {
3
4
  status,
4
5
  headers: { 'content-type': 'application/json' },
5
6
  });
6
7
  }
7
- function isChatMessage(value) {
8
- if (typeof value !== 'object' || value === null)
9
- return false;
10
- const m = value;
11
- return ((m.role === 'system' || m.role === 'user' || m.role === 'assistant') &&
12
- typeof m.content === 'string');
13
- }
14
- /** Parse a request body into a {@link StreamChatRequest}, or `null` when malformed. */
15
- function parseChatBody(body) {
16
- if (typeof body !== 'object' || body === null)
17
- return null;
18
- const { messages, system } = body;
19
- if (!Array.isArray(messages) || messages.length === 0 || !messages.every(isChatMessage)) {
20
- return null;
21
- }
22
- return {
23
- messages: messages,
24
- ...(typeof system === 'string' ? { system } : {}),
25
- };
26
- }
27
8
  /** Structurally validate a {@link GenerateStructuredRequest}, or `null` when malformed. */
28
9
  function parseGenerateBody(body) {
29
10
  if (typeof body !== 'object' || body === null)
@@ -36,22 +17,34 @@ function parseGenerateBody(body) {
36
17
  return null;
37
18
  return request;
38
19
  }
39
- /** The `kind` a body selects: explicit `kind`, else `messages` ⇒ chat (back-compat). */
40
- function bodyKind(body) {
41
- if (typeof body !== 'object' || body === null)
42
- return null;
43
- const kind = body.kind;
44
- if (kind === 'chat' || kind === 'generate')
45
- return kind;
46
- if (Array.isArray(body.messages))
47
- return 'chat';
48
- return null;
20
+ /**
21
+ * Fold the request-body `context` into the system prompt (EC-334). The client sends
22
+ * `context` as one string or ordered string sections (collections-catalog summary,
23
+ * client-resolved knowledge, EC-378's current-document block) — the route only
24
+ * ASSEMBLES them; it resolves no data itself (zero-data-access proxy). Malformed
25
+ * context is a structural 400, never silently dropped (validation-first).
26
+ */
27
+ export function assembleSystemPrompt(context) {
28
+ if (context === undefined || context === null)
29
+ return { ok: true };
30
+ const sections = typeof context === 'string'
31
+ ? [context]
32
+ : Array.isArray(context) && context.every((section) => typeof section === 'string')
33
+ ? context
34
+ : null;
35
+ if (sections === null)
36
+ return { ok: false };
37
+ const system = sections
38
+ .map((section) => section.trim())
39
+ .filter((section) => section.length > 0)
40
+ .join('\n\n');
41
+ return { ok: true, ...(system.length > 0 ? { system } : {}) };
49
42
  }
50
43
  /**
51
44
  * Build the mounted route handler. The returned function is a plain
52
45
  * `(Request) => Promise<Response>` — session-gated, soft-rate-limited, and a thin
53
- * proxy to `provider.streamChat`. Never throws to the caller: every failure is a
54
- * structured JSON response with an appropriate status.
46
+ * proxy to the provider. Never throws to the caller: every failure is a structured
47
+ * JSON response with an appropriate status.
55
48
  */
56
49
  export function createAiRouteHandler(options) {
57
50
  const maxPerMinute = options.limits?.maxRequestsPerMinute;
@@ -71,6 +64,7 @@ export function createAiRouteHandler(options) {
71
64
  }
72
65
  return async (request) => {
73
66
  // 1. Session gate — reject anonymous callers (the route holds spend, not data).
67
+ // Position is FROZEN: the 401 must precede any stream byte (EC-371/EC-334).
74
68
  let session;
75
69
  try {
76
70
  session = await options.getSession(request);
@@ -94,10 +88,12 @@ export function createAiRouteHandler(options) {
94
88
  catch {
95
89
  return jsonResponse({ error: 'invalid-json' }, 400);
96
90
  }
97
- const kind = bodyKind(body);
91
+ if (typeof body !== 'object' || body === null) {
92
+ return jsonResponse({ error: 'invalid-json' }, 400);
93
+ }
98
94
  // 5a. Structured generation — the thin proxy that lets `applyContentWithShape`
99
95
  // run client-side without the key ever reaching the browser.
100
- if (kind === 'generate') {
96
+ if (body.kind === 'generate') {
101
97
  const generate = parseGenerateBody(body);
102
98
  if (!generate) {
103
99
  return jsonResponse({ error: 'invalid-request', message: 'expected { kind: "generate", request }' }, 400);
@@ -110,16 +106,49 @@ export function createAiRouteHandler(options) {
110
106
  return jsonResponse({ error: 'provider-error' }, 502);
111
107
  }
112
108
  }
113
- // 5b. Chat stream (default / back-compat).
114
- const chat = kind === 'chat' ? parseChatBody(body) : null;
115
- if (!chat) {
116
- return jsonResponse({ error: 'invalid-request', message: 'expected { messages: AiChatMessage[] }' }, 400);
109
+ // 5b. Native chat branch (EC-334): `{ messages: UIMessage[], context? }`.
110
+ const rawMessages = body.messages;
111
+ if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
112
+ return jsonResponse({
113
+ error: 'invalid-request',
114
+ message: 'expected { kind: "generate", request } or { messages: UIMessage[] }',
115
+ }, 400);
116
+ }
117
+ const assembled = assembleSystemPrompt(body.context);
118
+ if (!assembled.ok) {
119
+ return jsonResponse({ error: 'invalid-request', message: 'context must be a string or string[]' }, 400);
117
120
  }
118
- if (!options.provider.streamChat) {
121
+ if (!options.provider.languageModel) {
119
122
  return jsonResponse({ error: 'provider-cannot-stream' }, 501);
120
123
  }
124
+ // The SDK loads lazily INSIDE the handler (the anthropic-provider discipline):
125
+ // the client-imported barrel re-exports this module, and a static edge would
126
+ // put the AI SDK on the studio critical path (pinned in bundle.test.ts).
127
+ const { validateUIMessages, convertToModelMessages, streamText, createUIMessageStreamResponse, toUIMessageStream, } = await import('ai');
128
+ // Structural UIMessage validation (id/role/parts). Tool parts validate by
129
+ // shape — deep input validation belongs to the client executor (slice 2).
130
+ let messages;
131
+ try {
132
+ messages = await validateUIMessages({ messages: rawMessages });
133
+ }
134
+ catch {
135
+ return jsonResponse({ error: 'invalid-request', message: 'expected { messages: UIMessage[] }' }, 400);
136
+ }
121
137
  try {
122
- return await options.provider.streamChat(chat);
138
+ const model = await options.provider.languageModel();
139
+ const result = streamText({
140
+ model,
141
+ ...(assembled.system !== undefined ? { system: assembled.system } : {}),
142
+ messages: await convertToModelMessages(messages),
143
+ // No `execute` on any tool — the SDK forwards each call to the client
144
+ // (ratified trust boundary). Continuation is therefore client-driven: one
145
+ // tool round-trip per REQUEST, throttled by the rate limit above; the
146
+ // per-turn step cap lands client-side with slice 2.
147
+ tools: chatToolDefinitions,
148
+ });
149
+ return createUIMessageStreamResponse({
150
+ stream: toUIMessageStream({ stream: result.stream, originalMessages: messages }),
151
+ });
123
152
  }
124
153
  catch {
125
154
  return jsonResponse({ error: 'provider-error' }, 502);
@@ -1 +1 @@
1
- {"version":3,"file":"route.js","sourceRoot":"","sources":["../../../src/lib/ai/route.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-route factory (EC-331): a streaming chat/completion endpoint the consuming\n * studio app mounts. Key custody = host route (never the browser, never Convex): the\n * `provider` holds the host-env key and this handler is a THIN model proxy — NO data\n * access, NO service credentials.\n *\n * Framework-agnostic on purpose: it returns a Web `Request → Response` handler the\n * host wires into whatever server entry it has (a TanStack Start server route, a\n * Convex `httpAction`, a plain fetch handler). The current demo studio builds as a\n * pure client SPA with server functions disabled (EC-234), so actually mounting this\n * is an EC-199 integration step; phase 1 ships the handler + its contract.\n *\n * Session-gated: `getSession` (host-wired, reading its own auth — this module never\n * touches `lib/auth`) must return a session for authenticated callers; anonymous\n * callers are rejected. Plus a soft, in-memory per-project rate counter (spend guard).\n *\n * EC-199: the route serves TWO model operations — both thin proxies, neither with any\n * data access — discriminated by the request body's `kind`:\n * - `{ kind: 'chat', messages, system? }` → `provider.streamChat` (text stream);\n * - `{ kind: 'generate', request }` → `provider.generateStructured` (JSON).\n * The structured mode is what lets `applyContentWithShape` run CLIENT-side against a\n * {@link createRouteProvider} whose key never touches the browser — the creation flow's\n * whole authoring loop proxies its model calls here while the tools (search/list/apply)\n * execute in the client with the user's own session. A body with `messages` and no\n * `kind` is still treated as a chat request (back-compat with the phase-1 contract).\n */\nimport type {\n AiChatMessage,\n AiRouteHandler,\n AiRouteHandlerOptions,\n AiSession,\n GenerateStructuredRequest,\n StreamChatRequest,\n} from './types'\n\nfunction jsonResponse(value: unknown, status: number): Response {\n return new Response(JSON.stringify(value), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\ninterface RateBucket {\n windowStart: number\n count: number\n}\n\nfunction isChatMessage(value: unknown): value is AiChatMessage {\n if (typeof value !== 'object' || value === null) return false\n const m = value as { role?: unknown; content?: unknown }\n return (\n (m.role === 'system' || m.role === 'user' || m.role === 'assistant') &&\n typeof m.content === 'string'\n )\n}\n\n/** Parse a request body into a {@link StreamChatRequest}, or `null` when malformed. */\nfunction parseChatBody(body: unknown): StreamChatRequest | null {\n if (typeof body !== 'object' || body === null) return null\n const { messages, system } = body as { messages?: unknown; system?: unknown }\n if (!Array.isArray(messages) || messages.length === 0 || !messages.every(isChatMessage)) {\n return null\n }\n return {\n messages: messages as AiChatMessage[],\n ...(typeof system === 'string' ? { system } : {}),\n }\n}\n\n/** The wire discriminator selecting which model operation the route proxies. */\nexport type AiRouteKind = 'chat' | 'generate'\n\n/** Structurally validate a {@link GenerateStructuredRequest}, or `null` when malformed. */\nfunction parseGenerateBody(body: unknown): GenerateStructuredRequest | null {\n if (typeof body !== 'object' || body === null) return null\n const request = (body as { request?: unknown }).request\n if (typeof request !== 'object' || request === null) return null\n const { prompt, schema } = request as { prompt?: unknown; schema?: unknown }\n if (typeof prompt !== 'string' || typeof schema !== 'object' || schema === null) return null\n return request as GenerateStructuredRequest\n}\n\n/** The `kind` a body selects: explicit `kind`, else `messages` ⇒ chat (back-compat). */\nfunction bodyKind(body: unknown): AiRouteKind | null {\n if (typeof body !== 'object' || body === null) return null\n const kind = (body as { kind?: unknown }).kind\n if (kind === 'chat' || kind === 'generate') return kind\n if (Array.isArray((body as { messages?: unknown }).messages)) return 'chat'\n return null\n}\n\n/**\n * Build the mounted route handler. The returned function is a plain\n * `(Request) => Promise<Response>` — session-gated, soft-rate-limited, and a thin\n * proxy to `provider.streamChat`. Never throws to the caller: every failure is a\n * structured JSON response with an appropriate status.\n */\nexport function createAiRouteHandler(options: AiRouteHandlerOptions): AiRouteHandler {\n const maxPerMinute = options.limits?.maxRequestsPerMinute\n const buckets = new Map<string, RateBucket>()\n const projectKey = options.projectKey ?? (() => 'global')\n\n function rateLimited(key: string): boolean {\n if (maxPerMinute === undefined || maxPerMinute <= 0) return false\n const now = Date.now()\n const bucket = buckets.get(key)\n if (!bucket || now - bucket.windowStart >= 60_000) {\n buckets.set(key, { windowStart: now, count: 1 })\n return false\n }\n bucket.count += 1\n return bucket.count > maxPerMinute\n }\n\n return async (request: Request): Promise<Response> => {\n // 1. Session gate — reject anonymous callers (the route holds spend, not data).\n let session: AiSession | null\n try {\n session = await options.getSession(request)\n } catch {\n return jsonResponse({ error: 'unauthorized' }, 401)\n }\n if (!session) return jsonResponse({ error: 'unauthorized' }, 401)\n\n // 2. Method.\n if (request.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405)\n\n // 3. Soft per-project rate / spend counter.\n if (rateLimited(projectKey(request))) return jsonResponse({ error: 'rate-limited' }, 429)\n\n // 4. Body → the requested model operation.\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return jsonResponse({ error: 'invalid-json' }, 400)\n }\n\n const kind = bodyKind(body)\n\n // 5a. Structured generation — the thin proxy that lets `applyContentWithShape`\n // run client-side without the key ever reaching the browser.\n if (kind === 'generate') {\n const generate = parseGenerateBody(body)\n if (!generate) {\n return jsonResponse(\n { error: 'invalid-request', message: 'expected { kind: \"generate\", request }' },\n 400,\n )\n }\n try {\n const result = await options.provider.generateStructured(generate)\n return jsonResponse(result, 200)\n } catch {\n return jsonResponse({ error: 'provider-error' }, 502)\n }\n }\n\n // 5b. Chat stream (default / back-compat).\n const chat = kind === 'chat' ? parseChatBody(body) : null\n if (!chat) {\n return jsonResponse(\n { error: 'invalid-request', message: 'expected { messages: AiChatMessage[] }' },\n 400,\n )\n }\n if (!options.provider.streamChat) {\n return jsonResponse({ error: 'provider-cannot-stream' }, 501)\n }\n try {\n return await options.provider.streamChat(chat)\n } catch {\n return jsonResponse({ error: 'provider-error' }, 502)\n }\n }\n}\n"]}
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../../../src/lib/ai/route.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-route factory (EC-331): the session-gated endpoint the consuming studio\n * app mounts. Key custody = host route (never the browser, never Convex): the\n * `provider` holds the host-env key and this handler is a THIN model proxy — NO data\n * access, NO service credentials, NO server-side tool execution.\n *\n * Framework-agnostic on purpose: it returns a Web `Request → Response` handler the\n * host wires into whatever server entry it has (`createAiDevPlugin` in dev,\n * `toNodeHandler` on a Vercel Node function, any fetch handler).\n *\n * Session-gated: `getSession` (host-wired, reading its own auth — this module never\n * touches `lib/auth`) must return a session for authenticated callers; anonymous\n * callers are rejected with 401 BEFORE any stream byte. Plus a soft, in-memory\n * per-project rate counter (spend guard) — also the backstop for the SDK's\n * client-driven tool-call continuations, each of which is a fresh request here.\n *\n * The route serves TWO model operations — both thin proxies, neither with any data\n * access:\n * - `{ kind: 'generate', request }` → `provider.generateStructured` (JSON) — the\n * proxy `applyContentWithShape` / `translateDocument` run CLIENT-side against,\n * so the key never touches the browser;\n * - `{ messages: UIMessage[], context? }` (EC-334, kind-less — the shape the AI\n * SDK's `DefaultChatTransport` posts) → the SDK's native streaming chat\n * protocol: `validateUIMessages` → `streamText` over\n * `provider.languageModel()` with {@link chatToolDefinitions} declared WITHOUT\n * `execute` (every tool call forwards to the client, which executes it against\n * the operations layer with the user's own session) →\n * `createUIMessageStreamResponse` + `toUIMessageStream` (the non-deprecated\n * pair — `result.toUIMessageStreamResponse()` dies in SDK v8). The `system`\n * prompt is assembled from the request's `context` sections; conversation\n * history rides natively in `messages`.\n *\n * BREAKING (EC-334, ratified): the legacy `{ kind: 'chat', messages:\n * AiChatMessage[] }` wire is gone — route factory and client ship lockstep in\n * `@elytracms/studio`, so consumers re-mount the same factory on upgrade.\n */\nimport type { AiRouteHandler, AiRouteHandlerOptions, AiSession, GenerateStructuredRequest } from './types'\nimport { chatToolDefinitions } from './tools'\n\nfunction jsonResponse(value: unknown, status: number): Response {\n return new Response(JSON.stringify(value), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\ninterface RateBucket {\n windowStart: number\n count: number\n}\n\n/**\n * The wire discriminator for the structured-generation branch. The native chat\n * branch (EC-334) is deliberately kind-less: the SDK's `DefaultChatTransport`\n * posts `{ messages, … }` with no `kind`.\n */\nexport type AiRouteKind = 'generate'\n\n/** Structurally validate a {@link GenerateStructuredRequest}, or `null` when malformed. */\nfunction parseGenerateBody(body: unknown): GenerateStructuredRequest | null {\n if (typeof body !== 'object' || body === null) return null\n const request = (body as { request?: unknown }).request\n if (typeof request !== 'object' || request === null) return null\n const { prompt, schema } = request as { prompt?: unknown; schema?: unknown }\n if (typeof prompt !== 'string' || typeof schema !== 'object' || schema === null) return null\n return request as GenerateStructuredRequest\n}\n\n/** The outcome of folding the request `context` into a system prompt. */\nexport type AssembledSystem = { ok: true; system?: string } | { ok: false }\n\n/**\n * Fold the request-body `context` into the system prompt (EC-334). The client sends\n * `context` as one string or ordered string sections (collections-catalog summary,\n * client-resolved knowledge, EC-378's current-document block) — the route only\n * ASSEMBLES them; it resolves no data itself (zero-data-access proxy). Malformed\n * context is a structural 400, never silently dropped (validation-first).\n */\nexport function assembleSystemPrompt(context: unknown): AssembledSystem {\n if (context === undefined || context === null) return { ok: true }\n const sections =\n typeof context === 'string'\n ? [context]\n : Array.isArray(context) && context.every((section) => typeof section === 'string')\n ? (context as string[])\n : null\n if (sections === null) return { ok: false }\n const system = sections\n .map((section) => section.trim())\n .filter((section) => section.length > 0)\n .join('\\n\\n')\n return { ok: true, ...(system.length > 0 ? { system } : {}) }\n}\n\n/**\n * Build the mounted route handler. The returned function is a plain\n * `(Request) => Promise<Response>` — session-gated, soft-rate-limited, and a thin\n * proxy to the provider. Never throws to the caller: every failure is a structured\n * JSON response with an appropriate status.\n */\nexport function createAiRouteHandler(options: AiRouteHandlerOptions): AiRouteHandler {\n const maxPerMinute = options.limits?.maxRequestsPerMinute\n const buckets = new Map<string, RateBucket>()\n const projectKey = options.projectKey ?? (() => 'global')\n\n function rateLimited(key: string): boolean {\n if (maxPerMinute === undefined || maxPerMinute <= 0) return false\n const now = Date.now()\n const bucket = buckets.get(key)\n if (!bucket || now - bucket.windowStart >= 60_000) {\n buckets.set(key, { windowStart: now, count: 1 })\n return false\n }\n bucket.count += 1\n return bucket.count > maxPerMinute\n }\n\n return async (request: Request): Promise<Response> => {\n // 1. Session gate — reject anonymous callers (the route holds spend, not data).\n // Position is FROZEN: the 401 must precede any stream byte (EC-371/EC-334).\n let session: AiSession | null\n try {\n session = await options.getSession(request)\n } catch {\n return jsonResponse({ error: 'unauthorized' }, 401)\n }\n if (!session) return jsonResponse({ error: 'unauthorized' }, 401)\n\n // 2. Method.\n if (request.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405)\n\n // 3. Soft per-project rate / spend counter.\n if (rateLimited(projectKey(request))) return jsonResponse({ error: 'rate-limited' }, 429)\n\n // 4. Body → the requested model operation.\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return jsonResponse({ error: 'invalid-json' }, 400)\n }\n if (typeof body !== 'object' || body === null) {\n return jsonResponse({ error: 'invalid-json' }, 400)\n }\n\n // 5a. Structured generation — the thin proxy that lets `applyContentWithShape`\n // run client-side without the key ever reaching the browser.\n if ((body as { kind?: unknown }).kind === 'generate') {\n const generate = parseGenerateBody(body)\n if (!generate) {\n return jsonResponse(\n { error: 'invalid-request', message: 'expected { kind: \"generate\", request }' },\n 400,\n )\n }\n try {\n const result = await options.provider.generateStructured(generate)\n return jsonResponse(result, 200)\n } catch {\n return jsonResponse({ error: 'provider-error' }, 502)\n }\n }\n\n // 5b. Native chat branch (EC-334): `{ messages: UIMessage[], context? }`.\n const rawMessages = (body as { messages?: unknown }).messages\n if (!Array.isArray(rawMessages) || rawMessages.length === 0) {\n return jsonResponse(\n {\n error: 'invalid-request',\n message: 'expected { kind: \"generate\", request } or { messages: UIMessage[] }',\n },\n 400,\n )\n }\n\n const assembled = assembleSystemPrompt((body as { context?: unknown }).context)\n if (!assembled.ok) {\n return jsonResponse(\n { error: 'invalid-request', message: 'context must be a string or string[]' },\n 400,\n )\n }\n\n if (!options.provider.languageModel) {\n return jsonResponse({ error: 'provider-cannot-stream' }, 501)\n }\n\n // The SDK loads lazily INSIDE the handler (the anthropic-provider discipline):\n // the client-imported barrel re-exports this module, and a static edge would\n // put the AI SDK on the studio critical path (pinned in bundle.test.ts).\n const {\n validateUIMessages,\n convertToModelMessages,\n streamText,\n createUIMessageStreamResponse,\n toUIMessageStream,\n } = await import('ai')\n\n // Structural UIMessage validation (id/role/parts). Tool parts validate by\n // shape — deep input validation belongs to the client executor (slice 2).\n let messages\n try {\n messages = await validateUIMessages({ messages: rawMessages })\n } catch {\n return jsonResponse(\n { error: 'invalid-request', message: 'expected { messages: UIMessage[] }' },\n 400,\n )\n }\n\n try {\n const model = await options.provider.languageModel()\n const result = streamText({\n model,\n ...(assembled.system !== undefined ? { system: assembled.system } : {}),\n messages: await convertToModelMessages(messages),\n // No `execute` on any tool — the SDK forwards each call to the client\n // (ratified trust boundary). Continuation is therefore client-driven: one\n // tool round-trip per REQUEST, throttled by the rate limit above; the\n // per-turn step cap lands client-side with slice 2.\n tools: chatToolDefinitions,\n })\n return createUIMessageStreamResponse({\n stream: toUIMessageStream({ stream: result.stream, originalMessages: messages }),\n })\n } catch {\n return jsonResponse({ error: 'provider-error' }, 502)\n }\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { CollectionDef, JsonSchema } from '@elytracms/core/cms-core';
2
- import type { RelationCandidates } from './types.js';
2
+ import type { AssetCandidates, RelationCandidates } from './types.js';
3
3
  /** A relation field's resolved candidate set — how the engine maps ids → reference envelopes. */
4
4
  export interface TargetRelationField {
5
5
  fieldName: string;
@@ -9,6 +9,13 @@ export interface TargetRelationField {
9
9
  /** The allowed candidate ids (the enum the model chose from). */
10
10
  candidateIds: string[];
11
11
  }
12
+ /** An asset field's resolved candidate set — how the engine maps ids → asset envelopes (EC-381). */
13
+ export interface TargetAssetField {
14
+ fieldName: string;
15
+ multiple: boolean;
16
+ /** The allowed library asset ids (the enum the model chose from). */
17
+ candidateIds: string[];
18
+ }
12
19
  /** The derived target shape + the metadata the engine needs to post-process the output. */
13
20
  export interface TargetShape {
14
21
  /** The JSON Schema the structured output must satisfy. */
@@ -19,11 +26,15 @@ export interface TargetShape {
19
26
  localizedFields: string[];
20
27
  /** Relation fields with their candidate enums. */
21
28
  relationFields: TargetRelationField[];
29
+ /** Asset fields with their candidate enums (EC-381). */
30
+ assetFields: TargetAssetField[];
22
31
  }
23
32
  export interface BuildTargetShapeInput {
24
33
  collection: CollectionDef;
25
34
  /** Relation candidate ids by field — a relation field with none is omitted from the shape. */
26
35
  candidates?: RelationCandidates;
36
+ /** Asset candidate ids by field — an asset field with none is omitted from the shape (EC-381). */
37
+ assetCandidates?: AssetCandidates;
27
38
  /** UI locale for resolving field labels into descriptions. */
28
39
  locale?: string;
29
40
  }
@@ -10,30 +10,33 @@
10
10
  * - relation fields become an ENUM over the provided candidate ids (or are omitted
11
11
  * when no candidates exist) — so a hallucinated relation id is not representable
12
12
  * as valid structured output.
13
- * - rich-text fields are emitted in the `{ version, doc }` ProseMirror storage shape
14
- * (the `allow` leash on embeds is enforced later by `validateRichTextEmbeds`).
13
+ * - asset/image fields become an ENUM over the searched library asset ids (or are
14
+ * omitted when none were searched) — EC-381, the SAME leash as relations: a
15
+ * proposed asset id must be a real, `listAssets`-returned library asset, and a
16
+ * create shape with no searched assets simply has no asset property (empty beats
17
+ * wrong; the assistant never generates or uploads).
18
+ * - rich-text fields are MARKDOWN STRINGS (EC-379): the description names the
19
+ * allowed subset, and the engine converts md → the `{ version, doc }` storage
20
+ * envelope at proposal-build time. The field's `allow` config leashes component
21
+ * EMBEDS only — markdown cannot express an embed, so the leash is satisfied by
22
+ * construction (and still enforced downstream by `validateRichTextEmbeds`).
15
23
  *
16
24
  * Localized fields stay flat in the shape (the model authors one locale at a time);
17
25
  * the engine routes their values under `localized[locale]`.
18
26
  */
19
27
  import { collectionJsonSchema, resolveLabel } from '@elytracms/core/cms-core';
20
- /** The rich-text `{ version, doc }` storage envelope, as a JSON Schema. */
21
- function richTextEnvelopeSchema() {
28
+ import { MARKDOWN_PROSE_DESCRIPTION } from '@elytracms/core/rich-text';
29
+ /**
30
+ * A rich-text field's model-facing schema (EC-379): a MARKDOWN string. The
31
+ * description carries the allowed subset (from the engine converter — one source
32
+ * of truth), so the model has something concrete to aim at; the engine converts
33
+ * the markdown into the `{ version, doc }` storage envelope at proposal-build
34
+ * time and rejects out-of-subset constructs with explicit issues.
35
+ */
36
+ function richTextMarkdownSchema() {
22
37
  return {
23
- type: 'object',
24
- description: 'Rich text in the ProseMirror storage shape: { version, doc: { type: "doc", content: [...] } }.',
25
- properties: {
26
- version: { type: 'number' },
27
- doc: {
28
- type: 'object',
29
- properties: {
30
- type: { type: 'string', const: 'doc' },
31
- content: { type: 'array', items: { type: 'object' } },
32
- },
33
- required: ['type', 'content'],
34
- },
35
- },
36
- required: ['version', 'doc'],
38
+ type: 'string',
39
+ description: `Rich text, written as ${MARKDOWN_PROSE_DESCRIPTION} Write genuine, publishable prose.`,
37
40
  };
38
41
  }
39
42
  /** A field's model-facing description: resolved label + optional `ai.hint`. */
@@ -56,6 +59,7 @@ export function buildTargetShape(input) {
56
59
  const excluded = [];
57
60
  const localizedFields = [];
58
61
  const relationFields = [];
62
+ const assetFields = [];
59
63
  for (const field of input.collection.fields) {
60
64
  if (field.localized)
61
65
  localizedFields.push(field.name);
@@ -89,8 +93,30 @@ export function buildTargetShape(input) {
89
93
  candidateIds: ids,
90
94
  });
91
95
  }
96
+ else if (field.type === 'asset') {
97
+ // EC-381 — same leash as relations: an asset field is an enum over the ids
98
+ // the model actually searched in the media library (`listAssets`). No
99
+ // searched assets ⇒ omit the field, so the proposal simply leaves it empty
100
+ // (an invented / to-be-uploaded id is structurally unrepresentable).
101
+ const candidates = input.assetCandidates?.[field.name] ?? [];
102
+ if (candidates.length === 0) {
103
+ delete properties[field.name];
104
+ continue;
105
+ }
106
+ const ids = candidates.map((c) => c.id);
107
+ const label = candidates
108
+ .map((c) => (c.label ? `${c.id} (${c.label})` : c.id))
109
+ .join(', ');
110
+ const idSchema = {
111
+ type: 'string',
112
+ enum: ids,
113
+ description: `Choose an EXISTING library asset id, one of: ${label}. Leave empty if none fits.`,
114
+ };
115
+ properties[field.name] = field.multiple ? { type: 'array', items: idSchema } : idSchema;
116
+ assetFields.push({ fieldName: field.name, multiple: field.multiple, candidateIds: ids });
117
+ }
92
118
  else if (field.type === 'richText') {
93
- properties[field.name] = richTextEnvelopeSchema();
119
+ properties[field.name] = richTextMarkdownSchema();
94
120
  }
95
121
  const prop = properties[field.name];
96
122
  if (prop) {
@@ -108,6 +134,6 @@ export function buildTargetShape(input) {
108
134
  additionalProperties: false,
109
135
  ...(required.length > 0 ? { required } : {}),
110
136
  };
111
- return { schema, excluded, localizedFields, relationFields };
137
+ return { schema, excluded, localizedFields, relationFields, assetFields };
112
138
  }
113
139
  //# sourceMappingURL=shape.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shape.js","sourceRoot":"","sources":["../../../src/lib/ai/shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Target-shape derivation for `applyContentWithShape` (EC-331).\n *\n * Reuses the existing collection→JSON-Schema derivation (`collectionJsonSchema`,\n * cms-core) rather than writing a new one, then applies the three leashes the AI\n * engine adds on top:\n *\n * - `ai.exclude` fields are removed entirely (the model never sees them → cannot\n * write them).\n * - relation fields become an ENUM over the provided candidate ids (or are omitted\n * when no candidates exist) — so a hallucinated relation id is not representable\n * as valid structured output.\n * - rich-text fields are emitted in the `{ version, doc }` ProseMirror storage shape\n * (the `allow` leash on embeds is enforced later by `validateRichTextEmbeds`).\n *\n * Localized fields stay flat in the shape (the model authors one locale at a time);\n * the engine routes their values under `localized[locale]`.\n */\nimport { collectionJsonSchema, resolveLabel } from '@elytracms/core/cms-core'\nimport type { CollectionDef, FieldDef, JsonSchema } from '@elytracms/core/cms-core'\nimport type { RelationCandidates } from './types'\n\n/** A relation field's resolved candidate set — how the engine maps ids → reference envelopes. */\nexport interface TargetRelationField {\n fieldName: string\n /** The relation's target collection id. */\n target: string\n multiple: boolean\n /** The allowed candidate ids (the enum the model chose from). */\n candidateIds: string[]\n}\n\n/** The derived target shape + the metadata the engine needs to post-process the output. */\nexport interface TargetShape {\n /** The JSON Schema the structured output must satisfy. */\n schema: JsonSchema\n /** Field names removed by `ai.exclude` (untouchable). */\n excluded: string[]\n /** Field names whose values route under `localized[locale]`. */\n localizedFields: string[]\n /** Relation fields with their candidate enums. */\n relationFields: TargetRelationField[]\n}\n\nexport interface BuildTargetShapeInput {\n collection: CollectionDef\n /** Relation candidate ids by field — a relation field with none is omitted from the shape. */\n candidates?: RelationCandidates\n /** UI locale for resolving field labels into descriptions. */\n locale?: string\n}\n\n/** The rich-text `{ version, doc }` storage envelope, as a JSON Schema. */\nfunction richTextEnvelopeSchema(): JsonSchema {\n return {\n type: 'object',\n description:\n 'Rich text in the ProseMirror storage shape: { version, doc: { type: \"doc\", content: [...] } }.',\n properties: {\n version: { type: 'number' },\n doc: {\n type: 'object',\n properties: {\n type: { type: 'string', const: 'doc' },\n content: { type: 'array', items: { type: 'object' } },\n },\n required: ['type', 'content'],\n },\n },\n required: ['version', 'doc'],\n }\n}\n\n/** A field's model-facing description: resolved label + optional `ai.hint`. */\nfunction describe(field: FieldDef, locale?: string): string | undefined {\n const label = resolveLabel(field.form?.label, locale) ?? field.name\n const hint = field.ai?.hint\n const parts = [label, hint].filter((p): p is string => typeof p === 'string' && p.length > 0)\n return parts.length > 0 ? parts.join(' — ') : undefined\n}\n\n/**\n * Build the leash-aware target shape from a collection + candidates. Deterministic;\n * never throws. Reuses `collectionJsonSchema` as the base, then rewrites relations /\n * rich text and strips excluded fields.\n */\nexport function buildTargetShape(input: BuildTargetShapeInput): TargetShape {\n const base = collectionJsonSchema(input.collection)\n const properties: Record<string, JsonSchema> = {\n ...((base.properties as Record<string, JsonSchema>) ?? {}),\n }\n const excluded: string[] = []\n const localizedFields: string[] = []\n const relationFields: TargetRelationField[] = []\n\n for (const field of input.collection.fields) {\n if (field.localized) localizedFields.push(field.name)\n\n if (field.ai?.exclude) {\n excluded.push(field.name)\n delete properties[field.name]\n continue\n }\n\n if (field.type === 'relation') {\n const candidates = input.candidates?.[field.name] ?? []\n if (candidates.length === 0) {\n // No candidate ids ⇒ omit the field: the model cannot emit a relation value\n // at all, so a hallucinated id is structurally impossible.\n delete properties[field.name]\n continue\n }\n const ids = candidates.map((c) => c.id)\n const label = candidates\n .map((c) => (c.label ? `${c.id} (${c.label})` : c.id))\n .join(', ')\n const idSchema: JsonSchema = {\n type: 'string',\n enum: ids,\n description: `Choose one of: ${label}`,\n }\n properties[field.name] = field.multiple ? { type: 'array', items: idSchema } : idSchema\n relationFields.push({\n fieldName: field.name,\n target: field.target,\n multiple: field.multiple,\n candidateIds: ids,\n })\n } else if (field.type === 'richText') {\n properties[field.name] = richTextEnvelopeSchema()\n }\n\n const prop = properties[field.name]\n if (prop) {\n const desc = describe(field, input.locale)\n // Preserve an existing description (e.g. relation enum / rich-text note).\n if (desc && typeof prop.description !== 'string') prop.description = desc\n }\n }\n\n const requiredIn = Array.isArray(base.required) ? (base.required as string[]) : []\n const required = requiredIn.filter((n) => !excluded.includes(n) && properties[n] !== undefined)\n\n const schema: JsonSchema = {\n type: 'object',\n properties,\n additionalProperties: false,\n ...(required.length > 0 ? { required } : {}),\n }\n return { schema, excluded, localizedFields, relationFields }\n}\n"]}
1
+ {"version":3,"file":"shape.js","sourceRoot":"","sources":["../../../src/lib/ai/shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Target-shape derivation for `applyContentWithShape` (EC-331).\n *\n * Reuses the existing collection→JSON-Schema derivation (`collectionJsonSchema`,\n * cms-core) rather than writing a new one, then applies the three leashes the AI\n * engine adds on top:\n *\n * - `ai.exclude` fields are removed entirely (the model never sees them → cannot\n * write them).\n * - relation fields become an ENUM over the provided candidate ids (or are omitted\n * when no candidates exist) — so a hallucinated relation id is not representable\n * as valid structured output.\n * - asset/image fields become an ENUM over the searched library asset ids (or are\n * omitted when none were searched) — EC-381, the SAME leash as relations: a\n * proposed asset id must be a real, `listAssets`-returned library asset, and a\n * create shape with no searched assets simply has no asset property (empty beats\n * wrong; the assistant never generates or uploads).\n * - rich-text fields are MARKDOWN STRINGS (EC-379): the description names the\n * allowed subset, and the engine converts md → the `{ version, doc }` storage\n * envelope at proposal-build time. The field's `allow` config leashes component\n * EMBEDS only — markdown cannot express an embed, so the leash is satisfied by\n * construction (and still enforced downstream by `validateRichTextEmbeds`).\n *\n * Localized fields stay flat in the shape (the model authors one locale at a time);\n * the engine routes their values under `localized[locale]`.\n */\nimport { collectionJsonSchema, resolveLabel } from '@elytracms/core/cms-core'\nimport type { CollectionDef, FieldDef, JsonSchema } from '@elytracms/core/cms-core'\nimport { MARKDOWN_PROSE_DESCRIPTION } from '@elytracms/core/rich-text'\nimport type { AssetCandidates, RelationCandidates } from './types'\n\n/** A relation field's resolved candidate set — how the engine maps ids → reference envelopes. */\nexport interface TargetRelationField {\n fieldName: string\n /** The relation's target collection id. */\n target: string\n multiple: boolean\n /** The allowed candidate ids (the enum the model chose from). */\n candidateIds: string[]\n}\n\n/** An asset field's resolved candidate set — how the engine maps ids → asset envelopes (EC-381). */\nexport interface TargetAssetField {\n fieldName: string\n multiple: boolean\n /** The allowed library asset ids (the enum the model chose from). */\n candidateIds: string[]\n}\n\n/** The derived target shape + the metadata the engine needs to post-process the output. */\nexport interface TargetShape {\n /** The JSON Schema the structured output must satisfy. */\n schema: JsonSchema\n /** Field names removed by `ai.exclude` (untouchable). */\n excluded: string[]\n /** Field names whose values route under `localized[locale]`. */\n localizedFields: string[]\n /** Relation fields with their candidate enums. */\n relationFields: TargetRelationField[]\n /** Asset fields with their candidate enums (EC-381). */\n assetFields: TargetAssetField[]\n}\n\nexport interface BuildTargetShapeInput {\n collection: CollectionDef\n /** Relation candidate ids by field — a relation field with none is omitted from the shape. */\n candidates?: RelationCandidates\n /** Asset candidate ids by field — an asset field with none is omitted from the shape (EC-381). */\n assetCandidates?: AssetCandidates\n /** UI locale for resolving field labels into descriptions. */\n locale?: string\n}\n\n/**\n * A rich-text field's model-facing schema (EC-379): a MARKDOWN string. The\n * description carries the allowed subset (from the engine converter — one source\n * of truth), so the model has something concrete to aim at; the engine converts\n * the markdown into the `{ version, doc }` storage envelope at proposal-build\n * time and rejects out-of-subset constructs with explicit issues.\n */\nfunction richTextMarkdownSchema(): JsonSchema {\n return {\n type: 'string',\n description: `Rich text, written as ${MARKDOWN_PROSE_DESCRIPTION} Write genuine, publishable prose.`,\n }\n}\n\n/** A field's model-facing description: resolved label + optional `ai.hint`. */\nfunction describe(field: FieldDef, locale?: string): string | undefined {\n const label = resolveLabel(field.form?.label, locale) ?? field.name\n const hint = field.ai?.hint\n const parts = [label, hint].filter((p): p is string => typeof p === 'string' && p.length > 0)\n return parts.length > 0 ? parts.join(' — ') : undefined\n}\n\n/**\n * Build the leash-aware target shape from a collection + candidates. Deterministic;\n * never throws. Reuses `collectionJsonSchema` as the base, then rewrites relations /\n * rich text and strips excluded fields.\n */\nexport function buildTargetShape(input: BuildTargetShapeInput): TargetShape {\n const base = collectionJsonSchema(input.collection)\n const properties: Record<string, JsonSchema> = {\n ...((base.properties as Record<string, JsonSchema>) ?? {}),\n }\n const excluded: string[] = []\n const localizedFields: string[] = []\n const relationFields: TargetRelationField[] = []\n const assetFields: TargetAssetField[] = []\n\n for (const field of input.collection.fields) {\n if (field.localized) localizedFields.push(field.name)\n\n if (field.ai?.exclude) {\n excluded.push(field.name)\n delete properties[field.name]\n continue\n }\n\n if (field.type === 'relation') {\n const candidates = input.candidates?.[field.name] ?? []\n if (candidates.length === 0) {\n // No candidate ids ⇒ omit the field: the model cannot emit a relation value\n // at all, so a hallucinated id is structurally impossible.\n delete properties[field.name]\n continue\n }\n const ids = candidates.map((c) => c.id)\n const label = candidates\n .map((c) => (c.label ? `${c.id} (${c.label})` : c.id))\n .join(', ')\n const idSchema: JsonSchema = {\n type: 'string',\n enum: ids,\n description: `Choose one of: ${label}`,\n }\n properties[field.name] = field.multiple ? { type: 'array', items: idSchema } : idSchema\n relationFields.push({\n fieldName: field.name,\n target: field.target,\n multiple: field.multiple,\n candidateIds: ids,\n })\n } else if (field.type === 'asset') {\n // EC-381 — same leash as relations: an asset field is an enum over the ids\n // the model actually searched in the media library (`listAssets`). No\n // searched assets ⇒ omit the field, so the proposal simply leaves it empty\n // (an invented / to-be-uploaded id is structurally unrepresentable).\n const candidates = input.assetCandidates?.[field.name] ?? []\n if (candidates.length === 0) {\n delete properties[field.name]\n continue\n }\n const ids = candidates.map((c) => c.id)\n const label = candidates\n .map((c) => (c.label ? `${c.id} (${c.label})` : c.id))\n .join(', ')\n const idSchema: JsonSchema = {\n type: 'string',\n enum: ids,\n description: `Choose an EXISTING library asset id, one of: ${label}. Leave empty if none fits.`,\n }\n properties[field.name] = field.multiple ? { type: 'array', items: idSchema } : idSchema\n assetFields.push({ fieldName: field.name, multiple: field.multiple, candidateIds: ids })\n } else if (field.type === 'richText') {\n properties[field.name] = richTextMarkdownSchema()\n }\n\n const prop = properties[field.name]\n if (prop) {\n const desc = describe(field, input.locale)\n // Preserve an existing description (e.g. relation enum / rich-text note).\n if (desc && typeof prop.description !== 'string') prop.description = desc\n }\n }\n\n const requiredIn = Array.isArray(base.required) ? (base.required as string[]) : []\n const required = requiredIn.filter((n) => !excluded.includes(n) && properties[n] !== undefined)\n\n const schema: JsonSchema = {\n type: 'object',\n properties,\n additionalProperties: false,\n ...(required.length > 0 ? { required } : {}),\n }\n return { schema, excluded, localizedFields, relationFields, assetFields }\n}\n"]}