@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.
- package/README.md +78 -7
- package/package.json +2 -2
- package/src/analytics-campaign.json +31 -0
- package/src/analytics-link-label.json +22 -0
- package/src/analytics-source-vocabulary.json +17 -0
- package/src/api.js +27 -5
- package/src/capabilities.js +2 -1
- package/src/changelog.json +38 -0
- package/src/cli.js +155 -7
- package/src/constants.js +4 -1
- package/src/credentials.js +55 -14
- package/src/marketing-api.js +179 -0
- package/src/marketing-contract.js +289 -0
- package/src/marketing-workspace.js +77 -0
- package/src/redirects-contract.js +0 -3
- package/src/reference-help.js +109 -3
- package/src/session.js +35 -1
- package/src/site-check-findings.js +0 -1
- package/src/site-check-rules.json +0 -1
- package/src/site-checks.js +0 -6
- package/src/typed-page-reference.js +19 -26
- package/src/typed-pages.js +51 -6
- package/src/utm-link.js +214 -0
- package/src/verbs/approve.js +25 -9
- package/src/verbs/index.js +12 -0
- package/src/verbs/link-build.js +195 -0
- package/src/verbs/login.js +33 -17
- package/src/verbs/logout.js +15 -1
- package/src/verbs/marketing-pull.js +80 -0
- package/src/verbs/marketing-push.js +250 -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 +63 -9
- package/src/verbs/whoami.js +42 -2
- package/src/workspace.js +3 -0
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";
|
|
@@ -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
|
|
454
|
-
*
|
|
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`
|
|
457
|
-
*
|
|
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
|
|
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.
|
|
1350
|
-
|
|
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
|
-
)
|
|
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
|
};
|
package/src/verbs/whoami.js
CHANGED
|
@@ -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
|
-
|
|
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: {
|