@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.
- package/README.md +28 -5
- package/package.json +1 -1
- package/src/api.js +27 -5
- package/src/capabilities.js +1 -0
- package/src/changelog.json +22 -0
- package/src/cli.js +40 -6
- package/src/constants.js +1 -1
- package/src/credentials.js +55 -14
- package/src/reference-help.js +3 -3
- package/src/session.js +34 -0
- package/src/transport.js +55 -17
- package/src/typed-page-reference.js +19 -26
- package/src/typed-pages.js +51 -6
- package/src/verbs/approve.js +25 -9
- package/src/verbs/login.js +33 -17
- package/src/verbs/logout.js +15 -1
- package/src/verbs/media-upload.js +2 -0
- package/src/verbs/pages-delete.js +6 -3
- package/src/verbs/pages-push.js +41 -3
- package/src/verbs/plan.js +6 -1
- package/src/verbs/pull.js +55 -6
- package/src/verbs/whoami.js +10 -1
- package/src/workspace.js +3 -0
package/src/verbs/logout.js
CHANGED
|
@@ -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
|
|
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.
|
package/src/verbs/pages-push.js
CHANGED
|
@@ -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
|
-
|
|
1421
|
-
|
|
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
|
|
454
|
-
*
|
|
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`
|
|
457
|
-
*
|
|
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
|
|
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);
|
package/src/verbs/whoami.js
CHANGED
|
@@ -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
|
-
|
|
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: {
|