@taprootio/site-authoring 0.20.0 → 0.21.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 +114 -28
- package/examples/riverbend-wellness/nav.json +3 -6
- package/examples/riverbend-wellness/pages/visit.md +1 -1
- package/package.json +2 -2
- package/src/api.js +118 -31
- package/src/changelog.js +48 -0
- package/src/changelog.json +71 -0
- package/src/cli-release.js +9 -1
- package/src/cli.js +122 -13
- package/src/constants.js +19 -0
- package/src/errors.js +3 -0
- package/src/footer-contract.js +5 -3
- package/src/index.js +1 -0
- package/src/output.js +9 -0
- package/src/presentation-reference.js +41 -6
- package/src/problems.js +6 -1
- package/src/redirects-contract.js +50 -140
- package/src/reference-help.js +77 -33
- package/src/reserved-paths.js +4 -10
- package/src/setting-reference.js +438 -0
- package/src/site-check-findings.js +99 -0
- package/src/site-check-rules.json +22 -0
- package/src/site-checks.js +337 -0
- package/src/staging-check.js +48 -9
- package/src/staging-served.js +320 -0
- package/src/transport.js +13 -0
- package/src/typed-page-reference.js +128 -4
- package/src/verbs/changelog.js +62 -0
- package/src/verbs/deploy.js +38 -10
- package/src/verbs/index.js +3 -0
- package/src/verbs/nav-push.js +49 -1
- package/src/verbs/pull.js +5 -1
- package/src/verbs/redirects-check.js +4 -1
- package/src/verbs/staging-review.js +1 -1
- package/src/verbs/status.js +35 -28
- package/src/verbs/validate.js +21 -0
package/README.md
CHANGED
|
@@ -119,6 +119,22 @@ dark logo on the rendered dark header at desktop and compact/mobile widths;
|
|
|
119
119
|
transparent canvas pixels do not count as contrast. Supply separate
|
|
120
120
|
scheme-specific assets when one visible mark cannot satisfy both surfaces.
|
|
121
121
|
|
|
122
|
+
`taproot-site help settings` maps every pulled setting to the app control that
|
|
123
|
+
shows it, says what it does and where it renders, and names the verb that
|
|
124
|
+
writes it (`help appearance` prints the same text beside each writable field,
|
|
125
|
+
and the shipped help is the authority if this table drifts). The ones that are
|
|
126
|
+
easy to confuse:
|
|
127
|
+
|
|
128
|
+
| App control | Workspace field | Where it renders |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| Brand & style > Page style > Background image | `lightTheme` / `darkTheme` `pageBackgroundImage` (with `pageBackgroundImageOpacity`) | One layer across the page box behind the content; the opaque header and footer cover it, so it shows behind the main content. |
|
|
131
|
+
| Brand & style > Page canvas > Canvas image | `lightCanvasImageId` / `darkCanvasImageId` in `taproot-styles.json` (with the two `...CanvasImageOpacity` fields) | Only the empty side margins of a page. Not on a standard site today, whose margins are 0 px wide. |
|
|
132
|
+
| Brand & style > Brand > Logo | `lightLogoId` / `darkLogoId` | The header brand, and the footer's brand once the footer is enabled. |
|
|
133
|
+
| Brand & style > Brand > Favicon | `brand.json` `faviconId` | The icon set in every page's head and `/favicon.ico`. |
|
|
134
|
+
|
|
135
|
+
Both background layers tile at the image's natural pixel size from the top-left;
|
|
136
|
+
neither sets a size, repeat or position.
|
|
137
|
+
|
|
122
138
|
`taproot-site validate` checks the pulled workspace `taproot-site.json` names,
|
|
123
139
|
with no credential, no network, and no write: every page source (metadata,
|
|
124
140
|
Markdown conversion, media references, content vocabulary, section contexts),
|
|
@@ -131,6 +147,15 @@ Warnings never refuse: `linkWarnings` names a link whose address is only
|
|
|
131
147
|
`http(s)://` plus its own text (`content.link_autolinked`, left by older editor
|
|
132
148
|
autolinking), and `pages push` reports the same for the pages it sends.
|
|
133
149
|
|
|
150
|
+
`validate` also runs the site checks that need nothing but the workspace, from
|
|
151
|
+
the same rule catalogue the editor and the site-health page read. `findings`
|
|
152
|
+
lists each one as a rule id (`link.page_missing`, a link to a page the workspace
|
|
153
|
+
does not have), a severity, the file, and the value found; a warning never
|
|
154
|
+
refuses. `siteChecks.checkedBySite` names the rules that need the site's own
|
|
155
|
+
records, such as placed images and forms: `status` reports those under
|
|
156
|
+
`siteHealth`; `siteChecks.checkedAtDeploy` names the ones only a deploy applies. A redirect source the site would refuse also names its rule in
|
|
157
|
+
`error.problems[].rule`.
|
|
158
|
+
|
|
134
159
|
`taproot-site validate <fixture-directory>` checks a complete offline fixture
|
|
135
160
|
the same way: a directory laid out like a
|
|
136
161
|
pulled workspace — `pages/`, `nav.json`, `redirects.json`, and the four
|
|
@@ -242,7 +267,7 @@ upward from the current directory through a bounded number of parents.
|
|
|
242
267
|
Exactly one must be found: none is `config.not_found`, more than one is
|
|
243
268
|
`config.ambiguous`, and `--config <path>` placed before the verb selects one
|
|
244
269
|
explicitly and bypasses discovery. `login`, `logout`, `sites`, `use`,
|
|
245
|
-
`whoami`, and `
|
|
270
|
+
`whoami`, `env`, and `changelog` need no configuration and no site.
|
|
246
271
|
|
|
247
272
|
```json
|
|
248
273
|
{
|
|
@@ -301,11 +326,12 @@ each step names the reference it relies on.
|
|
|
301
326
|
and favicon; `help fonts` lists the font families, by category, that the
|
|
302
327
|
font fields can name.
|
|
303
328
|
4. **Prepare media locally.** Raster files only (PNG, JPEG, GIF, WebP),
|
|
304
|
-
relative to the workspace root; make the transparent logo
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
329
|
+
relative to the workspace root; make the transparent logo at the highest
|
|
330
|
+
resolution you have (one file, not a 1x and `@2x` pair: the platform
|
|
331
|
+
generates the sizes), a dark-scheme logo when needed and a square favicon
|
|
332
|
+
with your own tools, upload them with `media upload`, then assign
|
|
333
|
+
`lightLogoId`, `darkLogoId` and `brand.faviconId` from the returned ids. There
|
|
334
|
+
is no recolor, crop or SVG upload command.
|
|
309
335
|
5. **Validate, preview, then push.** `validate` checks the whole workspace offline;
|
|
310
336
|
`theme push --dry-run` reads the site and lists the JSON paths at which
|
|
311
337
|
each settings file differs from it, and whether the baseline `pull`
|
|
@@ -320,10 +346,12 @@ each step names the reference it relies on.
|
|
|
320
346
|
change set. `pull` reads the four settings documents and the revision
|
|
321
347
|
from one snapshot, and `footer push` advances the recorded revision only
|
|
322
348
|
when its save replaced that same revision.
|
|
323
|
-
6. **Review on staging in both schemes.** `
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
349
|
+
6. **Review on staging in both schemes.** `staging review` (or
|
|
350
|
+
`deploy --staging --handoff`) returns a single-use handoff in
|
|
351
|
+
`stagingPreview.url`, on a managed Docs site too; open it once and use the
|
|
352
|
+
site's theme toggle. On a Standard site `deploy --staging` first waits until
|
|
353
|
+
staging serves the new release (see `stagingServed`). No page draft is
|
|
354
|
+
needed; `preview page` renders one draft and is a different tool.
|
|
327
355
|
7. **Promote only after review.** `deploy --production`.
|
|
328
356
|
|
|
329
357
|
## The authoring loop
|
|
@@ -362,7 +390,22 @@ its original publication date) and `coverImage`, which the site requires to be a
|
|
|
362
390
|
image the page itself uses (place it in the body too; `pages.cover_image_unused`). A page's template is fixed when
|
|
363
391
|
it is created: a source for another template is refused with
|
|
364
392
|
`pages.template_immutable`. `taproot-site help page <type>` gives each
|
|
365
|
-
template's fields and one worked example.
|
|
393
|
+
template's fields and one worked example. The structured legal page (a versioned
|
|
394
|
+
privacy policy or terms with an effective date) is created and published in the
|
|
395
|
+
app; the CLI records it as a metadata-only page, and `taproot-site help page
|
|
396
|
+
legal` says what a site-authoring key can and cannot do with one.
|
|
397
|
+
A privacy policy or other legal page that names Taproot should link the first
|
|
398
|
+
mention to <https://www.taproot.io>; `help page free-form` and `help page legal`
|
|
399
|
+
give a sample sentence.
|
|
400
|
+
|
|
401
|
+
A structured page prints its title as the page's `<h1>`, so its body starts at
|
|
402
|
+
`##`. A free-form page renders no automatic heading, so open its Markdown with
|
|
403
|
+
a `# Page title` line: the first level-1 heading becomes the `<h1>`. A
|
|
404
|
+
`hero-section` with a title, or a leading `image-banner` with overlay text,
|
|
405
|
+
claims the `<h1>` first, so do not also write a `#` line on a page with a
|
|
406
|
+
titled hero. A later `#` heading is rendered as an `<h2>`, never a second
|
|
407
|
+
`<h1>`, and the other headings are renumbered so none skips a level. The
|
|
408
|
+
Markdown subset accepts levels 1 to 4.
|
|
366
409
|
|
|
367
410
|
A `.pm.json` page's title, path, description and author live in the workspace
|
|
368
411
|
manifest rather than the file. `pages meta set <path> --title <text>
|
|
@@ -495,7 +538,10 @@ choose one on the Videos page, where an upload made there gets one automatically
|
|
|
495
538
|
title and caption there too: the poster, caption and shape are the video's own and show wherever
|
|
496
539
|
it is placed. The command reports its `videoId`, title, caption and file name for a `video`
|
|
497
540
|
component (`help component video`), and
|
|
498
|
-
`pages push` refuses a page that places a video the site does not have. `nav
|
|
541
|
+
`pages push` refuses a page that places a video the site does not have. `nav.json` and the footer
|
|
542
|
+
document leave defaults out (no empty `externalUrl` or `children` on a navigation item, no `bold: false`
|
|
543
|
+
on a footer text run); `pull`, `nav push` and `footer push` all write that form and accept either, so a
|
|
544
|
+
pull after a push shows only changes you made. `nav push`, `theme push`, and `footer push` replace the whole
|
|
499
545
|
navigation tree, the complete light/dark theme pair with its appearance
|
|
500
546
|
settings, and the closed footer document. `pull` writes each scheme's
|
|
501
547
|
complete effective theme — the stored theme resolved over the same defaults
|
|
@@ -546,14 +592,13 @@ cleaned-up snapshot answers `preview.not_found`.
|
|
|
546
592
|
It stages; nothing reaches an audience until `deploy --staging` publishes the
|
|
547
593
|
candidate and `deploy --production` promotes that completed staging
|
|
548
594
|
deployment. `status` reports the platform authoring switch, the CLI release,
|
|
549
|
-
deployments, readiness, image processing, and
|
|
550
|
-
permission. `
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
site.
|
|
595
|
+
deployments, readiness, image processing, and site health under Content
|
|
596
|
+
permission. `siteHealth` lists the site checks' findings over the latest editable
|
|
597
|
+
body of each page: a rule id, a severity (`error` or `warning`), the page, and
|
|
598
|
+
the value found. It reports `covered: true`, `errors`, `warnings`, `pages`, and
|
|
599
|
+
`findings`; an empty list means no check found anything. Output is bounded to 100
|
|
600
|
+
findings, with an explicit `findingsTruncated` flag. A denied, malformed, or
|
|
601
|
+
failed read fails `status` instead of implying a clean site.
|
|
557
602
|
|
|
558
603
|
## Output contract
|
|
559
604
|
|
|
@@ -645,6 +690,24 @@ Other C0/C1 controls, U+2028/U+2029, U+FEFF, and literal `<br>` tags are rejecte
|
|
|
645
690
|
with the exact fact field path. A label is optional and stays outside the value's link. See
|
|
646
691
|
`taproot-site help page free-form` and `taproot-site help component cta`.
|
|
647
692
|
|
|
693
|
+
### Staging serves the new release in 0.21.0
|
|
694
|
+
|
|
695
|
+
A deployment reports `completed` once Taproot has asked Cloudflare to switch the
|
|
696
|
+
host to the new release. The edge then needs up to about two minutes to follow:
|
|
697
|
+
the route propagates, and the Worker keeps it cached for up to a minute more. A
|
|
698
|
+
cache-busting query cannot help, because the release comes from the route and not
|
|
699
|
+
the URL. `deploy --staging` on a Standard site now reads the release staging
|
|
700
|
+
serves before it deploys, and after the deployment completes waits (up to three
|
|
701
|
+
minutes, one read every three seconds) until a different release answers twice in
|
|
702
|
+
a row. The result carries `stagingServed`: `served` with `servedAfterSeconds`,
|
|
703
|
+
`propagating` when staging still showed the previous release at the bound, or
|
|
704
|
+
`unverified` when the change cannot be attributed to this deployment (no readable
|
|
705
|
+
baseline, another deployment under way or just completed, or a request that joined
|
|
706
|
+
one already running; it carries `expectedBy`, or `waitedSeconds` when the last read at the bound
|
|
707
|
+
did not name the previous release), and a managed Docs site always reports `unverified`. The check reads from the machine running the CLI, so a reviewer in another
|
|
708
|
+
region can still see the previous release until the two-minute ceiling. The
|
|
709
|
+
deployment itself is never failed by this wait. The redirect note no longer says there is no propagation delay.
|
|
710
|
+
|
|
648
711
|
### Forms by id in 0.20.0
|
|
649
712
|
|
|
650
713
|
A page stores the form it places by the form's id, so renaming a form never
|
|
@@ -738,15 +801,37 @@ targets are refused (`content.link_rel`, `content.link_target`). The editor's
|
|
|
738
801
|
old default, `noopener noreferrer nofollow`, reads as no choice. See
|
|
739
802
|
`taproot-site help page free-form`.
|
|
740
803
|
|
|
804
|
+
### What changed between releases
|
|
805
|
+
|
|
806
|
+
Taproot accepts only the latest CLI, so an upgrade can be forced at any time.
|
|
807
|
+
After one, run `taproot-site changelog --since <the version you upgraded from>`:
|
|
808
|
+
it prints each release's note, the behavior it changed, and the help topics
|
|
809
|
+
(`taproot-site help <topic>`) to read again, newest first, offline and without a
|
|
810
|
+
sign-in. With no `--since` it prints the installed release's entry; `complete:
|
|
811
|
+
false` means `--since` starts before the oldest release the record covers.
|
|
812
|
+
`status` reports the installed release's note in `cliRelease.whatsNew`. An
|
|
813
|
+
outdated-CLI refusal carries `latestVersion` and `changelogCommand` as their own
|
|
814
|
+
fields beside the message, so the command to run after upgrading is in the
|
|
815
|
+
refusal itself. Every release's notes live in one file, `src/changelog.json`,
|
|
816
|
+
and a package test fails when the installed version has no entry there.
|
|
817
|
+
|
|
741
818
|
### Staging verification and deployment evidence
|
|
742
819
|
|
|
743
820
|
`deploy --staging` selects the same changed pages, settings groups and navigation
|
|
744
|
-
as the Deployments page
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
821
|
+
as the Deployments page. It reports no review URL by default, because the URL is
|
|
822
|
+
a live single-use credential and this JSON is often saved to logs:
|
|
823
|
+
`stagingPreview.reason` is `staging.handoff_not_requested` and
|
|
824
|
+
`stagingPreview.recovery` names `taproot-site staging review`. Run that command
|
|
825
|
+
for a single-use review handoff (on a managed Docs site too, where deploy stages
|
|
826
|
+
settings only), or pass `--handoff` to `deploy --staging` to get it in
|
|
827
|
+
`stagingPreview.url` straight away; `--no-handoff` states the default
|
|
828
|
+
explicitly. Open the URL once, then use the site's theme toggle to review both
|
|
829
|
+
schemes before promoting. `staging review --path about` (or
|
|
830
|
+
`deploy --staging --handoff --review-path about`) opens the review on that page
|
|
831
|
+
instead of the home page; a page path as `pull` records it works, with or
|
|
832
|
+
without the leading slash, and a query is allowed. When a requested handoff cannot be minted,
|
|
833
|
+
`stagingPreview.reason` and `stagingPreview.recovery` say why and what to do
|
|
834
|
+
next; `staging review` mints another on any surface. `status` checks that exact current candidate, so a
|
|
750
835
|
completed CLI stage counts as successful until that candidate changes.
|
|
751
836
|
|
|
752
837
|
A retained failed preview of selected page content refuses deployment as
|
|
@@ -770,8 +855,9 @@ Requests have a 90-second deadline with eight concurrent requests; large JSON
|
|
|
770
855
|
lists have explicit truncation, prioritizing failures. Progress output reports
|
|
771
856
|
each entry. A gate/transport failure never counts as verified.
|
|
772
857
|
|
|
773
|
-
`stagingPreview.url` is a fresh single-use two-minute handoff
|
|
774
|
-
checks
|
|
858
|
+
With `--handoff`, `stagingPreview.url` is a fresh single-use two-minute handoff
|
|
859
|
+
minted **after** checks; without it the check still authenticates with its own
|
|
860
|
+
handoff and reports none. Opening it consumes it and establishes a five-minute staging cookie.
|
|
775
861
|
Minting again replaces the same authority's previous handoff and cookie. Current
|
|
776
862
|
site, host and credential permission are rechecked; key revocation stops access.
|
|
777
863
|
Treat the URL as secret: it is returned only in final JSON, omitted from progress
|
|
@@ -5,22 +5,19 @@
|
|
|
5
5
|
"id": "a0000000-0000-4000-8000-000000000030",
|
|
6
6
|
"kind": "NAV_ITEM_KIND_PAGE",
|
|
7
7
|
"title": "Home",
|
|
8
|
-
"resourceId": "a0000000-0000-4000-8000-000000000011"
|
|
9
|
-
"children": []
|
|
8
|
+
"resourceId": "a0000000-0000-4000-8000-000000000011"
|
|
10
9
|
},
|
|
11
10
|
{
|
|
12
11
|
"id": "a0000000-0000-4000-8000-000000000031",
|
|
13
12
|
"kind": "NAV_ITEM_KIND_PAGE",
|
|
14
13
|
"title": "Visit",
|
|
15
|
-
"resourceId": "a0000000-0000-4000-8000-000000000013"
|
|
16
|
-
"children": []
|
|
14
|
+
"resourceId": "a0000000-0000-4000-8000-000000000013"
|
|
17
15
|
},
|
|
18
16
|
{
|
|
19
17
|
"id": "a0000000-0000-4000-8000-000000000032",
|
|
20
18
|
"kind": "NAV_ITEM_KIND_EXTERNAL_URL",
|
|
21
19
|
"title": "Book a class",
|
|
22
|
-
"externalUrl": "https://schedule.example.test/riverbend"
|
|
23
|
-
"children": []
|
|
20
|
+
"externalUrl": "https://schedule.example.test/riverbend"
|
|
24
21
|
}
|
|
25
22
|
]
|
|
26
23
|
}
|
|
@@ -5,7 +5,7 @@ description: An example second page for the offline authoring fixture.
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
:::section {"contentPadding":"standard","surface":"none"}
|
|
8
|
-
|
|
8
|
+
# Visit the studio
|
|
9
9
|
|
|
10
10
|
Riverbend Wellness is a fictional studio in the fictional town of Elm Harbor.
|
|
11
11
|
Nothing on this page describes a real place, and no address, telephone number,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@taprootio/espalier": "6.
|
|
31
|
+
"@taprootio/espalier": "6.3.0",
|
|
32
32
|
"mediabunny": "1.61.1"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
package/src/api.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
BANDWIDTH_ALLOWANCE_REFUSAL_REASON,
|
|
4
4
|
CANONICAL_TIMESTAMP,
|
|
5
5
|
CAPABILITY_REFUSAL_FIELD,
|
|
6
|
+
changelogCommandSince,
|
|
6
7
|
CLI_UPGRADE_COMMAND,
|
|
7
8
|
CLI_UPGRADE_REFUSAL_FIELD,
|
|
8
9
|
CLI_VERSION,
|
|
@@ -398,6 +399,7 @@ export function announceRefusal(error, onProgress, action = "request") {
|
|
|
398
399
|
+ "and no retry of this version can succeed.",
|
|
399
400
|
);
|
|
400
401
|
onProgress(` Upgrade with: ${CLI_UPGRADE_COMMAND}`);
|
|
402
|
+
onProgress(` Then see what changed: ${changelogCommandSince(CLI_VERSION)}`);
|
|
401
403
|
// The server names both versions and says when waiting is the right move
|
|
402
404
|
// (a deploy that landed ahead of its npm publish). Surface it verbatim
|
|
403
405
|
// rather than restating a policy this side does not own; the bounded
|
|
@@ -1123,39 +1125,51 @@ export async function listSiteImages(client, siteId, requestOptions = {}, { imag
|
|
|
1123
1125
|
return { images, summary, truncated: true };
|
|
1124
1126
|
}
|
|
1125
1127
|
|
|
1126
|
-
/**
|
|
1127
|
-
export async function
|
|
1128
|
-
const code = "api.
|
|
1129
|
-
const response = requireObject(
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
if (value
|
|
1138
|
-
if (!Array.isArray(value)) {
|
|
1128
|
+
/** The site checks' findings over the latest editable page bodies, authorized by Content. */
|
|
1129
|
+
export async function getSiteHealth(client, siteId) {
|
|
1130
|
+
const code = "api.site_health_contract";
|
|
1131
|
+
const response = requireObject(await client.request(sitePath(siteId, "health")), code, "site-health report");
|
|
1132
|
+
// Proto3 omits empty repeated fields and zero counts. A present non-array is a
|
|
1133
|
+
// bad report, not evidence that the site is healthy.
|
|
1134
|
+
if (response.findings !== undefined && !Array.isArray(response.findings)) {
|
|
1135
|
+
throw new SiteAuthoringError(code, "Taproot returned an invalid findings list.", { field: "findings" });
|
|
1136
|
+
}
|
|
1137
|
+
const count = (value, field) => {
|
|
1138
|
+
if (value === undefined) return 0;
|
|
1139
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
1139
1140
|
throw new SiteAuthoringError(code, `Taproot returned an invalid ${field}.`, { field });
|
|
1140
1141
|
}
|
|
1141
1142
|
return value;
|
|
1142
1143
|
};
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1144
|
+
const severities = {
|
|
1145
|
+
SITE_FINDING_SEVERITY_ERROR: "error",
|
|
1146
|
+
SITE_FINDING_SEVERITY_WARNING: "warning",
|
|
1147
|
+
};
|
|
1148
|
+
const findings = (response.findings ?? []).map((entry) => {
|
|
1149
|
+
const finding = requireObject(entry, code, "finding");
|
|
1150
|
+
const severity = severities[finding.severity];
|
|
1151
|
+
if (typeof finding.ruleId !== "string" || severity === undefined) {
|
|
1152
|
+
throw new SiteAuthoringError(code, "Taproot returned a finding with no rule or severity.", { field: "findings" });
|
|
1150
1153
|
}
|
|
1151
1154
|
return {
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1155
|
+
ruleId: finding.ruleId,
|
|
1156
|
+
severity,
|
|
1157
|
+
...(finding.pageId === undefined || finding.pageId === "" ? {} : {
|
|
1158
|
+
pageId: requireCanonicalUuid(finding.pageId, code, "pageId"),
|
|
1159
|
+
pageTitle: text(finding.pageTitle),
|
|
1160
|
+
pagePath: text(finding.pagePath),
|
|
1161
|
+
}),
|
|
1162
|
+
subject: text(finding.subject),
|
|
1163
|
+
detail: text(finding.detail),
|
|
1157
1164
|
};
|
|
1158
1165
|
});
|
|
1166
|
+
return {
|
|
1167
|
+
findings,
|
|
1168
|
+
errorCount: count(response.errorCount, "errorCount"),
|
|
1169
|
+
warningCount: count(response.warningCount, "warningCount"),
|
|
1170
|
+
pageCount: count(response.pageCount, "pageCount"),
|
|
1171
|
+
truncated: response.truncated === true,
|
|
1172
|
+
};
|
|
1159
1173
|
}
|
|
1160
1174
|
|
|
1161
1175
|
/** Use the same changed release set the Deployments page selects. */
|
|
@@ -1311,13 +1325,82 @@ export async function getStagingPreviewStatus(client, siteId) {
|
|
|
1311
1325
|
return { ready: true, stagingUrl: `https://${url.hostname}/` };
|
|
1312
1326
|
}
|
|
1313
1327
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1328
|
+
// The server accepts 2048, but the edge appends a check parameter before its own 2048 test.
|
|
1329
|
+
const STAGING_RETURN_PATH_MAXIMUM = 2_000;
|
|
1330
|
+
const STAGING_HANDOFF_PARAMETER = "__taproot_preview_handoff";
|
|
1331
|
+
// Transport-owned query keys the server refuses in a return path (StagingPreviewProtocol).
|
|
1332
|
+
const STAGING_RESERVED_QUERY_KEYS = new Set([
|
|
1333
|
+
STAGING_HANDOFF_PARAMETER,
|
|
1334
|
+
"__taproot_preview_check",
|
|
1335
|
+
"__taproot_preview_denied",
|
|
1336
|
+
]);
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* A page path for `staging review --path`, as the same-origin return path the
|
|
1340
|
+
* server accepts (TR01230). `about`, `/about` and `posts/one?draft=true` all
|
|
1341
|
+
* work; the rules mirror `StagingPreviewProtocol.TryValidateReturnPath`, so a
|
|
1342
|
+
* path the server would refuse is refused here before any request.
|
|
1343
|
+
*/
|
|
1344
|
+
export function normalizeStagingReviewPath(value) {
|
|
1345
|
+
const invalid = (reason) =>
|
|
1346
|
+
new SiteAuthoringError(
|
|
1347
|
+
"staging.review_path_invalid",
|
|
1348
|
+
`A review path must be a page path on this site, such as 'about' or '/posts/one'; ${reason}.`,
|
|
1349
|
+
{ field: "reviewPath", exitCode: 2 },
|
|
1350
|
+
);
|
|
1351
|
+
if (typeof value !== "string" || value.length === 0) throw invalid("it is empty");
|
|
1352
|
+
// URL parsing drops edge whitespace, so the server's URL would no longer match the path asked for.
|
|
1353
|
+
if (value !== value.trim()) throw invalid("it must not start or end with whitespace");
|
|
1354
|
+
const path = value.startsWith("/") ? value : `/${value}`;
|
|
1355
|
+
if (path.length > STAGING_RETURN_PATH_MAXIMUM) throw invalid("it is too long");
|
|
1356
|
+
// Checked before URL parsing, which would throw on some of these (such as '/\\') instead of naming the problem.
|
|
1357
|
+
if (path.includes("#") || path.includes("\\") || /[\u0000-\u001f\u007f-\u009f]/u.test(path)) {
|
|
1358
|
+
throw invalid("it must not contain a fragment, a backslash or control characters");
|
|
1359
|
+
}
|
|
1360
|
+
if (path.startsWith("//")) throw invalid("it must not start with two slashes");
|
|
1361
|
+
const queryStart = path.indexOf("?");
|
|
1362
|
+
if (queryStart >= 0) {
|
|
1363
|
+
for (const pair of path.slice(queryStart + 1).split("&")) {
|
|
1364
|
+
const separator = pair.indexOf("=");
|
|
1365
|
+
const rawName = separator < 0 ? pair : pair.slice(0, separator);
|
|
1366
|
+
let name = rawName;
|
|
1367
|
+
try {
|
|
1368
|
+
name = decodeURIComponent(rawName.replaceAll("+", " "));
|
|
1369
|
+
} catch {
|
|
1370
|
+
// A malformed escape cannot spell a reserved key once decoded; compare the raw text.
|
|
1371
|
+
}
|
|
1372
|
+
if (STAGING_RESERVED_QUERY_KEYS.has(name.toLowerCase())) throw invalid("it names a reserved preview parameter");
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
let resolved;
|
|
1376
|
+
try {
|
|
1377
|
+
resolved = new URL(path, "https://staging.invalid");
|
|
1378
|
+
} catch {
|
|
1379
|
+
throw invalid("it is not a valid path");
|
|
1380
|
+
}
|
|
1381
|
+
// Dot segments can resolve to '//x'; the edge refuses that after spending the single-use handoff.
|
|
1382
|
+
if (resolved.pathname.startsWith("//")) {
|
|
1383
|
+
throw invalid("it must not resolve to a path starting with two slashes");
|
|
1384
|
+
}
|
|
1385
|
+
// The edge re-encodes the path and query and adds its own check parameter before its length test.
|
|
1386
|
+
resolved.searchParams.set("__taproot_preview_check", "1");
|
|
1387
|
+
if (resolved.pathname.length + resolved.search.length > 2_048) throw invalid("it is too long once encoded");
|
|
1388
|
+
return path;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Mint a site-bound handoff; its URL is emitted only in final operational JSON.
|
|
1393
|
+
* `returnPath` is the page the browser lands on after the handoff is consumed;
|
|
1394
|
+
* `deadline` lets a bounded wait (the served-release poller) cap the mint too.
|
|
1395
|
+
*/
|
|
1396
|
+
export async function mintStagingPreviewHandoff(client, siteId, { now = Date.now, returnPath = "/", deadline } = {}) {
|
|
1316
1397
|
const code = "api.staging_handoff_contract";
|
|
1317
1398
|
const result = requireObject(
|
|
1318
1399
|
await client.request(sitePath(siteId, "staging-preview:mint-handoff"), {
|
|
1319
1400
|
method: "POST",
|
|
1320
|
-
body: { siteId },
|
|
1401
|
+
body: { siteId, ...(returnPath === "/" ? {} : { returnPath }) },
|
|
1402
|
+
// A poller's budget reaches the mint, so a slow API cannot stretch a bounded wait.
|
|
1403
|
+
...(deadline === undefined ? {} : { deadline, now }),
|
|
1321
1404
|
}),
|
|
1322
1405
|
code,
|
|
1323
1406
|
"staging handoff",
|
|
@@ -1332,12 +1415,16 @@ export async function mintStagingPreviewHandoff(client, siteId, { now = Date.now
|
|
|
1332
1415
|
}
|
|
1333
1416
|
const expires = requireCanonicalTimestamp(result.handoffExpiresAt, code, "handoffExpiresAt");
|
|
1334
1417
|
const current = now();
|
|
1418
|
+
// The URL is the requested path plus the handoff token as the last query parameter.
|
|
1419
|
+
const expectedUrl = new URL(returnPath, "https://staging.invalid");
|
|
1420
|
+
const expectedQuery = expectedUrl.search.replace(/^\?/u, "").replace(/&$/u, "");
|
|
1421
|
+
const tokenQuery = new RegExp(`^\\?(?:(.*)&)?${STAGING_HANDOFF_PARAMETER}=([A-Za-z0-9_-]{43})$`, "u")
|
|
1422
|
+
.exec(url.search);
|
|
1335
1423
|
if (
|
|
1336
1424
|
result.siteId !== siteId || origin.protocol !== "https:" || !CANONICAL_HOST.test(origin.hostname)
|
|
1337
1425
|
|| origin.port || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash
|
|
1338
|
-
|| url.origin !== origin.origin || url.username || url.password || url.pathname !==
|
|
1339
|
-
||
|
|
1340
|
-
|| !/^[A-Za-z0-9_-]{43}$/u.test(url.searchParams.get("__taproot_preview_handoff") ?? "")
|
|
1426
|
+
|| url.origin !== origin.origin || url.username || url.password || url.pathname !== expectedUrl.pathname
|
|
1427
|
+
|| url.hash || tokenQuery === null || (tokenQuery[1] ?? "") !== expectedQuery
|
|
1341
1428
|
|| expires.milliseconds <= current - AUTHORING_PREVIEW_CLOCK_SKEW_MILLISECONDS
|
|
1342
1429
|
|| expires.milliseconds > current + AUTHORING_PREVIEW_HANDOFF_LIFETIME_MILLISECONDS
|
|
1343
1430
|
+ AUTHORING_PREVIEW_CLOCK_SKEW_MILLISECONDS
|
package/src/changelog.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import changelogRecord from "./changelog.json" with { type: "json" };
|
|
2
|
+
import { compareCliVersions, parseCliVersion } from "./cli-release.js";
|
|
3
|
+
import { CLI_VERSION } from "./constants.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What changed in each release (TR01225), from the one record the package ships.
|
|
7
|
+
*
|
|
8
|
+
* `changelog.json` is the only place a release note is written: the `changelog`
|
|
9
|
+
* command and `status` both read it here, so neither carries prose of its own.
|
|
10
|
+
* The outdated-CLI refusal comes from a CLI that predates the release it
|
|
11
|
+
* refuses for, so it can only name the command to run after upgrading. An entry is `{ version, date?, note, behavior,
|
|
12
|
+
* help }`: `note` is the one-sentence summary, `behavior` the changes a script or
|
|
13
|
+
* agent can observe, and `help` the reference topics (`help <topic>`) whose text
|
|
14
|
+
* changed. The newest entry comes first. The version being prepared is
|
|
15
|
+
* "unreleased" until the release is stamped.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const UNRELEASED = "unreleased";
|
|
19
|
+
|
|
20
|
+
export const CHANGELOG = Object.freeze(changelogRecord.entries.map((entry) => Object.freeze(entry)));
|
|
21
|
+
|
|
22
|
+
/** The entry for one released version, or undefined. */
|
|
23
|
+
export function changelogEntryFor(version) {
|
|
24
|
+
return CHANGELOG.find((entry) => entry.version === version);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The released entries newer than `since` and no newer than `through`, newest
|
|
29
|
+
* first. The unreleased entry is never part of an installed release.
|
|
30
|
+
*/
|
|
31
|
+
export function changelogEntriesSince(since, through = CLI_VERSION) {
|
|
32
|
+
return CHANGELOG.filter((entry) => {
|
|
33
|
+
if (entry.version === UNRELEASED || parseCliVersion(entry.version) === undefined) return false;
|
|
34
|
+
return compareCliVersions(entry.version, since) > 0 && compareCliVersions(entry.version, through) <= 0;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The oldest released version the record covers, so a caller can say when a range starts before it. */
|
|
39
|
+
export function earliestRecordedVersion() {
|
|
40
|
+
const released = CHANGELOG.filter((entry) => parseCliVersion(entry.version) !== undefined);
|
|
41
|
+
return released.length === 0 ? undefined : released[released.length - 1].version;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The short note `status` shows for the installed version, or undefined when none is recorded. */
|
|
45
|
+
export function installedWhatsNew(version = CLI_VERSION) {
|
|
46
|
+
const entry = changelogEntryFor(version);
|
|
47
|
+
return entry === undefined ? undefined : { version: entry.version, note: entry.note, help: [...entry.help] };
|
|
48
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entries": [
|
|
3
|
+
{
|
|
4
|
+
"version": "0.21.0",
|
|
5
|
+
"note": "Review URLs stay out of deploy output unless you ask; deploy --staging waits until staging serves the new release; validate reports site checks; help corrections for free-form h1, legal pages, settings and logos; this changelog.",
|
|
6
|
+
"behavior": [
|
|
7
|
+
"deploy --staging and redirects check no longer report a live single-use review URL: stagingPreview.url is empty and stagingPreview.reason is staging.handoff_not_requested. Pass --handoff for a fresh URL in the result, or run staging review. --no-handoff states the default.",
|
|
8
|
+
"staging review takes --path <page> to open the review on that page instead of the home page; deploy --staging --handoff and redirects check --handoff take --review-path <page>. The path is checked offline (staging.review_path_invalid).",
|
|
9
|
+
"pull and nav push write nav.json without default-only fields (no empty externalUrl, resourceId or children), and nav push rewrites nav.json from the saved tree after a successful save; either form is accepted.",
|
|
10
|
+
"Footer text runs omit bold, italic and underline when they are false, in pull and footer push; either form is accepted.",
|
|
11
|
+
"New command: changelog [--since <version>] prints these notes offline, and status reports the installed version's note. The cli_outdated refusal now carries latestVersion and the changelog command to run after upgrading.",
|
|
12
|
+
"deploy --staging on a Standard site waits, bounded at 180 seconds, until staging serves the new release before it finishes, and reports stagingServed as served, propagating or unverified; the result never fails on the wait. Propagation can take up to about 120 seconds after the deployment completes.",
|
|
13
|
+
"validate runs the shared site check rules it can run offline and reports findings (ruleId, severity, pageId, subject) and siteChecks (ranOffline, checkedBySite, checkedAtDeploy); redirect refusals keep their codes and carry the rule id. status reads the site health summary; the broken-references report is gone, so an older CLI needs this upgrade.",
|
|
14
|
+
"Published forms mark required fields with an asterisk and a legend; help forms describes the mark.",
|
|
15
|
+
"Help corrections: a free-form page renders no automatic heading and its first level-1 heading is the h1; help pages lists the read-only legal page type and help page legal describes it; the new help settings topic maps every pulled setting to the app control that writes it and says where it renders; logo.png and logo@2x.png are not paired, so assign the highest-resolution file.",
|
|
16
|
+
"Espalier 6.3.0: a field fills one layer above the surface that contains it; the default site theme was rebuilt for it."
|
|
17
|
+
],
|
|
18
|
+
"help": [
|
|
19
|
+
"walkthrough",
|
|
20
|
+
"nav",
|
|
21
|
+
"redirects",
|
|
22
|
+
"preview",
|
|
23
|
+
"forms",
|
|
24
|
+
"pages",
|
|
25
|
+
"page",
|
|
26
|
+
"settings",
|
|
27
|
+
"appearance",
|
|
28
|
+
"theme",
|
|
29
|
+
"media",
|
|
30
|
+
"delivery"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"version": "0.20.0",
|
|
35
|
+
"note": "Pages place a form by its id, so renaming a form never breaks a page; form names are unique on a site.",
|
|
36
|
+
"behavior": [
|
|
37
|
+
"pages push turns a component:form formKey into the form's id, and refuses a key the site does not have (pages.form_unknown) or has archived (pages.form_archived) before writing anything; plan reports the same. pages pull writes the key back, and keeps an id the site no longer has.",
|
|
38
|
+
"forms validate and forms push refuse two forms with the same name, ignoring case (forms.name_duplicate, forms.name_taken).",
|
|
39
|
+
"The first pages pull after the server release can report pages.pull_conflict for a Markdown page that places a form, though its content is the same; run pages push for that page, or delete the file and pull again."
|
|
40
|
+
],
|
|
41
|
+
"help": [
|
|
42
|
+
"forms",
|
|
43
|
+
"component"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"version": "0.19.0",
|
|
48
|
+
"note": "Forms can email the site owner an hourly count of new submissions, and a newsletter sink needs a consent checkbox.",
|
|
49
|
+
"behavior": [
|
|
50
|
+
"A form file takes an optional top-level notify_owner boolean. New forms default to false, omitting it on an existing form keeps the site's choice, and it takes effect without a new version or publishing.",
|
|
51
|
+
"A newsletter sink requires a required email contact field and at least one consent checkbox, and every consent checkbox must be checked.",
|
|
52
|
+
"forms.concurrent_modification is also raised when the site's operational settings moved since the last read."
|
|
53
|
+
],
|
|
54
|
+
"help": [
|
|
55
|
+
"forms"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"version": "0.18.0",
|
|
60
|
+
"note": "Place categories can be set per site, and deploy shows the pages it will generate before it sends anything.",
|
|
61
|
+
"behavior": [
|
|
62
|
+
"New commands: places category list, places category set <placeId> <category> and places category clear <placeId> change the category this site files a place review under.",
|
|
63
|
+
"deploy --staging plans the candidate first and reports the pages it will generate and the allowance left (readiness.pageEstimate); a candidate that does not fit stops with deploy.page_allowance_exceeded before anything is sent.",
|
|
64
|
+
"A deploy refused for the pages-generated allowance or the bandwidth pause reports plan_limit with the time it can lift."
|
|
65
|
+
],
|
|
66
|
+
"help": [
|
|
67
|
+
"page"
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
package/src/cli-release.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
changelogCommandSince,
|
|
2
3
|
CLI_UPGRADE_COMMAND,
|
|
3
4
|
CLI_UPGRADE_REFUSAL_FIELD,
|
|
4
5
|
CLI_VERSION,
|
|
@@ -131,6 +132,11 @@ export function isBehindLatest(latestVersion, currentVersion = CLI_VERSION) {
|
|
|
131
132
|
* `api.request_rejected`.
|
|
132
133
|
*/
|
|
133
134
|
class CliOutdatedError extends SiteAuthoringError {
|
|
135
|
+
constructor(code, message, options, latestVersion) {
|
|
136
|
+
super(code, message, options);
|
|
137
|
+
this.latestVersion = latestVersion;
|
|
138
|
+
}
|
|
139
|
+
|
|
134
140
|
refusalKind() {
|
|
135
141
|
return REFUSAL_CLI_OUTDATED;
|
|
136
142
|
}
|
|
@@ -146,7 +152,9 @@ export function cliOutdatedError(latestVersion) {
|
|
|
146
152
|
`This CLI is version ${CLI_VERSION}, and the last Taproot sign-in exchange reported ${latestVersion} as the `
|
|
147
153
|
+ "latest published release. Taproot accepts only the latest, so every online verb is refused until this "
|
|
148
154
|
+ `package is upgraded. Upgrade with: ${CLI_UPGRADE_COMMAND}. If ${latestVersion} was published in the last `
|
|
149
|
-
+ "few minutes, npm may not serve it yet — wait a moment and retry the upgrade."
|
|
155
|
+
+ "few minutes, npm may not serve it yet — wait a moment and retry the upgrade. Once upgraded, run "
|
|
156
|
+
+ `'${changelogCommandSince(CLI_VERSION)}' to see what changed.`,
|
|
150
157
|
{ field: CLI_UPGRADE_REFUSAL_FIELD },
|
|
158
|
+
latestVersion,
|
|
151
159
|
);
|
|
152
160
|
}
|