@ultimat3/cli 23.0.0 → 25.0.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/CLAUDE.md +28 -13
- package/README.md +32 -7
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +30 -36
- package/src/api-registration.ts +89 -0
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +26 -25
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +4 -10
- package/src/app-mcp.ts +31 -46
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +123 -0
- package/src/app-permissions-site.ts +48 -0
- package/src/app-permissions.ts +7 -17
- package/src/app-root.ts +9 -5
- package/src/bin.ts +3 -7
- package/src/browser-launcher.ts +4 -2
- package/src/budgets-charged.ts +60 -0
- package/src/budgets-scan.ts +76 -0
- package/src/budgets-unmeasured.ts +91 -0
- package/src/budgets.ts +117 -154
- package/src/cdp-shot-driver.ts +20 -1
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +151 -22
- package/src/cmd-db-backfill.ts +26 -13
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +10 -6
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -356
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +57 -80
- package/src/cmd-env.ts +55 -16
- package/src/cmd-generate.ts +89 -32
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +21 -17
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +33 -140
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +71 -22
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +24 -8
- package/src/cmd-routes.ts +15 -5
- package/src/cmd-secrets.ts +47 -6
- package/src/cmd-shot-matrix.ts +22 -7
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +46 -41
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +14 -30
- package/src/cmd-verify.ts +4 -4
- package/src/coverage-lcov.ts +4 -2
- package/src/db-backfill.ts +2 -2
- package/src/db-replay-engine.ts +4 -10
- package/src/db-subscribes.ts +27 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +56 -30
- package/src/dev-lock.ts +100 -31
- package/src/dev-rebuild.ts +80 -0
- package/src/dev-restart-retire.ts +46 -0
- package/src/dev-route-table.ts +30 -8
- package/src/dev-supervisor.ts +114 -22
- package/src/dev-traces.ts +53 -20
- package/src/dispatch.ts +17 -13
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/drift.ts +29 -6
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +29 -3
- package/src/error-page-csp.ts +14 -15
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/exec.ts +8 -0
- package/src/favicon.ts +2 -2
- package/src/fix-command.ts +9 -1
- package/src/framework-env.ts +148 -0
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +25 -3
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-files.ts +4 -3
- package/src/generate-kinds.ts +18 -0
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +95 -0
- package/src/generate-write.ts +47 -13
- package/src/gh-target.ts +12 -7
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +25 -9
- package/src/i18n-index.ts +5 -6
- package/src/i18n-registration.ts +7 -9
- package/src/import-scan.ts +28 -3
- package/src/index.ts +2 -6
- package/src/island-bundle.ts +57 -79
- package/src/island-capture.ts +1 -1
- package/src/island-duplicate-refusal.ts +57 -0
- package/src/island-duplicates.ts +221 -0
- package/src/island-entry-missing.ts +64 -0
- package/src/island-harness-route.ts +20 -4
- package/src/island-harness-script.ts +23 -2
- package/src/island-identity.ts +17 -14
- package/src/island-package-dedupe.ts +150 -0
- package/src/island-realtime.ts +36 -16
- package/src/island-routes.ts +2 -2
- package/src/island-runtime.ts +241 -0
- package/src/island-sources.ts +105 -0
- package/src/island-states-load.ts +6 -0
- package/src/island-store.ts +90 -6
- package/src/jobs-json.ts +0 -26
- package/src/jobs-report.ts +36 -20
- package/src/local-cli-handoff.ts +90 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +20 -4
- package/src/mcp-host.ts +22 -14
- package/src/mcp-test-run.ts +38 -0
- package/src/mcp-ui-inspect.ts +3 -2
- package/src/mcp-ui-interact.ts +3 -2
- package/src/mcp-ui.ts +8 -7
- package/src/measure-scope.ts +15 -10
- package/src/messages.ts +29 -16
- package/src/metrics-endpoint.ts +28 -3
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +24 -0
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +19 -31
- package/src/parse.ts +0 -8
- package/src/permission-grants.ts +3 -3
- package/src/posix-path.ts +30 -0
- package/src/prerender-locales.ts +6 -4
- package/src/prerender-out.ts +34 -7
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +71 -103
- package/src/pwa-artifacts.ts +33 -94
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/registry.ts +2 -0
- package/src/retry-memo.ts +1 -1
- package/src/revalidated-response.ts +4 -3
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +124 -29
- package/src/role-start.ts +69 -85
- package/src/role-sync.ts +119 -25
- package/src/root-env.ts +157 -28
- package/src/route-islands.ts +69 -0
- package/src/runtime-admin.ts +4 -1
- package/src/runtime-assets.ts +26 -9
- package/src/runtime-bindings.ts +27 -7
- package/src/runtime-cache.ts +46 -55
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +76 -41
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +4 -4
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +43 -22
- package/src/runtime-realtime.ts +11 -45
- package/src/runtime-render.ts +136 -114
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +49 -26
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +13 -8
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +51 -12
- package/src/serve-drain.ts +11 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +31 -15
- package/src/serve.ts +27 -10
- package/src/server-barrels.ts +6 -3
- package/src/shot-browser.ts +1 -1
- package/src/shot-locale.ts +6 -33
- package/src/shot-settle.ts +3 -3
- package/src/shot-theme.ts +1 -1
- package/src/shot-viewport.ts +152 -0
- package/src/signal-shred.ts +26 -12
- package/src/site-asset-routes.ts +2 -2
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +7 -5
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-files.ts +19 -3
- package/src/static-report.ts +5 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/style-routes.ts +2 -2
- package/src/sw-artifacts.ts +8 -8
- package/src/sw-precache-plan.ts +34 -4
- package/src/sync-authenticator.ts +3 -3
- package/src/sync-url.ts +3 -3
- package/src/tasks-facts.ts +3 -29
- package/src/templates/action.ts +7 -4
- package/src/templates/backfill.ts +4 -4
- package/src/templates/entity.ts +2 -2
- package/src/templates/github/ci.yml.ts +28 -18
- package/src/templates/github/image.yml.ts +92 -0
- package/src/templates/github/runner.ts +18 -0
- package/src/templates/guard-bare-error.ts +16 -2
- package/src/templates/guard-unzoned-date.ts +56 -6
- package/src/templates/index.ts +1 -1
- package/src/templates/job-body-test.ts +2 -2
- package/src/templates/job.ts +6 -6
- package/src/templates/naming.ts +28 -0
- package/src/templates/query.ts +9 -8
- package/src/templates/resource-page.ts +9 -7
- package/src/templates/resource-service.ts +2 -2
- package/src/templates/route.ts +26 -14
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +8 -3
- package/src/templates/scaffold-auth.ts +205 -70
- package/src/templates/scaffold-bin.ts +122 -0
- package/src/templates/scaffold-claude-commands.ts +11 -11
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +16 -8
- package/src/templates/scaffold-dashboard-bare.ts +8 -4
- package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
- package/src/templates/scaffold-dashboard-example.ts +112 -172
- package/src/templates/scaffold-dashboard-shared.ts +62 -1
- package/src/templates/scaffold-dashboard.ts +9 -5
- package/src/templates/scaffold-db-client.ts +4 -2
- package/src/templates/scaffold-db-package.ts +2 -2
- package/src/templates/scaffold-docs.ts +20 -77
- package/src/templates/scaffold-domain-package.ts +0 -4
- package/src/templates/scaffold-entries.ts +4 -3
- package/src/templates/scaffold-env.ts +6 -3
- package/src/templates/scaffold-gitattributes.ts +59 -0
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +100 -19
- package/src/templates/scaffold-helm.ts +62 -5
- package/src/templates/scaffold-http.ts +1 -1
- package/src/templates/scaffold-i18n.ts +15 -2
- package/src/templates/scaffold-icon.ts +2 -2
- package/src/templates/scaffold-repo.ts +27 -16
- package/src/templates/scaffold-shell.ts +55 -37
- package/src/test-counts.ts +2 -6
- package/src/test-ignores.ts +25 -0
- package/src/test-only-shipped.ts +193 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +7 -15
- package/src/test-shards.ts +40 -95
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +8 -33
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +29 -48
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +207 -33
- package/src/verify-merge.ts +108 -43
- package/src/verify-part-step.ts +98 -0
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +32 -8
- package/src/verify-shard.ts +13 -2
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +5 -3
- package/src/worker-bundle.ts +2 -2
- package/src/workspace-checks.ts +53 -7
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +82 -7
- package/src/browser-launcher-fake-html.ts +0 -80
- package/src/browser-launcher-fake.ts +0 -168
- package/src/build-errors.ts +0 -54
- package/src/jobs-drain.ts +0 -163
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
- package/src/verify-errors.ts +0 -58
package/src/theme-boot.ts
CHANGED
|
@@ -1,40 +1,15 @@
|
|
|
1
|
-
// The no-flash theme script, inlined into every document a served or exported process writes,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// scripts the framework exported was wired into a document: an app that wanted to open dark had to
|
|
5
|
-
// write its own boot script AND admit it to the CSP by hand — which every app forgot in one of the
|
|
6
|
-
// two places. Sibling of `app-auth.ts`'s `loadSignInPath` and imports the config for the same
|
|
7
|
-
// reason: a regex over the app's source is the pattern `app-load.ts` refuses.
|
|
1
|
+
// The no-flash theme script, inlined into every document a served or exported process writes, with
|
|
2
|
+
// `theme.defaultMode` (off the one loader, `app-config-load.ts`) as its fallback — and the CSP source
|
|
3
|
+
// that admits it, so no app writes its own boot script and forgets to admit it.
|
|
8
4
|
|
|
9
|
-
// why: Bun exposes no path-join primitive, and the config path is app-root-relative — the same
|
|
10
|
-
// necessity `app-auth.ts` records.
|
|
11
|
-
import { join } from 'node:path';
|
|
12
5
|
import type { ThemeMode } from '@ultimat3/core';
|
|
13
6
|
import { cspHashSource } from '@ultimat3/http';
|
|
14
7
|
import { renderHead, themeScript, themeScriptBody } from '@ultimat3/render';
|
|
15
|
-
import {
|
|
16
|
-
import { APP_CONFIG_FILE } from './app-root';
|
|
8
|
+
import { loadAppConfig } from './app-config-load';
|
|
17
9
|
|
|
18
|
-
|
|
19
|
-
typeof value === 'object' && value !== null;
|
|
20
|
-
|
|
21
|
-
const isThemeMode = (value: unknown): value is ThemeMode =>
|
|
22
|
-
value === 'light' || value === 'dark' || value === 'system';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* `theme.defaultMode`, or `'system'` — the value `defineConfig` fills in — when the app declares
|
|
26
|
-
* none or the file is absent. Structural, never `instanceof`, for `loadSignInPath`'s reason.
|
|
27
|
-
*/
|
|
10
|
+
/** `theme.defaultMode`, or `'system'` — core's own default — for a root with no config file. */
|
|
28
11
|
export async function loadThemeMode(root: string): Promise<ThemeMode> {
|
|
29
|
-
|
|
30
|
-
if (!(await Bun.file(configPath).exists())) return 'system';
|
|
31
|
-
const module = (await import(configPath)) as Record<string, unknown>;
|
|
32
|
-
const config = module[APP_CONFIG_EXPORT];
|
|
33
|
-
if (!isRecord(config)) return 'system';
|
|
34
|
-
const theme = config['theme'];
|
|
35
|
-
if (!isRecord(theme)) return 'system';
|
|
36
|
-
const mode = theme['defaultMode'];
|
|
37
|
-
return isThemeMode(mode) ? mode : 'system';
|
|
12
|
+
return (await loadAppConfig(root))?.theme.defaultMode ?? 'system';
|
|
38
13
|
}
|
|
39
14
|
|
|
40
15
|
export interface ThemeBoot {
|
|
@@ -47,8 +22,8 @@ export interface ThemeBoot {
|
|
|
47
22
|
/**
|
|
48
23
|
* Tag and hash from ONE body: `themeScriptBody` is what `themeScript` inlines, so a policy hashed
|
|
49
24
|
* here admits the script the document carries and not a restatement of it. The storage key is
|
|
50
|
-
*
|
|
51
|
-
*
|
|
25
|
+
* `@ultimat3/core`'s `THEME_STORAGE_KEY`, the one `@ultimat3/ui`'s toggle writes — so the toggle
|
|
26
|
+
* writes the key the boot reads.
|
|
52
27
|
*/
|
|
53
28
|
export function themeBoot(mode: ThemeMode): ThemeBoot {
|
|
54
29
|
const options = { fallback: mode };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The app's brand, declared once as `export const brand = defineTheme(…)` in
|
|
2
|
+
// `apps/web/shared/theme.ts`, read here and nowhere else: the `<style>` every document the boot
|
|
3
|
+
// renders carries after its stylesheet, and the body `style-src` admits. No module, no brand: an
|
|
4
|
+
// unthemed app emits no tag and extends no policy.
|
|
5
|
+
|
|
6
|
+
// why: Bun exposes no path-join primitive, and the module path is app-root-relative.
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { ConfigInvalidError, describeValue } from '@ultimat3/core';
|
|
9
|
+
import { renderHead } from '@ultimat3/render';
|
|
10
|
+
|
|
11
|
+
/** The conventional module, beside `shared/global.ts` — one fixed path, as `API_INDEX` is. */
|
|
12
|
+
export const APP_THEME_MODULE = 'apps/web/shared/theme.ts';
|
|
13
|
+
/** The named export the module declares — `@ultimat3/ui`'s README spells it `brand`. */
|
|
14
|
+
export const APP_THEME_EXPORT = 'brand';
|
|
15
|
+
|
|
16
|
+
export interface ThemeBrand {
|
|
17
|
+
/** The rendered `<style>`, for `DocumentOptions.brandHead`. */
|
|
18
|
+
readonly head: string;
|
|
19
|
+
/** Exactly the text between the tags — the body a boot hands `inlineStyleSources`. */
|
|
20
|
+
readonly style: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Code to paste, as `loadAppConfig`'s is: the module path is already in the cause. */
|
|
24
|
+
const FIX = `export const ${APP_THEME_EXPORT} = defineTheme({ preset: 'scifi' });`;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `undefined` when the app has no theme module or its brand renders no CSS. A module that will not
|
|
28
|
+
* import throws as it did, so a palette `defineTheme()` refuses fails the boot with
|
|
29
|
+
* `X_UI_CONTRAST_INSUFFICIENT` (or its token code) and not a rewording of it.
|
|
30
|
+
*/
|
|
31
|
+
export async function loadThemeBrand(root: string): Promise<ThemeBrand | undefined> {
|
|
32
|
+
const path = join(root, APP_THEME_MODULE);
|
|
33
|
+
if (!(await Bun.file(path).exists())) return undefined;
|
|
34
|
+
const module = (await import(path)) as Record<string, unknown>;
|
|
35
|
+
const css = brandCss(path, module);
|
|
36
|
+
if (css.length === 0) return undefined;
|
|
37
|
+
// `renderHead` escapes `</` in a style body, which would make the tag's text differ from the
|
|
38
|
+
// hashed one — so a body that needs escaping is refused above rather than escaped here.
|
|
39
|
+
return { head: renderHead([{ kind: 'style', key: 'style:brand', content: css }]), style: css };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The brand's stylesheet, read structurally: `@ultimat3/ui` is a tier below and this package does
|
|
44
|
+
* not import it, so a `Brand` is `{ css: string }`. `defineTheme()` never emits `<` — its values
|
|
45
|
+
* are validated against grammars that exclude it — so one here came from a hand-built object, and
|
|
46
|
+
* it is the one character that could end the element and start markup.
|
|
47
|
+
*/
|
|
48
|
+
function brandCss(path: string, module: Record<string, unknown>): string {
|
|
49
|
+
const brand = Object.hasOwn(module, APP_THEME_EXPORT) ? module[APP_THEME_EXPORT] : undefined;
|
|
50
|
+
const css = typeof brand === 'object' && brand !== null && 'css' in brand ? brand.css : undefined;
|
|
51
|
+
if (typeof css !== 'string') {
|
|
52
|
+
throw new ConfigInvalidError({
|
|
53
|
+
cause: `${path} exports no ${APP_THEME_EXPORT} built by defineTheme() (${describeValue(brand)}), so no document can carry the app's theme — declare it with defineTheme from @ultimat3/ui`,
|
|
54
|
+
fix: FIX,
|
|
55
|
+
meta: { key: APP_THEME_EXPORT },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (css.includes('<')) {
|
|
59
|
+
throw new ConfigInvalidError({
|
|
60
|
+
cause: `${path}'s ${APP_THEME_EXPORT}.css contains "<", which defineTheme() never emits and which could close the <style> element it is inlined in`,
|
|
61
|
+
fix: FIX,
|
|
62
|
+
meta: { key: APP_THEME_EXPORT },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return css;
|
|
66
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The refusal of a malformed `TRUSTED_PROXY_HOPS` — its own code, never `X_PORT_INVALID`: a proxy
|
|
2
|
+
// count is not a port, and an operator told "is not a TCP port number" looked for the wrong setting.
|
|
3
|
+
|
|
4
|
+
import { UltimateError } from '@ultimat3/core';
|
|
5
|
+
import { MAX_PROXY_HOPS } from '@ultimat3/http';
|
|
6
|
+
import { quoteArg } from './shell-quote';
|
|
7
|
+
|
|
8
|
+
export class TrustedProxyHopsInvalidError extends UltimateError {
|
|
9
|
+
constructor(input: { readonly value: string }) {
|
|
10
|
+
super({
|
|
11
|
+
code: 'X_TRUSTED_PROXY_HOPS_INVALID',
|
|
12
|
+
cause: `TRUSTED_PROXY_HOPS=${quoteArg(input.value)} is not a whole number of proxies from 1 to ${MAX_PROXY_HOPS} — the count that append to x-forwarded-for in front of this process; unset means none`,
|
|
13
|
+
fix: 'TRUSTED_PROXY_HOPS=1 ROLE=web bun apps/web/server.ts',
|
|
14
|
+
meta: { value: input.value, max: MAX_PROXY_HOPS },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// How many proxies sit in front of this process, read from `TRUSTED_PROXY_HOPS`. Its own module
|
|
2
|
+
// so both the web role (`role-start.ts`) and the sync node (`role-sync.ts`) read it without one
|
|
3
|
+
// importing the other — `role-start` imports `role-sync`, and the reverse edge was a cycle.
|
|
4
|
+
|
|
5
|
+
import { MAX_PROXY_HOPS } from '@ultimat3/http';
|
|
6
|
+
import type { Env } from './runtime-bindings';
|
|
7
|
+
import { TrustedProxyHopsInvalidError } from './trusted-hops-error';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* How many proxies is too many, and the number is **`@ultimat3/http`'s**, not this file's.
|
|
11
|
+
*
|
|
12
|
+
* `defineHttpConfig` screens the same setting — `assertFiniteCount('trustedProxyHops', …,
|
|
13
|
+
* MAX_PROXY_HOPS)` — and this screen used to say 16 where that one says 64, so one setting had two
|
|
14
|
+
* ceilings and a deployment behind 20 hops was accepted by the library and refused by the boot.
|
|
15
|
+
* Widened rather than narrowed: tightening the library would break a shipped public API, while
|
|
16
|
+
* this end only ever refused topologies http already supports.
|
|
17
|
+
*
|
|
18
|
+
* It was a duplicated literal until 2026-08-26, because `@ultimat3/http` did not export the
|
|
19
|
+
* constant. It does now, so this file IMPORTS it — a downward edge, tier 5 to tier 2 — and there is
|
|
20
|
+
* no second number left to drift. `runtime-overrides.test.ts` still probes both screens for the
|
|
21
|
+
* highest count each accepts, and it is kept rather than deleted: it compares BEHAVIOUR, so it also
|
|
22
|
+
* catches the two disagreeing for a reason a shared constant cannot fix, such as one side gaining a
|
|
23
|
+
* range check the other does not have.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How many proxies append to `x-forwarded-for` between the client and this process, or `null`
|
|
28
|
+
* when nothing in front of it is trusted.
|
|
29
|
+
*
|
|
30
|
+
* Read from the environment and not from `app.config.ts`, for the reason `PORT` and `ROLE` are: it
|
|
31
|
+
* is a fact about the DEPLOYMENT — one image runs behind an ingress in one cluster and behind
|
|
32
|
+
* nothing on a laptop — and an app that hardcoded it would be wrong in one of the two. `x dev`
|
|
33
|
+
* sets neither and gets `trustProxy: false`, which is correct: there is no proxy.
|
|
34
|
+
*
|
|
35
|
+
* Without this seam a container behind an ingress reads `ctx.ip` as the ingress's own socket
|
|
36
|
+
* address on every request, so the rate limiter keys the entire fleet's anonymous traffic into ONE
|
|
37
|
+
* bucket and a single scanner 429s every real signup.
|
|
38
|
+
*/
|
|
39
|
+
export function trustedHopsFromEnv(env: Env): number | null {
|
|
40
|
+
const raw = env['TRUSTED_PROXY_HOPS']?.trim();
|
|
41
|
+
if (raw === undefined || raw === '') return null;
|
|
42
|
+
const hops = Number(raw);
|
|
43
|
+
// A malformed count is refused rather than defaulted: reading the header at the wrong index is
|
|
44
|
+
// trusting a value the client typed, which is the failure trusting a proxy exists to avoid.
|
|
45
|
+
if (!/^\d+$/.test(raw) || !Number.isInteger(hops) || hops < 1 || hops > MAX_PROXY_HOPS) {
|
|
46
|
+
throw new TrustedProxyHopsInvalidError({ value: raw });
|
|
47
|
+
}
|
|
48
|
+
return hops;
|
|
49
|
+
}
|
package/src/unscanned-admin.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// silently, which is how the reference app shipped an admin nobody could open.
|
|
5
5
|
|
|
6
6
|
import { renderFixShellArg, stripComments } from '@ultimat3/core';
|
|
7
|
-
import {
|
|
7
|
+
import { describePages } from '@ultimat3/render';
|
|
8
8
|
import { ADMIN_FILE } from './admin-registration';
|
|
9
9
|
import type { Finding } from './output';
|
|
10
10
|
import { hasPathSegment } from './path-segments';
|
|
@@ -17,7 +17,7 @@ const DECLARES = /\bdefineAdmin\(/;
|
|
|
17
17
|
|
|
18
18
|
/** Any admin this process mounted — then every declaration was reached, wherever it sits. */
|
|
19
19
|
export const adminMounted = (): boolean =>
|
|
20
|
-
|
|
20
|
+
describePages().some((route) => route.mount?.by === 'defineAdmin');
|
|
21
21
|
|
|
22
22
|
/** Where the file belongs: its own name under the admin's home, `index.ts` as the declaration. */
|
|
23
23
|
const homeOf = (file: string): string => {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The registered codes this repo no longer throws, as a list `error-unthrown.ts` reads — its own
|
|
2
|
+
// file, so the literals naming the waived codes are not mistaken for a throw site.
|
|
3
|
+
|
|
4
|
+
/** Where the list lives, so a finding can name the file to edit. */
|
|
5
|
+
export const UNTHROWN_CODES_FILE = 'packages/cli/src/unthrown-codes.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The codes this repo registers and no longer throws — each a decision, and each row on the
|
|
9
|
+
* reference page says so in words. A LIST, never a prose match: the waiver was any row containing
|
|
10
|
+
* "not thrown" or "thrown by nothing", so a sentence that happened to use either phrase silenced the
|
|
11
|
+
* rule for its code. A listing whose code is thrown again, or is not registered, is
|
|
12
|
+
* `X_ERROR_CODE_UNTHROWN_STALE`.
|
|
13
|
+
*/
|
|
14
|
+
export const UNTHROWN_CODES: ReadonlySet<string> = new Set([
|
|
15
|
+
// `x db studio` is planned and unreleased; it exits X_NOT_IMPLEMENTED.
|
|
16
|
+
'X_DB_STUDIO_FAILED',
|
|
17
|
+
// 21.0.0: there is no LiveClient to register (X_REALTIME_UNINSTALLED replaced it).
|
|
18
|
+
'X_LIVE_CLIENT_MISSING',
|
|
19
|
+
// 21.0.0: liveHookFor was deleted; useQuery reads any query.
|
|
20
|
+
'X_QUERY_NOT_SUBSCRIBABLE',
|
|
21
|
+
// 21.0.0: the worker no longer POSTs a flush endpoint nothing mounted.
|
|
22
|
+
'X_PWA_SYNC_FLUSH_FAILED',
|
|
23
|
+
'X_PWA_SYNC_INCOMPLETE',
|
|
24
|
+
// 24.0.0: `x test --worker` was deleted; `x verify --only unit --shard i/n` is the one split.
|
|
25
|
+
'X_TEST_SHARD_FAILED',
|
|
26
|
+
]);
|
package/src/verify-checks.ts
CHANGED
|
@@ -6,19 +6,13 @@ import { existsSync } from 'node:fs';
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
8
8
|
import type { Manifest } from '@ultimat3/manifest';
|
|
9
|
-
import {
|
|
10
|
-
AGENTS_MD_FILENAME,
|
|
11
|
-
assertNoDrift,
|
|
12
|
-
MANIFEST_FILENAME,
|
|
13
|
-
verifyContract,
|
|
14
|
-
} from '@ultimat3/manifest';
|
|
9
|
+
import { AGENTS_MD_FILENAME, MANIFEST_FILENAME, verifyContract } from '@ultimat3/manifest';
|
|
15
10
|
import type { MetaIssue } from '@ultimat3/seo';
|
|
16
11
|
import { validateMeta } from '@ultimat3/seo';
|
|
17
12
|
import { checkAgentsMd } from './app-agents-md';
|
|
18
13
|
import { readAppSources } from './app-boundaries';
|
|
19
14
|
import { envExampleFindings } from './app-env';
|
|
20
15
|
import { appManifest, readAppManifest } from './app-manifest';
|
|
21
|
-
import { OPENAPI_FILE, openApiStaleness } from './app-openapi';
|
|
22
16
|
import { policyFindings } from './app-permissions';
|
|
23
17
|
import { APP_CONFIG_FILE } from './app-root';
|
|
24
18
|
import { asyncPageFindings } from './async-pages';
|
|
@@ -33,6 +27,11 @@ import { catalogFindings } from './i18n-registration';
|
|
|
33
27
|
import { unregisteredJobFindings } from './job-registration';
|
|
34
28
|
import { liveRouteFindings } from './live-routes';
|
|
35
29
|
import { withLoadFindings, withoutLoadFindings } from './load-findings';
|
|
30
|
+
import {
|
|
31
|
+
hasCommittedContract,
|
|
32
|
+
manifestMissingFindings,
|
|
33
|
+
manifestStaleness,
|
|
34
|
+
} from './manifest-staleness';
|
|
36
35
|
import { msg } from './messages';
|
|
37
36
|
import type { Finding } from './output';
|
|
38
37
|
import { findingFrom } from './output';
|
|
@@ -47,7 +46,7 @@ import type { VerifyStep } from './verify-step';
|
|
|
47
46
|
import { fromExec, fromFindings, hostFindings } from './verify-step';
|
|
48
47
|
import { TEST_STEPS } from './verify-tests';
|
|
49
48
|
import { typecheckArgs } from './verify-typecheck';
|
|
50
|
-
import {
|
|
49
|
+
import { checkPackageShape, fileSizeReport, hasWorkspacePackages } from './workspace-checks';
|
|
51
50
|
import { checkWorkspaceDependencies } from './workspace-graph';
|
|
52
51
|
|
|
53
52
|
/** The one file that makes the `roadmap` step answerable, and therefore what `applies` reads. */
|
|
@@ -121,7 +120,17 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
121
120
|
{
|
|
122
121
|
name: 'filesize',
|
|
123
122
|
summary: 'one file, one job',
|
|
124
|
-
|
|
123
|
+
// The band below the ceiling rides as warnings: listed in `--json` on a pass, never a failure.
|
|
124
|
+
run: async (ctx) => {
|
|
125
|
+
const { findings, warnings } = await fileSizeReport(ctx.root);
|
|
126
|
+
return {
|
|
127
|
+
...fromFindings(findings),
|
|
128
|
+
warnings: warnings.map(
|
|
129
|
+
(warning) =>
|
|
130
|
+
`${warning.path}: ${warning.lines} lines, ${warning.ceiling - warning.lines} under the ${warning.ceiling} line ceiling — plan the split`,
|
|
131
|
+
),
|
|
132
|
+
};
|
|
133
|
+
},
|
|
125
134
|
},
|
|
126
135
|
{
|
|
127
136
|
name: 'package-shape',
|
|
@@ -198,10 +207,11 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
198
207
|
{
|
|
199
208
|
name: 'contract-diff',
|
|
200
209
|
summary: 'the published contract vs the committed manifest',
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
// A BREAKING change against the committed manifest, and nothing else. Whether the committed
|
|
211
|
+
// files are stale — `openapi.json` included — is the `manifest` step's, through the one check
|
|
212
|
+
// `x manifest --check` runs (`manifest-staleness.ts`): reported here as well it was two
|
|
213
|
+
// steps and one command answering one question, and the step named `manifest` saying green.
|
|
214
|
+
applies: async (ctx) => existsSync(join(ctx.root, MANIFEST_FILENAME)),
|
|
205
215
|
async run(ctx) {
|
|
206
216
|
const committed = await readAppManifest(ctx.root);
|
|
207
217
|
const { manifest, findings } = await appManifest(ctx.root);
|
|
@@ -209,14 +219,13 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
209
219
|
return withoutLoadFindings(ctx.root, [
|
|
210
220
|
...findings,
|
|
211
221
|
...(committed === undefined ? [] : contractFindings(committed, manifest)),
|
|
212
|
-
...(await openApiStaleness(ctx.root, manifest)),
|
|
213
222
|
]);
|
|
214
223
|
},
|
|
215
224
|
},
|
|
216
225
|
{
|
|
217
226
|
name: 'budgets',
|
|
218
227
|
summary:
|
|
219
|
-
'per-route JS bytes
|
|
228
|
+
'per-route JS bytes, the global style layer every document carries, and the routes that boot nothing to receive their live rows',
|
|
220
229
|
// The global-style assertion rides here rather than becoming a step of its own, because this
|
|
221
230
|
// step already asks the one question it asks: what does the document this build emits actually
|
|
222
231
|
// contain? It is also the same app load — `appManifest` fills render's stylesheet registry on
|
|
@@ -346,7 +355,6 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
346
355
|
const roleLoad = app ? roleLoadFindings(ctx.root, ctx.runner) : Promise.resolve([]);
|
|
347
356
|
const drift = await driftFindings(ctx.root);
|
|
348
357
|
const findings = [
|
|
349
|
-
...manifestMissingFindings(ctx.root),
|
|
350
358
|
...(app ? await withLoadFindings(ctx.root, drift) : drift),
|
|
351
359
|
// An app only: the framework monorepo declares no `defineApi`, so it has no list to be in.
|
|
352
360
|
...(existsSync(join(ctx.root, APP_CONFIG_FILE))
|
|
@@ -384,41 +392,14 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
384
392
|
];
|
|
385
393
|
|
|
386
394
|
/**
|
|
387
|
-
* The
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* thing that writes it. Nothing ran it: after `x new`, `bin/setup` and all thirteen generators,
|
|
391
|
-
* `find . -name '*.manifest.json'` returned nothing while this step reported green (#F7).
|
|
392
|
-
*
|
|
393
|
-
* An app root, never this repo: the framework monorepo emits `framework.manifest.json` and has no
|
|
394
|
-
* `x.manifest.json` to be missing, so the `app.config.ts` is what decides — the same discriminator
|
|
395
|
-
* `drift`, `budgets`, `seo`, `i18n` and `policy` each use.
|
|
395
|
+
* The committed generated files against the code — `x manifest --check`'s check, through its
|
|
396
|
+
* function. With no committed contract there is nothing to load an app for: an app root is told
|
|
397
|
+
* its manifest is missing, and the framework monorepo (which has neither file) is told nothing.
|
|
396
398
|
*/
|
|
397
|
-
function manifestMissingFindings(root: string): readonly Finding[] {
|
|
398
|
-
if (!existsSync(join(root, APP_CONFIG_FILE))) return [];
|
|
399
|
-
if (existsSync(join(root, MANIFEST_FILENAME))) return [];
|
|
400
|
-
return [
|
|
401
|
-
{
|
|
402
|
-
code: 'X_MANIFEST_MISSING',
|
|
403
|
-
cause: `${MANIFEST_FILENAME} does not exist, so every fact an agent reads about this app — route table, action schemas, policies, error codes — is unavailable`,
|
|
404
|
-
fix: 'x manifest',
|
|
405
|
-
docs: ERROR_DOCS_URL,
|
|
406
|
-
at: MANIFEST_FILENAME,
|
|
407
|
-
},
|
|
408
|
-
];
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/** `assertNoDrift` throws `X_MANIFEST_DRIFT`; a step reports, so the error becomes a finding. */
|
|
412
399
|
async function driftFindings(root: string): Promise<readonly Finding[]> {
|
|
413
|
-
|
|
414
|
-
if (!existsSync(path)) return [];
|
|
400
|
+
if (!hasCommittedContract(root)) return manifestMissingFindings(root);
|
|
415
401
|
const { manifest, findings } = await appManifest(root);
|
|
416
|
-
|
|
417
|
-
await assertNoDrift({ manifest, path });
|
|
418
|
-
return findings;
|
|
419
|
-
} catch (error) {
|
|
420
|
-
return [...findings, { ...findingFrom(error), at: MANIFEST_FILENAME }];
|
|
421
|
-
}
|
|
402
|
+
return [...findings, ...(await manifestStaleness(root, manifest))];
|
|
422
403
|
}
|
|
423
404
|
|
|
424
405
|
/** A breaking change is allowed — with a major bump. `verifyContract` is the one that decides. */
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { rm } from 'node:fs/promises';
|
|
15
15
|
// why: Bun ships no path-join primitive.
|
|
16
16
|
import { dirname, join } from 'node:path';
|
|
17
|
+
import { ISOLATED_ENV } from '@ultimat3/testing';
|
|
17
18
|
import type { CoverageMap } from './coverage-lcov';
|
|
18
19
|
import { fileCoverageOf, mergeCoverage, parseLcov } from './coverage-lcov';
|
|
19
20
|
import type { ExecResult, Runner } from './exec';
|
|
@@ -21,7 +22,7 @@ import { execOutput } from './exec';
|
|
|
21
22
|
import { msg } from './messages';
|
|
22
23
|
import { countsOf } from './test-counts';
|
|
23
24
|
import { testEnvOverrides } from './test-dotenv';
|
|
24
|
-
import { failureOf
|
|
25
|
+
import { failureOf } from './test-shards';
|
|
25
26
|
import { SLOT_HELD_ENV } from './test-slots';
|
|
26
27
|
import type { StepOutcome } from './verify-step';
|
|
27
28
|
|
|
@@ -113,7 +114,7 @@ export async function runCovered(options: CoveredRunOptions): Promise<CoveredRun
|
|
|
113
114
|
const isolate = options.isolate === true;
|
|
114
115
|
const overrides = {
|
|
115
116
|
...testEnvOverrides(options.root, options.env ?? Bun.env),
|
|
116
|
-
...(isolate ? { [
|
|
117
|
+
...(isolate ? { [ISOLATED_ENV]: '1' } : {}),
|
|
117
118
|
...(lease === undefined ? {} : { [SLOT_HELD_ENV]: '1' }),
|
|
118
119
|
};
|
|
119
120
|
const results = new Array<ExecResult>(slices.length);
|