@avocadostudio-ai/site-sdk 0.10.0 → 0.11.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.
@@ -27,6 +27,24 @@ export declare function createPagesHandler(getPages: () => PageDoc[] | Promise<P
27
27
  GET: (request: Request) => Promise<Response>;
28
28
  OPTIONS: (request: Request) => Response;
29
29
  };
30
+ /**
31
+ * Why an unconfigured publish endpoint is refused in production rather than
32
+ * left open.
33
+ *
34
+ * `publishSecret` was optional, and every scaffold and example wired it as
35
+ * `process.env.PUBLISH_TOKEN?.trim() || undefined` — a variable none of them
36
+ * generated, mentioned in their `.env.local`, or required. So the value was
37
+ * always `undefined`, `if (secret)` was always false, and `POST
38
+ * /api/editor/publish` accepted any caller's `pages` array and overwrote the
39
+ * site's content with it. A deployment that had followed its own README to the
40
+ * letter, `ACCESS_PASSWORD_HASH` and all, was gated on `/api/avocado/*` and
41
+ * wide open here: the two route groups are different handlers, and only one of
42
+ * them had ever been asked about auth.
43
+ *
44
+ * An optional guard on a write endpoint is not a guard. This one now fails
45
+ * closed wherever it matters and says which variable opens it.
46
+ */
47
+ export declare const OPEN_PUBLISH_HINT: string;
30
48
  export declare function createPublishHandler(onPublish: OnPublishFn, options?: {
31
49
  publishSecret?: string;
32
50
  }): {
@@ -29,12 +29,45 @@ export function createPagesHandler(getPages, getSiteConfig) {
29
29
  }
30
30
  };
31
31
  }
32
+ /**
33
+ * Why an unconfigured publish endpoint is refused in production rather than
34
+ * left open.
35
+ *
36
+ * `publishSecret` was optional, and every scaffold and example wired it as
37
+ * `process.env.PUBLISH_TOKEN?.trim() || undefined` — a variable none of them
38
+ * generated, mentioned in their `.env.local`, or required. So the value was
39
+ * always `undefined`, `if (secret)` was always false, and `POST
40
+ * /api/editor/publish` accepted any caller's `pages` array and overwrote the
41
+ * site's content with it. A deployment that had followed its own README to the
42
+ * letter, `ACCESS_PASSWORD_HASH` and all, was gated on `/api/avocado/*` and
43
+ * wide open here: the two route groups are different handlers, and only one of
44
+ * them had ever been asked about auth.
45
+ *
46
+ * An optional guard on a write endpoint is not a guard. This one now fails
47
+ * closed wherever it matters and says which variable opens it.
48
+ */
49
+ export const OPEN_PUBLISH_HINT = "This publish endpoint overwrites the site's content and has no publishSecret configured, " +
50
+ "so it refuses every request under NODE_ENV=production. Set PUBLISH_TOKEN (the same value the " +
51
+ "orchestrator sends as x-publish-token), or pass publishSecret to createEditorApiHandler().";
52
+ let warnedOpenPublish = false;
32
53
  export function createPublishHandler(onPublish, options) {
33
54
  return {
34
55
  OPTIONS: createEditorCorsOptionsHandler(),
35
56
  async POST(request) {
36
57
  // Verify publish token if configured
37
58
  const secret = options?.publishSecret;
59
+ if (!secret) {
60
+ if (process.env.NODE_ENV === "production") {
61
+ const res = new Response(JSON.stringify({ ok: false, error: "unauthorized", reason: OPEN_PUBLISH_HINT }), { status: 401, headers: { "Content-Type": "application/json" } });
62
+ return applyEditorCors(res, request.headers.get("origin"));
63
+ }
64
+ // Development: publishing to your own machine is the point, so this
65
+ // stays open — but it is the same code path that ships, so say so once.
66
+ if (!warnedOpenPublish) {
67
+ warnedOpenPublish = true;
68
+ console.warn(`[avocado] ${OPEN_PUBLISH_HINT}`);
69
+ }
70
+ }
38
71
  if (secret) {
39
72
  const provided = request.headers.get("x-publish-token")?.trim();
40
73
  if (!provided || provided !== secret) {
@@ -2,6 +2,21 @@ import type { OnPublishFn } from "../editor-routes.ts";
2
2
  /**
3
3
  * Publish handler that writes PageDoc[] to a local JSON file.
4
4
  *
5
+ * `shape` decides what lands in the file, and it has to match whatever reads
6
+ * it back. It used to be neither a parameter nor a choice: the handler always
7
+ * wrote `{ pages, siteConfig }` while this docstring, three of the four call
8
+ * sites, and `jsonFileAdapter.onPublish` all said `PageDoc[]`. Publishing then
9
+ * broke the site that had just published — the scaffold's `lib/content.ts`
10
+ * does `JSON.parse(...) as PageDoc[]` and its `.find` threw on an object, so
11
+ * every page answered 500, and `examples/sample-site` reads a slug-keyed
12
+ * object and quietly lost all nine pages. Only `apps/site` survived, because
13
+ * its reader had already grown a branch for both shapes.
14
+ *
15
+ * So: `array` is the default, because it is what the readers and the adapter
16
+ * expect. Pass `wrapper` when the reader wants `siteConfig` in the same file —
17
+ * a plain array has nowhere to put it, so site-level settings (name, logo,
18
+ * nav) are dropped on publish.
19
+ *
5
20
  * When `publicDir` is provided, inline assets (base64 images from the
6
21
  * orchestrator) are written to disk and their localhost URLs are rewritten
7
22
  * to relative paths in the JSON output.
@@ -21,4 +36,6 @@ import type { OnPublishFn } from "../editor-routes.ts";
21
36
  export declare function createJsonFilePublishHandler(filePath: string, options?: {
22
37
  publicDir?: string;
23
38
  imagePathPrefix?: string;
39
+ /** `array` writes `PageDoc[]` (default). `wrapper` writes `{ pages, siteConfig }`. */
40
+ shape?: "array" | "wrapper";
24
41
  }): OnPublishFn;
@@ -3,6 +3,21 @@ import { resolve } from "node:path";
3
3
  /**
4
4
  * Publish handler that writes PageDoc[] to a local JSON file.
5
5
  *
6
+ * `shape` decides what lands in the file, and it has to match whatever reads
7
+ * it back. It used to be neither a parameter nor a choice: the handler always
8
+ * wrote `{ pages, siteConfig }` while this docstring, three of the four call
9
+ * sites, and `jsonFileAdapter.onPublish` all said `PageDoc[]`. Publishing then
10
+ * broke the site that had just published — the scaffold's `lib/content.ts`
11
+ * does `JSON.parse(...) as PageDoc[]` and its `.find` threw on an object, so
12
+ * every page answered 500, and `examples/sample-site` reads a slug-keyed
13
+ * object and quietly lost all nine pages. Only `apps/site` survived, because
14
+ * its reader had already grown a branch for both shapes.
15
+ *
16
+ * So: `array` is the default, because it is what the readers and the adapter
17
+ * expect. Pass `wrapper` when the reader wants `siteConfig` in the same file —
18
+ * a plain array has nowhere to put it, so site-level settings (name, logo,
19
+ * nav) are dropped on publish.
20
+ *
6
21
  * When `publicDir` is provided, inline assets (base64 images from the
7
22
  * orchestrator) are written to disk and their localhost URLs are rewritten
8
23
  * to relative paths in the JSON output.
@@ -33,8 +48,8 @@ export function createJsonFilePublishHandler(filePath, options) {
33
48
  }
34
49
  output = JSON.parse(json);
35
50
  }
36
- const payload = JSON.stringify({ pages: output, siteConfig: config }, null, 2) + "\n";
37
- await writeFile(filePath, payload, "utf8");
51
+ const body = options?.shape === "wrapper" ? { pages: output, siteConfig: config } : output;
52
+ await writeFile(filePath, JSON.stringify(body, null, 2) + "\n", "utf8");
38
53
  return { ok: true };
39
54
  };
40
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avocadostudio-ai/site-sdk",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -147,17 +147,17 @@
147
147
  ],
148
148
  "dependencies": {
149
149
  "zod": "^4.3.6",
150
- "@avocadostudio-ai/preview-adapter": "^0.10.0",
151
- "@avocadostudio-ai/blocks": "^0.10.0",
152
- "@avocadostudio-ai/richtext": "^0.10.0",
153
- "@avocadostudio-ai/shared": "^0.10.0"
150
+ "@avocadostudio-ai/preview-adapter": "^0.11.0",
151
+ "@avocadostudio-ai/shared": "^0.11.0",
152
+ "@avocadostudio-ai/richtext": "^0.11.0",
153
+ "@avocadostudio-ai/blocks": "^0.11.0"
154
154
  },
155
155
  "peerDependencies": {
156
156
  "next": ">=15.0.0",
157
157
  "react": ">=19.0.0",
158
158
  "react-dom": ">=19.0.0",
159
159
  "better-sqlite3": ">=12.0.0",
160
- "@avocadostudio-ai/orchestrator-core": "^0.10.0"
160
+ "@avocadostudio-ai/orchestrator-core": "^0.11.0"
161
161
  },
162
162
  "peerDependenciesMeta": {
163
163
  "@avocadostudio-ai/orchestrator-core": {