@taprootio/site-authoring 0.23.0 → 0.24.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,6 +1,7 @@
1
1
  import { CLI_BINARY_NAME, VERB_LOGOUT } from "../constants.js";
2
2
  import { credentialApiOrigin, removeCredential } from "../credentials.js";
3
3
  import { openAnonymousSession, successResult } from "../session.js";
4
+ import { SiteAuthoringError } from "../errors.js";
4
5
 
5
6
  /**
6
7
  * `taproot-site logout` — local discard, and nothing more.
@@ -19,11 +20,20 @@ import { openAnonymousSession, successResult } from "../session.js";
19
20
  * discards is addressed by API origin.
20
21
  */
21
22
  export async function logout(invocation) {
22
- const { apiBaseUrl, onProgress } = await openAnonymousSession(invocation);
23
+ const { config, apiBaseUrl, onProgress } = await openAnonymousSession(invocation);
24
+ if (invocation.legalDrafts && !config?.siteId) {
25
+ throw new SiteAuthoringError("config.site_missing", "Select a site before discarding its legal draft grant.");
26
+ }
23
27
  const { path: credentialPath, removed } = await removeCredential(
24
28
  invocation.environment ?? process.env,
25
29
  credentialApiOrigin(apiBaseUrl),
30
+ { siteId: invocation.legalDrafts ? config.siteId : undefined },
26
31
  );
32
+ if (invocation.legalDrafts) {
33
+ onProgress(removed ? `Discarded the legal draft grant for site ${config.siteId} from ${credentialPath}.` : `No legal draft grant for site ${config.siteId} was found in ${credentialPath}.`);
34
+ if (removed) onProgress("This is a local discard; revoke the credential in Account -> Settings -> API keys.");
35
+ return successResult(VERB_LOGOUT, config.siteId, { removed, credentialPath, legalDrafts: true });
36
+ }
27
37
  onProgress(
28
38
  removed
29
39
  ? `Discarded the stored Taproot sign-in from ${credentialPath}.`
@@ -35,5 +45,9 @@ export async function logout(invocation) {
35
45
  + `Account -> Settings -> API keys; '${CLI_BINARY_NAME} ${VERB_LOGOUT}' cannot revoke it.`,
36
46
  );
37
47
  }
48
+ onProgress(
49
+ "Separate site legal draft grants, if any, remain stored. Select each site and run "
50
+ + `'${CLI_BINARY_NAME} --legal-drafts ${VERB_LOGOUT}' to discard its grant, or revoke the keys in Account -> Settings -> API keys.`,
51
+ );
38
52
  return successResult(VERB_LOGOUT, undefined, { removed, credentialPath });
39
53
  }
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { Readable } from "node:stream";
2
3
 
3
4
  import {
@@ -624,6 +625,7 @@ export async function mediaUpload(invocation) {
624
625
  contentType,
625
626
  fileSize: bytes.byteLength,
626
627
  contentHash: hash,
628
+ operationKey: randomUUID(),
627
629
  width,
628
630
  height,
629
631
  ownershipScope: IMAGE_OWNERSHIP_SCOPE_SITE,
@@ -14,7 +14,7 @@ import { FOOTER_SETTINGS_FILE } from "../footer-workspace.js";
14
14
  import { afterSubmitPage, FORM_FILE_EXTENSION, FORMS_DIRECTORY, normalizePagePath as normalizeFormPagePath } from "../forms-contract.js";
15
15
  import { normalizeRedirectPath } from "../site-checks.js";
16
16
  import { boundedList, openSession, successResult, warnIfExternalWritesPaused } from "../session.js";
17
- import { isAuthorableTemplateType, isGeneratedTemplateType } from "../typed-pages.js";
17
+ import { isAuthorableTemplateType, isGeneratedTemplateType, isLegalTemplateType } from "../typed-pages.js";
18
18
  import {
19
19
  deleteWorkspaceFile,
20
20
  displayPagePath,
@@ -194,10 +194,10 @@ function requireDeletable(manifest, entry, pagePath) {
194
194
  shown,
195
195
  );
196
196
  }
197
- if (!isAuthorableTemplateType(entry.templateType)) {
197
+ if (isLegalTemplateType(entry.templateType) || !isAuthorableTemplateType(entry.templateType)) {
198
198
  throw refusal(
199
199
  "pages.delete_not_authorable",
200
- `'${shown}' is a ${entry.templateType ?? "system-managed"} page, which this CLI does not author; delete it in the app.`,
200
+ `'${shown}' is a ${entry.templateType ?? "system-managed"} page, which this CLI cannot delete; delete it in the app.`,
201
201
  shown,
202
202
  );
203
203
  }
@@ -241,6 +241,9 @@ export async function pagesDelete(invocation) {
241
241
  shown,
242
242
  );
243
243
  }
244
+ if (live !== undefined && isLegalTemplateType(live.templateType)) {
245
+ throw refusal("pages.delete_not_authorable", `'${shown}' is a legal page; delete it in the app.`, shown);
246
+ }
244
247
  const alreadyGone = live === undefined || isGonePage(live);
245
248
  if (!alreadyGone) {
246
249
  // The fence `pages push` uses, against the revision this workspace last reconciled with.
@@ -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";
@@ -38,9 +38,12 @@ import { ApiError } from "../transport.js";
38
38
  import { placesFormIds, readFormIndex, withFormKeys } from "../form-references.js";
39
39
  import {
40
40
  generatedIdentity,
41
+ legalIdentity,
42
+ isLegalTemplateType,
41
43
  isAuthorableTemplateType,
42
44
  isGeneratedTemplateType,
43
45
  sentDisplayDate,
46
+ TEMPLATE_LEGAL,
44
47
  workspaceDocumentFromPage,
45
48
  } from "../typed-pages.js";
46
49
  import {
@@ -450,11 +453,11 @@ async function resolveTrackedSources(workspaceDir, pages, registry, onProgress)
450
453
  }
451
454
 
452
455
  /**
453
- * Reads the previous manifest's source registry, tolerating everything a
454
- * strict read would refuse.
456
+ * Reads the previous manifest's source registry, tolerating damaged tracked
457
+ * entries but protecting unsaved drafts whose metadata has no server identity.
455
458
  *
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:
459
+ * `pull` repairs damaged tracked entries instead of stranding the workspace.
460
+ * It is also the only verb that reads a manifest it is about to replace:
458
461
  * a version-4 manifest still records the `pageId → file` mapping this needs,
459
462
  * and honoring it is what repairs an existing workspace in one pull instead of
460
463
  * by hand. Every other verb keeps refusing an old manifest outright — the site
@@ -474,7 +477,22 @@ async function readSourceRegistry(workspaceDir, siteId) {
474
477
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || parsed.siteId !== siteId) {
475
478
  return new Map();
476
479
  }
477
- const registry = pageSourceRegistry(withoutRetiredReadOnlyProjections(parsed));
480
+ const manifest = withoutRetiredReadOnlyProjections(parsed);
481
+ for (const entry of Array.isArray(manifest.pages) ? manifest.pages : []) {
482
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry) || isCanonicalUuid(entry.pageId)) continue;
483
+ if (!["file", "title", "path"].some((field) => Object.hasOwn(entry, field))) continue;
484
+ const field = sanitizeDiagnostic(entry.file, MANIFEST_FILE_NAME);
485
+ const label = sanitizeDiagnostic(entry.path, field);
486
+ // A new source's metadata lives only here until its first save.
487
+ throw new SiteAuthoringError(
488
+ "pages.source_conflict",
489
+ `The pending page entry '${label}' has no server page id. Pull would discard its saved metadata. `
490
+ + "Save the draft with 'taproot-site pages push' first, or back up and move its source file and manifest "
491
+ + "entry before running 'taproot-site pull' again. Nothing was changed.",
492
+ { field },
493
+ );
494
+ }
495
+ const registry = pageSourceRegistry(manifest);
478
496
  return parsed.manifestVersion === MANIFEST_VERSION ? registry : withoutSupersededRemoteHashes(registry);
479
497
  }
480
498
 
@@ -750,6 +768,33 @@ export async function pull(invocation) {
750
768
  new Set(retiredGeneratedSources.map((source) => source.file)),
751
769
  );
752
770
 
771
+ // A fresh destination has no ownership baseline. Protect legal drafts even
772
+ // when the remote page has another template, and refuse unreadable sources.
773
+ for (const summary of authorable) {
774
+ if (tracked.has(summary.pageId)) continue;
775
+ const file = pageFiles.get(summary.pageId);
776
+ let protectedSource;
777
+ try {
778
+ if (!await workspaceFileExists(config.workspaceDir, file)) continue;
779
+ protectedSource = isLegalTemplateType(summary.templateType);
780
+ if (!protectedSource) {
781
+ const document = await readWorkspaceJson(config.workspaceDir, file, WORKSPACE_LIMITS.documentBytes);
782
+ protectedSource = document?.template === TEMPLATE_LEGAL;
783
+ }
784
+ } catch {
785
+ protectedSource = true;
786
+ }
787
+ if (protectedSource) {
788
+ throw new SiteAuthoringError(
789
+ "pages.source_conflict",
790
+ `Page ${summary.pageId} would use '${file}', but this workspace does not track that file as its source. `
791
+ + "The file was kept. Back up and move its source file and any manifest entry, or reconcile it before "
792
+ + "running 'taproot-site pull' again.",
793
+ { field: file },
794
+ );
795
+ }
796
+ }
797
+
753
798
  // The redirect map, with the revision a later push is fenced by (TR00702).
754
799
  // Read here, before the first workspace write, for the same reason the
755
800
  // settings groups below are: a failure must strand nothing. A Taproot that
@@ -920,6 +965,7 @@ export async function pull(invocation) {
920
965
  // The page is still the generated page it was; losing the record would
921
966
  // let the next push take an edit to its identity for the owner's.
922
967
  ...(source.generated === undefined ? {} : { generated: source.generated }),
968
+ ...(source.legal === undefined ? {} : { legal: source.legal }),
923
969
  description: metadataKept ? source.metadata.description : description,
924
970
  title: metadataKept ? source.metadata.title : titleFromRead(page, summary),
925
971
  path: metadataKept ? source.metadata.path : pathFromRead(page, summary),
@@ -1036,6 +1082,7 @@ export async function pull(invocation) {
1036
1082
  if (metadataEdited) reportPendingAuthorDisagreement(source, summary, onProgress);
1037
1083
  trackedPlans.set(summary.pageId, {
1038
1084
  ...(generatedPage ? { generated: generatedIdentity(body.data) } : {}),
1085
+ ...(isLegalTemplateType(summary.templateType) ? { legal: legalIdentity(body.data) } : {}),
1039
1086
  description: metadataEdited ? source.metadata.description : description,
1040
1087
  title: metadataEdited ? source.metadata.title : titleFromRead(page, summary),
1041
1088
  path: metadataEdited ? source.metadata.path : pathFromRead(page, summary),
@@ -1252,6 +1299,7 @@ export async function pull(invocation) {
1252
1299
  if (plan.path !== undefined) entry.path = plan.path;
1253
1300
  // What a generated page is, so a push can refuse a source that changed it.
1254
1301
  if (plan.generated !== undefined) entry.generated = plan.generated;
1302
+ if (plan.legal !== undefined) entry.legal = plan.legal;
1255
1303
  if (plan.file !== undefined) {
1256
1304
  entry.file = plan.file;
1257
1305
  entry.sourceFormat = plan.sourceFormat;
@@ -1285,6 +1333,7 @@ export async function pull(invocation) {
1285
1333
  } else {
1286
1334
  entry.file = pageFiles.get(summary.pageId);
1287
1335
  if (isGeneratedTemplateType(summary.templateType)) entry.generated = generatedIdentity(body.data);
1336
+ if (isLegalTemplateType(summary.templateType)) entry.legal = legalIdentity(body.data);
1288
1337
  const source = Buffer.from(`${JSON.stringify(body, undefined, 2)}\n`, "utf8");
1289
1338
  entry.workspaceMode = PAGE_WORKSPACE_MODE_EDITABLE;
1290
1339
  entry.sourceFormat = pageSourceFormat(entry.file);
@@ -16,6 +16,7 @@ import { 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
@@ -81,7 +82,10 @@ export async function whoami(invocation) {
81
82
  const environment = invocation.environment ?? process.env;
82
83
  const { config, apiBaseUrl, onProgress } = await openAnonymousSession({ ...invocation, client: null });
83
84
  const apiOrigin = credentialApiOrigin(apiBaseUrl);
84
- const { path: credentialPath, credential } = await findCredential(environment, apiOrigin);
85
+ if (invocation.legalDrafts && !config?.siteId) {
86
+ throw new SiteAuthoringError("config.site_missing", "Select a site before inspecting its legal draft grant.");
87
+ }
88
+ const { path: credentialPath, credential } = await findCredential(environment, apiOrigin, { siteId: invocation.legalDrafts ? config.siteId : undefined });
85
89
 
86
90
  // Reported, not resolved. An operator debugging a permission surprise needs
87
91
  // to know the environment override is in play; printing its value would put
@@ -92,6 +96,11 @@ export async function whoami(invocation) {
92
96
  const expired = credential?.keyExpiresAt !== undefined && Date.parse(credential.keyExpiresAt) <= now;
93
97
 
94
98
  onProgress(`Taproot: ${apiOrigin} (${environmentNameFor(apiBaseUrl)})`);
99
+ if (invocation.legalDrafts) {
100
+ 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'.`}`);
101
+ if (environmentOverride) onProgress(`Unset ${PUBLISH_KEY_ENVIRONMENT_VARIABLE} to use this legal draft grant.`);
102
+ 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 }) });
103
+ }
95
104
  if (environmentOverride) {
96
105
  onProgress(
97
106
  `${PUBLISH_KEY_ENVIRONMENT_VARIABLE} is set, so commands use that site credential and skip the sign-in.`,
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: {