@taprootio/site-authoring 0.3.0 → 0.4.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 +81 -9
- package/examples/riverbend-wellness/manifest.fixture.json +6 -1
- package/examples/riverbend-wellness/redirects.json +33 -0
- package/package.json +1 -1
- package/src/api.js +175 -3
- package/src/cli-release.js +152 -0
- package/src/cli.js +70 -5
- package/src/constants.js +37 -1
- package/src/credentials.js +39 -4
- package/src/errors.js +25 -0
- package/src/fixture-contract.js +1 -0
- package/src/json-path-diff.js +167 -0
- package/src/output.js +9 -0
- package/src/redirects-contract.js +444 -0
- package/src/redirects-workspace.js +72 -0
- package/src/reference-help.js +122 -5
- package/src/session.js +161 -7
- package/src/transport.js +23 -5
- package/src/verbs/approve.js +7 -2
- package/src/verbs/deploy.js +29 -2
- package/src/verbs/footer-push.js +7 -2
- package/src/verbs/index.js +7 -1
- package/src/verbs/login.js +11 -1
- package/src/verbs/media-upload.js +7 -2
- package/src/verbs/nav-push.js +7 -2
- package/src/verbs/pages-push.js +93 -8
- package/src/verbs/preview-page.js +7 -2
- package/src/verbs/preview-revoke.js +7 -2
- package/src/verbs/pull.js +350 -18
- package/src/verbs/redirects-pull.js +90 -0
- package/src/verbs/redirects-push.js +100 -0
- package/src/verbs/status.js +90 -3
- package/src/verbs/theme-push.js +7 -2
- package/src/verbs/validate.js +74 -0
- package/src/verbs/whoami.js +79 -1
- package/src/workspace.js +153 -15
package/src/verbs/login.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
CLI_AUTHORIZATION_CLAIM_STATUS_PENDING,
|
|
10
10
|
poll,
|
|
11
11
|
startCliAuthorization,
|
|
12
|
+
announceRefusal,
|
|
12
13
|
} from "../api.js";
|
|
13
14
|
import {
|
|
14
15
|
AUTHORIZE_CLI_PATH,
|
|
@@ -328,7 +329,16 @@ export async function login(invocation) {
|
|
|
328
329
|
try {
|
|
329
330
|
authorization = await startCliAuthorization(client, { keyName });
|
|
330
331
|
} catch (error) {
|
|
331
|
-
|
|
332
|
+
// Announced only when the refusal survives translation: sign-in is the
|
|
333
|
+
// earlier of the two calls that carry this CLI's version, so it is where
|
|
334
|
+
// an outdated CLI is told to upgrade, and the remedy is a command the
|
|
335
|
+
// operator has to be handed rather than a field name (TR00703). A throttle
|
|
336
|
+
// is translated instead, because the shared guidance names a credential
|
|
337
|
+
// that does not exist yet, and announcing it first would contradict the
|
|
338
|
+
// translated line that follows.
|
|
339
|
+
const translated = translateStartApiError(error);
|
|
340
|
+
if (translated === error) announceRefusal(error, onProgress, "sign-in");
|
|
341
|
+
throw translated;
|
|
332
342
|
}
|
|
333
343
|
|
|
334
344
|
const verificationUrl = verificationUrlFor(apiBaseUrl);
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { LIMITS, VERB_MEDIA_UPLOAD } from "../constants.js";
|
|
13
13
|
import { SiteAuthoringError } from "../errors.js";
|
|
14
14
|
import { contentHash, inspectImageBytes } from "../image-metadata.js";
|
|
15
|
-
import { boundedList, openSession, successResult } from "../session.js";
|
|
15
|
+
import { boundedList, openSession, successResult, warnIfExternalWritesPaused } from "../session.js";
|
|
16
16
|
import {
|
|
17
17
|
ensureWorkspaceRoot,
|
|
18
18
|
inspectWorkspaceEntry,
|
|
@@ -164,7 +164,12 @@ async function waitForProcessing(client, siteId, imageIds, { onProgress, now })
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
export async function mediaUpload(invocation) {
|
|
167
|
-
const
|
|
167
|
+
const session = await openSession(invocation);
|
|
168
|
+
const { client, config, siteId, now, onProgress } = session;
|
|
169
|
+
// One advisory line before this verb does any work, and only when the
|
|
170
|
+
// exchange said the platform is paused. It changes nothing else: the write
|
|
171
|
+
// still runs and its refusal still classifies as platform_paused (TR00692).
|
|
172
|
+
warnIfExternalWritesPaused(session, VERB_MEDIA_UPLOAD);
|
|
168
173
|
await ensureWorkspaceRoot(config);
|
|
169
174
|
|
|
170
175
|
// Positional arguments name the files or directories to upload — `cli.js`
|
package/src/verbs/nav-push.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
isContactOrWebUrl,
|
|
16
16
|
} from "../contact-url.js";
|
|
17
17
|
import { SiteAuthoringError } from "../errors.js";
|
|
18
|
-
import { boundedList, openSession, successResult } from "../session.js";
|
|
18
|
+
import { boundedList, openSession, successResult, warnIfExternalWritesPaused } from "../session.js";
|
|
19
19
|
import {
|
|
20
20
|
NAVIGATION_FILE_NAME,
|
|
21
21
|
readWorkspaceJson,
|
|
@@ -276,7 +276,12 @@ export function validateNavigationWorkspaceDocument(parsed, siteId, knownPages)
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
export async function navPush(invocation) {
|
|
279
|
-
const
|
|
279
|
+
const session = await openSession(invocation);
|
|
280
|
+
const { client, config, siteId, onProgress } = session;
|
|
281
|
+
// One advisory line before this verb does any work, and only when the
|
|
282
|
+
// exchange said the platform is paused. It changes nothing else: the write
|
|
283
|
+
// still runs and its refusal still classifies as platform_paused (TR00692).
|
|
284
|
+
warnIfExternalWritesPaused(session, VERB_NAV_PUSH);
|
|
280
285
|
if (!await workspaceFileExists(config.workspaceDir, NAVIGATION_FILE_NAME)) {
|
|
281
286
|
throw new SiteAuthoringError(
|
|
282
287
|
"nav.file_missing",
|
package/src/verbs/pages-push.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "../api.js";
|
|
10
10
|
import { VERB_PAGES_PUSH } from "../constants.js";
|
|
11
11
|
import { SiteAuthoringError } from "../errors.js";
|
|
12
|
-
import { boundedList, openSession, successResult } from "../session.js";
|
|
12
|
+
import { boundedList, openSession, successResult, warnIfExternalWritesPaused } from "../session.js";
|
|
13
13
|
import { SETTINGS_TYPE_TAPROOT_STYLES } from "../settings-catalog.js";
|
|
14
14
|
import {
|
|
15
15
|
hasNamedFreeFormSectionContext,
|
|
@@ -18,7 +18,10 @@ import {
|
|
|
18
18
|
} from "../content/free-form-sections.js";
|
|
19
19
|
import { CONTENT_ERROR_CODES } from "../content/vocabulary.js";
|
|
20
20
|
import {
|
|
21
|
+
deleteWorkspaceFile,
|
|
22
|
+
internalPageObservedRevisionFile,
|
|
21
23
|
MEDIA_MANIFEST_FILE_NAME,
|
|
24
|
+
normalizePageBodyRevision,
|
|
22
25
|
normalizePagePath,
|
|
23
26
|
PAGE_SOURCE_EXTENSIONS,
|
|
24
27
|
PAGE_SOURCE_FORMAT_MARKDOWN,
|
|
@@ -28,6 +31,7 @@ import {
|
|
|
28
31
|
PAGES_DIRECTORY,
|
|
29
32
|
readManifest,
|
|
30
33
|
readMediaManifest,
|
|
34
|
+
readObservedPageRevision,
|
|
31
35
|
readOnlySystem404Projections,
|
|
32
36
|
readWorkspaceFile,
|
|
33
37
|
readWorkspaceJson,
|
|
@@ -38,6 +42,7 @@ import {
|
|
|
38
42
|
walkWorkspaceFiles,
|
|
39
43
|
WORKSPACE_LIMITS,
|
|
40
44
|
workspaceContentHash,
|
|
45
|
+
workspaceFileExists,
|
|
41
46
|
writeManifest,
|
|
42
47
|
} from "../workspace.js";
|
|
43
48
|
|
|
@@ -457,6 +462,45 @@ export async function readWorkspacePageSource({ workspaceDir, file, manifestEntr
|
|
|
457
462
|
};
|
|
458
463
|
}
|
|
459
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Refuses to overwrite a live page that moved since this workspace last
|
|
467
|
+
* reconciled with it.
|
|
468
|
+
*
|
|
469
|
+
* TR00622 left one detection gap open deliberately: a remote edit made between
|
|
470
|
+
* this workspace's own push and its next pull was adopted as the new baseline
|
|
471
|
+
* rather than reported, because the bytes a push sends are not comparable with
|
|
472
|
+
* the bytes a read returns and closing it needed a revision on the read
|
|
473
|
+
* contract. This is that revision, so the gap closes here: the site states the
|
|
474
|
+
* revision of each page's stored authoring state, the workspace records the one
|
|
475
|
+
* it last wrote or pulled, and a push into a page carrying a different one is
|
|
476
|
+
* refused before a single mutation is sent.
|
|
477
|
+
*
|
|
478
|
+
* The guard is armed by knowledge, not by policy. A page the workspace has
|
|
479
|
+
* never reconciled with records no revision, and a Taproot that predates the
|
|
480
|
+
* contract reports none; neither is evidence of a concurrent edit, and refusing
|
|
481
|
+
* on absence would make the ordinary first push of a workspace impossible.
|
|
482
|
+
* Both leave this exactly where TR00622 left it, which is where the caller
|
|
483
|
+
* already is.
|
|
484
|
+
*/
|
|
485
|
+
function requireReconciledRevision({ file, pagePath, target, entry, observedRevision }) {
|
|
486
|
+
// What the operator was shown by a refused pull outranks what the manifest
|
|
487
|
+
// last reconciled with. Without that, this guard would also refuse the pull
|
|
488
|
+
// conflict's own documented recovery, and the only way out of a conflict
|
|
489
|
+
// would be to abandon the local source.
|
|
490
|
+
const recorded = observedRevision ?? normalizePageBodyRevision(entry?.baseline?.revision);
|
|
491
|
+
const live = target.bodyRevision;
|
|
492
|
+
if (recorded === undefined || live === undefined || recorded === live) return;
|
|
493
|
+
const selector = pagePath || "/";
|
|
494
|
+
throw new SiteAuthoringError(
|
|
495
|
+
"pages.push_conflict",
|
|
496
|
+
`Page '${selector}' changed on the site since this workspace last reconciled with it: it now reports revision `
|
|
497
|
+
+ `${live}, and '${file}' was written against ${recorded}. No page was pushed. Run `
|
|
498
|
+
+ `'taproot-site pull' to see what differs and to adopt the site's version, or delete '${file}' and pull `
|
|
499
|
+
+ "again to take the site's document as this page's source.",
|
|
500
|
+
{ field: file, alternatives: [recorded, live] },
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
460
504
|
/**
|
|
461
505
|
* The metadata checks that are *not* part of deciding which page a source is.
|
|
462
506
|
*
|
|
@@ -557,7 +601,12 @@ export async function validateWorkspacePageDocument({
|
|
|
557
601
|
}
|
|
558
602
|
|
|
559
603
|
export async function pagesPush(invocation) {
|
|
560
|
-
const
|
|
604
|
+
const session = await openSession(invocation);
|
|
605
|
+
const { client, config, siteId, onProgress } = session;
|
|
606
|
+
// One advisory line before this verb does any work, and only when the
|
|
607
|
+
// exchange said the platform is paused. It changes nothing else: the write
|
|
608
|
+
// still runs and its refusal still classifies as platform_paused (TR00692).
|
|
609
|
+
warnIfExternalWritesPaused(session, VERB_PAGES_PUSH);
|
|
561
610
|
const allowRawHtml = invocation.allowRawHtml === true;
|
|
562
611
|
// Both manifests are bound to the site before anything is planned or sent:
|
|
563
612
|
// every id in them is site-scoped, and a workspace pulled from another site
|
|
@@ -837,6 +886,18 @@ export async function pagesPush(invocation) {
|
|
|
837
886
|
file,
|
|
838
887
|
);
|
|
839
888
|
}
|
|
889
|
+
// The record this push may spend has to be removable after the send:
|
|
890
|
+
// a directory or a symlink at its path is refused here, before any
|
|
891
|
+
// page is written, rather than discovered after the site has changed.
|
|
892
|
+
const observedRecordFile = internalPageObservedRevisionFile(target.pageId);
|
|
893
|
+
if (observedRecordFile !== undefined) await workspaceFileExists(config.workspaceDir, observedRecordFile);
|
|
894
|
+
requireReconciledRevision({
|
|
895
|
+
file,
|
|
896
|
+
pagePath,
|
|
897
|
+
target,
|
|
898
|
+
entry: manifestByPageId.get(target.pageId),
|
|
899
|
+
observedRevision: await readObservedPageRevision(config.workspaceDir, target.pageId),
|
|
900
|
+
});
|
|
840
901
|
}
|
|
841
902
|
|
|
842
903
|
// The duplicate-source check above only catches two workspace files
|
|
@@ -942,15 +1003,39 @@ export async function pagesPush(invocation) {
|
|
|
942
1003
|
file: page.file,
|
|
943
1004
|
sourceFormat: page.sourceFormat,
|
|
944
1005
|
// The page's remote body is now derived from exactly these bytes.
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
|
|
1006
|
+
// No remote *hash* is recorded: a read re-projects image delivery and
|
|
1007
|
+
// re-serializes component data, so a hash of what was sent is not
|
|
1008
|
+
// comparable with a hash of what a pull would read, and the next pull
|
|
1009
|
+
// establishes that half from what it actually reads.
|
|
1010
|
+
//
|
|
1011
|
+
// The revision is different in kind and is recorded here. It is the
|
|
1012
|
+
// site's own statement about the stored state this mutation just
|
|
1013
|
+
// wrote, so it is directly comparable with the revision the next read
|
|
1014
|
+
// reports — which is what lets the *next* push tell "nobody else
|
|
1015
|
+
// touched this page" from "someone did", without a pull in between.
|
|
1016
|
+
// Absent against a Taproot that predates the contract, and then
|
|
1017
|
+
// dropped rather than carried forward: this push moved the stored
|
|
1018
|
+
// state, so whatever was recorded describes a version that no longer
|
|
1019
|
+
// exists, and keeping it would refuse the *next* push over a change
|
|
1020
|
+
// this workspace made itself.
|
|
1021
|
+
baseline: {
|
|
1022
|
+
sourceHash: page.sourceHash,
|
|
1023
|
+
...(summary.bodyRevision === undefined ? {} : { revision: summary.bodyRevision }),
|
|
1024
|
+
},
|
|
951
1025
|
pendingApproval: true,
|
|
952
1026
|
});
|
|
953
1027
|
if (existing === undefined) manifest.pages.push(record);
|
|
1028
|
+
// The override this page's record permitted has now been taken, and
|
|
1029
|
+
// the baseline above records what the site holds because of it.
|
|
1030
|
+
// Keeping the record would let a *later* push overwrite somebody
|
|
1031
|
+
// else's edit without anyone having been shown it. Removed after the
|
|
1032
|
+
// manifest record is written, so a failure here can never leave a
|
|
1033
|
+
// successful mutation unrecorded; the record's shape was checked
|
|
1034
|
+
// before anything was sent.
|
|
1035
|
+
const observedRevisionFile = internalPageObservedRevisionFile(summary.pageId);
|
|
1036
|
+
if (observedRevisionFile !== undefined) {
|
|
1037
|
+
await deleteWorkspaceFile(config.workspaceDir, observedRevisionFile);
|
|
1038
|
+
}
|
|
954
1039
|
}
|
|
955
1040
|
} finally {
|
|
956
1041
|
if (manifestDirty) await writeManifest(config.workspaceDir, manifest);
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "../api.js";
|
|
8
8
|
import { VERB_PREVIEW_PAGE } from "../constants.js";
|
|
9
9
|
import { asSiteAuthoringError, isCanonicalUuid, SiteAuthoringError } from "../errors.js";
|
|
10
|
-
import { openSession, successResult } from "../session.js";
|
|
10
|
+
import { openSession, successResult, warnIfExternalWritesPaused } from "../session.js";
|
|
11
11
|
import { normalizePagePath, readManifest } from "../workspace.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -40,7 +40,12 @@ export async function previewPage(invocation) {
|
|
|
40
40
|
{ field: "pageSelector", exitCode: 2 },
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
|
-
const
|
|
43
|
+
const session = await openSession(invocation);
|
|
44
|
+
const { client, config, siteId, now, onProgress } = session;
|
|
45
|
+
// One advisory line before this verb does any work, and only when the
|
|
46
|
+
// exchange said the platform is paused. It changes nothing else: the write
|
|
47
|
+
// still runs and its refusal still classifies as platform_paused (TR00692).
|
|
48
|
+
warnIfExternalWritesPaused(session, VERB_PREVIEW_PAGE);
|
|
44
49
|
let pageId = selector;
|
|
45
50
|
if (!isCanonicalUuid(selector)) {
|
|
46
51
|
const pagePath = normalizePagePath(selector);
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "../api.js";
|
|
7
7
|
import { VERB_PREVIEW_REVOKE } from "../constants.js";
|
|
8
8
|
import { isCanonicalUuid, SiteAuthoringError } from "../errors.js";
|
|
9
|
-
import { openSession, successResult } from "../session.js";
|
|
9
|
+
import { openSession, successResult, warnIfExternalWritesPaused } from "../session.js";
|
|
10
10
|
|
|
11
11
|
export async function previewRevoke(invocation) {
|
|
12
12
|
if (!Array.isArray(invocation.previewIds) || invocation.previewIds.length !== 2) {
|
|
@@ -31,7 +31,12 @@ export async function previewRevoke(invocation) {
|
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
const [pageId, snapshotId] = invocation.previewIds;
|
|
34
|
-
const
|
|
34
|
+
const session = await openSession(invocation);
|
|
35
|
+
const { client, siteId, onProgress } = session;
|
|
36
|
+
// One advisory line before this verb does any work, and only when the
|
|
37
|
+
// exchange said the platform is paused. It changes nothing else: the write
|
|
38
|
+
// still runs and its refusal still classifies as platform_paused (TR00692).
|
|
39
|
+
warnIfExternalWritesPaused(session, VERB_PREVIEW_REVOKE);
|
|
35
40
|
try {
|
|
36
41
|
return await withRefusalGuidance(onProgress, "authoring preview revocation", async () => {
|
|
37
42
|
onProgress("Revoking the authoring preview and scheduling its artifacts for cleanup.");
|