@taprootio/site-authoring 0.8.4 → 0.8.6

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 CHANGED
@@ -29,6 +29,17 @@ npm install --global @taprootio/site-authoring
29
29
  taproot-site --help
30
30
  ```
31
31
 
32
+ Or run the current release without installing it:
33
+
34
+ ```bash
35
+ npx --yes @taprootio/site-authoring@latest --version
36
+ ```
37
+
38
+ Always name `@latest` with `npx`: an unversioned `npx @taprootio/site-authoring`
39
+ reuses whatever copy npx cached, which is how a design pass ran an old release.
40
+ `--version` prints what is running; `status` reports the latest known release
41
+ as `cliRelease`, and every online verb refuses an outdated CLI (below).
42
+
32
43
  Node 22 or later. The package has one runtime dependency,
33
44
  `@taprootio/espalier`, pinned to an exact version: the CLI validates themes
34
45
  against one Espalier theme contract, and a floating range would let an install
@@ -257,6 +268,39 @@ distribution, and every other host is refused before a bearer can be built.
257
268
  Sign-ins are stored per origin, so switching away and back finds the one that
258
269
  was already there.
259
270
 
271
+ ## Design walkthrough
272
+
273
+ `taproot-site help walkthrough` is the self-contained version of this pass;
274
+ each step names the reference it relies on.
275
+
276
+ 1. **Select a supported site.** `login`, then `sites` and
277
+ `use <site-name-or-id>`. A standard site takes every verb; a managed Docs
278
+ site takes presentation only (`pull`, `theme push`, `footer push`,
279
+ `media upload`, `deploy`, `status`) because its pages, navigation and
280
+ redirects come from the Docs artifact; a prebuilt Docs site takes no verb.
281
+ 2. **Pull and keep the baseline.** `pull` writes both schemes' complete
282
+ effective themes, the appearance files and the footer. Edit that pull;
283
+ never build a theme from memory or an example.
284
+ 3. **Edit both schemes and the appearance.** `help theme` lists every field,
285
+ the role slots, how anchor references resolve and what wins when a pin and
286
+ a role disagree; `help appearance` covers the default scheme, header, logos
287
+ and favicon.
288
+ 4. **Prepare media locally.** Raster files only (PNG, JPEG, GIF, WebP),
289
+ relative to the workspace root; make the transparent logo, its `@2x` copy,
290
+ a dark-scheme logo when needed and a square favicon with your own tools,
291
+ upload them with `media upload`, then assign `lightLogoId`, `darkLogoId`
292
+ and `brand.faviconId` from the returned ids. There is no recolor, crop or
293
+ SVG upload command.
294
+ 5. **Validate, then push.** `validate` runs offline; `theme push` writes the
295
+ footer colours, the appearance scalars and then both themes in that order,
296
+ not atomically, and a failure names `completedWrites` — pull, reconcile,
297
+ push again.
298
+ 6. **Review on staging in both schemes.** `deploy --staging` returns a
299
+ single-use handoff in `stagingPreview.url`, on a managed Docs site too;
300
+ open it once and use the site's theme toggle. No page draft is needed;
301
+ `preview page` renders one draft and is a different tool.
302
+ 7. **Promote only after review.** `deploy --production`.
303
+
260
304
  ## The authoring loop
261
305
 
262
306
  ```bash
@@ -292,7 +336,12 @@ moved since this workspace last reconciled with it, naming both revisions;
292
336
  `media upload [path...]` uploads raster files and records component-ready
293
337
  delivery fields. `nav push`, `theme push`, and `footer push` replace the whole
294
338
  navigation tree, the complete light/dark theme pair with its appearance
295
- settings, and the closed footer document. `redirects pull` writes the site's
339
+ settings, and the closed footer document. `pull` writes each scheme's
340
+ complete effective theme — the stored theme resolved over the same defaults
341
+ every consumer renders — so a fresh workspace validates as pulled, and it
342
+ keeps only authored `semanticMappings` pins (listed in
343
+ `explicitMappingTokens`) because a pinned token shadows its role.
344
+ `redirects pull` writes the site's
296
345
  redirect map to `redirects.json` with a baseline, and `redirects push`
297
346
  validates the file offline and replaces the whole map; `help redirects`
298
347
  states the entry shape (`path`, `kind` redirect or gone, `target`, `status`
@@ -304,7 +353,10 @@ result. **Each handoff is single-use.** Opening the URL consumes it; a reused,
304
353
  shared, or bookmarked preview URL answers `Not found`, which is the handoff
305
354
  being spent rather than the preview being broken. Run `preview page` again
306
355
  for another. The handoff also expires two minutes after it is minted;
307
- `preview revoke <page-id> <snapshot-id>` releases a preview early.
356
+ `preview revoke <page-id> <snapshot-id>` releases a preview early. It
357
+ addresses the snapshot, so it still works after `approve` has consumed the
358
+ draft; revoking an already revoked snapshot succeeds again, and an unknown or
359
+ cleaned-up snapshot answers `preview.not_found`.
308
360
 
309
361
  `approve [page-path...]` publishes drafts into the approved candidate pool.
310
362
  It stages; nothing reaches an audience until `deploy --staging` publishes the
@@ -406,7 +458,12 @@ with the exact fact field path. A label is optional and stays outside the value'
406
458
  ### Staging verification and deployment evidence
407
459
 
408
460
  `deploy --staging` selects the same changed pages, settings groups and navigation
409
- as the Deployments page. `status` checks that exact current candidate, so a
461
+ as the Deployments page, and returns a single-use staging review handoff in
462
+ `stagingPreview.url` — on a managed Docs site too, where it stages settings
463
+ only: open the URL once, then use the site's theme toggle to review both
464
+ schemes before promoting. When no handoff can be minted, `stagingPreview.reason`
465
+ and `stagingPreview.recovery` say why and what to do next; `staging review`
466
+ mints another on any surface. `status` checks that exact current candidate, so a
410
467
  completed CLI stage counts as successful until that candidate changes.
411
468
 
412
469
  A retained failed preview of selected page content refuses deployment as
@@ -441,6 +498,31 @@ are calculated only between recorded transitions. `truncated` marks missing
441
498
  older history; `known: false` marks legacy rows. CLI polling never invents phase
442
499
  timestamps. Progress reports the same recorded timestamps and durations.
443
500
 
501
+ ### Verify what visitors receive
502
+
503
+ A completed deployment job is not a verified delivery. `delivery check
504
+ (--staging | --production --url https://<published-domain>/)` reads the target
505
+ the way a visitor does and reports each dimension separately: the latest
506
+ completed deployment for the target, HTTP delivery of the authored routes and
507
+ the favicon, images, module preloads and site bundle they declare (status and
508
+ content type, no redirects followed), internal link targets, accidental
509
+ local-only references, and the declared runtime — the major pointer's stream,
510
+ entry and capability modules, and whether the pointer lags the fallback copy
511
+ the site shipped with. The browser dimension (a fresh and a returning load
512
+ compared against the pointer's entry, every document request in the browser,
513
+ popups included, held to the site origin before it is sent, and cache reuse
514
+ on the returning load observed rather than assumed)
515
+ runs only when Playwright resolves from the CLI's own install (install
516
+ `@taprootio/site-authoring` and `playwright` together globally and run the
517
+ installed `taproot-site` command; a copy run through `npx` cannot see a
518
+ global Playwright) and is reported as `unchecked` with that recovery
519
+ otherwise. `--wait <seconds>` re-checks
520
+ failures once for edge propagation; `--no-browser` skips the browser; `--url`
521
+ is a production option, since staging always checks the acknowledged host
522
+ with an authorized preview session. Only same-origin assets and the declared
523
+ runtime are fetched, and the check is read-only and bounded; `help delivery`
524
+ states the limits and verdicts.
525
+
444
526
  ### Export a pulled workspace as an offline fixture
445
527
 
446
528
  From a pulled workspace (or its configured project), run:
@@ -256,100 +256,7 @@
256
256
  "success": 150,
257
257
  "warning": 90
258
258
  },
259
- "semanticMappings": {
260
- "actionBackground": {
261
- "lightness": "raised3",
262
- "source": "primary"
263
- },
264
- "actionText": {
265
- "lightness": "ink",
266
- "source": "primary"
267
- },
268
- "background": {
269
- "lightness": "surface",
270
- "source": "primary"
271
- },
272
- "border": {
273
- "lightness": "border",
274
- "source": "primary"
275
- },
276
- "dangerText": {
277
- "lightness": "text",
278
- "source": "danger"
279
- },
280
- "headings": {
281
- "lightness": "muted",
282
- "source": "primary"
283
- },
284
- "headingsHover": {
285
- "lightness": "text",
286
- "source": "primary"
287
- },
288
- "inputCaret": {
289
- "lightness": "ink",
290
- "source": "triadic-right"
291
- },
292
- "inputSelection": {
293
- "lightness": "text",
294
- "source": "complementary"
295
- },
296
- "inputSelectionBg": {
297
- "lightness": "raised2",
298
- "source": "triadic-left"
299
- },
300
- "layer1": {
301
- "lightness": "raised1",
302
- "source": "primary"
303
- },
304
- "layer2": {
305
- "lightness": "raised2",
306
- "source": "primary"
307
- },
308
- "layer3": {
309
- "lightness": "raised3",
310
- "source": "primary"
311
- },
312
- "layer4": {
313
- "lightness": "raised4",
314
- "source": "primary"
315
- },
316
- "link": {
317
- "lightness": "accent",
318
- "source": "complementary"
319
- },
320
- "linkHover": {
321
- "lightness": "text",
322
- "source": "complementary"
323
- },
324
- "linkHoverBg": {
325
- "lightness": "raised2",
326
- "source": "triadic-left"
327
- },
328
- "shadow": {
329
- "lightness": "shadow",
330
- "source": "primary"
331
- },
332
- "text": {
333
- "lightness": "text",
334
- "source": "primary"
335
- },
336
- "typeCaption": {
337
- "lightness": "text",
338
- "source": "primary"
339
- },
340
- "typeLabel": {
341
- "lightness": "text",
342
- "source": "primary"
343
- },
344
- "typeLead": {
345
- "lightness": "text",
346
- "source": "primary"
347
- },
348
- "typeOverline": {
349
- "lightness": "text",
350
- "source": "primary"
351
- }
352
- },
259
+ "semanticMappings": {},
353
260
  "spaceRatio": 1.618,
354
261
  "stylesheets": [],
355
262
  "tones": {},
@@ -613,100 +520,7 @@
613
520
  "success": 150,
614
521
  "warning": 90
615
522
  },
616
- "semanticMappings": {
617
- "actionBackground": {
618
- "lightness": "raised3",
619
- "source": "primary"
620
- },
621
- "actionText": {
622
- "lightness": "ink",
623
- "source": "primary"
624
- },
625
- "background": {
626
- "lightness": "surface",
627
- "source": "primary"
628
- },
629
- "border": {
630
- "lightness": "border",
631
- "source": "primary"
632
- },
633
- "dangerText": {
634
- "lightness": "text",
635
- "source": "danger"
636
- },
637
- "headings": {
638
- "lightness": "muted",
639
- "source": "primary"
640
- },
641
- "headingsHover": {
642
- "lightness": "text",
643
- "source": "primary"
644
- },
645
- "inputCaret": {
646
- "lightness": "ink",
647
- "source": "triadic-right"
648
- },
649
- "inputSelection": {
650
- "lightness": "text",
651
- "source": "complementary"
652
- },
653
- "inputSelectionBg": {
654
- "lightness": "raised2",
655
- "source": "triadic-left"
656
- },
657
- "layer1": {
658
- "lightness": "raised1",
659
- "source": "primary"
660
- },
661
- "layer2": {
662
- "lightness": "raised2",
663
- "source": "primary"
664
- },
665
- "layer3": {
666
- "lightness": "raised3",
667
- "source": "primary"
668
- },
669
- "layer4": {
670
- "lightness": "raised4",
671
- "source": "primary"
672
- },
673
- "link": {
674
- "lightness": "accent",
675
- "source": "complementary"
676
- },
677
- "linkHover": {
678
- "lightness": "text",
679
- "source": "complementary"
680
- },
681
- "linkHoverBg": {
682
- "lightness": "raised2",
683
- "source": "triadic-left"
684
- },
685
- "shadow": {
686
- "lightness": "shadow",
687
- "source": "primary"
688
- },
689
- "text": {
690
- "lightness": "text",
691
- "source": "primary"
692
- },
693
- "typeCaption": {
694
- "lightness": "text",
695
- "source": "primary"
696
- },
697
- "typeLabel": {
698
- "lightness": "text",
699
- "source": "primary"
700
- },
701
- "typeLead": {
702
- "lightness": "text",
703
- "source": "primary"
704
- },
705
- "typeOverline": {
706
- "lightness": "text",
707
- "source": "primary"
708
- }
709
- },
523
+ "semanticMappings": {},
710
524
  "spaceRatio": 1.618,
711
525
  "stylesheets": [],
712
526
  "tones": {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taprootio/site-authoring",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "Taproot site authoring CLI: sign in once in the browser, then pull, validate, push, preview, approve, and deploy a site from the terminal",
5
5
  "type": "module",
6
6
  "license": "ISC",
package/src/api.js CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  CLI_VERSION,
8
8
  EXTERNAL_WRITES_SETTING_KEY,
9
9
  EXTERNAL_WRITES_SETTING_LOCATION,
10
+ GRPC_NOT_FOUND,
11
+ HTTP_NOT_FOUND,
10
12
  LIMITS,
11
13
  REFUSAL_CAPABILITY_MISSING,
12
14
  REFUSAL_CLI_OUTDATED,
@@ -101,6 +103,15 @@ export const DEPLOYMENT_STATUSES = Object.freeze([
101
103
  DEPLOYMENT_STATUS_FAILED,
102
104
  ]);
103
105
 
106
+ // DeploySite's admission outcome (TR00839): whether this request created the
107
+ // deployment or coalesced into an already-anchored one.
108
+ export const DEPLOYMENT_REQUEST_OUTCOME_ACCEPTED = "DEPLOYMENT_REQUEST_OUTCOME_ACCEPTED";
109
+ export const DEPLOYMENT_REQUEST_OUTCOME_COALESCED = "DEPLOYMENT_REQUEST_OUTCOME_COALESCED";
110
+ export const DEPLOYMENT_REQUEST_OUTCOMES = Object.freeze([
111
+ DEPLOYMENT_REQUEST_OUTCOME_ACCEPTED,
112
+ DEPLOYMENT_REQUEST_OUTCOME_COALESCED,
113
+ ]);
114
+
104
115
  export const IMAGE_PROCESSING_STATE_UNKNOWN = "IMAGE_PROCESSING_STATE_UNKNOWN";
105
116
  export const IMAGE_PROCESSING_STATE_COMPLETE = "IMAGE_PROCESSING_STATE_COMPLETE";
106
117
  export const IMAGE_PROCESSING_STATE_FAILED = "IMAGE_PROCESSING_STATE_FAILED";
@@ -158,6 +169,7 @@ const AUTHORING_PREVIEW_FIELDS = Object.freeze({
158
169
  siteCapacity: "AuthoringPreviewCapacity",
159
170
  authorityCapacity: "AuthoringPreviewAuthorityCapacity",
160
171
  manifest: "AuthoringPreviewManifest",
172
+ snapshot: "AuthoringPreview",
161
173
  });
162
174
  const AUTHORING_PREVIEW_FIELD_ERRORS = Object.freeze({
163
175
  [AUTHORING_PREVIEW_FIELDS.draft]: [
@@ -348,7 +360,11 @@ const REFUSAL_GUIDANCE = Object.freeze({
348
360
  [REFUSAL_CREDENTIAL_REJECTED]:
349
361
  "The site authoring credential was rejected: it is invalid, revoked, or bound to a different site. "
350
362
  + "Stop and re-issue it; retrying cannot succeed.",
351
- [REFUSAL_THROTTLED]: "This credential exceeded its request budget. Back off and retry with delay.",
363
+ // Covers both the per-key request-budget throttle and the durable
364
+ // per-site/per-account deployment request limit (TR00839): both surface as
365
+ // gRPC ResourceExhausted / HTTP 429, and the remedy is identical either way.
366
+ [REFUSAL_THROTTLED]:
367
+ "Too many deployment requests for this site or account. Back off and retry after the delay the server reported.",
352
368
  });
353
369
 
354
370
  /**
@@ -433,6 +449,12 @@ export function announceRefusal(error, onProgress, action = "request") {
433
449
  }
434
450
  const guidance = REFUSAL_GUIDANCE[kind];
435
451
  if (guidance) onProgress(guidance);
452
+ // The retry-after trailer (TR00839) is the exact delay the server measured;
453
+ // surface it when the transport captured it rather than leaving the agent
454
+ // to guess a backoff.
455
+ if (kind === REFUSAL_THROTTLED && typeof error.retryAfterSeconds === "number") {
456
+ onProgress(` Retry after: ${error.retryAfterSeconds}s.`);
457
+ }
436
458
  }
437
459
 
438
460
  /**
@@ -1014,6 +1036,9 @@ export function normalizeDeployment(value) {
1014
1036
  errorMessage: text(deployment.errorMessage),
1015
1037
  pageCount: safeCount(deployment.pageCount),
1016
1038
  phaseTimings: normalizePhaseTimings(deployment.phaseHistory),
1039
+ // Identical requests coalesced into this deployment inside its settling
1040
+ // window (TR00839). Zero for a deployment that never anchored one.
1041
+ coalescedRequestCount: safeCount(deployment.coalescedRequestCount),
1017
1042
  };
1018
1043
  }
1019
1044
 
@@ -1071,7 +1096,19 @@ export async function deploySite(client, siteId, body) {
1071
1096
  }
1072
1097
  throw error;
1073
1098
  }
1074
- return normalizeDeployment(response.deployment);
1099
+ const deployment = normalizeDeployment(response.deployment);
1100
+ // `DEPLOYMENT_REQUEST_OUTCOME_ACCEPTED` is the zero-adjacent default: an
1101
+ // older server that omits the field, or the immutable Standard/Docs
1102
+ // branches that never run through the admission seam, both read as this
1103
+ // request creating its own deployment (TR00839).
1104
+ const outcome = enumValue(
1105
+ response.outcome,
1106
+ DEPLOYMENT_REQUEST_OUTCOMES,
1107
+ DEPLOYMENT_REQUEST_OUTCOME_ACCEPTED,
1108
+ "api.deployment_contract",
1109
+ "deployment request outcome",
1110
+ );
1111
+ return { ...deployment, outcome };
1075
1112
  }
1076
1113
 
1077
1114
  /**
@@ -1924,6 +1961,38 @@ export async function claimCliAuthorization(client, deviceCode, requestOptions =
1924
1961
  }
1925
1962
 
1926
1963
  /** Maps only preview-domain validation fields; authentication/refusal identity stays an ApiError. */
1964
+ /**
1965
+ * Revocation addresses one snapshot, so its refusals are snapshot outcomes.
1966
+ * A NotFound (the page or the snapshot is not available to this caller on
1967
+ * this site) and the server's in-transaction `AuthoringPreview` unavailability
1968
+ * both become `preview.not_found`; the wording never distinguishes another
1969
+ * caller's snapshot from a missing one, mirroring the server. A snapshot that
1970
+ * is already revoked is not an error at all: the server answers REVOKED and
1971
+ * the verb reports that idempotent success. Every other refusal keeps the
1972
+ * shared preview translation (TR00773).
1973
+ */
1974
+ export function translateAuthoringPreviewRevokeApiError(error) {
1975
+ if (!(error instanceof ApiError) || error.refusalKind() !== REFUSAL_UNCLASSIFIED) return error;
1976
+ if (
1977
+ error.httpStatus === HTTP_NOT_FOUND || error.grpcCode === GRPC_NOT_FOUND
1978
+ || error.hasField(AUTHORING_PREVIEW_FIELDS.snapshot)
1979
+ ) {
1980
+ return new SiteAuthoringError(
1981
+ "preview.not_found",
1982
+ "No authoring preview with that snapshot ID is available for that page on this site. It may have expired "
1983
+ + "and been cleaned up, or the IDs may belong to another page or site; a snapshot that was already revoked "
1984
+ + "reports a successful revocation instead.",
1985
+ // A NotFound can name the page as readily as the snapshot, so only the
1986
+ // in-transaction snapshot refusal pins the field a caller should fix.
1987
+ {
1988
+ ...(error.hasField(AUTHORING_PREVIEW_FIELDS.snapshot) ? { field: "snapshotId" } : {}),
1989
+ status: error.status,
1990
+ },
1991
+ );
1992
+ }
1993
+ return translateAuthoringPreviewApiError(error);
1994
+ }
1995
+
1927
1996
  export function translateAuthoringPreviewApiError(error) {
1928
1997
  if (!(error instanceof ApiError) || error.refusalKind() !== REFUSAL_UNCLASSIFIED) return error;
1929
1998
  for (const [field, [code, message]] of Object.entries(AUTHORING_PREVIEW_FIELD_ERRORS)) {