@taprootio/site-authoring 0.6.2 → 0.7.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/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]: ["preview.no_draft", "The requested page does not have a persisted draft to preview."],
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
- + "Wait for one to finish rendering, or revoke a preview that is still queued or rendering.",
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 = /^(?=.{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;
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(` Taproot's own detail: ${
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(` Taproot's own detail: ${
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
- const entries = Array.isArray(response.entries) ? response.entries : [];
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(siteId, `publishing/readiness${query([
776
- ["stagedPageIds", selection.stagedPageIds],
777
- ["selectedSettingsTypes", selection.selectedSettingsTypes],
778
- ["includeNavigation", selection.includeNavigation],
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(siteId, `deployments${query([
882
- ["pageSize", pageSize],
883
- ["includeStaging", true],
884
- ["environment", environment],
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
- const response = requireObject(
899
- await client.request(sitePath(siteId, "deploy"), { method: "POST", body }),
900
- "api.deployment_contract",
901
- "deploy",
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(sitePath(
1195
- expected.siteId,
1196
- `authoring-previews/pages/${encodeURIComponent(expected.pageId)}/${encodeURIComponent(expected.snapshotId)}`,
1197
- ), requestOptions),
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(sitePath(
1205
- expected.siteId,
1206
- `authoring-previews/pages/${encodeURIComponent(expected.pageId)}/${encodeURIComponent(expected.snapshotId)}`,
1207
- ), { method: "DELETE" }),
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("preview.render_failed", "Taproot could not render the authoring preview.", {
1232
- status: preview.status,
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: () => lastStatus === AUTHORING_PREVIEW_STATUS_QUEUED
1261
- ? new SiteAuthoringError(
1262
- "preview.timeout",
1263
- `The render service has not claimed this job before the bounded deadline. Revoke it with: taproot-site preview revoke ${created.pageId} ${created.snapshotId}`,
1264
- { status: lastStatus },
1265
- )
1266
- : new SiteAuthoringError(
1267
- "preview.timeout",
1268
- `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}`,
1269
- { status: lastStatus },
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(sitePath(
1401
- ready.siteId,
1402
- `authoring-previews/pages/${encodeURIComponent(ready.pageId)}/${encodeURIComponent(ready.snapshotId)}:mint-handoff`,
1403
- ), {
1404
- body: { siteId: ready.siteId, pageId: ready.pageId, snapshotId: ready.snapshotId },
1405
- deadline: ready.expiresAtMilliseconds,
1406
- now,
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
- ? { signInExpiresAt: requireCanonicalTimestamp(response.signInExpiresAt, "exchange.contract", "signInExpiresAt").value }
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