@taprootio/site-authoring 0.23.1 → 0.26.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.
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  createPage,
3
+ getPage,
4
+ PAGE_STATUS_DRAFT,
3
5
  importVideoEmbedPoster,
4
6
  isGonePage,
5
7
  listSitePages,
@@ -38,6 +40,10 @@ import {
38
40
  COVER_IMAGE_UNUSED_MESSAGE,
39
41
  documentTemplate,
40
42
  generatedMarkdownFault,
43
+ legalMarkdownFault,
44
+ legalIdentity,
45
+ workspaceDocumentFromPage,
46
+ TEMPLATE_LEGAL,
41
47
  generatedPageDescription,
42
48
  generatedPageTitle,
43
49
  placedVideoIds,
@@ -289,6 +295,7 @@ function parseFrontMatter(source, file) {
289
295
  try {
290
296
  template = requireTemplateName(fields.get("template"), file);
291
297
  if (template === TEMPLATE_GENERATED) record(generatedMarkdownFault(file));
298
+ if (template === TEMPLATE_LEGAL) record(legalMarkdownFault(file));
292
299
  } catch (error) {
293
300
  record(error);
294
301
  }
@@ -1100,6 +1107,8 @@ export async function planPages({
1100
1107
  requestedPaths,
1101
1108
  getSharedThemeContexts,
1102
1109
  refreshAuthors,
1110
+ legalDrafts = false,
1111
+ readLegalPage,
1103
1112
  onProgress = () => {},
1104
1113
  }) {
1105
1114
  const problems = [];
@@ -1282,6 +1291,7 @@ export async function planPages({
1282
1291
  onProgress(`Validating '${file}'.`);
1283
1292
  const manifestEntry = manifestByFile.get(file);
1284
1293
  let observedRecord;
1294
+ let legalRevisionGuard;
1285
1295
  requireCompletePageMetadata(source);
1286
1296
  const document_ = await convertWorkspacePageSource({ source, mediaManifest, content });
1287
1297
 
@@ -1298,6 +1308,9 @@ export async function planPages({
1298
1308
  );
1299
1309
  }
1300
1310
  const generated = documentTemplate(document_) === TEMPLATE_GENERATED;
1311
+ if (documentTemplate(document_) === TEMPLATE_LEGAL && target === undefined && manifestEntry?.pageId !== undefined) {
1312
+ throw documentError("pages.legal_missing", `'${file}' tracks a legal page that the site no longer lists. Pull and reconcile it before creating another page.`, file);
1313
+ }
1301
1314
  if (generated && target === undefined) {
1302
1315
  if (targeted) {
1303
1316
  throw documentError(
@@ -1417,8 +1430,11 @@ export async function planPages({
1417
1430
  entry: manifestByPageId.get(target.pageId),
1418
1431
  observedRevision: observed?.revision,
1419
1432
  };
1420
- requireReconciledRevision(guard);
1421
- requireReconciledDisplayDate({ ...guard, document_, observed });
1433
+ if (sourceTemplate === TEMPLATE_LEGAL) legalRevisionGuard = guard;
1434
+ else {
1435
+ requireReconciledRevision(guard);
1436
+ requireReconciledDisplayDate({ ...guard, document_, observed });
1437
+ }
1422
1438
  }
1423
1439
  }
1424
1440
 
@@ -1474,6 +1490,24 @@ export async function planPages({
1474
1490
  // An author the site no longer holds is a change to send, however the
1475
1491
  // source compares with the record.
1476
1492
  const unchanged = isUnchanged(observedRecord) && !(online && target !== undefined && authorship.sendAuthor);
1493
+ if (documentTemplate(document_) === TEMPLATE_LEGAL && !unchanged && online) {
1494
+ if (legalRevisionGuard !== undefined) requireReconciledRevision(legalRevisionGuard);
1495
+ if (!legalDrafts) {
1496
+ throw documentError("pages.legal_grant_required", "Legal draft changes require an explicit site grant. Run 'taproot-site login --legal-drafts', then 'taproot-site --legal-drafts pages push'. If TAPROOT_SITE_KEY is set, unset it first.", file);
1497
+ }
1498
+ if (target !== undefined) {
1499
+ const recorded = targetEntry?.legal;
1500
+ const livePath = normalizePagePath(target.path) ?? target.path;
1501
+ const livePage = await readLegalPage(target);
1502
+ const liveDocument = workspaceDocumentFromPage(livePage, target.templateType);
1503
+ const identity = legalIdentity(document_.data);
1504
+ const same = (other) => other !== undefined && other.documentKey === identity.documentKey && other.documentRole === identity.documentRole;
1505
+ if (targetEntry === undefined || normalizePagePath(targetEntry.path) !== pagePath || livePath !== pagePath
1506
+ || !same(recorded) || documentTemplate(liveDocument) !== TEMPLATE_LEGAL || !same(legalIdentity(liveDocument.data))) {
1507
+ throw documentError("pages.legal_identity", `'${file}' changes or lacks the recorded legal document key, role or path. Pull and reconcile the page before editing.`, file);
1508
+ }
1509
+ }
1510
+ }
1477
1511
  // Only for a page this push sends: a description stored before the limit
1478
1512
  // existed must not block a push that leaves it alone (TR01194). A
1479
1513
  // generated page's derived description is not the owner's to shorten;
@@ -1605,6 +1639,8 @@ export async function pagesPush(invocation) {
1605
1639
  files,
1606
1640
  livePages,
1607
1641
  online: true,
1642
+ legalDrafts: session.legalDrafts === true,
1643
+ readLegalPage: (target) => getPage(client, target.pageId, target.hasDraft ? PAGE_STATUS_DRAFT : target.status),
1608
1644
  requestedPaths,
1609
1645
  refreshAuthors: liveAuthorsRefresher({
1610
1646
  client,
@@ -1735,7 +1771,8 @@ export async function pagesPush(invocation) {
1735
1771
  // and a raw TypeError here would collapse to an opaque `site.failed`
1736
1772
  // *after* pages had already been created — the one place in this verb
1737
1773
  // where a crash costs more than a refusal.
1738
- const existing = manifest.pages.find((entry) => entry?.pageId === summary.pageId);
1774
+ const existing = manifest.pages.find((entry) => entry?.pageId === summary.pageId)
1775
+ ?? manifest.pages.find((entry) => entry?.pageId === undefined && entry?.file === page.file);
1739
1776
  const record = existing ?? {};
1740
1777
  // The author the site holds now: what it reports, else what was just
1741
1778
  // accepted, else what this workspace already knew it held.
@@ -1785,6 +1822,7 @@ export async function pagesPush(invocation) {
1785
1822
  });
1786
1823
  // What the source names, kept beside the site's own for a .pm.json
1787
1824
  // page, whose manifest entry is the only place it is written down.
1825
+ if (documentTemplate(page.document) === TEMPLATE_LEGAL) record.legal = legalIdentity(page.document.data);
1788
1826
  if (page.author === "") delete record.author;
1789
1827
  else record.author = page.author;
1790
1828
  if (existing === undefined) manifest.pages.push(record);
package/src/verbs/plan.js CHANGED
@@ -5,6 +5,8 @@ import { footerImageIds } from "../footer-contract.js";
5
5
  import { SETTINGS_TYPE_BRAND, SETTINGS_TYPE_SITE_HEADER, SETTINGS_TYPE_TAPROOT_STYLES } from "../settings-catalog.js";
6
6
  import {
7
7
  getNavigation,
8
+ getPage,
9
+ PAGE_STATUS_DRAFT,
8
10
  IMAGE_PROCESSING_STATE_COMPLETE,
9
11
  listSiteImages,
10
12
  isGonePage,
@@ -276,6 +278,8 @@ async function preflight(session, invocation) {
276
278
  files: await walkWorkspaceFiles(workspaceDir, PAGES_DIRECTORY, PAGE_SOURCE_EXTENSIONS),
277
279
  livePages,
278
280
  online: true,
281
+ legalDrafts: session.legalDrafts === true,
282
+ readLegalPage: (target) => getPage(client, target.pageId, target.hasDraft ? PAGE_STATUS_DRAFT : target.status),
279
283
  // Asked of the site but never written down: `plan` leaves the workspace as it found it.
280
284
  refreshAuthors: liveAuthorsRefresher({ client, workspaceDir, siteId, write: false, onProgress }),
281
285
  // From the theme validated above, so a broken styles file is one problem, not one per page.
@@ -550,7 +554,8 @@ export async function apply(invocation) {
550
554
 
551
555
  const ready = planned.steps.filter((entry) => entry.status === READY);
552
556
  const completed = [];
553
- const stepInvocation = { ...invocation, client: session.client, resolvedConfig: session.config };
557
+ const stepInvocation = { ...invocation, client: session.client, resolvedConfig: session.config,
558
+ ...(session.legalDrafts ? { resolvedSession: session } : {}) };
554
559
  await enforceWorkspaceReads(planned.ledger, () => runSteps(ready, completed, stepInvocation, planned, onProgress));
555
560
  onProgress(
556
561
  completed.length === 0
package/src/verbs/pull.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  VERB_PULL,
19
19
  } from "../constants.js";
20
20
  import { AUTHORS_FILE_NAME, parseAuthorReference, projectAuthorsForWorkspace } from "../authors-contract.js";
21
- import { sanitizeDiagnostic, SiteAuthoringError } from "../errors.js";
21
+ import { isCanonicalUuid, sanitizeDiagnostic, SiteAuthoringError } from "../errors.js";
22
22
  import { appearanceManifestEntry, footerManifestEntry, presentationManifestEntry } from "../footer-workspace.js";
23
23
  import { describeJsonDifferences, reportableDifferencePaths } from "../json-path-diff.js";
24
24
  import { projectRedirectMapForWorkspace, REDIRECTS_FILE_NAME } from "../redirects-contract.js";
@@ -36,11 +36,15 @@ import {
36
36
  } from "../settings-catalog.js";
37
37
  import { ApiError } from "../transport.js";
38
38
  import { placesFormIds, readFormIndex, withFormKeys } from "../form-references.js";
39
+ import { MARKETING_MANIFEST_KEY } from "../marketing-workspace.js";
39
40
  import {
40
41
  generatedIdentity,
42
+ legalIdentity,
43
+ isLegalTemplateType,
41
44
  isAuthorableTemplateType,
42
45
  isGeneratedTemplateType,
43
46
  sentDisplayDate,
47
+ TEMPLATE_LEGAL,
44
48
  workspaceDocumentFromPage,
45
49
  } from "../typed-pages.js";
46
50
  import {
@@ -450,11 +454,11 @@ async function resolveTrackedSources(workspaceDir, pages, registry, onProgress)
450
454
  }
451
455
 
452
456
  /**
453
- * Reads the previous manifest's source registry, tolerating everything a
454
- * strict read would refuse.
457
+ * Reads the previous manifest's source registry, tolerating damaged tracked
458
+ * entries but protecting unsaved drafts whose metadata has no server identity.
455
459
  *
456
- * `pull` is the verb that repairs a workspace, so refusing here would strand
457
- * one. It is also the only verb that reads a manifest it is about to replace:
460
+ * `pull` repairs damaged tracked entries instead of stranding the workspace.
461
+ * It is also the only verb that reads a manifest it is about to replace:
458
462
  * a version-4 manifest still records the `pageId → file` mapping this needs,
459
463
  * and honoring it is what repairs an existing workspace in one pull instead of
460
464
  * by hand. Every other verb keeps refusing an old manifest outright — the site
@@ -474,7 +478,22 @@ async function readSourceRegistry(workspaceDir, siteId) {
474
478
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || parsed.siteId !== siteId) {
475
479
  return new Map();
476
480
  }
477
- const registry = pageSourceRegistry(withoutRetiredReadOnlyProjections(parsed));
481
+ const manifest = withoutRetiredReadOnlyProjections(parsed);
482
+ for (const entry of Array.isArray(manifest.pages) ? manifest.pages : []) {
483
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry) || isCanonicalUuid(entry.pageId)) continue;
484
+ if (!["file", "title", "path"].some((field) => Object.hasOwn(entry, field))) continue;
485
+ const field = sanitizeDiagnostic(entry.file, MANIFEST_FILE_NAME);
486
+ const label = sanitizeDiagnostic(entry.path, field);
487
+ // A new source's metadata lives only here until its first save.
488
+ throw new SiteAuthoringError(
489
+ "pages.source_conflict",
490
+ `The pending page entry '${label}' has no server page id. Pull would discard its saved metadata. `
491
+ + "Save the draft with 'taproot-site pages push' first, or back up and move its source file and manifest "
492
+ + "entry before running 'taproot-site pull' again. Nothing was changed.",
493
+ { field },
494
+ );
495
+ }
496
+ const registry = pageSourceRegistry(manifest);
478
497
  return parsed.manifestVersion === MANIFEST_VERSION ? registry : withoutSupersededRemoteHashes(registry);
479
498
  }
480
499
 
@@ -750,6 +769,33 @@ export async function pull(invocation) {
750
769
  new Set(retiredGeneratedSources.map((source) => source.file)),
751
770
  );
752
771
 
772
+ // A fresh destination has no ownership baseline. Protect legal drafts even
773
+ // when the remote page has another template, and refuse unreadable sources.
774
+ for (const summary of authorable) {
775
+ if (tracked.has(summary.pageId)) continue;
776
+ const file = pageFiles.get(summary.pageId);
777
+ let protectedSource;
778
+ try {
779
+ if (!await workspaceFileExists(config.workspaceDir, file)) continue;
780
+ protectedSource = isLegalTemplateType(summary.templateType);
781
+ if (!protectedSource) {
782
+ const document = await readWorkspaceJson(config.workspaceDir, file, WORKSPACE_LIMITS.documentBytes);
783
+ protectedSource = document?.template === TEMPLATE_LEGAL;
784
+ }
785
+ } catch {
786
+ protectedSource = true;
787
+ }
788
+ if (protectedSource) {
789
+ throw new SiteAuthoringError(
790
+ "pages.source_conflict",
791
+ `Page ${summary.pageId} would use '${file}', but this workspace does not track that file as its source. `
792
+ + "The file was kept. Back up and move its source file and any manifest entry, or reconcile it before "
793
+ + "running 'taproot-site pull' again.",
794
+ { field: file },
795
+ );
796
+ }
797
+ }
798
+
753
799
  // The redirect map, with the revision a later push is fenced by (TR00702).
754
800
  // Read here, before the first workspace write, for the same reason the
755
801
  // settings groups below are: a failure must strand nothing. A Taproot that
@@ -920,6 +966,7 @@ export async function pull(invocation) {
920
966
  // The page is still the generated page it was; losing the record would
921
967
  // let the next push take an edit to its identity for the owner's.
922
968
  ...(source.generated === undefined ? {} : { generated: source.generated }),
969
+ ...(source.legal === undefined ? {} : { legal: source.legal }),
923
970
  description: metadataKept ? source.metadata.description : description,
924
971
  title: metadataKept ? source.metadata.title : titleFromRead(page, summary),
925
972
  path: metadataKept ? source.metadata.path : pathFromRead(page, summary),
@@ -1036,6 +1083,7 @@ export async function pull(invocation) {
1036
1083
  if (metadataEdited) reportPendingAuthorDisagreement(source, summary, onProgress);
1037
1084
  trackedPlans.set(summary.pageId, {
1038
1085
  ...(generatedPage ? { generated: generatedIdentity(body.data) } : {}),
1086
+ ...(isLegalTemplateType(summary.templateType) ? { legal: legalIdentity(body.data) } : {}),
1039
1087
  description: metadataEdited ? source.metadata.description : description,
1040
1088
  title: metadataEdited ? source.metadata.title : titleFromRead(page, summary),
1041
1089
  path: metadataEdited ? source.metadata.path : pathFromRead(page, summary),
@@ -1252,6 +1300,7 @@ export async function pull(invocation) {
1252
1300
  if (plan.path !== undefined) entry.path = plan.path;
1253
1301
  // What a generated page is, so a push can refuse a source that changed it.
1254
1302
  if (plan.generated !== undefined) entry.generated = plan.generated;
1303
+ if (plan.legal !== undefined) entry.legal = plan.legal;
1255
1304
  if (plan.file !== undefined) {
1256
1305
  entry.file = plan.file;
1257
1306
  entry.sourceFormat = plan.sourceFormat;
@@ -1285,6 +1334,7 @@ export async function pull(invocation) {
1285
1334
  } else {
1286
1335
  entry.file = pageFiles.get(summary.pageId);
1287
1336
  if (isGeneratedTemplateType(summary.templateType)) entry.generated = generatedIdentity(body.data);
1337
+ if (isLegalTemplateType(summary.templateType)) entry.legal = legalIdentity(body.data);
1288
1338
  const source = Buffer.from(`${JSON.stringify(body, undefined, 2)}\n`, "utf8");
1289
1339
  entry.workspaceMode = PAGE_WORKSPACE_MODE_EDITABLE;
1290
1340
  entry.sourceFormat = pageSourceFormat(entry.file);
@@ -1346,10 +1396,13 @@ export async function pull(invocation) {
1346
1396
 
1347
1397
  // `forms pull` owns the forms baseline, and a plain pull does not read forms.
1348
1398
  // Carrying it forward keeps the next `forms push` fenced instead of refusing
1349
- // it as never pulled. An unreadable old manifest has nothing to carry.
1350
- const formsBaseline = (await readManifest(config.workspaceDir, siteId, { required: false }).catch(() =>
1399
+ // it as never pulled. The marketing baseline is carried for the same reason
1400
+ // (TR01301). An unreadable old manifest has nothing to carry.
1401
+ const previousManifest = await readManifest(config.workspaceDir, siteId, { required: false }).catch(() =>
1351
1402
  undefined
1352
- ))?.forms;
1403
+ );
1404
+ const formsBaseline = previousManifest?.forms;
1405
+ const marketingBaseline = previousManifest?.[MARKETING_MANIFEST_KEY];
1353
1406
  const manifest = {
1354
1407
  manifestVersion: MANIFEST_VERSION,
1355
1408
  siteId,
@@ -1371,6 +1424,7 @@ export async function pull(invocation) {
1371
1424
  ),
1372
1425
  ...(presentationRevision === undefined ? {} : { presentation: presentationManifestEntry(presentationRevision) }),
1373
1426
  ...(formsBaseline === undefined ? {} : { forms: formsBaseline }),
1427
+ ...(marketingBaseline === undefined ? {} : { [MARKETING_MANIFEST_KEY]: marketingBaseline }),
1374
1428
  pages: manifestPages,
1375
1429
  ...(retiredGeneratedSources.length === 0 ? {} : { retiredGeneratedSources }),
1376
1430
  };
@@ -12,10 +12,11 @@ import {
12
12
  } from "../constants.js";
13
13
  import { isBehindLatest } from "../cli-release.js";
14
14
  import { credentialApiOrigin, findCredential } from "../credentials.js";
15
- import { SITE_AUTHORING_CAPABILITIES } from "../capabilities.js";
15
+ import { CAPABILITY_CONTENT, SITE_AUTHORING_CAPABILITIES } from "../capabilities.js";
16
16
  import { openAnonymousSession, successResult } from "../session.js";
17
17
  import { environmentNameFor } from "../settings.js";
18
18
  import { describeSurface } from "../surface.js";
19
+ import { SiteAuthoringError } from "../errors.js";
19
20
 
20
21
  /**
21
22
  * What the last exchange recorded about the platform authoring switch, dated
@@ -63,6 +64,33 @@ function cliReleaseLine(lastExchange) {
63
64
  : `Latest CLI release ${when}: ${lastExchange.latestCliVersion}. This CLI is ${CLI_VERSION}.`;
64
65
  }
65
66
 
67
+ /**
68
+ * Whether the credential can manage the site's campaigns and placements (TR01301), from what is known locally.
69
+ *
70
+ * The Content capability carries both permissions those verbs need, so the answer is whether the last exchange
71
+ * held it. Like the rest of this verb it is a recording: `granted` is "what that exchange held", and `exchange`
72
+ * says no exchange has happened yet, when the next command that needs it will mint one.
73
+ */
74
+ function marketingAccess(lastExchange, environmentOverride) {
75
+ if (environmentOverride) return { capability: CAPABILITY_CONTENT, access: "unknown" };
76
+ if (lastExchange === undefined) return { capability: CAPABILITY_CONTENT, access: "on_exchange" };
77
+ return {
78
+ capability: CAPABILITY_CONTENT,
79
+ access: lastExchange.capabilities.includes(CAPABILITY_CONTENT) ? "granted" : "not_in_last_exchange",
80
+ };
81
+ }
82
+
83
+ function marketingLine({ capability, access }) {
84
+ const verbs = "'marketing pull', 'marketing push' and 'link build'";
85
+ return access === "granted"
86
+ ? `Campaigns and placements (${verbs}): allowed; the last exchange held ${capability}.`
87
+ : access === "not_in_last_exchange"
88
+ ? `Campaigns and placements (${verbs}): the last exchange did not hold ${capability}, which the next command that needs it requests; a sign-in's ceiling includes it.`
89
+ : access === "on_exchange"
90
+ ? `Campaigns and placements (${verbs}): carried by ${capability}, which the next command that needs it requests.`
91
+ : `Campaigns and placements (${verbs}): needs ${capability}; ${PUBLISH_KEY_ENVIRONMENT_VARIABLE} supplies the credential, so this command cannot tell.`;
92
+ }
93
+
66
94
  /**
67
95
  * `taproot-site whoami` — which Taproot, which account, which site, until when.
68
96
  *
@@ -81,7 +109,10 @@ export async function whoami(invocation) {
81
109
  const environment = invocation.environment ?? process.env;
82
110
  const { config, apiBaseUrl, onProgress } = await openAnonymousSession({ ...invocation, client: null });
83
111
  const apiOrigin = credentialApiOrigin(apiBaseUrl);
84
- const { path: credentialPath, credential } = await findCredential(environment, apiOrigin);
112
+ if (invocation.legalDrafts && !config?.siteId) {
113
+ throw new SiteAuthoringError("config.site_missing", "Select a site before inspecting its legal draft grant.");
114
+ }
115
+ const { path: credentialPath, credential } = await findCredential(environment, apiOrigin, { siteId: invocation.legalDrafts ? config.siteId : undefined });
85
116
 
86
117
  // Reported, not resolved. An operator debugging a permission surprise needs
87
118
  // to know the environment override is in play; printing its value would put
@@ -92,6 +123,11 @@ export async function whoami(invocation) {
92
123
  const expired = credential?.keyExpiresAt !== undefined && Date.parse(credential.keyExpiresAt) <= now;
93
124
 
94
125
  onProgress(`Taproot: ${apiOrigin} (${environmentNameFor(apiBaseUrl)})`);
126
+ if (invocation.legalDrafts) {
127
+ onProgress(`Legal draft grant for site ${config.siteId}: ${credential ? `${credential.keyPrefix} (id ${credential.keyId}), ${expired ? "expired" : "expires"} ${credential.keyExpiresAt}` : `not found; run '${CLI_BINARY_NAME} ${VERB_LOGIN} --legal-drafts'.`}`);
128
+ if (environmentOverride) onProgress(`Unset ${PUBLISH_KEY_ENVIRONMENT_VARIABLE} to use this legal draft grant.`);
129
+ return successResult(VERB_WHOAMI, config.siteId, { apiOrigin, legalDrafts: true, signedIn: credential !== undefined, environmentOverride, ...(credential === undefined ? {} : { accountId: credential.accountId, keyId: credential.keyId, keyPrefix: credential.keyPrefix, keyExpiresAt: credential.keyExpiresAt, expired, credentialPath }) });
130
+ }
95
131
  if (environmentOverride) {
96
132
  onProgress(
97
133
  `${PUBLISH_KEY_ENVIRONMENT_VARIABLE} is set, so commands use that site credential and skip the sign-in.`,
@@ -146,6 +182,9 @@ export async function whoami(invocation) {
146
182
  onProgress(cliReleaseLine(lastExchange));
147
183
  }
148
184
 
185
+ const marketing = marketingAccess(lastExchange, environmentOverride);
186
+ onProgress(marketingLine(marketing));
187
+
149
188
  return successResult(VERB_WHOAMI, config?.siteId, {
150
189
  apiOrigin,
151
190
  // Named as well as spelled out, so "am I on local right now" is answerable
@@ -167,6 +206,7 @@ export async function whoami(invocation) {
167
206
  // Named for what each is. `capabilities` alone would conflate the ceiling
168
207
  // with the grant, which are different numbers and differently trustworthy.
169
208
  capabilityCeiling: SITE_AUTHORING_CAPABILITIES,
209
+ marketing,
170
210
  ...(environmentOverride || lastExchange === undefined ? {} : {
171
211
  lastExchange: {
172
212
  siteId: lastExchange.siteId,
package/src/workspace.js CHANGED
@@ -484,6 +484,9 @@ export function pageSourceRegistry(manifest) {
484
484
  ...(entry.generated !== null && typeof entry.generated === "object" && !Array.isArray(entry.generated)
485
485
  ? { generated: entry.generated }
486
486
  : {}),
487
+ ...(entry.legal !== null && typeof entry.legal === "object" && !Array.isArray(entry.legal)
488
+ ? { legal: entry.legal }
489
+ : {}),
487
490
  // What this workspace says the page's metadata is; a .pm.json page's only
488
491
  // record of it (TR01194).
489
492
  metadata: {