create-nextblock 0.14.6 → 0.15.0
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/package.json +1 -1
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +190 -1
- package/templates/nextblock-template/app/api/mcp/route.ts +82 -13
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +5 -1
- package/templates/nextblock-template/app/cms/media/import-external-image.ts +97 -11
- package/templates/nextblock-template/app/cms/settings/site-scripts/actions.ts +346 -0
- package/templates/nextblock-template/app/cms/settings/site-scripts/components/SiteScriptManager.tsx +492 -0
- package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -0
- package/templates/nextblock-template/app/layout.tsx +33 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +9 -3
- package/templates/nextblock-template/components/SiteScripts.tsx +56 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +1 -9
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +121 -0
- package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -0
- package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -0
- package/templates/nextblock-template/lib/site-scripts/types.ts +46 -0
- package/templates/nextblock-template/next-env.d.ts +2 -2
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The loading behaviour of an external script is the admin's choice, stored per row
|
|
3
|
+
* as `load_strategy` (default / defer / async). A blocking load is sometimes the
|
|
4
|
+
* required one — anti-flicker snippets and consent gates have to run before render —
|
|
5
|
+
* so this file renders what was asked for instead of forcing `defer` on everything.
|
|
6
|
+
*/
|
|
7
|
+
/* eslint-disable @next/next/no-sync-scripts */
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
import { escapeInlineScript, type SiteScript, type SiteScriptPlacement } from '../lib/site-scripts/types';
|
|
11
|
+
|
|
12
|
+
interface SiteScriptsProps {
|
|
13
|
+
nonce: string;
|
|
14
|
+
placement: SiteScriptPlacement;
|
|
15
|
+
scripts: SiteScript[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Render the admin-authored site scripts for one injection point.
|
|
20
|
+
*
|
|
21
|
+
* Emitted as plain <script> elements rather than next/script: these are arbitrary
|
|
22
|
+
* author-supplied snippets that frequently expect to run at a specific position in
|
|
23
|
+
* the document, and next/script's strategies would relocate them. The CSP nonce is
|
|
24
|
+
* applied here so the snippets satisfy the policy without it needing 'unsafe-inline'.
|
|
25
|
+
*/
|
|
26
|
+
export default function SiteScripts({ nonce, placement, scripts }: SiteScriptsProps) {
|
|
27
|
+
const forPlacement = scripts.filter((script) => script.placement === placement);
|
|
28
|
+
|
|
29
|
+
if (forPlacement.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
{forPlacement.map((script) =>
|
|
36
|
+
script.src ? (
|
|
37
|
+
<script
|
|
38
|
+
key={script.id}
|
|
39
|
+
src={script.src}
|
|
40
|
+
nonce={nonce || undefined}
|
|
41
|
+
{...(script.load_strategy === 'async' ? { async: true } : {})}
|
|
42
|
+
{...(script.load_strategy === 'defer' ? { defer: true } : {})}
|
|
43
|
+
data-nb-script={script.id}
|
|
44
|
+
/>
|
|
45
|
+
) : (
|
|
46
|
+
<script
|
|
47
|
+
key={script.id}
|
|
48
|
+
nonce={nonce || undefined}
|
|
49
|
+
data-nb-script={script.id}
|
|
50
|
+
dangerouslySetInnerHTML={{ __html: escapeInlineScript(script.code) }}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
)}
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { headers } from 'next/headers';
|
|
3
3
|
import ClientTextBlockRenderer from "./ClientTextBlockRenderer";
|
|
4
4
|
import type { VisualEditAttributes } from "../../../lib/visual-editing/types";
|
|
5
|
+
import { addNonceToInlineScripts } from "../../../lib/blocks/inlineScriptNonce";
|
|
5
6
|
import { substitutePrivacyMergeTags } from "../../../lib/privacy/contact-emails";
|
|
6
7
|
|
|
7
8
|
export type TextBlockContent = {
|
|
@@ -15,15 +16,6 @@ interface TextBlockRendererProps {
|
|
|
15
16
|
renderContext?: 'prose' | 'section';
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
function addNonceToInlineScripts(html: string, nonce: string): string {
|
|
19
|
-
if (!html || !nonce) return html || '';
|
|
20
|
-
// Add nonce to <script> tags that do not already have a nonce
|
|
21
|
-
// and do not have a src attribute (inline scripts)
|
|
22
|
-
return html.replace(/<script(?![^>]*\bsrc=)([^>]*)(?<!nonce=["'][^"']*["'])>/gi, (_m, attrs) => {
|
|
23
|
-
return `<script nonce="${nonce}"${attrs}>`;
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
19
|
const TextBlockRenderer: React.FC<TextBlockRendererProps> = async ({
|
|
28
20
|
content,
|
|
29
21
|
languageId,
|
|
@@ -1556,3 +1556,124 @@ When modifying Cortex AI, keep these invariants:
|
|
|
1556
1556
|
11. If a side-effecting tool succeeds and the model fails afterward, report the tool result instead of retrying blindly.
|
|
1557
1557
|
12. Free OpenRouter models are useful but unstable; guard against 429s, malformed tool-call text, invalid HTML fragments, and no-output generation.
|
|
1558
1558
|
13. Multilingual mutations should use active rows from `languages`, not hardcoded assumptions.
|
|
1559
|
+
14. MCP runs with the service-role client, so RLS is not an authorization boundary
|
|
1560
|
+
there. Privileged tools re-check the actor's CMS role themselves.
|
|
1561
|
+
15. A substituted actor identity is for attribution only, never authorization.
|
|
1562
|
+
16. The audit log for site scripts is append-only in the database. Reverting writes a
|
|
1563
|
+
new revision; it never removes one.
|
|
1564
|
+
17. Inline scripts in block HTML need the CSP nonce, and must wait for hydration
|
|
1565
|
+
before touching server-rendered DOM.
|
|
1566
|
+
|
|
1567
|
+
## MCP Server: Security Model and Operator Guide
|
|
1568
|
+
|
|
1569
|
+
The MCP endpoint (`apps/nextblock/app/api/mcp/route.ts`) exposes the same typed
|
|
1570
|
+
tools to external clients — Claude Code, Claude Desktop, Cursor. The route is a thin
|
|
1571
|
+
HTTP shim; the registry lives in `libs/cortex/src/lib/mcp-tool-registry.ts`.
|
|
1572
|
+
|
|
1573
|
+
### The four failure shapes to check when adding a tool
|
|
1574
|
+
|
|
1575
|
+
Tools were originally written for the in-app agent, which always has a signed-in
|
|
1576
|
+
user and an open editor. MCP has neither, so every new tool must be checked against
|
|
1577
|
+
all four of these:
|
|
1578
|
+
|
|
1579
|
+
1. **Cookie-session auth.** `createClient()` + `auth.getUser()` returns nobody over
|
|
1580
|
+
MCP. Pass a pre-authorized `actorUserId` instead and keep the role check.
|
|
1581
|
+
2. **`pageContext` dependence.** The route sets it to `null`. A tool that edits
|
|
1582
|
+
"the current item" must accept an explicit `cmsTarget`.
|
|
1583
|
+
3. **Untyped `z.any()` parameters.** They serialize to `{}` in JSON Schema, so hosts
|
|
1584
|
+
send `"29.99"` where a number is expected. Coerce rather than reject.
|
|
1585
|
+
4. **Staged artifacts with no finisher.** Anything that stages something (a Live
|
|
1586
|
+
Draft) needs a tool that can complete it, or MCP callers cannot finish the job.
|
|
1587
|
+
|
|
1588
|
+
### Authorization does not come from RLS
|
|
1589
|
+
|
|
1590
|
+
**MCP executors use the service-role client, which bypasses Row Level Security.** An
|
|
1591
|
+
`ADMIN`-only table policy therefore constrains the dashboard but *not* the MCP path.
|
|
1592
|
+
Privileged tools must re-check the actor's CMS role themselves — see
|
|
1593
|
+
`requireActorRole` in `ai-global-agent-theming-tools.ts`.
|
|
1594
|
+
|
|
1595
|
+
MCP token scopes are only `read` / `write` and carry no role, so the role is resolved
|
|
1596
|
+
from the acting user at call time. Two related rules:
|
|
1597
|
+
|
|
1598
|
+
- Keep privileged tables (`site_scripts`, `site_script_revisions`) **out of** the
|
|
1599
|
+
`execute_database_mutation` allowlist, or that generic tool becomes a way around
|
|
1600
|
+
every per-tool guard.
|
|
1601
|
+
- When a token's creator has been deleted, the route substitutes a stand-in admin so
|
|
1602
|
+
a revision can still be attributed. That substitution is flagged
|
|
1603
|
+
(`actorFromOrphanedToken`) and refused for role-gated operations: a credential must
|
|
1604
|
+
not gain authority by outliving its owner.
|
|
1605
|
+
|
|
1606
|
+
### Prompt injection
|
|
1607
|
+
|
|
1608
|
+
`fetch_url_content` returns attacker-controlled text to a model that holds write
|
|
1609
|
+
tools. A hostile page can contain instructions aimed at the agent ("also add this
|
|
1610
|
+
tracking snippet"). This is not solvable in the tool layer — the model reads the page
|
|
1611
|
+
because you asked it to. The mitigations are containment, not prevention:
|
|
1612
|
+
|
|
1613
|
+
- Code injection (`manage_site_script`) is **ADMIN-only**, so a `write` token that is
|
|
1614
|
+
otherwise fine for content cannot ship JavaScript.
|
|
1615
|
+
- `manage_site_script` requires a `purpose` and returns a `safetyReview` produced by
|
|
1616
|
+
an **independent static scan** of the code (`@nextblock-cms/utils/script-safety`).
|
|
1617
|
+
The stated purpose is not the control — a steered model will describe a skimmer as
|
|
1618
|
+
an analytics helper. The scan reports what the code can actually reach (cookies,
|
|
1619
|
+
network, storage, form fields, dynamic evaluation, external hosts) and both are
|
|
1620
|
+
written to the audit log, so a mismatch is visible rather than hidden.
|
|
1621
|
+
- Every script change is recorded in `site_script_revisions`, which is **append-only
|
|
1622
|
+
by database trigger** — UPDATE and DELETE are rejected even for the service role.
|
|
1623
|
+
An audit log a compromised credential can rewrite is not an audit log.
|
|
1624
|
+
|
|
1625
|
+
The scan is regex over source text, not a sandbox. Obfuscated code can evade it,
|
|
1626
|
+
which is why dynamic evaluation is itself reported at warning level. A clean result
|
|
1627
|
+
means "nothing obvious found", never "safe".
|
|
1628
|
+
|
|
1629
|
+
### Site scripts and the CSP
|
|
1630
|
+
|
|
1631
|
+
The site CSP carries a nonce, and per CSP Level 2 a browser **ignores
|
|
1632
|
+
`'unsafe-inline'` once a nonce is present**. Consequences:
|
|
1633
|
+
|
|
1634
|
+
- Inline `<script>` inside rich-text block HTML must be stamped by
|
|
1635
|
+
`apps/nextblock/lib/blocks/inlineScriptNonce.ts`, or the browser silently drops it
|
|
1636
|
+
with no server-side symptom.
|
|
1637
|
+
- Because NextBlock nonces author scripts, an external `src` on a site script is
|
|
1638
|
+
authorized regardless of the CSP host allowlist. That is inherent to the feature
|
|
1639
|
+
and a reason it is ADMIN-only.
|
|
1640
|
+
|
|
1641
|
+
Author scripts must also not fight React hydration: a script that changes text,
|
|
1642
|
+
classes, or attributes of server-rendered markup before hydration makes the client
|
|
1643
|
+
disagree with the SSR HTML and React discards that subtree. Wrap DOM work as:
|
|
1644
|
+
|
|
1645
|
+
```js
|
|
1646
|
+
function run() { /* ... */ }
|
|
1647
|
+
if (document.readyState === 'complete') run();
|
|
1648
|
+
else window.addEventListener('load', run);
|
|
1649
|
+
```
|
|
1650
|
+
|
|
1651
|
+
### SSRF
|
|
1652
|
+
|
|
1653
|
+
`fetch_url_content` and the media importer perform server-side HTTP on a
|
|
1654
|
+
caller-supplied URL, and `fetch_url_content` is a **read**-scoped tool — so its
|
|
1655
|
+
blocklist is what stops a read-only token from reaching internal services. The
|
|
1656
|
+
blocklist is duplicated (`isBlockedFetchHost` in cortex, `isBlockedImportHost` in the
|
|
1657
|
+
app) because a published lib cannot import from the app: **fix both together.**
|
|
1658
|
+
Regression tests live in `ai-global-agent-ssrf.test.ts`; IPv4-mapped IPv6
|
|
1659
|
+
(`::ffff:127.0.0.1`) previously bypassed both.
|
|
1660
|
+
|
|
1661
|
+
### Building a whole site in one pass
|
|
1662
|
+
|
|
1663
|
+
The tools below exist specifically so a site can be built without a human clicking
|
|
1664
|
+
through the dashboard. Rough order for a from-scratch build:
|
|
1665
|
+
|
|
1666
|
+
| Step | Tools |
|
|
1667
|
+
| --- | --- |
|
|
1668
|
+
| Ground yourself | `get_database_schema`, `list_media`, `list_site_themes`, `list_product_categories` |
|
|
1669
|
+
| Brand it | `manage_site_theme`, `update_global_css` |
|
|
1670
|
+
| Assets | `search_stock_media`, `upload_media` |
|
|
1671
|
+
| Catalogue | `manage_product_category`, `create_cms_product`, `manage_product_variants` |
|
|
1672
|
+
| Pages | `generate_jsonb_layout` then `publish_content_draft` |
|
|
1673
|
+
| Navigation | `update_site_navigation`, `update_footer` |
|
|
1674
|
+
| Locales | `manage_language` then `translate_content_bulk` |
|
|
1675
|
+
| Motion | `update_global_css` plus `manage_site_script` |
|
|
1676
|
+
|
|
1677
|
+
`manage_language` must run before any translation: `translate_page` and
|
|
1678
|
+
`translate_content_bulk` can only target a language that already exists and is
|
|
1679
|
+
active.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stamp the request's CSP nonce onto inline <script> tags inside block HTML.
|
|
3
|
+
*
|
|
4
|
+
* Rich-text blocks may legitimately carry an inline <script> (an editor or Cortex AI
|
|
5
|
+
* adding a small animation or widget). The site's CSP lists a nonce in `script-src`,
|
|
6
|
+
* and per CSP Level 2 a browser IGNORES `'unsafe-inline'` once a nonce or hash is
|
|
7
|
+
* present — so an un-nonced inline script is silently blocked, with no server-side
|
|
8
|
+
* symptom. Adding the nonce is what makes authored JS actually run.
|
|
9
|
+
*
|
|
10
|
+
* Only inline scripts are touched: a `src=` script loads a remote file and is
|
|
11
|
+
* governed by the host allowlist instead, and re-stamping one that already carries a
|
|
12
|
+
* nonce would corrupt it.
|
|
13
|
+
*/
|
|
14
|
+
export function addNonceToInlineScripts(html: string, nonce: string): string {
|
|
15
|
+
if (!html || !nonce) return html || '';
|
|
16
|
+
|
|
17
|
+
return html.replace(/<script(?![^>]*\bsrc=)([^>]*)(?<!nonce=["'][^"']*["'])>/gi, (_match, attrs) => {
|
|
18
|
+
return `<script nonce="${nonce}"${attrs}>`;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { SiteScript } from './types';
|
|
2
|
+
|
|
3
|
+
export type SiteScriptRevisionType = 'create' | 'update' | 'delete' | 'revert';
|
|
4
|
+
export type SiteScriptRevisionSource = 'cms' | 'mcp';
|
|
5
|
+
|
|
6
|
+
export interface SiteScriptRevision {
|
|
7
|
+
id: string;
|
|
8
|
+
script_id: string | null;
|
|
9
|
+
script_name: string;
|
|
10
|
+
revision_type: SiteScriptRevisionType;
|
|
11
|
+
actor_user_id: string | null;
|
|
12
|
+
source: SiteScriptRevisionSource;
|
|
13
|
+
summary: string | null;
|
|
14
|
+
snapshot: SiteScriptSnapshot;
|
|
15
|
+
created_at: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The restorable state of a script.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately excludes `id`, `created_at`, and `updated_at`: restoring a revision
|
|
22
|
+
* writes these fields onto the live row (or recreates it), and carrying identity or
|
|
23
|
+
* timestamps across would either clash with the existing row or fake its history.
|
|
24
|
+
*/
|
|
25
|
+
export interface SiteScriptSnapshot {
|
|
26
|
+
name: string;
|
|
27
|
+
description: string | null;
|
|
28
|
+
code: string;
|
|
29
|
+
src: string | null;
|
|
30
|
+
placement: string;
|
|
31
|
+
load_strategy: string;
|
|
32
|
+
is_active: boolean;
|
|
33
|
+
sort_order: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const SITE_SCRIPT_REVISION_COLUMNS =
|
|
37
|
+
'id, script_id, script_name, revision_type, actor_user_id, source, summary, snapshot, created_at';
|
|
38
|
+
|
|
39
|
+
/** Normalise a script row, a partial payload, or an existing snapshot into a snapshot. */
|
|
40
|
+
export function buildSiteScriptSnapshot(
|
|
41
|
+
row: Partial<SiteScript> | SiteScriptSnapshot | Record<string, unknown>
|
|
42
|
+
): SiteScriptSnapshot {
|
|
43
|
+
const value = row as Record<string, unknown>;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
code: typeof value['code'] === 'string' ? value['code'] : '',
|
|
47
|
+
description: typeof value['description'] === 'string' ? value['description'] : null,
|
|
48
|
+
is_active: Boolean(value['is_active']),
|
|
49
|
+
load_strategy: typeof value['load_strategy'] === 'string' ? value['load_strategy'] : 'default',
|
|
50
|
+
name: typeof value['name'] === 'string' ? value['name'] : '',
|
|
51
|
+
placement: typeof value['placement'] === 'string' ? value['placement'] : 'body_end',
|
|
52
|
+
sort_order: Number.isFinite(value['sort_order']) ? Number(value['sort_order']) : 0,
|
|
53
|
+
src: typeof value['src'] === 'string' && value['src'] ? (value['src'] as string) : null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One-line human description of what a revision did, shown in the history list. */
|
|
58
|
+
export function describeSiteScriptRevision(revision: SiteScriptRevision): string {
|
|
59
|
+
if (revision.summary) return revision.summary;
|
|
60
|
+
|
|
61
|
+
switch (revision.revision_type) {
|
|
62
|
+
case 'create':
|
|
63
|
+
return `Created “${revision.script_name}”`;
|
|
64
|
+
case 'delete':
|
|
65
|
+
return `Deleted “${revision.script_name}”`;
|
|
66
|
+
case 'revert':
|
|
67
|
+
return `Restored “${revision.script_name}”`;
|
|
68
|
+
default:
|
|
69
|
+
return `Updated “${revision.script_name}”`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Where a site script's tag is emitted in the document. */
|
|
2
|
+
export type SiteScriptPlacement = 'head' | 'body_start' | 'body_end';
|
|
3
|
+
|
|
4
|
+
/** Loading hint for external (`src`) scripts. Inline code ignores it. */
|
|
5
|
+
export type SiteScriptLoadStrategy = 'default' | 'defer' | 'async';
|
|
6
|
+
|
|
7
|
+
export interface SiteScript {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string | null;
|
|
11
|
+
/** Raw JS without the surrounding <script> tag. Ignored when `src` is set. */
|
|
12
|
+
code: string;
|
|
13
|
+
/** External script URL. Takes precedence over `code`. */
|
|
14
|
+
src: string | null;
|
|
15
|
+
placement: SiteScriptPlacement;
|
|
16
|
+
load_strategy: SiteScriptLoadStrategy;
|
|
17
|
+
is_active: boolean;
|
|
18
|
+
sort_order: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const SITE_SCRIPT_PLACEMENTS: SiteScriptPlacement[] = ['head', 'body_start', 'body_end'];
|
|
22
|
+
export const SITE_SCRIPT_LOAD_STRATEGIES: SiteScriptLoadStrategy[] = ['default', 'defer', 'async'];
|
|
23
|
+
|
|
24
|
+
export const SITE_SCRIPT_COLUMNS =
|
|
25
|
+
'id, name, description, code, src, placement, load_strategy, is_active, sort_order';
|
|
26
|
+
|
|
27
|
+
export function isSiteScriptPlacement(value: unknown): value is SiteScriptPlacement {
|
|
28
|
+
return typeof value === 'string' && (SITE_SCRIPT_PLACEMENTS as string[]).includes(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isSiteScriptLoadStrategy(value: unknown): value is SiteScriptLoadStrategy {
|
|
32
|
+
return typeof value === 'string' && (SITE_SCRIPT_LOAD_STRATEGIES as string[]).includes(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Guard against a script closing its own tag and escaping into markup.
|
|
37
|
+
*
|
|
38
|
+
* The code is emitted inside a <script> element, where the HTML parser ends the
|
|
39
|
+
* element at the first literal `</script`, regardless of JavaScript syntax — so a
|
|
40
|
+
* string containing it would terminate the script early and let whatever follows be
|
|
41
|
+
* parsed as HTML. Escaping the slash keeps the sequence inert to the parser while
|
|
42
|
+
* remaining the same string to JavaScript.
|
|
43
|
+
*/
|
|
44
|
+
export function escapeInlineScript(code: string): string {
|
|
45
|
+
return code.replace(/<\/(script)/gi, '<\\/$1');
|
|
46
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/types/routes.d.ts";
|
|
4
|
-
import "./.next/types/root-params.d.ts";
|
|
3
|
+
import "./.next/dev/types/routes.d.ts";
|
|
4
|
+
import "./.next/dev/types/root-params.d.ts";
|
|
5
5
|
|
|
6
6
|
// NOTE: This file should not be edited
|
|
7
7
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|