@avocadostudio-ai/site-sdk 0.7.0 → 0.8.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/dist/create-site-page.js +47 -84
- package/dist/draft-common.d.ts +1 -0
- package/dist/draft-common.js +13 -0
- package/dist/draft-context-core.d.ts +1 -1
- package/dist/draft-context-core.js +2 -3
- package/dist/draft-context.d.ts +1 -1
- package/dist/draft-context.js +1 -3
- package/dist/draft-routes.d.ts +7 -0
- package/dist/draft-routes.js +8 -3
- package/dist/editor-api-handler-core.d.ts +85 -0
- package/dist/editor-api-handler-core.js +101 -0
- package/dist/editor-api-handler.d.ts +7 -49
- package/dist/editor-api-handler.js +12 -66
- package/dist/editor-rewrite-core.d.ts +71 -0
- package/dist/editor-rewrite-core.js +37 -0
- package/dist/lens/register.d.ts +6 -8
- package/dist/lens/register.js +203 -29
- package/dist/lens/types.d.ts +36 -0
- package/dist/page-metadata.d.ts +21 -0
- package/dist/page-metadata.js +64 -0
- package/dist/proxy.d.ts +9 -30
- package/dist/proxy.js +16 -22
- package/dist/resolve-page-render.d.ts +94 -0
- package/dist/resolve-page-render.js +80 -0
- package/dist/routes-core.d.ts +12 -0
- package/dist/routes-core.js +24 -0
- package/package.json +20 -15
- package/dist/cli/register-notice.test.d.ts +0 -1
- package/dist/cli/register-notice.test.js +0 -21
- package/dist/draft-context-core.test.d.ts +0 -10
- package/dist/draft-context-core.test.js +0 -146
- package/dist/draft-fetch.test.d.ts +0 -1
- package/dist/draft-fetch.test.js +0 -153
- package/dist/editor-api-blocks-catalogue.test.d.ts +0 -1
- package/dist/editor-api-blocks-catalogue.test.js +0 -73
- package/dist/editor-cors.test.d.ts +0 -1
- package/dist/editor-cors.test.js +0 -85
- package/dist/editor-render.test.d.ts +0 -1
- package/dist/editor-render.test.js +0 -44
- package/dist/lens/lens.test.d.ts +0 -1
- package/dist/lens/lens.test.js +0 -221
- package/dist/lens/register.test.d.ts +0 -1
- package/dist/lens/register.test.js +0 -81
- package/dist/manifest-utils.test.d.ts +0 -1
- package/dist/manifest-utils.test.js +0 -72
- package/dist/markers.test.d.ts +0 -1
- package/dist/markers.test.js +0 -35
- package/dist/next-config.test.d.ts +0 -1
- package/dist/next-config.test.js +0 -393
- package/dist/page-metadata.test.d.ts +0 -1
- package/dist/page-metadata.test.js +0 -105
- package/dist/proxy.test.d.ts +0 -1
- package/dist/proxy.test.js +0 -182
- package/dist/publish/field-diff.test.d.ts +0 -1
- package/dist/publish/field-diff.test.js +0 -350
package/dist/proxy.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { DEFAULT_PREVIEW_ROUTE, buildEditorMatcher } from "./editor-matcher.js";
|
|
3
|
-
import {
|
|
3
|
+
import { decideEditorRewrite } from "./editor-rewrite-core.js";
|
|
4
4
|
export { DEFAULT_PREVIEW_ROUTE, buildEditorMatcher } from "./editor-matcher.js";
|
|
5
|
+
export { decideEditorRewrite } from "./editor-rewrite-core.js";
|
|
5
6
|
/**
|
|
6
7
|
* Create a Next.js proxy function that rewrites editor/draft requests
|
|
7
8
|
* to a dynamic preview route, keeping the main page route fully static.
|
|
@@ -110,29 +111,22 @@ export function trailingSlashRedirect(request) {
|
|
|
110
111
|
* {@link trailingSlashRedirect} for the composition.
|
|
111
112
|
*/
|
|
112
113
|
export function editorPreviewRewrite(request, options) {
|
|
113
|
-
const previewRoute = options?.previewRoute ?? DEFAULT_PREVIEW_ROUTE;
|
|
114
|
-
const editorParam = options?.editorParam ?? "__editor";
|
|
115
|
-
const draftCookie = options?.draftCookie === undefined ? "__prerender_bypass" : options.draftCookie;
|
|
116
|
-
const isEditor = request.nextUrl.searchParams.get(editorParam) === "1";
|
|
117
|
-
const hasDraftCookie = draftCookie !== false && request.cookies.has(draftCookie);
|
|
118
|
-
if (!isEditor && !hasDraftCookie)
|
|
119
|
-
return null;
|
|
120
|
-
const url = request.nextUrl.clone();
|
|
121
|
-
url.pathname = `${previewRoute}${url.pathname}`;
|
|
122
114
|
/*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* A layout receives no `searchParams`, and the root layout is exactly where a
|
|
127
|
-
* site mounts its consent banner, analytics and tag manager — so every
|
|
128
|
-
* integration loads all three into the editor iframe until somebody notices
|
|
129
|
-
* the pageviews. Stamping the rewritten request lets `isEditorRender()` answer
|
|
130
|
-
* in a layout without the site threading anything down to it.
|
|
131
|
-
*
|
|
132
|
-
* Set, not appended: a client-supplied header of the same name is replaced, so
|
|
133
|
-
* what a layout reads here is the proxy's own answer.
|
|
115
|
+
* `nextUrl`, not `new URL(request.url)`: it has `basePath` stripped, and the
|
|
116
|
+
* preview prefix has to be prepended to the path Next routes on. The decision
|
|
117
|
+
* itself is shared with every other host — see `decideEditorRewrite`.
|
|
134
118
|
*/
|
|
119
|
+
const decision = decideEditorRewrite({
|
|
120
|
+
pathname: request.nextUrl.pathname,
|
|
121
|
+
searchParams: request.nextUrl.searchParams,
|
|
122
|
+
hasCookie: (name) => request.cookies.has(name),
|
|
123
|
+
}, options);
|
|
124
|
+
if (decision.kind === "pass")
|
|
125
|
+
return null;
|
|
126
|
+
const url = request.nextUrl.clone();
|
|
127
|
+
url.pathname = decision.pathname;
|
|
135
128
|
const headers = new Headers(request.headers);
|
|
136
|
-
|
|
129
|
+
for (const [name, value] of Object.entries(decision.headers))
|
|
130
|
+
headers.set(name, value);
|
|
137
131
|
return NextResponse.rewrite(url, { request: { headers } });
|
|
138
132
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { PageDoc, SiteConfig } from "@avocadostudio-ai/shared";
|
|
2
|
+
import type { SearchParamsRecord } from "./types.ts";
|
|
3
|
+
import { type DraftModeAdapter } from "./draft-context-core.ts";
|
|
4
|
+
import { type NavItem } from "./navigation.ts";
|
|
5
|
+
import type { SiteHeaderBlock } from "./navigation.ts";
|
|
6
|
+
export type PageSourceConfig = {
|
|
7
|
+
siteId: string;
|
|
8
|
+
/** Orchestrator draft session. @default "dev" */
|
|
9
|
+
session?: string;
|
|
10
|
+
getPage: (slug: string) => Promise<PageDoc | null> | PageDoc | null;
|
|
11
|
+
getSlugs: () => Promise<string[]> | string[];
|
|
12
|
+
getSiteConfig?: () => Promise<SiteConfig | undefined> | SiteConfig | undefined;
|
|
13
|
+
/** @default "/logo.svg" */
|
|
14
|
+
defaultLogo?: string;
|
|
15
|
+
};
|
|
16
|
+
/** The editor session this render belongs to, when it is one. */
|
|
17
|
+
export type EditorRenderContext = {
|
|
18
|
+
session: string;
|
|
19
|
+
siteId: string;
|
|
20
|
+
/** `postMessage` target and the frame permitted to drive inline edits. Empty when none is trusted. */
|
|
21
|
+
editorOrigin: string;
|
|
22
|
+
/**
|
|
23
|
+
* `?session=…&siteId=…&editorOrigin=…`, to append to every link the preview
|
|
24
|
+
* renders. Without it the first in-preview navigation lands on the published
|
|
25
|
+
* page — the editor's own nav goes dead one click in.
|
|
26
|
+
*/
|
|
27
|
+
editorQuery: string;
|
|
28
|
+
};
|
|
29
|
+
/** Site chrome, already resolved against the right slug list. */
|
|
30
|
+
export type PageChrome = {
|
|
31
|
+
navItems: NavItem[];
|
|
32
|
+
siteName: string;
|
|
33
|
+
siteLogo: string;
|
|
34
|
+
homeHref: string;
|
|
35
|
+
/** The header as a block, for a host that renders Avocado's own chrome. */
|
|
36
|
+
header: SiteHeaderBlock;
|
|
37
|
+
siteConfig: SiteConfig;
|
|
38
|
+
};
|
|
39
|
+
export type PageRenderOutcome =
|
|
40
|
+
/** Render this page. `editor` is null for a published render. */
|
|
41
|
+
{
|
|
42
|
+
kind: "page";
|
|
43
|
+
slug: string;
|
|
44
|
+
page: PageDoc;
|
|
45
|
+
editor: EditorRenderContext | null;
|
|
46
|
+
chrome: PageChrome;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* No such page. The host must answer with a real 404 status — a rendered
|
|
50
|
+
* "404" body under HTTP 200 is a soft 404, which crawlers index as a page.
|
|
51
|
+
*/
|
|
52
|
+
| {
|
|
53
|
+
kind: "not-found";
|
|
54
|
+
slug: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* An editor render whose draft could not be fetched and which has no
|
|
58
|
+
* published fallback. Distinct from `not-found` because the page may well
|
|
59
|
+
* exist: the orchestrator is unreachable, and telling an editor "404" sends
|
|
60
|
+
* them looking for a content bug that is not there.
|
|
61
|
+
*/
|
|
62
|
+
| {
|
|
63
|
+
kind: "draft-unavailable";
|
|
64
|
+
slug: string;
|
|
65
|
+
editor: EditorRenderContext;
|
|
66
|
+
chrome: PageChrome;
|
|
67
|
+
};
|
|
68
|
+
export type ResolvePageRenderInput = {
|
|
69
|
+
slug: string;
|
|
70
|
+
config: PageSourceConfig;
|
|
71
|
+
/**
|
|
72
|
+
* - `auto`: draft mode or a resolvable editor context switches to the editor render.
|
|
73
|
+
* - `static`: published content only. Search params are not read.
|
|
74
|
+
* - `preview`: always the editor render.
|
|
75
|
+
* @default "auto"
|
|
76
|
+
*/
|
|
77
|
+
mode?: "auto" | "static" | "preview";
|
|
78
|
+
/** Required unless `mode` is `"static"`. */
|
|
79
|
+
search?: SearchParamsRecord;
|
|
80
|
+
/**
|
|
81
|
+
* How this runtime reports draft mode and reads cookies. Omit under
|
|
82
|
+
* `mode: "static"`; omit under `"preview"` only if the host has no draft-mode
|
|
83
|
+
* concept, in which case authorization rests on the `secret` query parameter.
|
|
84
|
+
*/
|
|
85
|
+
adapter?: DraftModeAdapter;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Resolve what a page render should produce, without producing it.
|
|
89
|
+
*
|
|
90
|
+
* The published path reads the CMS. The editor path prefers the orchestrator's
|
|
91
|
+
* draft and falls back to the CMS for each of page and slug list independently
|
|
92
|
+
* — a session that has edited one page still needs the other twenty in its nav.
|
|
93
|
+
*/
|
|
94
|
+
export declare function resolvePageRender(input: ResolvePageRenderInput): Promise<PageRenderOutcome>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { resolveDraftContextCore } from "./draft-context-core.js";
|
|
2
|
+
import { fetchEditorPage, fetchEditorSlugs } from "./draft-fetch.js";
|
|
3
|
+
import { buildNavItems, buildSiteHeaderBlock } from "./navigation.js";
|
|
4
|
+
const NO_DRAFT_MODE = { isDraftMode: false, getCookie: () => undefined };
|
|
5
|
+
async function buildChrome(slug, navSlugs, siteConfig, config, editorQuery) {
|
|
6
|
+
const { navItems, siteName, siteLogo, homeHref } = buildNavItems({
|
|
7
|
+
navSlugs,
|
|
8
|
+
currentSlug: slug,
|
|
9
|
+
siteConfig,
|
|
10
|
+
siteId: config.siteId,
|
|
11
|
+
editorQuery,
|
|
12
|
+
defaultLogo: config.defaultLogo ?? "/logo.svg",
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
navItems,
|
|
16
|
+
siteName,
|
|
17
|
+
siteLogo,
|
|
18
|
+
homeHref,
|
|
19
|
+
header: buildSiteHeaderBlock({ navItems, siteName, siteLogo, activePath: slug }),
|
|
20
|
+
siteConfig,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve what a page render should produce, without producing it.
|
|
25
|
+
*
|
|
26
|
+
* The published path reads the CMS. The editor path prefers the orchestrator's
|
|
27
|
+
* draft and falls back to the CMS for each of page and slug list independently
|
|
28
|
+
* — a session that has edited one page still needs the other twenty in its nav.
|
|
29
|
+
*/
|
|
30
|
+
export async function resolvePageRender(input) {
|
|
31
|
+
const { slug, config } = input;
|
|
32
|
+
const mode = input.mode ?? "auto";
|
|
33
|
+
const defaultSession = config.session ?? "dev";
|
|
34
|
+
const getSiteConfig = () => config.getSiteConfig ? Promise.resolve(config.getSiteConfig()) : Promise.resolve({});
|
|
35
|
+
const editorCtx = mode === "static"
|
|
36
|
+
? null
|
|
37
|
+
: await resolveDraftContextCore(input.search ?? {}, input.adapter ?? NO_DRAFT_MODE, {
|
|
38
|
+
defaultSession,
|
|
39
|
+
defaultSiteId: config.siteId,
|
|
40
|
+
});
|
|
41
|
+
const isEditorRender = mode === "preview" || (mode === "auto" && (input.adapter?.isDraftMode === true || Boolean(editorCtx)));
|
|
42
|
+
if (!isEditorRender) {
|
|
43
|
+
const [page, navSlugs, siteConfig] = await Promise.all([
|
|
44
|
+
config.getPage(slug),
|
|
45
|
+
config.getSlugs(),
|
|
46
|
+
getSiteConfig(),
|
|
47
|
+
]);
|
|
48
|
+
// Before the chrome is built: none of it survives a 404.
|
|
49
|
+
if (!page)
|
|
50
|
+
return { kind: "not-found", slug };
|
|
51
|
+
return {
|
|
52
|
+
kind: "page",
|
|
53
|
+
slug,
|
|
54
|
+
page,
|
|
55
|
+
editor: null,
|
|
56
|
+
chrome: await buildChrome(slug, navSlugs, siteConfig ?? {}, config, ""),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const session = editorCtx?.session ?? defaultSession;
|
|
60
|
+
const currentSiteId = editorCtx?.siteId ?? config.siteId;
|
|
61
|
+
const editorOrigin = editorCtx?.editorOrigin ?? "";
|
|
62
|
+
const [page, navSlugs, siteConfig] = await Promise.all([
|
|
63
|
+
fetchEditorPage(slug, session, currentSiteId).then((p) => p ?? config.getPage(slug)),
|
|
64
|
+
fetchEditorSlugs(session, currentSiteId).then((s) => (s.length > 0 ? s : config.getSlugs())),
|
|
65
|
+
getSiteConfig(),
|
|
66
|
+
]);
|
|
67
|
+
const params = new URLSearchParams({ session, siteId: currentSiteId });
|
|
68
|
+
if (editorOrigin)
|
|
69
|
+
params.set("editorOrigin", editorOrigin);
|
|
70
|
+
const editor = {
|
|
71
|
+
session,
|
|
72
|
+
siteId: currentSiteId,
|
|
73
|
+
editorOrigin,
|
|
74
|
+
editorQuery: `?${params.toString()}`,
|
|
75
|
+
};
|
|
76
|
+
const chrome = await buildChrome(slug, navSlugs, siteConfig ?? {}, config, editor.editorQuery);
|
|
77
|
+
if (!page)
|
|
78
|
+
return { kind: "draft-unavailable", slug, editor, chrome };
|
|
79
|
+
return { kind: "page", slug, page, editor, chrome };
|
|
80
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { createDraftEnableHandlerCore, createDraftDisableHandlerCore } from "./draft-routes-core.ts";
|
|
2
|
+
export type { DraftRouteAdapter } from "./draft-routes-core.ts";
|
|
3
|
+
export { createEditorApiHandlerCore } from "./editor-api-handler-core.ts";
|
|
4
|
+
export type { EditorApiHandlerConfig, EditorApiCoreConfig } from "./editor-api-handler-core.ts";
|
|
5
|
+
export { createBlocksHandler, createPagesHandler, createPublishHandler } from "./editor-routes.ts";
|
|
6
|
+
export type { OnPublishFn, InlineAsset, PublishContext } from "./editor-routes.ts";
|
|
7
|
+
export { isSafeImageUrl, createImageResolver } from "./publish-utils.ts";
|
|
8
|
+
export type { ImageUploader } from "./publish-utils.ts";
|
|
9
|
+
export { createRevalidateHandler } from "./revalidate-handler.ts";
|
|
10
|
+
export type { RevalidateHandlerConfig } from "./revalidate-handler.ts";
|
|
11
|
+
export { getManifestImageFields } from "./manifest-utils.ts";
|
|
12
|
+
export { DRAFT_SESSION_COOKIE, DRAFT_SITE_COOKIE, EDITOR_ORIGIN_COOKIE, normalizeOrigin, single, } from "./draft-common.ts";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The editor API, for a host that is not Next.
|
|
3
|
+
*
|
|
4
|
+
* This is `./routes` with the two Next-shaped pieces replaced by their cores:
|
|
5
|
+
* the draft handlers take an adapter instead of calling `draftMode()`, and the
|
|
6
|
+
* catch-all handler takes the matched path instead of a Next route context.
|
|
7
|
+
* Everything else in `./routes` was already portable and is re-exported here
|
|
8
|
+
* unchanged — the same functions, not parallel implementations.
|
|
9
|
+
*/
|
|
10
|
+
// Draft route cores — supply a DraftRouteAdapter for your runtime.
|
|
11
|
+
export { createDraftEnableHandlerCore, createDraftDisableHandlerCore } from "./draft-routes-core.js";
|
|
12
|
+
// Catch-all editor API core.
|
|
13
|
+
export { createEditorApiHandlerCore } from "./editor-api-handler-core.js";
|
|
14
|
+
// Individual editor route handlers — already framework-free.
|
|
15
|
+
export { createBlocksHandler, createPagesHandler, createPublishHandler } from "./editor-routes.js";
|
|
16
|
+
// Publish utilities (SSRF check, image resolution).
|
|
17
|
+
export { isSafeImageUrl, createImageResolver } from "./publish-utils.js";
|
|
18
|
+
// Revalidation handler factory — takes a `revalidate` callback, so the host's
|
|
19
|
+
// cache API never appears here.
|
|
20
|
+
export { createRevalidateHandler } from "./revalidate-handler.js";
|
|
21
|
+
// Manifest utilities (derive image fields from block manifest).
|
|
22
|
+
export { getManifestImageFields } from "./manifest-utils.js";
|
|
23
|
+
// Cookie names and the query-value helper the draft contract is written in.
|
|
24
|
+
export { DRAFT_SESSION_COOKIE, DRAFT_SITE_COOKIE, EDITOR_ORIGIN_COOKIE, normalizeOrigin, single, } from "./draft-common.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/site-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
"default": "./dist/draft-context-core.js"
|
|
68
68
|
},
|
|
69
69
|
"./routes/core": {
|
|
70
|
-
"types": "./dist/
|
|
71
|
-
"import": "./dist/
|
|
72
|
-
"default": "./dist/
|
|
70
|
+
"types": "./dist/routes-core.d.ts",
|
|
71
|
+
"import": "./dist/routes-core.js",
|
|
72
|
+
"default": "./dist/routes-core.js"
|
|
73
73
|
},
|
|
74
74
|
"./publish-handlers/json-file": {
|
|
75
75
|
"types": "./dist/publish-handlers/json-file.d.ts",
|
|
@@ -91,6 +91,11 @@
|
|
|
91
91
|
"import": "./dist/proxy.js",
|
|
92
92
|
"default": "./dist/proxy.js"
|
|
93
93
|
},
|
|
94
|
+
"./proxy/core": {
|
|
95
|
+
"types": "./dist/editor-rewrite-core.d.ts",
|
|
96
|
+
"import": "./dist/editor-rewrite-core.js",
|
|
97
|
+
"default": "./dist/editor-rewrite-core.js"
|
|
98
|
+
},
|
|
94
99
|
"./matcher": {
|
|
95
100
|
"types": "./dist/editor-matcher.d.ts",
|
|
96
101
|
"import": "./dist/editor-matcher.js",
|
|
@@ -106,6 +111,11 @@
|
|
|
106
111
|
"import": "./dist/create-site-page.js",
|
|
107
112
|
"default": "./dist/create-site-page.js"
|
|
108
113
|
},
|
|
114
|
+
"./page/core": {
|
|
115
|
+
"types": "./dist/resolve-page-render.d.ts",
|
|
116
|
+
"import": "./dist/resolve-page-render.js",
|
|
117
|
+
"default": "./dist/resolve-page-render.js"
|
|
118
|
+
},
|
|
109
119
|
"./seo": {
|
|
110
120
|
"types": "./dist/page-metadata.d.ts",
|
|
111
121
|
"import": "./dist/page-metadata.js",
|
|
@@ -121,11 +131,6 @@
|
|
|
121
131
|
"default": "./next-config.mjs"
|
|
122
132
|
}
|
|
123
133
|
},
|
|
124
|
-
"repository": {
|
|
125
|
-
"type": "git",
|
|
126
|
-
"url": "https://github.com/avocadostudio-ai/avocado.git",
|
|
127
|
-
"directory": "packages/site-sdk"
|
|
128
|
-
},
|
|
129
134
|
"publishConfig": {
|
|
130
135
|
"registry": "https://registry.npmjs.org",
|
|
131
136
|
"access": "public"
|
|
@@ -137,17 +142,17 @@
|
|
|
137
142
|
],
|
|
138
143
|
"dependencies": {
|
|
139
144
|
"zod": "^4.3.6",
|
|
140
|
-
"@avocadostudio-ai/blocks": "^0.
|
|
141
|
-
"@avocadostudio-ai/
|
|
142
|
-
"@avocadostudio-ai/
|
|
143
|
-
"@avocadostudio-ai/
|
|
145
|
+
"@avocadostudio-ai/blocks": "^0.8.0",
|
|
146
|
+
"@avocadostudio-ai/preview-adapter": "^0.8.0",
|
|
147
|
+
"@avocadostudio-ai/richtext": "^0.8.0",
|
|
148
|
+
"@avocadostudio-ai/shared": "^0.8.0"
|
|
144
149
|
},
|
|
145
150
|
"peerDependencies": {
|
|
146
151
|
"next": ">=15.0.0",
|
|
147
152
|
"react": ">=19.0.0",
|
|
148
153
|
"react-dom": ">=19.0.0",
|
|
149
154
|
"better-sqlite3": ">=12.0.0",
|
|
150
|
-
"@avocadostudio-ai/orchestrator-core": "^0.
|
|
155
|
+
"@avocadostudio-ai/orchestrator-core": "^0.8.0"
|
|
151
156
|
},
|
|
152
157
|
"peerDependenciesMeta": {
|
|
153
158
|
"@avocadostudio-ai/orchestrator-core": {
|
|
@@ -177,7 +182,7 @@
|
|
|
177
182
|
"license": "Apache-2.0",
|
|
178
183
|
"homepage": "https://docs.avocadostudio.dev",
|
|
179
184
|
"bugs": {
|
|
180
|
-
"url": "https://
|
|
185
|
+
"url": "https://docs.avocadostudio.dev"
|
|
181
186
|
},
|
|
182
187
|
"scripts": {
|
|
183
188
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { orchestratorMismatchNotice } from "./register-notice.js";
|
|
4
|
-
/*
|
|
5
|
-
* `avocado-register` ends with "the site should appear in the dashboard", and
|
|
6
|
-
* for a library-mode site that is a promise it cannot keep: the registration
|
|
7
|
-
* went to the site's own handler, and the editor reads one
|
|
8
|
-
* `VITE_ORCHESTRATOR_URL` baked in at build time. The dashboard then opens on
|
|
9
|
-
* a different registry's sites and reports no problem, because there is none —
|
|
10
|
-
* there are simply two registries.
|
|
11
|
-
*/
|
|
12
|
-
test("the default target says nothing — that is the path almost everyone is on", () => {
|
|
13
|
-
assert.equal(orchestratorMismatchNotice("http://localhost:4200"), null);
|
|
14
|
-
assert.equal(orchestratorMismatchNotice("http://localhost:4200/"), null, "a trailing slash is the same URL");
|
|
15
|
-
});
|
|
16
|
-
test("a library-mode target names the variable the editor needs", () => {
|
|
17
|
-
const notice = orchestratorMismatchNotice("http://localhost:3002/api/avocado");
|
|
18
|
-
assert.ok(notice);
|
|
19
|
-
assert.match(notice, /VITE_ORCHESTRATOR_URL=http:\/\/localhost:3002\/api\/avocado/);
|
|
20
|
-
assert.match(notice, /sites, or none/, "and names the symptom, which is what the reader is looking at");
|
|
21
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Who may see unpublished content, and which frame may drive the editor.
|
|
3
|
-
*
|
|
4
|
-
* Both questions used to be answered by the query string. `?__editor=1` alone
|
|
5
|
-
* turned on the draft content store — in production, on the customer's own
|
|
6
|
-
* domain — and `editorOrigin` was accepted from the same URL after a check that
|
|
7
|
-
* only asked whether it parsed as http(s). One GET rendered another session's
|
|
8
|
-
* drafts and pointed the postMessage bridge wherever the caller said.
|
|
9
|
-
*/
|
|
10
|
-
export {};
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Who may see unpublished content, and which frame may drive the editor.
|
|
3
|
-
*
|
|
4
|
-
* Both questions used to be answered by the query string. `?__editor=1` alone
|
|
5
|
-
* turned on the draft content store — in production, on the customer's own
|
|
6
|
-
* domain — and `editorOrigin` was accepted from the same URL after a check that
|
|
7
|
-
* only asked whether it parsed as http(s). One GET rendered another session's
|
|
8
|
-
* drafts and pointed the postMessage bridge wherever the caller said.
|
|
9
|
-
*/
|
|
10
|
-
import { test, describe, afterEach } from "node:test";
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { resolveDraftContextCore } from "./draft-context-core.js";
|
|
13
|
-
import { resolveTrustedEditorOrigin, editorOriginAllowlist, isLoopbackOrigin } from "./draft-common.js";
|
|
14
|
-
const SECRET = "s3cr3t-draft-key";
|
|
15
|
-
const saved = {};
|
|
16
|
-
const KEYS = ["NODE_ENV", "DRAFT_MODE_SECRET", "NEXT_PUBLIC_EDITOR_ORIGIN", "AVOCADO_EDITOR_ORIGINS"];
|
|
17
|
-
function setEnv(values) {
|
|
18
|
-
for (const key of KEYS) {
|
|
19
|
-
if (!(key in saved))
|
|
20
|
-
saved[key] = process.env[key];
|
|
21
|
-
}
|
|
22
|
-
for (const [key, value] of Object.entries(values)) {
|
|
23
|
-
if (value === undefined)
|
|
24
|
-
delete process.env[key];
|
|
25
|
-
else
|
|
26
|
-
process.env[key] = value;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
for (const key of KEYS) {
|
|
31
|
-
if (saved[key] === undefined)
|
|
32
|
-
delete process.env[key];
|
|
33
|
-
else
|
|
34
|
-
process.env[key] = saved[key];
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
function adapter(overrides) {
|
|
38
|
-
return { isDraftMode: false, getCookie: () => undefined, ...overrides };
|
|
39
|
-
}
|
|
40
|
-
describe("resolveDraftContextCore — production gate", () => {
|
|
41
|
-
test("__editor=1 alone no longer opens the content store", async () => {
|
|
42
|
-
setEnv({ NODE_ENV: "production", DRAFT_MODE_SECRET: SECRET });
|
|
43
|
-
const ctx = await resolveDraftContextCore({ __editor: "1", siteId: "acme", session: "victim" }, adapter());
|
|
44
|
-
assert.equal(ctx, null);
|
|
45
|
-
});
|
|
46
|
-
test("a wrong secret is refused", async () => {
|
|
47
|
-
setEnv({ NODE_ENV: "production", DRAFT_MODE_SECRET: SECRET });
|
|
48
|
-
const ctx = await resolveDraftContextCore({ __editor: "1", siteId: "acme", secret: "guess" }, adapter());
|
|
49
|
-
assert.equal(ctx, null);
|
|
50
|
-
});
|
|
51
|
-
test("the configured secret opens it — the cookie-blocked iframe case", async () => {
|
|
52
|
-
setEnv({ NODE_ENV: "production", DRAFT_MODE_SECRET: SECRET });
|
|
53
|
-
const ctx = await resolveDraftContextCore({ __editor: "1", siteId: "acme", secret: SECRET }, adapter());
|
|
54
|
-
assert.equal(ctx?.siteId, "acme");
|
|
55
|
-
});
|
|
56
|
-
test("draft mode already enabled opens it — the secret-gated cookie path", async () => {
|
|
57
|
-
setEnv({ NODE_ENV: "production", DRAFT_MODE_SECRET: SECRET });
|
|
58
|
-
const ctx = await resolveDraftContextCore({ siteId: "acme" }, adapter({ isDraftMode: true }));
|
|
59
|
-
assert.equal(ctx?.siteId, "acme");
|
|
60
|
-
});
|
|
61
|
-
test("with no secret configured, production stays shut rather than open", async () => {
|
|
62
|
-
setEnv({ NODE_ENV: "production", DRAFT_MODE_SECRET: undefined });
|
|
63
|
-
const ctx = await resolveDraftContextCore({ __editor: "1", siteId: "acme", secret: "anything" }, adapter());
|
|
64
|
-
assert.equal(ctx, null);
|
|
65
|
-
});
|
|
66
|
-
test("development is unchanged — the parameter alone still works", async () => {
|
|
67
|
-
setEnv({ NODE_ENV: "development", DRAFT_MODE_SECRET: undefined });
|
|
68
|
-
const ctx = await resolveDraftContextCore({ __editor: "1", siteId: "acme" }, adapter());
|
|
69
|
-
assert.equal(ctx?.siteId, "acme");
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
describe("resolveDraftContextCore — editor origin", () => {
|
|
73
|
-
test("an unlisted origin from the URL is ignored in favour of the configured one", async () => {
|
|
74
|
-
setEnv({
|
|
75
|
-
NODE_ENV: "production",
|
|
76
|
-
DRAFT_MODE_SECRET: SECRET,
|
|
77
|
-
NEXT_PUBLIC_EDITOR_ORIGIN: "https://editor.example.com"
|
|
78
|
-
});
|
|
79
|
-
const ctx = await resolveDraftContextCore({ siteId: "acme", secret: SECRET, editorOrigin: "https://attacker.example" }, adapter());
|
|
80
|
-
assert.equal(ctx?.editorOrigin, "https://editor.example.com");
|
|
81
|
-
});
|
|
82
|
-
test("a listed origin from the URL is honoured", async () => {
|
|
83
|
-
setEnv({
|
|
84
|
-
NODE_ENV: "production",
|
|
85
|
-
DRAFT_MODE_SECRET: SECRET,
|
|
86
|
-
NEXT_PUBLIC_EDITOR_ORIGIN: "https://editor.example.com",
|
|
87
|
-
AVOCADO_EDITOR_ORIGINS: "https://staging-editor.example.com, https://other.example.com"
|
|
88
|
-
});
|
|
89
|
-
const ctx = await resolveDraftContextCore({ siteId: "acme", secret: SECRET, editorOrigin: "https://staging-editor.example.com" }, adapter());
|
|
90
|
-
assert.equal(ctx?.editorOrigin, "https://staging-editor.example.com");
|
|
91
|
-
});
|
|
92
|
-
test("a hostile origin in the cookie is ignored too", async () => {
|
|
93
|
-
setEnv({
|
|
94
|
-
NODE_ENV: "production",
|
|
95
|
-
DRAFT_MODE_SECRET: SECRET,
|
|
96
|
-
NEXT_PUBLIC_EDITOR_ORIGIN: "https://editor.example.com"
|
|
97
|
-
});
|
|
98
|
-
const ctx = await resolveDraftContextCore({ siteId: "acme", secret: SECRET }, adapter({ getCookie: (name) => (name === "editor_origin" ? "https%3A%2F%2Fattacker.example" : undefined) }));
|
|
99
|
-
assert.equal(ctx?.editorOrigin, "https://editor.example.com");
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
describe("resolveTrustedEditorOrigin", () => {
|
|
103
|
-
test("keeps the fallback when there is no candidate", () => {
|
|
104
|
-
assert.equal(resolveTrustedEditorOrigin({ candidate: undefined, fallback: "https://a.example", isDev: false, env: {} }), "https://a.example");
|
|
105
|
-
});
|
|
106
|
-
test("accepts loopback in development, at any port", () => {
|
|
107
|
-
for (const candidate of ["http://localhost:4100", "http://localhost:4101", "http://127.0.0.1:5173"]) {
|
|
108
|
-
assert.equal(resolveTrustedEditorOrigin({ candidate, fallback: "http://localhost:4100", isDev: true, env: {} }), candidate);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
test("refuses loopback in production — an operator must list it", () => {
|
|
112
|
-
assert.equal(resolveTrustedEditorOrigin({
|
|
113
|
-
candidate: "http://localhost:4100",
|
|
114
|
-
fallback: "https://editor.example.com",
|
|
115
|
-
isDev: false,
|
|
116
|
-
env: {}
|
|
117
|
-
}), "https://editor.example.com");
|
|
118
|
-
});
|
|
119
|
-
test("the fallback itself is always trusted", () => {
|
|
120
|
-
assert.equal(resolveTrustedEditorOrigin({
|
|
121
|
-
candidate: "https://editor.example.com",
|
|
122
|
-
fallback: "https://editor.example.com",
|
|
123
|
-
isDev: false,
|
|
124
|
-
env: {}
|
|
125
|
-
}), "https://editor.example.com");
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
describe("editorOriginAllowlist", () => {
|
|
129
|
-
test("merges both variables, normalises, and de-duplicates", () => {
|
|
130
|
-
const list = editorOriginAllowlist({
|
|
131
|
-
AVOCADO_EDITOR_ORIGINS: "https://a.example/, https://b.example",
|
|
132
|
-
NEXT_PUBLIC_EDITOR_ORIGIN: "https://a.example"
|
|
133
|
-
});
|
|
134
|
-
assert.deepEqual(list, ["https://a.example", "https://b.example"]);
|
|
135
|
-
});
|
|
136
|
-
test("drops entries that are not http(s) origins", () => {
|
|
137
|
-
const list = editorOriginAllowlist({ AVOCADO_EDITOR_ORIGINS: "javascript:alert(1), not-a-url, ftp://x.example" });
|
|
138
|
-
assert.deepEqual(list, []);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
test("isLoopbackOrigin recognises the local spellings and nothing else", () => {
|
|
142
|
-
assert.equal(isLoopbackOrigin("http://localhost:4100"), true);
|
|
143
|
-
assert.equal(isLoopbackOrigin("http://127.0.0.1:3000"), true);
|
|
144
|
-
assert.equal(isLoopbackOrigin("https://localhost.attacker.example"), false);
|
|
145
|
-
assert.equal(isLoopbackOrigin("https://example.com"), false);
|
|
146
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|