create-nextblock 0.11.1 → 0.11.3

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 (59) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/actions/interactions.test.ts +301 -0
  3. package/templates/nextblock-template/app/actions/interactions.ts +372 -0
  4. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +4 -4
  5. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +2 -2
  6. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +56 -57
  7. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +1 -1
  8. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +951 -0
  9. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +6 -0
  10. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +4 -0
  11. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +7 -2
  12. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +4 -0
  13. package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -0
  14. package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -0
  15. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +4 -3
  16. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +1 -1
  17. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +3 -5
  18. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +1 -1
  19. package/templates/nextblock-template/app/page.tsx +2 -2
  20. package/templates/nextblock-template/app/product/[slug]/page.tsx +2 -0
  21. package/templates/nextblock-template/components/AppShell.tsx +1 -1
  22. package/templates/nextblock-template/components/PostCommentsSection.tsx +369 -0
  23. package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -0
  24. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +2 -0
  25. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +62 -19
  26. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -19
  27. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +4 -4
  28. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +7 -0
  29. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +2 -0
  30. package/templates/nextblock-template/lib/setup/actions.ts +3 -1
  31. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +40 -0
  32. package/templates/nextblock-template/lib/updates/check-upstream.ts +38 -4
  33. package/templates/nextblock-template/package.json +2 -1
  34. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +2 -4
  35. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +1 -1
  36. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +1 -1
  37. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +1 -1
  38. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  39. package/templates/nextblock-template/lib/ai-block-generation.ts +0 -339
  40. package/templates/nextblock-template/lib/ai-client.ts +0 -247
  41. package/templates/nextblock-template/lib/ai-config.ts +0 -98
  42. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +0 -125
  43. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +0 -363
  44. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +0 -405
  45. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +0 -1228
  46. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +0 -5
  47. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +0 -223
  48. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +0 -2183
  49. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +0 -4807
  50. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +0 -70
  51. package/templates/nextblock-template/lib/ai-key-crypto.ts +0 -132
  52. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +0 -49
  53. package/templates/nextblock-template/lib/ai-model-catalog.ts +0 -41
  54. package/templates/nextblock-template/lib/ai-model-registry.test.ts +0 -231
  55. package/templates/nextblock-template/lib/ai-model-registry.ts +0 -522
  56. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +0 -199
  57. package/templates/nextblock-template/lib/cortex-widget-registry.ts +0 -88
  58. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +0 -237
  59. package/templates/nextblock-template/lib/cortex-widget-schema.ts +0 -393
@@ -1,522 +0,0 @@
1
- import { APICallError } from 'ai';
2
-
3
- export const CORTEX_AI_OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
4
- export const CORTEX_AI_OPENROUTER_FREE_ROUTER_MODEL = 'openrouter/free';
5
-
6
- export const CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY = [
7
- 'qwen/qwen3-next-80b-a3b-instruct:free',
8
- 'nvidia/nemotron-3-super-120b-a12b:free',
9
- 'nvidia/nemotron-nano-9b-v2:free',
10
- ] as const;
11
-
12
- export const CORTEX_AI_REQUIRED_MODEL_PARAMETERS = ['tools', 'structured_outputs'] as const;
13
-
14
- const CORTEX_AI_OPTIONAL_MODEL_PARAMETER_MAP = {
15
- frequencyPenalty: 'frequency_penalty',
16
- logitBias: 'logit_bias',
17
- presencePenalty: 'presence_penalty',
18
- seed: 'seed',
19
- stopSequences: 'stop',
20
- temperature: 'temperature',
21
- topK: 'top_k',
22
- topP: 'top_p',
23
- } as const;
24
-
25
- export const CORTEX_AI_MODEL_REGISTRY = {
26
- defaultFreeRouter: CORTEX_AI_OPENROUTER_FREE_ROUTER_MODEL,
27
- defaultStructuredOutputModel: CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY[0],
28
- defaultToolCallingModel: CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY[0],
29
- freeFallbacks: CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY,
30
- structuredJsonPreferred: CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY,
31
- toolCallingPreferred: CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY,
32
- } as const;
33
-
34
- export type CortexAiOpenRouterModelId =
35
- | typeof CORTEX_AI_OPENROUTER_FREE_ROUTER_MODEL
36
- | (typeof CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY)[number]
37
- | (string & {});
38
-
39
- export type CortexAiRoutingCredentialSource = 'env' | 'manual' | 'stored';
40
-
41
- export type CortexAiOpenRouterModelPricing = Record<string, string>;
42
-
43
- export type CortexAiCompatibleOpenRouterModel = {
44
- contextLength: number | null;
45
- created: number | null;
46
- expirationDate: string | null;
47
- id: CortexAiOpenRouterModelId;
48
- name: string;
49
- pricing: CortexAiOpenRouterModelPricing;
50
- supportedParameters: readonly string[];
51
- };
52
-
53
- export type CortexAiStoredModelSelection = {
54
- contextLength: number | null;
55
- modelId: CortexAiOpenRouterModelId;
56
- name: string;
57
- pricing: CortexAiOpenRouterModelPricing;
58
- supportedParameters: readonly string[];
59
- updatedAt: string;
60
- };
61
-
62
- export type CortexAiRoutingPolicy = {
63
- credentialSource: CortexAiRoutingCredentialSource;
64
- ignoredRequestedModelId: CortexAiOpenRouterModelId | null;
65
- modelIds: readonly CortexAiOpenRouterModelId[];
66
- modelSelection: CortexAiStoredModelSelection | null;
67
- };
68
-
69
- export type CortexAiModelAttempt = {
70
- errorMessage?: string;
71
- modelId: CortexAiOpenRouterModelId;
72
- rateLimited: boolean;
73
- status: 'success' | 'rate_limited' | 'retried' | 'failed';
74
- };
75
-
76
- export class CortexAiRoutingError extends Error {
77
- readonly attempts: readonly CortexAiModelAttempt[];
78
-
79
- constructor(message: string, attempts: readonly CortexAiModelAttempt[], cause?: unknown) {
80
- super(message);
81
- this.name = 'CortexAiRoutingError';
82
- this.attempts = attempts;
83
- this.cause = cause;
84
- }
85
- }
86
-
87
- function uniqueModelIds(modelIds: readonly CortexAiOpenRouterModelId[]) {
88
- return Array.from(new Set(modelIds.filter(Boolean)));
89
- }
90
-
91
- function readRecord(value: unknown): Record<string, unknown> | null {
92
- return value && typeof value === 'object' && !Array.isArray(value)
93
- ? (value as Record<string, unknown>)
94
- : null;
95
- }
96
-
97
- function readString(value: unknown) {
98
- return typeof value === 'string' && value.trim() ? value.trim() : null;
99
- }
100
-
101
- function readStringArray(value: unknown) {
102
- return Array.isArray(value)
103
- ? value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
104
- : [];
105
- }
106
-
107
- function readNumberLike(value: unknown) {
108
- const parsed = typeof value === 'number' ? value : Number(value);
109
- return Number.isFinite(parsed) ? parsed : null;
110
- }
111
-
112
- function readStringRecord(value: unknown): CortexAiOpenRouterModelPricing {
113
- const record = readRecord(value);
114
-
115
- if (!record) {
116
- return {};
117
- }
118
-
119
- return Object.fromEntries(
120
- Object.entries(record)
121
- .filter(([, entryValue]) => entryValue !== null && entryValue !== undefined)
122
- .map(([key, entryValue]) => [key, String(entryValue)])
123
- );
124
- }
125
-
126
- function supportsRequiredModelParameters(supportedParameters: readonly string[]) {
127
- const supported = new Set(supportedParameters);
128
- return CORTEX_AI_REQUIRED_MODEL_PARAMETERS.every((parameter) => supported.has(parameter));
129
- }
130
-
131
- function isTextOutputModel(record: Record<string, unknown>) {
132
- const architecture = readRecord(record.architecture);
133
- return readStringArray(architecture?.output_modalities).includes('text');
134
- }
135
-
136
- function getExpirationTimestamp(value: unknown) {
137
- if (value === null || value === undefined) {
138
- return null;
139
- }
140
-
141
- if (typeof value === 'number') {
142
- return value > 10_000_000_000 ? value : value * 1000;
143
- }
144
-
145
- if (typeof value === 'string' && value.trim()) {
146
- const parsed = Date.parse(value);
147
- return Number.isFinite(parsed) ? parsed : null;
148
- }
149
-
150
- return null;
151
- }
152
-
153
- function isExpiredOpenRouterModel(value: unknown, now: Date) {
154
- const expirationTimestamp = getExpirationTimestamp(value);
155
- return expirationTimestamp !== null && expirationTimestamp <= now.getTime();
156
- }
157
-
158
- function readOpenRouterModelContextLength(record: Record<string, unknown>) {
159
- const topProvider = readRecord(record.top_provider);
160
- return (
161
- readNumberLike(record.context_length) ??
162
- readNumberLike(record.contextLength) ??
163
- readNumberLike(topProvider?.context_length) ??
164
- null
165
- );
166
- }
167
-
168
- function readOpenRouterModelExpirationDate(record: Record<string, unknown>) {
169
- const expirationDate = record.expiration_date ?? record.expirationDate;
170
- return typeof expirationDate === 'string' && expirationDate.trim()
171
- ? expirationDate.trim()
172
- : null;
173
- }
174
-
175
- export function isCortexAiFreeModelId(modelId: CortexAiOpenRouterModelId | null | undefined) {
176
- return Boolean(
177
- modelId &&
178
- (CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY as readonly string[]).includes(modelId)
179
- );
180
- }
181
-
182
- export function safeParseCortexAiModelSelection(
183
- value: unknown
184
- ): CortexAiStoredModelSelection | null {
185
- const record = readRecord(value);
186
-
187
- if (!record) {
188
- return null;
189
- }
190
-
191
- const modelId = readString(record.modelId);
192
- const name = readString(record.name);
193
- const supportedParameters = readStringArray(record.supportedParameters);
194
- const updatedAt = readString(record.updatedAt);
195
-
196
- if (!modelId || !name || !updatedAt || !supportsRequiredModelParameters(supportedParameters)) {
197
- return null;
198
- }
199
-
200
- return {
201
- contextLength: readNumberLike(record.contextLength),
202
- modelId,
203
- name,
204
- pricing: readStringRecord(record.pricing),
205
- supportedParameters,
206
- updatedAt,
207
- };
208
- }
209
-
210
- export function createCortexAiStoredModelSelection(
211
- model: CortexAiCompatibleOpenRouterModel,
212
- now = new Date()
213
- ): CortexAiStoredModelSelection {
214
- return {
215
- contextLength: model.contextLength,
216
- modelId: model.id,
217
- name: model.name,
218
- pricing: model.pricing,
219
- supportedParameters: [...model.supportedParameters],
220
- updatedAt: now.toISOString(),
221
- };
222
- }
223
-
224
- export function filterCortexAiCompatibleOpenRouterModels(
225
- value: unknown,
226
- now = new Date()
227
- ): CortexAiCompatibleOpenRouterModel[] {
228
- const root = readRecord(value);
229
- const rawModels = Array.isArray(value)
230
- ? value
231
- : Array.isArray(root?.data)
232
- ? root.data
233
- : [];
234
-
235
- const compatibleModels: CortexAiCompatibleOpenRouterModel[] = [];
236
-
237
- for (const rawModel of rawModels) {
238
- const record = readRecord(rawModel);
239
- const id = readString(record?.id);
240
- const name = readString(record?.name);
241
- const supportedParameters = readStringArray(record?.supported_parameters);
242
-
243
- if (
244
- !record ||
245
- !id ||
246
- !name ||
247
- !isTextOutputModel(record) ||
248
- isExpiredOpenRouterModel(record.expiration_date, now) ||
249
- !supportsRequiredModelParameters(supportedParameters)
250
- ) {
251
- continue;
252
- }
253
-
254
- compatibleModels.push({
255
- contextLength: readOpenRouterModelContextLength(record),
256
- created: readNumberLike(record.created),
257
- expirationDate: readOpenRouterModelExpirationDate(record),
258
- id,
259
- name,
260
- pricing: readStringRecord(record.pricing),
261
- supportedParameters,
262
- });
263
- }
264
-
265
- return compatibleModels.sort((left, right) => left.name.localeCompare(right.name));
266
- }
267
-
268
- export function buildCortexAiModelFallbackChain(params?: {
269
- fallbackModelIds?: readonly CortexAiOpenRouterModelId[];
270
- modelId?: CortexAiOpenRouterModelId | null;
271
- }) {
272
- return uniqueModelIds([
273
- params?.modelId || CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY[0],
274
- ...(params?.fallbackModelIds || CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY),
275
- ]);
276
- }
277
-
278
- export function buildCortexAiRoutingPolicy(params: {
279
- credentialSource: CortexAiRoutingCredentialSource;
280
- fallbackModelIds?: readonly CortexAiOpenRouterModelId[];
281
- requestedModelId?: CortexAiOpenRouterModelId | null;
282
- selectedModel?: CortexAiStoredModelSelection | null;
283
- }): CortexAiRoutingPolicy {
284
- const requestedModelId = params.requestedModelId?.trim() || null;
285
-
286
- if (params.credentialSource === 'env') {
287
- return {
288
- credentialSource: params.credentialSource,
289
- ignoredRequestedModelId:
290
- requestedModelId && !isCortexAiFreeModelId(requestedModelId)
291
- ? requestedModelId
292
- : null,
293
- modelIds: [...CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY],
294
- modelSelection: null,
295
- };
296
- }
297
-
298
- const fallbackModelIds = uniqueModelIds(
299
- params.fallbackModelIds?.length
300
- ? params.fallbackModelIds
301
- : CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY
302
- );
303
- const preferredModelId =
304
- params.credentialSource === 'stored'
305
- ? params.selectedModel?.modelId || fallbackModelIds[0]
306
- : requestedModelId || params.selectedModel?.modelId || fallbackModelIds[0];
307
-
308
- return {
309
- credentialSource: params.credentialSource,
310
- ignoredRequestedModelId:
311
- params.credentialSource === 'stored' &&
312
- requestedModelId &&
313
- requestedModelId !== preferredModelId
314
- ? requestedModelId
315
- : null,
316
- modelIds: uniqueModelIds([preferredModelId, ...fallbackModelIds]),
317
- modelSelection: params.selectedModel || null,
318
- };
319
- }
320
-
321
- export function omitUnsupportedCortexAiModelOptions<TOptions extends Record<string, unknown>>(
322
- options: TOptions,
323
- params: {
324
- modelId: CortexAiOpenRouterModelId;
325
- modelSelection?: CortexAiStoredModelSelection | null;
326
- }
327
- ): TOptions {
328
- const modelSelection = params.modelSelection;
329
-
330
- if (!modelSelection || modelSelection.modelId !== params.modelId) {
331
- return options;
332
- }
333
-
334
- const supportedParameters = new Set(modelSelection.supportedParameters);
335
- const unsupportedOptionKeys = Object.entries(CORTEX_AI_OPTIONAL_MODEL_PARAMETER_MAP)
336
- .filter(([optionKey, parameterName]) => optionKey in options && !supportedParameters.has(parameterName))
337
- .map(([optionKey]) => optionKey);
338
-
339
- if (unsupportedOptionKeys.length === 0) {
340
- return options;
341
- }
342
-
343
- const nextOptions = { ...options };
344
-
345
- for (const optionKey of unsupportedOptionKeys) {
346
- delete nextOptions[optionKey as keyof TOptions];
347
- }
348
-
349
- return nextOptions;
350
- }
351
-
352
- function readNumericProperty(value: unknown, property: string) {
353
- if (!value || typeof value !== 'object' || !(property in value)) {
354
- return null;
355
- }
356
-
357
- const raw = (value as Record<string, unknown>)[property];
358
- const parsed = typeof raw === 'number' ? raw : Number(raw);
359
- return Number.isFinite(parsed) ? parsed : null;
360
- }
361
-
362
- export function getHttpStatusCode(error: unknown): number | null {
363
- if (APICallError.isInstance(error)) {
364
- return error.statusCode ?? null;
365
- }
366
-
367
- const directStatus = readNumericProperty(error, 'statusCode') ?? readNumericProperty(error, 'status');
368
-
369
- if (directStatus) {
370
- return directStatus;
371
- }
372
-
373
- if (error && typeof error === 'object' && 'response' in error) {
374
- const response = (error as { response?: unknown }).response;
375
- const responseStatus = readNumericProperty(response, 'status');
376
-
377
- if (responseStatus) {
378
- return responseStatus;
379
- }
380
- }
381
-
382
- if (error && typeof error === 'object' && 'cause' in error) {
383
- return getHttpStatusCode((error as { cause?: unknown }).cause);
384
- }
385
-
386
- return null;
387
- }
388
-
389
- export function isOpenRouterRateLimitError(error: unknown) {
390
- return getHttpStatusCode(error) === 429;
391
- }
392
-
393
- function getDeepErrorMessage(error: unknown): string {
394
- if (!error) {
395
- return '';
396
- }
397
-
398
- if (error instanceof Error) {
399
- const causeMessage = 'cause' in error ? getDeepErrorMessage(error.cause) : '';
400
- return [error.message, causeMessage].filter(Boolean).join('\n');
401
- }
402
-
403
- if (typeof error === 'object') {
404
- const record = error as Record<string, unknown>;
405
- return ['message', 'error', 'text', 'cause']
406
- .map((key) => getDeepErrorMessage(record[key]))
407
- .filter(Boolean)
408
- .join('\n');
409
- }
410
-
411
- return String(error);
412
- }
413
-
414
- export function isOpenRouterRecoverableRoutingError(error: unknown) {
415
- if (isOpenRouterRateLimitError(error)) {
416
- return true;
417
- }
418
-
419
- return /No endpoints found|no longer available|not available as a free model|transitioned to a paid model/i.test(
420
- getDeepErrorMessage(error)
421
- );
422
- }
423
-
424
- function truncateErrorMessage(message: string, maxLength = 900) {
425
- const normalized = message.replace(/\s+/g, ' ').trim();
426
- return normalized.length > maxLength
427
- ? `${normalized.slice(0, maxLength - 1).trimEnd()}...`
428
- : normalized;
429
- }
430
-
431
- function getErrorMessage(error: unknown) {
432
- const message = getDeepErrorMessage(error);
433
- return message ? truncateErrorMessage(message) : 'Unknown OpenRouter error.';
434
- }
435
-
436
- export function summarizeCortexAiRoutingError(
437
- error: unknown,
438
- fallbackMessage = 'Cortex AI request failed.'
439
- ) {
440
- if (error instanceof CortexAiRoutingError) {
441
- const attemptMessages = error.attempts
442
- .map((attempt) => attempt.errorMessage)
443
- .filter((message): message is string => Boolean(message?.trim()));
444
- const firstAttemptMessage = attemptMessages[0];
445
- const lastAttemptMessage = attemptMessages[attemptMessages.length - 1];
446
- const causeMessage = truncateErrorMessage(getDeepErrorMessage(error.cause));
447
-
448
- if (
449
- firstAttemptMessage &&
450
- lastAttemptMessage &&
451
- firstAttemptMessage !== lastAttemptMessage
452
- ) {
453
- return `First model error: ${firstAttemptMessage} Last model error: ${lastAttemptMessage}`;
454
- }
455
-
456
- return lastAttemptMessage || firstAttemptMessage || causeMessage || error.message;
457
- }
458
-
459
- return truncateErrorMessage(getDeepErrorMessage(error)) || fallbackMessage;
460
- }
461
-
462
- export async function runWithCortexAiModelFallback<T>(params: {
463
- execute: (modelId: CortexAiOpenRouterModelId) => Promise<T>;
464
- modelIds: readonly CortexAiOpenRouterModelId[];
465
- shouldRetry?: (error: unknown) => boolean;
466
- }): Promise<{
467
- attempts: readonly CortexAiModelAttempt[];
468
- modelId: CortexAiOpenRouterModelId;
469
- result: T;
470
- }> {
471
- const modelIds = uniqueModelIds(params.modelIds);
472
- const shouldRetry = params.shouldRetry || isOpenRouterRecoverableRoutingError;
473
- let attempts: readonly CortexAiModelAttempt[] = [];
474
- let lastError: unknown = null;
475
-
476
- for (const modelId of modelIds) {
477
- try {
478
- const result = await params.execute(modelId);
479
- attempts = [
480
- ...attempts,
481
- {
482
- modelId,
483
- rateLimited: false,
484
- status: 'success',
485
- },
486
- ];
487
-
488
- return {
489
- attempts,
490
- modelId,
491
- result,
492
- };
493
- } catch (error) {
494
- const rateLimited = isOpenRouterRateLimitError(error);
495
- const retryable = shouldRetry(error);
496
- lastError = error;
497
- attempts = [
498
- ...attempts,
499
- {
500
- errorMessage: getErrorMessage(error),
501
- modelId,
502
- rateLimited,
503
- status: rateLimited ? 'rate_limited' : retryable ? 'retried' : 'failed',
504
- },
505
- ];
506
-
507
- if (!retryable) {
508
- throw new CortexAiRoutingError(
509
- `OpenRouter request failed for model "${modelId}".`,
510
- attempts,
511
- error
512
- );
513
- }
514
- }
515
- }
516
-
517
- throw new CortexAiRoutingError(
518
- 'OpenRouter fallback exhausted all configured Cortex AI models.',
519
- attempts,
520
- lastError
521
- );
522
- }
@@ -1,199 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
- vi.mock('@nextblock-cms/utils', async () => {
4
- const { z } = await import('zod');
5
- const fieldKeyPattern = /^[a-z][a-z0-9_]*$/;
6
- const slugPattern = /^[a-z][a-z0-9-]*$/;
7
- const customBlockFieldKeySchema = z.string().trim().min(1).max(80).regex(fieldKeyPattern);
8
- const customBlockSlugSchema = z.string().trim().min(1).max(120).regex(slugPattern);
9
- const fieldBaseSchema = z.strictObject({
10
- description: z.string().trim().max(500).optional(),
11
- key: customBlockFieldKeySchema,
12
- label: z.string().trim().min(1).max(120),
13
- required: z.boolean().default(false),
14
- });
15
- const fieldSchema = z.discriminatedUnion('type', [
16
- fieldBaseSchema.extend({
17
- default_value: z.string().max(5000).optional(),
18
- max_length: z.number().int().positive().max(10000).optional(),
19
- min_length: z.number().int().min(0).max(10000).optional(),
20
- placeholder: z.string().max(250).optional(),
21
- type: z.literal('text'),
22
- }),
23
- fieldBaseSchema.extend({
24
- default_value: z.string().max(50000).optional(),
25
- placeholder: z.string().max(250).optional(),
26
- type: z.literal('rich-text'),
27
- }),
28
- fieldBaseSchema.extend({
29
- accept: z.array(z.string()).max(20).optional(),
30
- default_value: z
31
- .strictObject({
32
- alt: z.string().max(300).optional(),
33
- file_name: z.string().trim().min(1).max(255).optional(),
34
- file_type: z.string().trim().min(1).max(120).optional(),
35
- height: z.number().int().positive().optional(),
36
- object_key: z.string().trim().min(1).max(1024),
37
- size_bytes: z.number().int().positive().optional(),
38
- url: z.string().trim().min(1).max(2048),
39
- width: z.number().int().positive().optional(),
40
- })
41
- .optional(),
42
- max_bytes: z.number().int().positive().max(50 * 1024 * 1024).optional(),
43
- type: z.literal('image_r2'),
44
- }),
45
- fieldBaseSchema.extend({
46
- default_value: z.union([z.string(), z.array(z.string()), z.null()]).optional(),
47
- display_column: z.string().trim().min(1).max(80).default('title'),
48
- filters: z.record(z.string(), z.unknown()).optional(),
49
- multiple: z.boolean().default(false),
50
- table: z.string().trim().min(1).max(80).regex(fieldKeyPattern),
51
- type: z.literal('db_relation'),
52
- value_column: z.string().trim().min(1).max(80).default('id'),
53
- }),
54
- ]);
55
- const elementSchema = z.enum([
56
- 'article',
57
- 'aside',
58
- 'blockquote',
59
- 'div',
60
- 'figure',
61
- 'figcaption',
62
- 'h2',
63
- 'h3',
64
- 'img',
65
- 'p',
66
- 'section',
67
- 'span',
68
- ]);
69
- const layoutNodeSchema: any = z.lazy(() =>
70
- z.discriminatedUnion('type', [
71
- z.strictObject({
72
- as: elementSchema.optional(),
73
- children: z.array(layoutNodeSchema).max(200).default([]),
74
- className: z.string().trim().max(4000).optional(),
75
- type: z.literal('container'),
76
- }),
77
- z.strictObject({
78
- as: elementSchema.optional(),
79
- className: z.string().trim().max(4000).optional(),
80
- emptyFallback: z.string().max(300).optional(),
81
- field_key: customBlockFieldKeySchema,
82
- type: z.literal('field_render'),
83
- }),
84
- ])
85
- );
86
- const customBlockDefinitionCreateSchema = z.strictObject({
87
- description: z.string().trim().max(1000).default(''),
88
- fields: z.array(fieldSchema).max(80).default([]),
89
- is_original: z.boolean().default(true),
90
- layout_schema: layoutNodeSchema,
91
- name: z.string().trim().min(1).max(160),
92
- slug: customBlockSlugSchema,
93
- });
94
- const customBlockDefinitionRowSchema = customBlockDefinitionCreateSchema.safeExtend({
95
- id: z.string().uuid(),
96
- is_original: z.boolean(),
97
- });
98
-
99
- return {
100
- customBlockDefinitionCreateSchema,
101
- customBlockDefinitionRowSchema,
102
- customBlockFieldKeySchema,
103
- customBlockSlugSchema,
104
- };
105
- });
106
-
107
- import { buildCortexProfileCardVerificationDefinition } from './cortex-widget-schema';
108
- import {
109
- CortexWidgetRegistryInsertError,
110
- buildCortexWidgetDefinitionInsertPayload,
111
- insertCortexWidgetDefinition,
112
- } from './cortex-widget-registry';
113
-
114
- class MockInsertQuery {
115
- payload: unknown;
116
-
117
- constructor(
118
- private readonly result: {
119
- data: unknown;
120
- error: { code?: string; message?: string } | null;
121
- }
122
- ) {}
123
-
124
- insert(payload: unknown) {
125
- this.payload = payload;
126
- return this;
127
- }
128
-
129
- select() {
130
- return this;
131
- }
132
-
133
- single() {
134
- return Promise.resolve(this.result);
135
- }
136
- }
137
-
138
- describe('cortex widget registry insert', () => {
139
- it('builds a strict atomic insert payload for custom_block_definitions', () => {
140
- const definition = buildCortexProfileCardVerificationDefinition();
141
- const payload = buildCortexWidgetDefinitionInsertPayload(definition);
142
-
143
- expect(payload).toMatchObject({
144
- description: definition.description,
145
- is_original: true,
146
- name: definition.name,
147
- slug: definition.slug,
148
- });
149
- expect(payload.fields).toEqual(definition.fields);
150
- expect(payload.layout_schema).toEqual(definition.layout_schema);
151
- });
152
-
153
- it('inserts the generated widget definition and parses the returned row', async () => {
154
- const definition = buildCortexProfileCardVerificationDefinition();
155
- const query = new MockInsertQuery({
156
- data: {
157
- ...buildCortexWidgetDefinitionInsertPayload(definition),
158
- id: '66666666-6666-4666-8666-666666666666',
159
- },
160
- error: null,
161
- });
162
- const supabase = {
163
- from: (table: string) => {
164
- expect(table).toBe('custom_block_definitions');
165
- return query;
166
- },
167
- };
168
-
169
- const inserted = await insertCortexWidgetDefinition(supabase as any, definition);
170
-
171
- expect(query.payload).toMatchObject({
172
- is_original: true,
173
- slug: 'cortex-profile-card',
174
- });
175
- expect(inserted).toMatchObject({
176
- id: '66666666-6666-4666-8666-666666666666',
177
- is_original: true,
178
- slug: 'cortex-profile-card',
179
- });
180
- });
181
-
182
- it('maps unique slug failures to a 409 registry error', async () => {
183
- const definition = buildCortexProfileCardVerificationDefinition();
184
- const query = new MockInsertQuery({
185
- data: null,
186
- error: {
187
- code: '23505',
188
- message: 'duplicate key value violates unique constraint',
189
- },
190
- });
191
-
192
- await expect(
193
- insertCortexWidgetDefinition({ from: () => query } as any, definition)
194
- ).rejects.toMatchObject<CortexWidgetRegistryInsertError>({
195
- code: '23505',
196
- status: 409,
197
- });
198
- });
199
- });