@avocadostudio-ai/orchestrator-core 0.26.0 → 0.28.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.
@@ -65,7 +65,7 @@ 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
67
  import { editorCredentials } from "../http/editor-credentials.js";
68
- import { declareLibraryMount, observeLibraryMount } from "./library-mount.js";
68
+ import { clearLibraryDispatch, declareLibraryMount, observeLibraryMount, registerLibraryDispatch } from "./library-mount.js";
69
69
  import { checkAuth, resolveAuth, CLOSED_HINT } from "./auth.js";
70
70
  import { setSiteAssetLister, invalidateSiteAssets } from "../state/site-assets.js";
71
71
  async function buildRuntime(config) {
@@ -538,9 +538,23 @@ export function createOrchestrator(config = {}) {
538
538
  // See `library-mount.ts` for why this is not simply a config value.
539
539
  if (config.previewUrl)
540
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"]);
541
552
  const handler = async function handler(request) {
542
553
  const url = new URL(request.url);
543
- 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}`);
544
558
  const path = stripBasePath(url.pathname, basePath);
545
559
  const cors = corsHeadersFor(request, config);
546
560
  if (request.method === "OPTIONS") {
@@ -566,9 +580,12 @@ export function createOrchestrator(config = {}) {
566
580
  * (the /auth exchange itself, and image reads an <img> tag cannot
567
581
  * authenticate) pass through inside checkAuth.
568
582
  */
569
- const gate = await checkAuth({ request, path, auth: config.auth, cors, log: gateLogger });
570
- if (gate.response)
571
- 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
+ }
572
589
  if (request.method === "GET" && path === "/auth/status") {
573
590
  /*
574
591
  * `gateEnabled` answers one question for the editor's PasswordGate: "do I
@@ -2647,7 +2664,13 @@ export function createOrchestrator(config = {}) {
2647
2664
  "Use the Fastify orchestrator at apps/orchestrator for the full surface (sites, the session directory, the agent surface, etc.)."
2648
2665
  }, { status: 405, cors });
2649
2666
  };
2667
+ const dispatchInProcess = (request) => {
2668
+ inProcessRequests.add(request);
2669
+ return handler(request);
2670
+ };
2671
+ registerLibraryDispatch(basePath, dispatchInProcess);
2650
2672
  handler.dispose = async () => {
2673
+ clearLibraryDispatch(dispatchInProcess);
2651
2674
  if (!runtimePromise)
2652
2675
  return;
2653
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
+ }
@@ -766,6 +766,73 @@ function _requireListDiscriminator(manifestByType, blockType, listKey, item) {
766
766
  const choices = admissible.length > 0 ? `: ${admissible.join(", ")}` : "";
767
767
  throw new OperationError(`A new ${listKey} row must set "${discriminator}", which names its type${choices}`, { category: "schema_violation" });
768
768
  }
769
+ /*
770
+ * A list row must use the list's own field names.
771
+ *
772
+ * Integrations declare item schemas loose, so bookkeeping keys a CMS adds
773
+ * (`_type`, `_key`) ride through validation, and every field is optional. Such
774
+ * a schema accepts any object. A planner that had just written Avocado's
775
+ * built-in FAQ shape, `{ q, a }`, into a site whose accordion reads
776
+ * `{ title, body }` got an "applied" and a row that rendered empty, with the
777
+ * text present in the draft under names nothing reads.
778
+ *
779
+ * So a key is refused when the list neither declares it nor already holds it
780
+ * on another row: copying an existing row, undeclared keys and all, still
781
+ * works, and only a name the list has never seen is refused. The error names
782
+ * the fields the list does use, so the planner can correct itself. Lists with
783
+ * no declared item fields are left alone, since there is nothing to check
784
+ * against.
785
+ */
786
+ /** Property names an item of `listKey` may carry, per the block's JSON Schema. */
787
+ function _schemaItemKeys(definition, listKey) {
788
+ const props = definition.propsSchema?.properties;
789
+ const listSchema = props?.[listKey];
790
+ const keys = new Set();
791
+ const visit = (node) => {
792
+ if (!node || typeof node !== "object")
793
+ return;
794
+ const n = node;
795
+ for (const key of Object.keys(n.properties ?? {}))
796
+ keys.add(key);
797
+ for (const branch of [...(n.oneOf ?? []), ...(n.anyOf ?? []), ...(n.allOf ?? [])])
798
+ visit(branch);
799
+ };
800
+ visit(listSchema?.items);
801
+ return [...keys];
802
+ }
803
+ function _requireKnownItemKeys(manifestByType, blockType, listKey, list, item) {
804
+ const definition = manifestByType.get(blockType);
805
+ if (!definition)
806
+ return;
807
+ const listField = resolveManifestFieldMeta(definition).listFields[listKey];
808
+ if (!listField)
809
+ return;
810
+ const declared = new Set(Object.keys(listField.itemFields ?? {}));
811
+ for (const fields of Object.values(listField.itemFieldsByType ?? {})) {
812
+ for (const key of Object.keys(fields ?? {}))
813
+ declared.add(key);
814
+ }
815
+ if (listField.discriminator)
816
+ declared.add(listField.discriminator);
817
+ // The props schema names every item property, optional ones included; the
818
+ // field meta above names only what the panel offers.
819
+ for (const key of _schemaItemKeys(definition, listKey))
820
+ declared.add(key);
821
+ if (declared.size === 0)
822
+ return;
823
+ const known = new Set(declared);
824
+ for (const row of list) {
825
+ if (row && typeof row === "object" && !Array.isArray(row)) {
826
+ for (const key of Object.keys(row))
827
+ known.add(key);
828
+ }
829
+ }
830
+ const unknown = Object.keys(item).filter((key) => key !== "id" && !key.startsWith("_") && !known.has(key));
831
+ if (unknown.length === 0)
832
+ return;
833
+ throw new OperationError(`${listKey} rows have no field ${unknown.map((k) => `"${k}"`).join(", ")}. ` +
834
+ `Use the list's own fields: ${[...declared].join(", ")}`, { category: "schema_violation" });
835
+ }
769
836
  function _requireManifestComponent(manifestByType, blockType, operationName) {
770
837
  if (manifestByType.size === 0)
771
838
  return;
@@ -1449,6 +1516,7 @@ async function _applyOpsAtomicallyUnsafe(session, ops, options) {
1449
1516
  // silently resolve to the first occurrence.
1450
1517
  const newItem = structuredClone(op.item);
1451
1518
  _requireListDiscriminator(manifestByType, block.type, op.listKey, newItem);
1519
+ _requireKnownItemKeys(manifestByType, block.type, op.listKey, list, newItem);
1452
1520
  if (typeof newItem.id !== "string" ||
1453
1521
  newItem.id.length === 0 ||
1454
1522
  _itemIndexById(nextList, newItem.id) !== -1) {
@@ -1476,6 +1544,7 @@ async function _applyOpsAtomicallyUnsafe(session, ops, options) {
1476
1544
  // A patch must never clobber the stable item id (it's the addressing key).
1477
1545
  const itemPatch = { ...op.patch };
1478
1546
  delete itemPatch.id;
1547
+ _requireKnownItemKeys(manifestByType, block.type, op.listKey, list, itemPatch);
1479
1548
  const nextList = list.map((entry, idx) => {
1480
1549
  if (idx !== targetIndex)
1481
1550
  return entry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avocadostudio-ai/orchestrator-core",
3
- "version": "0.26.0",
3
+ "version": "0.28.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.26.0",
26
- "@avocadostudio-ai/shared": "^0.26.0"
25
+ "@avocadostudio-ai/migration-sdk": "^0.28.0",
26
+ "@avocadostudio-ai/shared": "^0.28.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@anthropic-ai/claude-agent-sdk": "^0.3.220",