@everystack/cli 0.4.87 → 0.4.88
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/cli/commands/audit.ts +3 -1
- package/src/cli/commands/bundle.ts +5 -2
- package/src/cli/commands/cache.ts +76 -16
- package/src/cli/commands/diag.ts +3 -1
- package/src/cli/commands/lighthouse.ts +3 -1
- package/src/cli/commands/security-probe.ts +3 -1
- package/src/cli/config.ts +19 -0
- package/src/cli/discover-distribution.ts +5 -2
- package/src/cli/discover.ts +36 -3
- package/src/cli/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to `@everystack/cli` are documented here.
|
|
|
4
4
|
|
|
5
5
|
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## [0.4.88] - 2026-09-21
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- cache:purge probes a site's own hostname, and takes --host
|
|
12
|
+
|
|
7
13
|
## [0.4.87] - 2026-09-21
|
|
8
14
|
|
|
9
15
|
### BREAKING CHANGES
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the heavier `audit:lighthouse` stay separate commands (run them explicitly).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { resolveConfig, opsFunction, probeUrl } from '../config';
|
|
11
|
+
import { resolveConfig, opsFunction, probeUrl, ambiguousSiteNote } from '../config';
|
|
12
12
|
import { runBundleAudit, runBundleWeightAudit, reportBundleAudit } from './bundle';
|
|
13
13
|
import { runUiAudit, reportUiAudit } from './ui-audit';
|
|
14
14
|
import { checkRunbook } from './runbook';
|
|
@@ -33,6 +33,8 @@ export async function auditCommand(flags: Record<string, string>): Promise<void>
|
|
|
33
33
|
fail('No router URL to audit. Pass --host or ensure the stage has a deployed URL.');
|
|
34
34
|
process.exit(1);
|
|
35
35
|
}
|
|
36
|
+
const ambiguous = flags.host ? undefined : ambiguousSiteNote(config);
|
|
37
|
+
if (ambiguous) info(ambiguous);
|
|
36
38
|
info(`Region: ${config.region}, URL: ${base}`);
|
|
37
39
|
|
|
38
40
|
let failures = 0;
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
DEFAULT_UI_BLOAT_BUDGET,
|
|
49
49
|
type UiBloatBudget,
|
|
50
50
|
} from '../ui-bloat';
|
|
51
|
-
import { resolveConfig, probeUrl } from '../config';
|
|
51
|
+
import { resolveConfig, probeUrl, ambiguousSiteNote } from '../config';
|
|
52
52
|
import { fail, info, step, success, warn } from '../output';
|
|
53
53
|
|
|
54
54
|
// Expo Router web export puts client chunks under client/_expo/...; a plain
|
|
@@ -430,7 +430,10 @@ export async function bundleAuditCommand(
|
|
|
430
430
|
let target = positional || flags.url;
|
|
431
431
|
if (!target) {
|
|
432
432
|
try {
|
|
433
|
-
|
|
433
|
+
const stageConfig = await resolveConfig(flags.stage);
|
|
434
|
+
target = probeUrl(stageConfig);
|
|
435
|
+
const ambiguous = ambiguousSiteNote(stageConfig);
|
|
436
|
+
if (ambiguous) info(ambiguous);
|
|
434
437
|
} catch (err: any) {
|
|
435
438
|
fail(err.message);
|
|
436
439
|
info('Pass a URL positionally or use --stage <name>.');
|
|
@@ -13,6 +13,9 @@ import { resolveConfig, probeUrl } from '../config';
|
|
|
13
13
|
import { putKvsKey, createInvalidation, getDistributionStatus, getFromS3 } from '../aws';
|
|
14
14
|
import { step, success, warn, fail, info } from '../output';
|
|
15
15
|
|
|
16
|
+
/** One black-holed candidate must not stall the gate for the OS connect timeout. */
|
|
17
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
18
|
+
|
|
16
19
|
const VALID_ORIGINS = ['api', 'media', 'web'] as const;
|
|
17
20
|
type CacheOrigin = typeof VALID_ORIGINS[number];
|
|
18
21
|
|
|
@@ -34,10 +37,28 @@ export async function cachePurgeCommand(flags: Record<string, string>): Promise<
|
|
|
34
37
|
|
|
35
38
|
const force = flags.force === 'true';
|
|
36
39
|
|
|
40
|
+
// What the gate probes: --host wins, else every discovered candidate (aliases before the domain).
|
|
41
|
+
let candidates: string[];
|
|
42
|
+
if (flags.host !== undefined) {
|
|
43
|
+
// parseFlags gives a value-less --host the string 'true'; '' is --host= . Neither is a host.
|
|
44
|
+
const host = flags.host === 'true' || flags.host === '' ? undefined : normalizeHost(flags.host);
|
|
45
|
+
if (!host) {
|
|
46
|
+
fail(`--host "${flags.host}" is not a hostname or URL.`);
|
|
47
|
+
info('Pass the site\'s public address, for example --host https://www.example.com');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (host.startsWith('http://')) {
|
|
51
|
+
warn('--host is plaintext http://: anyone on the path can forge the release header and pass the gate.');
|
|
52
|
+
}
|
|
53
|
+
candidates = [host];
|
|
54
|
+
} else {
|
|
55
|
+
candidates = config.siteUrls?.length ? config.siteUrls : [probeUrl(config)].filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
// Safety gate: verify the origin is serving the expected release and the distribution is stable.
|
|
38
59
|
if (!force) {
|
|
39
60
|
const channel = flags.channel || flags.stage || 'production';
|
|
40
|
-
const blocked = await preflight(config.region,
|
|
61
|
+
const blocked = await preflight(config.region, candidates, config.updatesBucket, channel, config.distributionId);
|
|
41
62
|
if (blocked) {
|
|
42
63
|
fail(blocked);
|
|
43
64
|
info('Pass --force to override this check.');
|
|
@@ -105,13 +126,24 @@ export async function cachePurgeCommand(flags: Record<string, string>): Promise<
|
|
|
105
126
|
}
|
|
106
127
|
}
|
|
107
128
|
|
|
129
|
+
/** `www.example.com` or `https://www.example.com/` to `https://www.example.com`; undefined if it is not a host. */
|
|
130
|
+
function normalizeHost(value: string): string | undefined {
|
|
131
|
+
const withScheme = /^https?:\/\//i.test(value) ? value : `https://${value}`;
|
|
132
|
+
try {
|
|
133
|
+
const u = new URL(withScheme);
|
|
134
|
+
return u.host && !/\s/.test(value) ? `${u.protocol}//${u.host}` : undefined;
|
|
135
|
+
} catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
108
140
|
/**
|
|
109
141
|
* Pre-purge safety checks. Returns an error message if blocked, null if clear.
|
|
110
142
|
* @internal Exported for testing.
|
|
111
143
|
*/
|
|
112
144
|
export async function preflight(
|
|
113
145
|
region: string,
|
|
114
|
-
siteUrl: string,
|
|
146
|
+
siteUrl: string | readonly string[],
|
|
115
147
|
updatesBucket: string,
|
|
116
148
|
channel: string,
|
|
117
149
|
distributionId?: string,
|
|
@@ -133,27 +165,55 @@ export async function preflight(
|
|
|
133
165
|
return `Channel manifest for "${channel}" has no updateId.`;
|
|
134
166
|
}
|
|
135
167
|
|
|
136
|
-
// 2. Probe
|
|
137
|
-
|
|
168
|
+
// 2. Probe for the serving release. The gate passes when ANY candidate serves the expected
|
|
169
|
+
// release: a CloudFront Function can gate by Host, so the raw cloudfront.net address may answer
|
|
170
|
+
// 403 while the site's own hostname answers 200 with x-everystack-release.
|
|
171
|
+
const urls = (typeof siteUrl === 'string' ? [siteUrl] : [...siteUrl]).filter(Boolean);
|
|
172
|
+
if (urls.length === 0) {
|
|
138
173
|
return (
|
|
139
174
|
'No site URL for this stage, so the origin cannot be probed. ' +
|
|
140
|
-
'
|
|
141
|
-
'
|
|
175
|
+
'Pass --host <the site\'s public URL>, set CDN_URL on the API function and redeploy, or tag the distribution sst:app/sst:stage ' +
|
|
176
|
+
'and re-run with --refresh. Or pass --force to skip this check.'
|
|
142
177
|
);
|
|
143
178
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
179
|
+
const outcomes: Array<{ url: string; serving?: string | null; status?: number; error?: string }> = [];
|
|
180
|
+
let served = false;
|
|
181
|
+
for (const url of urls) {
|
|
182
|
+
try {
|
|
183
|
+
const res = await fetch(`${url}?_probe=${Date.now()}`, { redirect: 'follow', signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
184
|
+
const serving = res.headers.get('x-everystack-release');
|
|
185
|
+
if (serving === expectedUpdateId) {
|
|
186
|
+
served = true;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
outcomes.push({ url, serving, status: res.status });
|
|
190
|
+
} catch (err: any) {
|
|
191
|
+
outcomes.push({ url, error: err.message });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!served) {
|
|
195
|
+
const want = expectedUpdateId.slice(0, 12);
|
|
196
|
+
const stale = 'Purging now would expose stale containers to uncached traffic.';
|
|
197
|
+
if (outcomes.length === 1) {
|
|
198
|
+
const o = outcomes[0];
|
|
199
|
+
if (o.error !== undefined) return `Could not reach origin at ${o.url}: ${o.error}`;
|
|
149
200
|
return (
|
|
150
|
-
`Origin is serving release ${serving ? serving.slice(0, 12) + '...' : '(none)'} ` +
|
|
151
|
-
`but the channel pointer is ${
|
|
152
|
-
|
|
201
|
+
`Origin is serving release ${o.serving ? o.serving.slice(0, 12) + '...' : '(none)'} ` +
|
|
202
|
+
`but the channel pointer is ${want}.... Probed ${o.url}. ${stale}` +
|
|
203
|
+
(o.serving ? '' : " If the site's public hostname differs, pass --host <url>.")
|
|
153
204
|
);
|
|
154
205
|
}
|
|
155
|
-
|
|
156
|
-
|
|
206
|
+
const tried = outcomes
|
|
207
|
+
.map((o) =>
|
|
208
|
+
o.error !== undefined
|
|
209
|
+
? `${o.url} (unreachable: ${o.error})`
|
|
210
|
+
: `${o.url} (${o.serving ? 'release ' + o.serving.slice(0, 12) + '...' : `HTTP ${o.status}, no x-everystack-release header`})`,
|
|
211
|
+
)
|
|
212
|
+
.join(', ');
|
|
213
|
+
return (
|
|
214
|
+
`No probed URL serves release ${want}...: ${tried}. ${stale} ` +
|
|
215
|
+
"If the site's public hostname is not listed, pass --host <url>."
|
|
216
|
+
);
|
|
157
217
|
}
|
|
158
218
|
|
|
159
219
|
// 3. Check distribution status (if discoverable).
|
package/src/cli/commands/diag.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* everystack diag [url] --hydration # Analyze SSR hydration issues
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { resolveConfig, probeUrl } from '../config';
|
|
16
|
+
import { resolveConfig, probeUrl, ambiguousSiteNote } from '../config';
|
|
17
17
|
import { getFromS3 } from '../aws';
|
|
18
18
|
import { step, success, fail, info, warn } from '../output';
|
|
19
19
|
import { analyzeHtml, generateHydrationReport } from '../hydration-analyzer';
|
|
@@ -54,6 +54,8 @@ export async function diagCommand(
|
|
|
54
54
|
let url = positionalUrl;
|
|
55
55
|
if (!url) {
|
|
56
56
|
url = probeUrl(config);
|
|
57
|
+
const ambiguous = ambiguousSiteNote(config);
|
|
58
|
+
if (ambiguous) info(ambiguous);
|
|
57
59
|
if (!url) {
|
|
58
60
|
fail('No URL provided and no site URL for this stage. Pass a URL, set CDN_URL on the API function, or use --stage on a deployed stage.');
|
|
59
61
|
process.exit(1);
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type ExtractedRun,
|
|
22
22
|
type LhReport,
|
|
23
23
|
} from '../lighthouse';
|
|
24
|
-
import { resolveConfig, probeUrl } from '../config';
|
|
24
|
+
import { resolveConfig, probeUrl, ambiguousSiteNote } from '../config';
|
|
25
25
|
import { step, info, fail, warn } from '../output';
|
|
26
26
|
|
|
27
27
|
function runLighthouse(url: string, outPath: string): LhReport {
|
|
@@ -70,6 +70,8 @@ export async function auditLighthouseCommand(flags: Record<string, string>): Pro
|
|
|
70
70
|
try {
|
|
71
71
|
const config = await resolveConfig(flags.stage);
|
|
72
72
|
host = probeUrl(config);
|
|
73
|
+
const ambiguous = ambiguousSiteNote(config);
|
|
74
|
+
if (ambiguous) info(ambiguous);
|
|
73
75
|
} catch (err: any) {
|
|
74
76
|
fail(err.message);
|
|
75
77
|
info('Pass --host <url> to audit a specific URL without resolving a stage.');
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
type CatalogGrant,
|
|
27
27
|
type ProbeResult,
|
|
28
28
|
} from '../security-probe';
|
|
29
|
-
import { resolveConfig, opsFunction, probeUrl } from '../config';
|
|
29
|
+
import { resolveConfig, opsFunction, probeUrl, ambiguousSiteNote } from '../config';
|
|
30
30
|
import { invokeAction } from '../aws';
|
|
31
31
|
import { step, info, fail, success, warn } from '../output';
|
|
32
32
|
|
|
@@ -193,6 +193,8 @@ export async function securityProbeCommand(flags: Record<string, string>): Promi
|
|
|
193
193
|
fail('No router URL to probe. Pass --host or ensure the stage has a deployed URL.');
|
|
194
194
|
process.exit(1);
|
|
195
195
|
}
|
|
196
|
+
const ambiguous = flags.host ? undefined : ambiguousSiteNote(config);
|
|
197
|
+
if (ambiguous) info(ambiguous);
|
|
196
198
|
info(`Region: ${config.region}, Function: ${opsFunction(config)}, URL: ${base}`);
|
|
197
199
|
|
|
198
200
|
let results: ProbeResult[];
|
package/src/cli/config.ts
CHANGED
|
@@ -16,6 +16,10 @@ export interface CliConfig {
|
|
|
16
16
|
baseUrl: string;
|
|
17
17
|
/** What cache:purge and update's release check probe. Falls back to baseUrl when absent. */
|
|
18
18
|
siteUrl?: string;
|
|
19
|
+
/** Candidate URLs to probe, best first (see DiscoveredConfig.siteUrls). Absent on older configs. */
|
|
20
|
+
siteUrls?: string[];
|
|
21
|
+
/** The stage distribution's concrete aliases (empty when CDN_URL is set). */
|
|
22
|
+
siteAliases?: string[];
|
|
19
23
|
region: string;
|
|
20
24
|
apiFunctionName: string;
|
|
21
25
|
/** Dedicated operator function (db/console actions). Falls back to apiFunctionName. */
|
|
@@ -41,6 +45,20 @@ export function probeUrl(config: Pick<CliConfig, 'baseUrl' | 'siteUrl'>): string
|
|
|
41
45
|
return config.siteUrl || config.baseUrl || '';
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
/**
|
|
49
|
+
* One line for single-probe commands when the stage's distribution has several aliases and no
|
|
50
|
+
* CDN_URL: siteUrl is then the cloudfront.net domain, not a chosen host. A green there is a result
|
|
51
|
+
* for a host users may never hit, so say which one it is, every run. Undefined when unambiguous.
|
|
52
|
+
*/
|
|
53
|
+
export function ambiguousSiteNote(config: Pick<CliConfig, 'baseUrl' | 'siteUrl' | 'siteAliases'>): string | undefined {
|
|
54
|
+
const aliases = config.siteAliases ?? [];
|
|
55
|
+
if (config.baseUrl || aliases.length < 2) return undefined;
|
|
56
|
+
return (
|
|
57
|
+
`Site URL is ambiguous: this stage's distribution has aliases ${aliases.join(', ')}. Probing ${probeUrl(config)}. ` +
|
|
58
|
+
'Choose one with --host <alias> (or pass the URL to diag and bundle:audit).'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
/** Operator invoke target — dedicated ops function when deployed, else the API function. */
|
|
45
63
|
export function opsFunction(config: CliConfig): string {
|
|
46
64
|
return config.opsFunctionName ?? config.apiFunctionName;
|
|
@@ -168,6 +186,7 @@ export async function resolveConfig(stage?: string): Promise<CliConfig> {
|
|
|
168
186
|
return {
|
|
169
187
|
baseUrl: outputs.routerUrl,
|
|
170
188
|
siteUrl: outputs.routerUrl,
|
|
189
|
+
siteUrls: [outputs.routerUrl],
|
|
171
190
|
region,
|
|
172
191
|
apiFunctionName: outputs.apiFunctionName,
|
|
173
192
|
opsFunctionName: outputs.opsFunctionName,
|
|
@@ -33,13 +33,16 @@ interface DistributionSummaryLite {
|
|
|
33
33
|
*/
|
|
34
34
|
export interface DistributionMatch {
|
|
35
35
|
id: string;
|
|
36
|
-
/** cloudfront.net domain, no scheme
|
|
36
|
+
/** cloudfront.net domain, no scheme. A CloudFront Function may still answer 403 on it. */
|
|
37
37
|
domain?: string;
|
|
38
|
+
/** Concrete custom-domain aliases (wildcards dropped), in CloudFront's order: the site's real hostnames. */
|
|
39
|
+
aliases: string[];
|
|
38
40
|
by: 'host' | 'comment' | 'tag';
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
function toMatch(dist: DistributionSummaryLite, by: DistributionMatch['by']): DistributionMatch | undefined {
|
|
42
|
-
|
|
44
|
+
const aliases = (dist.Aliases?.Items ?? []).filter((a) => !a.includes('*'));
|
|
45
|
+
return dist.Id ? { id: dist.Id, domain: dist.DomainName, aliases, by } : undefined;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
function matchByHost(items: readonly DistributionSummaryLite[], routerHost?: string): DistributionMatch | undefined {
|
package/src/cli/discover.ts
CHANGED
|
@@ -27,6 +27,14 @@ interface DiscoveredConfig {
|
|
|
27
27
|
* tag-confirmed CloudFront distribution, else ''. Probe-only; never shipped.
|
|
28
28
|
*/
|
|
29
29
|
siteUrl: string;
|
|
30
|
+
/**
|
|
31
|
+
* Every URL worth probing, best first: CDN_URL alone when set, else the distribution's custom
|
|
32
|
+
* aliases then its cloudfront.net domain. A CloudFront Function can gate by Host and answer 403 on
|
|
33
|
+
* the raw domain, so the gate passes if ANY of these serves the release. siteUrl is the first.
|
|
34
|
+
*/
|
|
35
|
+
siteUrls: string[];
|
|
36
|
+
/** The distribution's concrete aliases; two or more make siteUrl a guess, which commands announce. */
|
|
37
|
+
siteAliases: string[];
|
|
30
38
|
region: string;
|
|
31
39
|
apiFunctionName: string;
|
|
32
40
|
opsFunctionName?: string;
|
|
@@ -309,6 +317,8 @@ export async function discoverConfig(
|
|
|
309
317
|
|
|
310
318
|
// Distribution discovery is best-effort (status CDN section, cache:purge --invalidate)
|
|
311
319
|
let distributionId: string | undefined;
|
|
320
|
+
let distributionHosts: string[] = [];
|
|
321
|
+
let distributionAliases: string[] = [];
|
|
312
322
|
let distributionDomain: string | undefined;
|
|
313
323
|
try {
|
|
314
324
|
const { discoverDistributionMatch } = await import('./discover-distribution');
|
|
@@ -323,7 +333,11 @@ export async function discoverConfig(
|
|
|
323
333
|
distributionId = match?.id;
|
|
324
334
|
// A Comment-substring match can name another app's stage; only a confirmed one may say
|
|
325
335
|
// which site a purge probes.
|
|
326
|
-
if (match && match.by !== 'comment')
|
|
336
|
+
if (match && match.by !== 'comment') {
|
|
337
|
+
distributionAliases = match.aliases;
|
|
338
|
+
distributionDomain = match.domain;
|
|
339
|
+
distributionHosts = [...match.aliases, ...(match.domain ? [match.domain] : [])];
|
|
340
|
+
}
|
|
327
341
|
} catch {
|
|
328
342
|
// Non-fatal
|
|
329
343
|
}
|
|
@@ -331,7 +345,18 @@ export async function discoverConfig(
|
|
|
331
345
|
// CDN_URL is optional on the API Lambda. Without it the stage's own distribution supplies the
|
|
332
346
|
// probe URL — and only that: baseUrl stays as deployed (see DiscoveredConfig).
|
|
333
347
|
const baseUrl = lambdaResult.routerUrl || '';
|
|
334
|
-
|
|
348
|
+
// Gate candidates: every host worth trying, best first. siteUrl is what single-probe commands use
|
|
349
|
+
// with no fallback, so it must not be a guess: CDN_URL, else the ONE alias if there is exactly one,
|
|
350
|
+
// else the cloudfront.net domain (loud on a hostname-gating site, and commands say which host).
|
|
351
|
+
const siteUrls = baseUrl ? [baseUrl] : distributionHosts.map((h) => `https://${h}`);
|
|
352
|
+
const siteAliases = baseUrl ? [] : distributionAliases;
|
|
353
|
+
const siteUrl = baseUrl
|
|
354
|
+
? baseUrl
|
|
355
|
+
: distributionAliases.length === 1
|
|
356
|
+
? `https://${distributionAliases[0]}`
|
|
357
|
+
: distributionDomain
|
|
358
|
+
? `https://${distributionDomain}`
|
|
359
|
+
: '';
|
|
335
360
|
|
|
336
361
|
if (!bucketsResult.updatesBucket) {
|
|
337
362
|
throw new Error(
|
|
@@ -368,6 +393,8 @@ export async function discoverConfig(
|
|
|
368
393
|
return {
|
|
369
394
|
baseUrl,
|
|
370
395
|
siteUrl,
|
|
396
|
+
siteUrls,
|
|
397
|
+
siteAliases,
|
|
371
398
|
region,
|
|
372
399
|
apiFunctionName: lambdaResult.functionName,
|
|
373
400
|
opsFunctionName: opsFn,
|
|
@@ -387,13 +414,15 @@ export async function discoverConfig(
|
|
|
387
414
|
// ---------------------------------------------------------------------------
|
|
388
415
|
|
|
389
416
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
390
|
-
const CACHE_VERSION =
|
|
417
|
+
const CACHE_VERSION = 7; // Bump when CachedOutputs schema changes to invalidate stale caches (5: +backupsBucket, 6: +siteUrl, 7: +siteUrls; v6 pinned the cloudfront.net domain, which a hostname-gating function answers 403)
|
|
391
418
|
|
|
392
419
|
interface CachedOutputs {
|
|
393
420
|
_cachedAt: number;
|
|
394
421
|
_version?: number;
|
|
395
422
|
routerUrl?: string;
|
|
396
423
|
siteUrl?: string;
|
|
424
|
+
siteUrls?: string[];
|
|
425
|
+
siteAliases?: string[];
|
|
397
426
|
apiFunctionName?: string;
|
|
398
427
|
opsFunctionName?: string;
|
|
399
428
|
imageFunctionName?: string;
|
|
@@ -422,6 +451,8 @@ export async function getCachedConfig(stage: string): Promise<DiscoveredConfig |
|
|
|
422
451
|
return {
|
|
423
452
|
baseUrl: cached.routerUrl || '',
|
|
424
453
|
siteUrl: cached.siteUrl ?? cached.routerUrl ?? '',
|
|
454
|
+
siteUrls: cached.siteUrls ?? [],
|
|
455
|
+
siteAliases: cached.siteAliases ?? [],
|
|
425
456
|
region: process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1',
|
|
426
457
|
apiFunctionName: cached.apiFunctionName,
|
|
427
458
|
opsFunctionName: cached.opsFunctionName,
|
|
@@ -500,6 +531,8 @@ export async function setCachedConfig(stage: string, config: DiscoveredConfig):
|
|
|
500
531
|
_version: CACHE_VERSION,
|
|
501
532
|
routerUrl: config.baseUrl,
|
|
502
533
|
siteUrl: config.siteUrl,
|
|
534
|
+
siteUrls: config.siteUrls,
|
|
535
|
+
siteAliases: config.siteAliases,
|
|
503
536
|
apiFunctionName: config.apiFunctionName,
|
|
504
537
|
opsFunctionName: config.opsFunctionName,
|
|
505
538
|
imageFunctionName: config.imageFunctionName,
|
package/src/cli/index.ts
CHANGED
|
@@ -367,6 +367,7 @@ Usage:
|
|
|
367
367
|
everystack cache:purge [--stage <name>] --origin api Bust API origin cache (api|media|web)
|
|
368
368
|
everystack cache:purge [--stage <name>] --path "/api/posts" Bust specific path cache
|
|
369
369
|
everystack cache:purge [--stage <name>] --invalidate Also create CloudFront edge invalidation
|
|
370
|
+
everystack cache:purge [--stage <name>] --host <url> Probe this public URL for the release check (a site whose CloudFront Function gates by hostname)
|
|
370
371
|
everystack diag [url] [--path /about] [--channel <name>] [--stage <name>] Diagnose page freshness
|
|
371
372
|
everystack diag [url] --hydration Analyze SSR hydration issues (fetches full HTML)
|
|
372
373
|
everystack analyze:ssr [--app ./app] Scan app code for SSR anti-patterns
|