@elytracms/cli 0.0.12 → 0.0.66

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
@@ -27,7 +27,9 @@ elytra init First-run setup: provision Convex, generate the auth signing
27
27
  elytra doctor Diagnose the active deployment's readiness and print each fix.
28
28
  Includes the expected deployment env vars, reported set/missing
29
29
  by NAME only — values are never read into the report or printed.
30
- Read-only; exits non-zero if any check fails.
30
+ When the publish revalidate pair is set, also sends a signed
31
+ no-op ping to the endpoint to prove the publish → live chain
32
+ end to end. Read-only; exits non-zero if any check fails.
31
33
 
32
34
  elytra help Show help
33
35
  elytra version Show version
@@ -87,19 +89,55 @@ set on the wrong plane:
87
89
  3. **Convex deployment env** — set with `npx convex env set` (from `studio/`);
88
90
  lives on the deployment itself, visible in the Convex dashboard but in no
89
91
  file: the auth signing keys (`JWT_PRIVATE_KEY`, `JWKS` — set via
90
- `npx @convex-dev/auth`), `SITE_URL`, and the publish/draft pair
92
+ `npx @convex-dev/auth`), `SITE_URL`, the instant-publish trio
91
93
  `CANVAS_PROJECT_SCOPE` / `CANVAS_REVALIDATE_ENDPOINT` /
92
- `CANVAS_REVALIDATE_SECRET` and `DRAFT_PREVIEW_TOKEN` (each must match its
93
- frontend-plane counterpart). `elytra doctor` diagnoses this plane's
94
- readiness.
94
+ `CANVAS_REVALIDATE_SECRET`, and `DRAFT_PREVIEW_TOKEN` (each of the latter
95
+ four must match its frontend-plane counterpart). `elytra doctor` diagnoses
96
+ this plane's readiness.
95
97
 
96
98
  Because plane 3 lives in no file, it is the easiest to overlook — `elytra
97
99
  doctor` therefore enumerates the expected vars there (auth keys, `SITE_URL`,
98
- plus the AI provider key on the studio-host plane when the AI route is
99
- present) and reports each one **set or missing, by name only**: the doctor
100
- parses names out of `npx convex env list` and discards every value on the
101
- spot — values are never stored, printed, or logged. A missing required var
102
- (the auth keys) fails the run with its exact fix line; a missing recommended
103
- var (`SITE_URL`, the AI key) is a named warning that doesn't change the exit
100
+ the instant-publish trio, the draft-preview token, plus the AI provider key
101
+ on the studio-host plane when the AI route is present) and reports each one
102
+ **set or missing, by name only**: the doctor parses names out of `npx convex
103
+ env list` and discards every value on the spot — values are never stored,
104
+ printed, or logged. A missing required var (the auth keys) fails the run
105
+ with its exact fix line; a missing recommended var (`SITE_URL`, the AI key,
106
+ the draft-preview token) is a named warning that doesn't change the exit
104
107
  code. The studio's boot-time "Deployment setup" screen lists the same
105
108
  expected vars, so the CLI and the studio tell one story.
109
+
110
+ The **instant-publish trio** — `CANVAS_PROJECT_SCOPE`,
111
+ `CANVAS_REVALIDATE_ENDPOINT`, and `CANVAS_REVALIDATE_SECRET` — gets special
112
+ treatment, because the publish path **silently no-ops** while any of the
113
+ three is unset: `CANVAS_PROJECT_SCOPE` gates the write-chokepoint trigger
114
+ that *schedules* the revalidate webhook (checked FIRST, before the pair is
115
+ even consulted), and the pair then gates whether the scheduled webhook
116
+ actually *delivers*. Either way, publishes never update the live site until
117
+ a redeploy, with no error anywhere. All three missing is a warning carrying
118
+ exactly that consequence (a project without a delivery frontend may
119
+ legitimately leave them unset); **any partial subset — one or two of the
120
+ three set — fails** the run, because a half-configured trio has no
121
+ legitimate reading (documented together in the scaffold's CONNECT.md, set in
122
+ one shell block, so a partial state is far more likely an interrupted setup
123
+ than a deliberate choice). When the pair (`CANVAS_REVALIDATE_ENDPOINT` +
124
+ `CANVAS_REVALIDATE_SECRET`) IS set, the doctor goes one step further and
125
+ proves the chain end to end: it POSTs a **signed no-op ping** (an empty tag
126
+ list — the route revalidates nothing) to the endpoint. A 200
127
+ `{"revalidated":[]}` passes (endpoint reachable, and the frontend's
128
+ `REVALIDATE_SECRET` matches); a 401 fails as a secret mismatch between
129
+ Convex and the frontend host; a network error only warns (unreachable from
130
+ your machine is not the same as broken — Convex may still reach it). The
131
+ pair's values are read in-process for the ping only and never printed. The
132
+ ping proves only the pair's reachability/secret-match, not scheduling — a
133
+ missing `CANVAS_PROJECT_SCOPE` still fails its own row (and the run)
134
+ independently, even when the ping passes.
135
+
136
+ The **draft-preview token** — `DRAFT_PREVIEW_TOKEN` — gates the deployment's
137
+ `draftSnapshot` query and must equal the frontend host's own
138
+ `DRAFT_PREVIEW_TOKEN` (the same secret that gates the frontend's
139
+ preview-enable route); the doctor can only see the Convex-deployment side; it
140
+ has no `pairWith` escalation because its real partner lives on a plane this
141
+ doctor cannot read. Missing it is a warning: draft preview is closed
142
+ everywhere (401 on both sides) until it's set — a project that never uses
143
+ draft preview may legitimately leave it unset.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytracms/cli",
3
3
  "author": "finaldream <hi@finaldream.de>",
4
- "version": "0.0.12",
4
+ "version": "0.0.66",
5
5
  "description": "The Elytra CLI — run a consuming repo's studio, frontend, and Convex backend under one command.",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "node": ">=20"
18
18
  },
19
19
  "dependencies": {
20
- "@elytracms/core": "^0.0.64"
20
+ "@elytracms/core": "^0.0.66"
21
21
  },
22
22
  "devDependencies": {
23
23
  "vitest": "^4.1.10"
package/src/cli.js CHANGED
@@ -25,7 +25,9 @@ Commands:
25
25
  verify readiness. Idempotent — safe to re-run at any point.
26
26
  doctor Diagnose the active deployment's readiness (URL, reachability, auth
27
27
  signing keys, expected deployment env vars — set/missing by NAME,
28
- values never shown — config, membership) and print each fix. Exit
28
+ values never shown — config, membership) and print each fix. When the
29
+ instant-publish revalidate pair is set, also sends a signed no-op ping
30
+ to the endpoint to prove the publish → live chain end to end. Exit
29
31
  non-zero if any check fails. Read-only. Optional: --url <convex url>.
30
32
  help Show this help
31
33
  version Show version
package/src/doctor.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import {
2
+ REVALIDATE_ENV_ENDPOINT,
3
+ REVALIDATE_ENV_SECRET,
2
4
  backendSiteUrl,
3
5
  checkDeploymentReady,
4
6
  expectedDeploymentEnv,
5
7
  probeAuthSigningKeys,
8
+ probeRevalidateEndpoint,
6
9
  } from '@elytracms/core/export-sync/deployment-ready'
7
- import { defaultConvexExec } from './provision.js'
10
+ import { convexEnvGet, defaultConvexExec } from './provision.js'
8
11
 
9
12
  /**
10
13
  * `elytra doctor` (EC-259) — the pre-boot counterpart to the studio's boot
@@ -177,6 +180,50 @@ export function gatherDeploymentEnvFacts({ env = {}, target, studioDir, aiRouteC
177
180
  return facts
178
181
  }
179
182
 
183
+ // --- Signed revalidate ping (EC-421) -------------------------------------------
184
+ //
185
+ // When the presence checks say the instant-publish pair IS set, the doctor proves the
186
+ // chain end to end: a POST to the endpoint signed with the secret, carrying an EMPTY
187
+ // tag list — by the receiving route's own semantics a harmless no-op answering 200
188
+ // `{"revalidated":[]}`. Default-on (no flag): the ping is read-only in effect and the
189
+ // silent-failure class it catches (secret mismatch — publishes rejected with 401
190
+ // forever, no error anywhere) is exactly what the doctor exists for.
191
+ //
192
+ // The ping needs the pair's VALUES — the one sanctioned value read: captured
193
+ // `npx convex env get` subprocess stdout (`convexEnvGet`), held in locals inside this
194
+ // function, passed into the probe, and never attached to a fact, printed, or logged.
195
+
196
+ /**
197
+ * Gather the revalidate-ping fact for the report, or `undefined` when the ping does
198
+ * not apply (pair not fully present / presence not enumerated — the presence checks
199
+ * already tell that story; a probe row would be noise). A pair that is PRESENT but
200
+ * whose values can't be read yields an honest `unknown`, never a fabricated outcome.
201
+ */
202
+ export async function gatherRevalidateProbe({
203
+ envFacts,
204
+ studioDir,
205
+ exec,
206
+ fetchImpl = fetch,
207
+ probe = probeRevalidateEndpoint,
208
+ } = {}) {
209
+ const names = envFacts?.deploymentSetNames
210
+ if (!names || !names.includes(REVALIDATE_ENV_ENDPOINT) || !names.includes(REVALIDATE_ENV_SECRET)) {
211
+ return undefined
212
+ }
213
+ // In-process value read (the ONLY sanctioned one): captured subprocess stdout.
214
+ const endpoint = convexEnvGet(REVALIDATE_ENV_ENDPOINT, { cwd: studioDir, exec })
215
+ const secret = convexEnvGet(REVALIDATE_ENV_SECRET, { cwd: studioDir, exec })
216
+ if (endpoint === '' || secret === '') {
217
+ return {
218
+ status: 'unknown',
219
+ detail:
220
+ 'The revalidate pair is set, but its values could not be read here (`npx convex env get` failed) — ' +
221
+ 'the signed ping was not run.',
222
+ }
223
+ }
224
+ return probe(endpoint, secret, fetchImpl)
225
+ }
226
+
180
227
  // --- Formatting (pure) -------------------------------------------------------
181
228
 
182
229
  const GLYPH = { ok: '✓', warn: '!', fail: '✗', unknown: '?' }
@@ -226,7 +273,8 @@ export function formatReport(report) {
226
273
  * `url`, and `exec` are injectable for tests. `studioDir` (where `convex env list`
227
274
  * runs) and `aiRouteConfigured` enable the deployment-env bucket (EC-399); without a
228
275
  * studio dir the env checks stay honest `unknown`s. Playground stays untouched — no
229
- * deployment, no env bucket.
276
+ * deployment, no env bucket. When the instant-publish revalidate pair is present, a
277
+ * signed no-op ping proves the publish → live chain end to end (EC-421, default-on).
230
278
  */
231
279
  export async function runDoctor({
232
280
  env = {},
@@ -254,6 +302,14 @@ export async function runDoctor({
254
302
  const facts = await gatherFacts(target, { fetchImpl })
255
303
  if (target.kind === 'convex') {
256
304
  facts.deploymentEnv = gatherDeploymentEnvFacts({ env, target, studioDir, aiRouteConfigured, exec })
305
+ // EC-421: default-on signed no-op ping whenever the revalidate pair is present.
306
+ const revalidateProbe = await gatherRevalidateProbe({
307
+ envFacts: facts.deploymentEnv,
308
+ studioDir,
309
+ exec,
310
+ fetchImpl,
311
+ })
312
+ if (revalidateProbe) facts.revalidateProbe = revalidateProbe
257
313
  }
258
314
  const report = checkDeploymentReady(facts)
259
315
  log(formatReport(report))