@avocadostudio-ai/orchestrator-core 0.25.0 → 0.27.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.
@@ -64,7 +64,8 @@ import { createCmsBootstrapCache } from "../cms/bootstrap.js";
64
64
  import { resolveCapabilities } from "../cms/adapter.js";
65
65
  import { mediaSourceFromUnknown } from "../cms/media-sources.js";
66
66
  import { extractAccessToken, isAccessGateEnabled, isValidAccessToken, mintAccessToken, verifyAccessPassword } from "../http/access-tokens.js";
67
- import { declareLibraryMount, observeLibraryMount } from "./library-mount.js";
67
+ import { editorCredentials } from "../http/editor-credentials.js";
68
+ import { clearLibraryDispatch, declareLibraryMount, observeLibraryMount, registerLibraryDispatch } from "./library-mount.js";
68
69
  import { checkAuth, resolveAuth, CLOSED_HINT } from "./auth.js";
69
70
  import { setSiteAssetLister, invalidateSiteAssets } from "../state/site-assets.js";
70
71
  async function buildRuntime(config) {
@@ -262,6 +263,7 @@ const SUPPORTED_ROUTES = [
262
263
  "GET /health",
263
264
  "GET /auth/status",
264
265
  "POST /auth/verify",
266
+ "GET /editor/credentials",
265
267
  "POST /chat",
266
268
  "POST /chat/stream",
267
269
  "POST /chat/start",
@@ -536,9 +538,23 @@ export function createOrchestrator(config = {}) {
536
538
  // See `library-mount.ts` for why this is not simply a config value.
537
539
  if (config.previewUrl)
538
540
  declareLibraryMount(`${config.previewUrl.replace(/\/+$/, "")}${basePath}`);
541
+ /*
542
+ * Requests this process's own preview dispatches in-process (see
543
+ * `library-mount.ts`). Membership is the credential: a request that arrived
544
+ * over the network is a different object, so it can never be in the set, and
545
+ * nothing about it — no header, no URL — can put it there. The bypass covers
546
+ * the three GET draft reads the SDK makes and nothing else, so code that
547
+ * reaches the dispatcher can read the drafts the preview renders, but never
548
+ * edit or publish.
549
+ */
550
+ const inProcessRequests = new WeakSet();
551
+ const IN_PROCESS_READS = new Set(["/draft/pages", "/draft/slugs", "/draft/site-config"]);
539
552
  const handler = async function handler(request) {
540
553
  const url = new URL(request.url);
541
- observeLibraryMount(`${url.origin}${basePath}`);
554
+ const inProcess = inProcessRequests.has(request);
555
+ // An in-process URL is synthetic; recording it would mislead every HTTP reader.
556
+ if (!inProcess)
557
+ observeLibraryMount(`${url.origin}${basePath}`);
542
558
  const path = stripBasePath(url.pathname, basePath);
543
559
  const cors = corsHeadersFor(request, config);
544
560
  if (request.method === "OPTIONS") {
@@ -564,9 +580,12 @@ export function createOrchestrator(config = {}) {
564
580
  * (the /auth exchange itself, and image reads an <img> tag cannot
565
581
  * authenticate) pass through inside checkAuth.
566
582
  */
567
- const gate = await checkAuth({ request, path, auth: config.auth, cors, log: gateLogger });
568
- if (gate.response)
569
- return gate.response;
583
+ const trustedRead = inProcess && request.method === "GET" && IN_PROCESS_READS.has(path);
584
+ if (!trustedRead) {
585
+ const gate = await checkAuth({ request, path, auth: config.auth, cors, log: gateLogger });
586
+ if (gate.response)
587
+ return gate.response;
588
+ }
570
589
  if (request.method === "GET" && path === "/auth/status") {
571
590
  /*
572
591
  * `gateEnabled` answers one question for the editor's PasswordGate: "do I
@@ -643,6 +662,16 @@ export function createOrchestrator(config = {}) {
643
662
  }
644
663
  return jsonResponse({ ok: true, accessToken: mintAccessToken() }, { cors });
645
664
  }
665
+ /*
666
+ * The site's draft secret and publish token, for a caller the gate above has
667
+ * already let in. See `http/editor-credentials.ts` for why these no longer
668
+ * travel in the editor's HTML. Not in `PUBLIC_PATHS`, and that is the point:
669
+ * on a mount with a credential, a request without a valid token never gets
670
+ * here.
671
+ */
672
+ if (request.method === "GET" && path === "/editor/credentials") {
673
+ return jsonResponse(editorCredentials(), { cors: { ...cors, "cache-control": "no-store" } });
674
+ }
646
675
  if (request.method === "POST" && path === "/chat") {
647
676
  const runtime = await getRuntime();
648
677
  await runtime.ready;
@@ -2635,7 +2664,13 @@ export function createOrchestrator(config = {}) {
2635
2664
  "Use the Fastify orchestrator at apps/orchestrator for the full surface (sites, the session directory, the agent surface, etc.)."
2636
2665
  }, { status: 405, cors });
2637
2666
  };
2667
+ const dispatchInProcess = (request) => {
2668
+ inProcessRequests.add(request);
2669
+ return handler(request);
2670
+ };
2671
+ registerLibraryDispatch(basePath, dispatchInProcess);
2638
2672
  handler.dispose = async () => {
2673
+ clearLibraryDispatch(dispatchInProcess);
2639
2674
  if (!runtimePromise)
2640
2675
  return;
2641
2676
  try {
@@ -1,6 +1,11 @@
1
1
  export declare const LIBRARY_MOUNT_KEY = "__avocado_library_mount__";
2
+ export type LibraryDispatch = (request: Request) => Promise<Response>;
2
3
  /** Called at construction, from `previewUrl` + `basePath`. */
3
4
  export declare function declareLibraryMount(url: string): void;
4
5
  /** Called on each request, from the URL the runtime actually received. */
5
6
  export declare function observeLibraryMount(url: string): void;
6
7
  export declare function getLibraryMount(): string | null;
8
+ /** Called at construction: this handler answers in-process draft reads. */
9
+ export declare function registerLibraryDispatch(basePath: string, dispatch: LibraryDispatch): void;
10
+ /** Called on dispose, and only clears the slot if it is still this handler's. */
11
+ export declare function clearLibraryDispatch(dispatch: LibraryDispatch): void;
@@ -43,3 +43,17 @@ export function getLibraryMount() {
43
43
  const s = slot();
44
44
  return s.observed ?? s.declared ?? null;
45
45
  }
46
+ /** Called at construction: this handler answers in-process draft reads. */
47
+ export function registerLibraryDispatch(basePath, dispatch) {
48
+ const s = slot();
49
+ s.basePath = basePath.replace(/\/+$/, "");
50
+ s.dispatch = dispatch;
51
+ }
52
+ /** Called on dispose, and only clears the slot if it is still this handler's. */
53
+ export function clearLibraryDispatch(dispatch) {
54
+ const s = slot();
55
+ if (s.dispatch !== dispatch)
56
+ return;
57
+ delete s.dispatch;
58
+ delete s.basePath;
59
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The two site credentials a signed-in editor needs, served by
3
+ * `GET /editor/credentials` instead of being written into the editor's HTML.
4
+ *
5
+ * - `siteDraftSecret` rides on every preview URL the editor builds, because the
6
+ * draft cookie is routinely blocked in a cross-origin iframe. Without it a
7
+ * production site renders its published pages and nothing is selectable.
8
+ * - `publishToken` is the `x-publish-token` the standalone server checks on
9
+ * publish (library mode authenticates publish with the access token instead).
10
+ *
11
+ * Both used to reach the browser only through `window.__AVOCADO_CONFIG__`,
12
+ * which the self-host CLI writes into `index.html`. That page is served before
13
+ * the password gate runs, so an editor hosted on a public URL handed the site's
14
+ * draft secret to anyone who loaded it. With that secret, anyone could read the
15
+ * CMS's unpublished drafts through the site's preview route, password or not.
16
+ * Served from here, they reach only a caller the access gate has already let in.
17
+ *
18
+ * Absent values are omitted rather than sent as empty strings, so the editor
19
+ * keeps whatever it was configured with when this process holds nothing.
20
+ */
21
+ export interface EditorCredentials {
22
+ siteDraftSecret?: string;
23
+ publishToken?: string;
24
+ }
25
+ export declare function editorCredentials(env?: Record<string, string | undefined>): EditorCredentials;
@@ -0,0 +1,9 @@
1
+ import { getConfiguredDraftSecret } from "@avocadostudio-ai/shared";
2
+ export function editorCredentials(env = process.env) {
3
+ const siteDraftSecret = getConfiguredDraftSecret(env) ?? undefined;
4
+ const publishToken = env.PUBLISH_TOKEN?.trim() || undefined;
5
+ return {
6
+ ...(siteDraftSecret ? { siteDraftSecret } : {}),
7
+ ...(publishToken ? { publishToken } : {})
8
+ };
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avocadostudio-ai/orchestrator-core",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",
@@ -22,8 +22,8 @@
22
22
  "openai": "^4.87.1",
23
23
  "sharp": "^0.35.4",
24
24
  "zod": "^4.3.6",
25
- "@avocadostudio-ai/migration-sdk": "^0.25.0",
26
- "@avocadostudio-ai/shared": "^0.25.0"
25
+ "@avocadostudio-ai/migration-sdk": "^0.27.0",
26
+ "@avocadostudio-ai/shared": "^0.27.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@anthropic-ai/claude-agent-sdk": "^0.3.220",