@taprootio/site-authoring 0.6.3 → 0.7.1
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 +103 -10
- package/package.json +2 -2
- package/src/api.js +281 -61
- package/src/cli.js +73 -26
- package/src/constants.js +2 -1
- package/src/content/components.js +7 -33
- package/src/content/free-form-section-registry.json +4 -2
- package/src/content/inline-facts.js +6 -0
- package/src/content/tiptap-prosemirror.ts +3 -4
- package/src/content/vocabulary.js +6 -2
- package/src/errors.js +27 -0
- package/src/fixture-contract.js +4 -4
- package/src/fixture-init.js +231 -0
- package/src/output.js +12 -2
- package/src/reference-help.js +139 -89
- package/src/staging-check.js +145 -0
- package/src/theme-validation.js +31 -12
- package/src/transport.js +12 -3
- package/src/verbs/deploy.js +35 -101
- package/src/verbs/index.js +4 -1
- package/src/verbs/pull.js +22 -5
- package/src/verbs/redirects-check.js +18 -0
- package/src/verbs/status.js +34 -17
- package/src/verbs/theme-push.js +44 -9
- package/src/verbs/validate.js +51 -12
package/src/api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeLatestCliVersion } from "./cli-release.js";
|
|
1
2
|
import {
|
|
2
3
|
CANONICAL_TIMESTAMP,
|
|
3
4
|
CAPABILITY_REFUSAL_FIELD,
|
|
@@ -15,12 +16,11 @@ import {
|
|
|
15
16
|
REFUSAL_THROTTLED,
|
|
16
17
|
REFUSAL_UNCLASSIFIED,
|
|
17
18
|
} from "./constants.js";
|
|
18
|
-
import { normalizeLatestCliVersion } from "./cli-release.js";
|
|
19
19
|
// The display-prefix shape lives with the store because acceptance on claim
|
|
20
20
|
// must equal acceptance on save — a prefix admitted here but refused there
|
|
21
21
|
// would turn a successful mint into an orphaned key.
|
|
22
22
|
import { KEY_PREFIX } from "./credentials.js";
|
|
23
|
-
import { isCanonicalUuid, sanitizeDiagnostic, SiteAuthoringError } from "./errors.js";
|
|
23
|
+
import { isCanonicalUuid, normalizePreviewDiagnostic, sanitizeDiagnostic, SiteAuthoringError } from "./errors.js";
|
|
24
24
|
import {
|
|
25
25
|
DEFAULT_REDIRECT_STATUS,
|
|
26
26
|
GONE_STATUS,
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
REDIRECT_ORIGIN_AUTHORED,
|
|
30
30
|
REDIRECT_ORIGIN_PATH_HISTORY,
|
|
31
31
|
} from "./redirects-contract.js";
|
|
32
|
+
import { CANDIDATE_SELECTABLE_SETTINGS_TYPES } from "./settings-catalog.js";
|
|
32
33
|
import { ApiError, isWellFormedCredential } from "./transport.js";
|
|
33
34
|
// The revision's shape lives with the manifest that records it: a value this
|
|
34
35
|
// module admitted but the registry then dropped would be recorded on a push
|
|
@@ -158,7 +159,10 @@ const AUTHORING_PREVIEW_FIELDS = Object.freeze({
|
|
|
158
159
|
manifest: "AuthoringPreviewManifest",
|
|
159
160
|
});
|
|
160
161
|
const AUTHORING_PREVIEW_FIELD_ERRORS = Object.freeze({
|
|
161
|
-
[AUTHORING_PREVIEW_FIELDS.draft]: [
|
|
162
|
+
[AUTHORING_PREVIEW_FIELDS.draft]: [
|
|
163
|
+
"preview.no_draft",
|
|
164
|
+
"The requested page does not have a persisted draft to preview.",
|
|
165
|
+
],
|
|
162
166
|
[AUTHORING_PREVIEW_FIELDS.staging]: [
|
|
163
167
|
"preview.staging_unavailable",
|
|
164
168
|
"The site does not have an available staging hostname for authoring previews.",
|
|
@@ -172,7 +176,7 @@ const AUTHORING_PREVIEW_FIELD_ERRORS = Object.freeze({
|
|
|
172
176
|
[AUTHORING_PREVIEW_FIELDS.siteCapacity]: [
|
|
173
177
|
"preview.site_capacity",
|
|
174
178
|
"This site already has the maximum number of queued or rendering authoring previews. "
|
|
175
|
-
|
|
179
|
+
+ "Wait for one to finish rendering, or revoke a preview that is still queued or rendering.",
|
|
176
180
|
],
|
|
177
181
|
[AUTHORING_PREVIEW_FIELDS.authorityCapacity]: [
|
|
178
182
|
"preview.authority_capacity",
|
|
@@ -184,7 +188,8 @@ const AUTHORING_PREVIEW_FIELD_ERRORS = Object.freeze({
|
|
|
184
188
|
],
|
|
185
189
|
});
|
|
186
190
|
const CANONICAL_SHA256 = /^sha256:[0-9a-f]{64}$/u;
|
|
187
|
-
const CANONICAL_HOST =
|
|
191
|
+
const CANONICAL_HOST =
|
|
192
|
+
/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u;
|
|
188
193
|
const CANONICAL_HANDOFF = /^[A-Za-z0-9_-]{43}$/u;
|
|
189
194
|
const AUTHORING_PREVIEW_PATH_PREFIX = "/_taproot/preview/pages";
|
|
190
195
|
const AUTHORING_PREVIEW_LIFETIME_MILLISECONDS = 60 * 60_000;
|
|
@@ -342,8 +347,7 @@ const REFUSAL_GUIDANCE = Object.freeze({
|
|
|
342
347
|
[REFUSAL_CREDENTIAL_REJECTED]:
|
|
343
348
|
"The site authoring credential was rejected: it is invalid, revoked, or bound to a different site. "
|
|
344
349
|
+ "Stop and re-issue it; retrying cannot succeed.",
|
|
345
|
-
[REFUSAL_THROTTLED]:
|
|
346
|
-
"This credential exceeded its request budget. Back off and retry with delay.",
|
|
350
|
+
[REFUSAL_THROTTLED]: "This credential exceeded its request budget. Back off and retry with delay.",
|
|
347
351
|
});
|
|
348
352
|
|
|
349
353
|
/**
|
|
@@ -375,9 +379,9 @@ export function announceRefusal(error, onProgress, action = "request") {
|
|
|
375
379
|
// ApiError message stays the fallback for a server that sent no
|
|
376
380
|
// description.
|
|
377
381
|
const serverDescription = error.descriptionFor?.(CLI_UPGRADE_REFUSAL_FIELD);
|
|
378
|
-
onProgress(
|
|
379
|
-
sanitizeDiagnostic(serverDescription ?? error.message, "the request was rejected.")
|
|
380
|
-
|
|
382
|
+
onProgress(
|
|
383
|
+
` Taproot's own detail: ${sanitizeDiagnostic(serverDescription ?? error.message, "the request was rejected.")}`,
|
|
384
|
+
);
|
|
381
385
|
onProgress("");
|
|
382
386
|
return;
|
|
383
387
|
}
|
|
@@ -420,9 +424,9 @@ export function announceRefusal(error, onProgress, action = "request") {
|
|
|
420
424
|
// verbatim (sanitized) rather than paraphrasing policy the CLI does not
|
|
421
425
|
// own. The bounded ApiError message remains as the fallback.
|
|
422
426
|
const serverDescription = error.descriptionFor?.("UpgradePrompt");
|
|
423
|
-
onProgress(
|
|
424
|
-
sanitizeDiagnostic(serverDescription ?? error.message, "the request was rejected.")
|
|
425
|
-
|
|
427
|
+
onProgress(
|
|
428
|
+
` Taproot's own detail: ${sanitizeDiagnostic(serverDescription ?? error.message, "the request was rejected.")}`,
|
|
429
|
+
);
|
|
426
430
|
onProgress("");
|
|
427
431
|
return;
|
|
428
432
|
}
|
|
@@ -619,7 +623,10 @@ function requireRedirectMap(value, siteId) {
|
|
|
619
623
|
{ field: "siteId" },
|
|
620
624
|
);
|
|
621
625
|
}
|
|
622
|
-
|
|
626
|
+
if (response.entries !== undefined && !Array.isArray(response.entries)) {
|
|
627
|
+
throw new SiteAuthoringError("api.redirects_contract", "Taproot returned an invalid redirect entries list.");
|
|
628
|
+
}
|
|
629
|
+
const entries = response.entries ?? [];
|
|
623
630
|
return {
|
|
624
631
|
revision: response.revision,
|
|
625
632
|
entries: entries.map((entry) => normalizeRedirectMapEntry(entry)),
|
|
@@ -770,13 +777,84 @@ export async function listSiteImages(client, siteId, requestOptions = {}) {
|
|
|
770
777
|
return { images, summary, truncated: true };
|
|
771
778
|
}
|
|
772
779
|
|
|
780
|
+
/** Broken targets in the latest editable page bodies, authorized by Content. */
|
|
781
|
+
export async function getSiteBrokenReferences(client, siteId) {
|
|
782
|
+
const code = "api.broken_references_contract";
|
|
783
|
+
const response = requireObject(
|
|
784
|
+
await client.request(sitePath(siteId, "broken-references")),
|
|
785
|
+
code,
|
|
786
|
+
"broken-reference report",
|
|
787
|
+
);
|
|
788
|
+
// Proto3 omits empty repeated fields. A present non-array is a bad report,
|
|
789
|
+
// not evidence that the site has no broken links.
|
|
790
|
+
const repeated = (value, field) => {
|
|
791
|
+
if (value === undefined) return [];
|
|
792
|
+
if (!Array.isArray(value)) {
|
|
793
|
+
throw new SiteAuthoringError(code, `Taproot returned an invalid ${field}.`, { field });
|
|
794
|
+
}
|
|
795
|
+
return value;
|
|
796
|
+
};
|
|
797
|
+
return repeated(response.pages, "pages").map((entry) => {
|
|
798
|
+
const page = requireObject(entry, code, "broken-reference page");
|
|
799
|
+
const missingPagePaths = repeated(page.missingPagePaths, "missingPagePaths");
|
|
800
|
+
if (missingPagePaths.some((path) => typeof path !== "string")) {
|
|
801
|
+
throw new SiteAuthoringError(code, "Taproot returned an invalid missing page path.", {
|
|
802
|
+
field: "missingPagePaths",
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
pageId: requireCanonicalUuid(page.pageId, code, "pageId"),
|
|
807
|
+
pageTitle: text(page.pageTitle),
|
|
808
|
+
missingImageIds: repeated(page.missingImageIds, "missingImageIds")
|
|
809
|
+
.map((id) => requireCanonicalUuid(id, code, "missingImageIds")),
|
|
810
|
+
missingPagePaths,
|
|
811
|
+
};
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** Use the same changed release set the Deployments page selects. */
|
|
816
|
+
export async function getDeploySelection(client, siteId) {
|
|
817
|
+
const code = "api.deploy_review_contract";
|
|
818
|
+
const review = requireObject(await client.request(sitePath(siteId, "deploy/review")), code, "deploy review");
|
|
819
|
+
const repeated = (value, field) => {
|
|
820
|
+
if (value === undefined) return [];
|
|
821
|
+
if (!Array.isArray(value)) throw new SiteAuthoringError(code, `Taproot returned an invalid ${field}.`, { field });
|
|
822
|
+
return value;
|
|
823
|
+
};
|
|
824
|
+
const stagedPageIds = repeated(review.stagedPages, "stagedPages").map((page) =>
|
|
825
|
+
requireCanonicalUuid(page?.pageId, code, "stagedPages.pageId")
|
|
826
|
+
);
|
|
827
|
+
const changed = repeated(review.settingsChanges, "settingsChanges").filter((group) => {
|
|
828
|
+
requireObject(group, code, "settings change");
|
|
829
|
+
if (!CANDIDATE_SELECTABLE_SETTINGS_TYPES.includes(group.settingsType)) {
|
|
830
|
+
throw new SiteAuthoringError(code, "Taproot returned an unsupported candidate settings group.");
|
|
831
|
+
}
|
|
832
|
+
return repeated(group.changes, "settings changes").length > 0;
|
|
833
|
+
});
|
|
834
|
+
if (review.navigationChanged !== undefined && typeof review.navigationChanged !== "boolean") {
|
|
835
|
+
throw new SiteAuthoringError(code, "Taproot returned an invalid navigation change flag.");
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
stagedPageIds: [...new Set(stagedPageIds)],
|
|
839
|
+
selectedSettingsTypes: CANDIDATE_SELECTABLE_SETTINGS_TYPES.filter((type) =>
|
|
840
|
+
changed.some((group) => group.settingsType === type)
|
|
841
|
+
),
|
|
842
|
+
includeNavigation: review.navigationChanged === true,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
773
846
|
export async function getPublishingReadiness(client, siteId, selection = {}) {
|
|
774
847
|
const response = requireObject(
|
|
775
|
-
await client.request(sitePath(
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
848
|
+
await client.request(sitePath(
|
|
849
|
+
siteId,
|
|
850
|
+
`publishing/readiness${
|
|
851
|
+
query([
|
|
852
|
+
["stagedPageIds", selection.stagedPageIds],
|
|
853
|
+
["selectedSettingsTypes", selection.selectedSettingsTypes],
|
|
854
|
+
["includeNavigation", selection.includeNavigation],
|
|
855
|
+
])
|
|
856
|
+
}`,
|
|
857
|
+
)),
|
|
780
858
|
"api.readiness_contract",
|
|
781
859
|
"publishing readiness",
|
|
782
860
|
);
|
|
@@ -840,6 +918,79 @@ export async function getStagingPreviewStatus(client, siteId) {
|
|
|
840
918
|
return { ready: true, stagingUrl: `https://${url.hostname}/` };
|
|
841
919
|
}
|
|
842
920
|
|
|
921
|
+
/** Mint a site-bound handoff; its URL is emitted only in final operational JSON. */
|
|
922
|
+
export async function mintStagingPreviewHandoff(client, siteId, { now = Date.now } = {}) {
|
|
923
|
+
const code = "api.staging_handoff_contract";
|
|
924
|
+
const result = requireObject(
|
|
925
|
+
await client.request(sitePath(siteId, "staging-preview:mint-handoff"), {
|
|
926
|
+
method: "POST",
|
|
927
|
+
body: { siteId },
|
|
928
|
+
}),
|
|
929
|
+
code,
|
|
930
|
+
"staging handoff",
|
|
931
|
+
);
|
|
932
|
+
let origin;
|
|
933
|
+
let url;
|
|
934
|
+
try {
|
|
935
|
+
origin = new URL(result.stagingUrl);
|
|
936
|
+
url = new URL(result.url);
|
|
937
|
+
} catch {
|
|
938
|
+
throw new SiteAuthoringError(code, "Taproot returned an invalid staging handoff URL.");
|
|
939
|
+
}
|
|
940
|
+
const expires = requireCanonicalTimestamp(result.handoffExpiresAt, code, "handoffExpiresAt");
|
|
941
|
+
const current = now();
|
|
942
|
+
if (
|
|
943
|
+
result.siteId !== siteId || origin.protocol !== "https:" || !CANONICAL_HOST.test(origin.hostname)
|
|
944
|
+
|| origin.port || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash
|
|
945
|
+
|| url.origin !== origin.origin || url.username || url.password || url.pathname !== "/" || url.hash
|
|
946
|
+
|| [...url.searchParams].length !== 1
|
|
947
|
+
|| !/^[A-Za-z0-9_-]{43}$/u.test(url.searchParams.get("__taproot_preview_handoff") ?? "")
|
|
948
|
+
|| expires.milliseconds <= current - AUTHORING_PREVIEW_CLOCK_SKEW_MILLISECONDS
|
|
949
|
+
|| expires.milliseconds > current + AUTHORING_PREVIEW_HANDOFF_LIFETIME_MILLISECONDS
|
|
950
|
+
+ AUTHORING_PREVIEW_CLOCK_SKEW_MILLISECONDS
|
|
951
|
+
) {
|
|
952
|
+
throw new SiteAuthoringError(
|
|
953
|
+
code,
|
|
954
|
+
"Taproot returned a staging handoff outside its site, host or lifetime contract.",
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
return { stagingUrl: origin.href, url: url.href, handoffExpiresAt: expires.value };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function normalizePhaseTimings(history) {
|
|
961
|
+
if (history === undefined || history === null) return { known: false };
|
|
962
|
+
const code = "api.deployment_phase_contract";
|
|
963
|
+
requireObject(history, code, "deployment phase history");
|
|
964
|
+
if (
|
|
965
|
+
!Array.isArray(history.phases) || history.phases.length === 0 || history.phases.length > 32
|
|
966
|
+
|| (history.truncated !== undefined && typeof history.truncated !== "boolean")
|
|
967
|
+
) {
|
|
968
|
+
throw new SiteAuthoringError(code, "Taproot returned an invalid deployment phase history.");
|
|
969
|
+
}
|
|
970
|
+
const phases = history.phases.map((phase) => {
|
|
971
|
+
requireObject(phase, code, "deployment phase");
|
|
972
|
+
return {
|
|
973
|
+
status: enumValue(phase.status, DEPLOYMENT_STATUSES, DEPLOYMENT_STATUS_QUEUED, code, "phase status"),
|
|
974
|
+
at: requireCanonicalTimestamp(phase.enteredAt, code, "phase.enteredAt"),
|
|
975
|
+
};
|
|
976
|
+
});
|
|
977
|
+
return {
|
|
978
|
+
known: true,
|
|
979
|
+
truncated: history.truncated === true,
|
|
980
|
+
phases: phases.map((phase, index) => {
|
|
981
|
+
const next = phases[index + 1];
|
|
982
|
+
if (next && (next.at.milliseconds < phase.at.milliseconds || next.status === phase.status)) {
|
|
983
|
+
throw new SiteAuthoringError(code, "Taproot returned unordered or duplicate deployment phase evidence.");
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
status: phase.status,
|
|
987
|
+
enteredAt: phase.at.value,
|
|
988
|
+
...(next ? { endedAt: next.at.value, durationMilliseconds: next.at.milliseconds - phase.at.milliseconds } : {}),
|
|
989
|
+
};
|
|
990
|
+
}),
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
843
994
|
export function normalizeDeployment(value) {
|
|
844
995
|
const deployment = requireObject(value, "api.deployment_contract", "deployment");
|
|
845
996
|
return {
|
|
@@ -861,6 +1012,7 @@ export function normalizeDeployment(value) {
|
|
|
861
1012
|
completedAt: text(deployment.completedAt),
|
|
862
1013
|
errorMessage: text(deployment.errorMessage),
|
|
863
1014
|
pageCount: safeCount(deployment.pageCount),
|
|
1015
|
+
phaseTimings: normalizePhaseTimings(deployment.phaseHistory),
|
|
864
1016
|
};
|
|
865
1017
|
}
|
|
866
1018
|
|
|
@@ -878,11 +1030,16 @@ export async function listDeployments(
|
|
|
878
1030
|
) {
|
|
879
1031
|
const response = requireObject(
|
|
880
1032
|
await client.request(
|
|
881
|
-
sitePath(
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1033
|
+
sitePath(
|
|
1034
|
+
siteId,
|
|
1035
|
+
`deployments${
|
|
1036
|
+
query([
|
|
1037
|
+
["pageSize", pageSize],
|
|
1038
|
+
["includeStaging", true],
|
|
1039
|
+
["environment", environment],
|
|
1040
|
+
])
|
|
1041
|
+
}`,
|
|
1042
|
+
),
|
|
886
1043
|
requestOptions,
|
|
887
1044
|
),
|
|
888
1045
|
"api.deployment_contract",
|
|
@@ -895,11 +1052,24 @@ export async function listDeployments(
|
|
|
895
1052
|
}
|
|
896
1053
|
|
|
897
1054
|
export async function deploySite(client, siteId, body) {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1055
|
+
let response;
|
|
1056
|
+
try {
|
|
1057
|
+
response = requireObject(
|
|
1058
|
+
await client.request(sitePath(siteId, "deploy"), { method: "POST", body }),
|
|
1059
|
+
"api.deployment_contract",
|
|
1060
|
+
"deploy",
|
|
1061
|
+
);
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
if (error instanceof ApiError && error.hasField("AuthoringPreviewFailed")) {
|
|
1064
|
+
throw new SiteAuthoringError(
|
|
1065
|
+
"deploy.preview_failed",
|
|
1066
|
+
(error.descriptionFor("AuthoringPreviewFailed") ?? "A retained preview of a selected page failed.")
|
|
1067
|
+
+ " Use --allow-failed-preview only to explicitly override this refusal.",
|
|
1068
|
+
{ field: "AuthoringPreviewFailed", status: error.status },
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
throw error;
|
|
1072
|
+
}
|
|
903
1073
|
return normalizeDeployment(response.deployment);
|
|
904
1074
|
}
|
|
905
1075
|
|
|
@@ -962,6 +1132,7 @@ export async function poll({
|
|
|
962
1132
|
|
|
963
1133
|
export async function waitForDeployment(client, { siteId, deploymentId, environment, onProgress, now }) {
|
|
964
1134
|
let misses = 0;
|
|
1135
|
+
let lastEvidence;
|
|
965
1136
|
return await poll({
|
|
966
1137
|
client,
|
|
967
1138
|
now,
|
|
@@ -983,6 +1154,23 @@ export async function waitForDeployment(client, { siteId, deploymentId, environm
|
|
|
983
1154
|
return { done: false, progress: "Waiting for the deployment to appear in the deployment log." };
|
|
984
1155
|
}
|
|
985
1156
|
misses = 0;
|
|
1157
|
+
const evidence = JSON.stringify(deployment.phaseTimings);
|
|
1158
|
+
if (evidence !== lastEvidence) {
|
|
1159
|
+
lastEvidence = evidence;
|
|
1160
|
+
if (deployment.phaseTimings.known) {
|
|
1161
|
+
for (const phase of deployment.phaseTimings.phases) {
|
|
1162
|
+
onProgress(
|
|
1163
|
+
`Deployment phase ${phase.status} entered ${phase.enteredAt}`
|
|
1164
|
+
+ (phase.durationMilliseconds === undefined
|
|
1165
|
+
? "."
|
|
1166
|
+
: `; ${phase.durationMilliseconds}ms until ${phase.endedAt}.`),
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
if (deployment.phaseTimings.truncated) {
|
|
1170
|
+
onProgress("Earlier deployment phase evidence was truncated by the server.");
|
|
1171
|
+
}
|
|
1172
|
+
} else onProgress("Deployment phase timings are unknown for this legacy deployment.");
|
|
1173
|
+
}
|
|
986
1174
|
if (deployment.status === DEPLOYMENT_STATUS_COMPLETED) return { done: true, value: deployment };
|
|
987
1175
|
if (deployment.status === DEPLOYMENT_STATUS_FAILED) {
|
|
988
1176
|
throw new SiteAuthoringError(
|
|
@@ -1098,7 +1286,14 @@ export function normalizeAuthoringPreview(value, expected = {}) {
|
|
|
1098
1286
|
draftRevision: preview.draftRevision,
|
|
1099
1287
|
failureCode,
|
|
1100
1288
|
stagingHost: requireCanonicalStagingHost(preview.stagingHost),
|
|
1289
|
+
failureDiagnostic: normalizePreviewDiagnostic(preview.failureDiagnostic),
|
|
1101
1290
|
};
|
|
1291
|
+
if (
|
|
1292
|
+
preview.failureDiagnostic !== undefined && preview.failureDiagnostic !== null
|
|
1293
|
+
&& (!normalized.failureDiagnostic || normalized.failureDiagnostic.pageId !== normalized.pageId)
|
|
1294
|
+
) {
|
|
1295
|
+
throw new SiteAuthoringError("preview.status_contract", "Taproot returned an invalid preview failure diagnostic.");
|
|
1296
|
+
}
|
|
1102
1297
|
for (const field of ["siteId", "pageId", "snapshotId"]) {
|
|
1103
1298
|
if (expected[field] !== undefined && normalized[field] !== expected[field]) {
|
|
1104
1299
|
throw new SiteAuthoringError(
|
|
@@ -1191,20 +1386,26 @@ export async function createAuthoringPreview(client, siteId, pageId) {
|
|
|
1191
1386
|
|
|
1192
1387
|
export async function getAuthoringPreview(client, expected, requestOptions = {}) {
|
|
1193
1388
|
return normalizeAuthoringPreview(
|
|
1194
|
-
await client.request(
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1389
|
+
await client.request(
|
|
1390
|
+
sitePath(
|
|
1391
|
+
expected.siteId,
|
|
1392
|
+
`authoring-previews/pages/${encodeURIComponent(expected.pageId)}/${encodeURIComponent(expected.snapshotId)}`,
|
|
1393
|
+
),
|
|
1394
|
+
requestOptions,
|
|
1395
|
+
),
|
|
1198
1396
|
expected,
|
|
1199
1397
|
);
|
|
1200
1398
|
}
|
|
1201
1399
|
|
|
1202
1400
|
export async function revokeAuthoringPreview(client, expected) {
|
|
1203
1401
|
return normalizeAuthoringPreview(
|
|
1204
|
-
await client.request(
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1402
|
+
await client.request(
|
|
1403
|
+
sitePath(
|
|
1404
|
+
expected.siteId,
|
|
1405
|
+
`authoring-previews/pages/${encodeURIComponent(expected.pageId)}/${encodeURIComponent(expected.snapshotId)}`,
|
|
1406
|
+
),
|
|
1407
|
+
{ method: "DELETE" },
|
|
1408
|
+
),
|
|
1208
1409
|
expected,
|
|
1209
1410
|
);
|
|
1210
1411
|
}
|
|
@@ -1228,9 +1429,19 @@ function assertAuthoringPreviewState(preview, now) {
|
|
|
1228
1429
|
{ status: preview.status },
|
|
1229
1430
|
);
|
|
1230
1431
|
}
|
|
1231
|
-
throw new SiteAuthoringError(
|
|
1232
|
-
|
|
1233
|
-
|
|
1432
|
+
throw new SiteAuthoringError(
|
|
1433
|
+
"preview.render_failed",
|
|
1434
|
+
"Taproot could not render the authoring preview."
|
|
1435
|
+
+ (preview.failureDiagnostic
|
|
1436
|
+
? ` Class ${preview.failureDiagnostic.errorClass}; page ${preview.pageId}`
|
|
1437
|
+
+ (preview.failureDiagnostic.nodeType ? `; node ${preview.failureDiagnostic.nodeType}` : "")
|
|
1438
|
+
+ (preview.failureDiagnostic.attribute ? `; attribute ${preview.failureDiagnostic.attribute}` : "") + "."
|
|
1439
|
+
: ""),
|
|
1440
|
+
{
|
|
1441
|
+
status: preview.status,
|
|
1442
|
+
previewDiagnostic: preview.failureDiagnostic,
|
|
1443
|
+
},
|
|
1444
|
+
);
|
|
1234
1445
|
}
|
|
1235
1446
|
}
|
|
1236
1447
|
|
|
@@ -1257,17 +1468,18 @@ export async function waitForAuthoringPreview(client, { created, onProgress, now
|
|
|
1257
1468
|
return { done: false, progress: `Waiting for the authoring preview (${preview.status}).` };
|
|
1258
1469
|
},
|
|
1259
1470
|
timeoutCode: "preview.timeout",
|
|
1260
|
-
timeoutError: () =>
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1471
|
+
timeoutError: () =>
|
|
1472
|
+
lastStatus === AUTHORING_PREVIEW_STATUS_QUEUED
|
|
1473
|
+
? new SiteAuthoringError(
|
|
1474
|
+
"preview.timeout",
|
|
1475
|
+
`The render service has not claimed this job before the bounded deadline. Revoke it with: taproot-site preview revoke ${created.pageId} ${created.snapshotId}`,
|
|
1476
|
+
{ status: lastStatus },
|
|
1477
|
+
)
|
|
1478
|
+
: new SiteAuthoringError(
|
|
1479
|
+
"preview.timeout",
|
|
1480
|
+
`The render service claimed this job but did not finish before the bounded deadline. Revoke it with: taproot-site preview revoke ${created.pageId} ${created.snapshotId}`,
|
|
1481
|
+
{ status: lastStatus },
|
|
1482
|
+
),
|
|
1271
1483
|
});
|
|
1272
1484
|
}
|
|
1273
1485
|
|
|
@@ -1397,14 +1609,19 @@ export function normalizeAuthoringPreviewHandoff(value, ready, now) {
|
|
|
1397
1609
|
export async function mintAuthoringPreviewHandoff(client, ready, { now }) {
|
|
1398
1610
|
let response;
|
|
1399
1611
|
try {
|
|
1400
|
-
response = await client.replaceablePost(
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1612
|
+
response = await client.replaceablePost(
|
|
1613
|
+
sitePath(
|
|
1614
|
+
ready.siteId,
|
|
1615
|
+
`authoring-previews/pages/${encodeURIComponent(ready.pageId)}/${
|
|
1616
|
+
encodeURIComponent(ready.snapshotId)
|
|
1617
|
+
}:mint-handoff`,
|
|
1618
|
+
),
|
|
1619
|
+
{
|
|
1620
|
+
body: { siteId: ready.siteId, pageId: ready.pageId, snapshotId: ready.snapshotId },
|
|
1621
|
+
deadline: ready.expiresAtMilliseconds,
|
|
1622
|
+
now,
|
|
1623
|
+
},
|
|
1624
|
+
);
|
|
1408
1625
|
} catch (error) {
|
|
1409
1626
|
if (error instanceof SiteAuthoringError && error.code === "transport.deadline") {
|
|
1410
1627
|
throw new SiteAuthoringError("preview.expired", "The authoring preview expired before a handoff was minted.");
|
|
@@ -1531,7 +1748,7 @@ export async function startCliAuthorization(client, { keyName }, requestOptions
|
|
|
1531
1748
|
*/
|
|
1532
1749
|
export async function listAuthorableSites(client, requestOptions = {}) {
|
|
1533
1750
|
const response = requireObject(
|
|
1534
|
-
await client.request(AUTHORABLE_SITES_PATH, requestOptions),
|
|
1751
|
+
await client.request(`${AUTHORABLE_SITES_PATH}${query([["cliVersion", CLI_VERSION]])}`, requestOptions),
|
|
1535
1752
|
"sites.contract",
|
|
1536
1753
|
"authorable site list",
|
|
1537
1754
|
);
|
|
@@ -1584,7 +1801,10 @@ export async function exchangeSiteAuthoringToken(client, { siteId, capabilities
|
|
|
1584
1801
|
// The sign-in's refreshed deadline. Not a secret, and absent on a server
|
|
1585
1802
|
// that predates the sliding window.
|
|
1586
1803
|
...(typeof response.signInExpiresAt === "string" && response.signInExpiresAt.length > 0
|
|
1587
|
-
? {
|
|
1804
|
+
? {
|
|
1805
|
+
signInExpiresAt:
|
|
1806
|
+
requireCanonicalTimestamp(response.signInExpiresAt, "exchange.contract", "signInExpiresAt").value,
|
|
1807
|
+
}
|
|
1588
1808
|
: {}),
|
|
1589
1809
|
// Whether the platform is accepting external authoring writes at all
|
|
1590
1810
|
// (TR00692). Read strictly: only a real boolean is a state, so a server
|