@pithy-sh/core 0.1.2 → 0.1.3
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/dist/address/address.d.ts +57 -0
- package/dist/address/address.d.ts.map +1 -0
- package/dist/address/address.js +71 -0
- package/dist/audit/auditEvent.d.ts +89 -0
- package/dist/audit/auditEvent.d.ts.map +1 -0
- package/dist/audit/auditEvent.js +69 -0
- package/dist/audit/recorder.d.ts +18 -0
- package/dist/audit/recorder.d.ts.map +1 -0
- package/dist/audit/recorder.js +8 -0
- package/dist/capability/bindings.d.ts +62 -0
- package/dist/capability/bindings.d.ts.map +1 -0
- package/dist/capability/bindings.js +97 -0
- package/dist/capability/capability.d.ts +501 -0
- package/dist/capability/capability.d.ts.map +1 -0
- package/dist/capability/capability.js +16 -0
- package/dist/capability/client.d.ts +70 -0
- package/dist/capability/client.d.ts.map +1 -0
- package/dist/capability/client.js +86 -0
- package/dist/capability/compose.d.ts +25 -0
- package/dist/capability/compose.d.ts.map +1 -0
- package/dist/capability/compose.js +54 -0
- package/dist/capability/composition.d.ts +26 -0
- package/dist/capability/composition.d.ts.map +1 -0
- package/dist/capability/composition.js +84 -0
- package/dist/capability/config.d.ts +7 -0
- package/dist/capability/config.d.ts.map +1 -0
- package/dist/capability/config.js +14 -0
- package/dist/capability/devSecret.d.ts +32 -0
- package/dist/capability/devSecret.d.ts.map +1 -0
- package/dist/capability/devSecret.js +28 -0
- package/dist/capability/manifest.d.ts +298 -0
- package/dist/capability/manifest.d.ts.map +1 -0
- package/dist/capability/manifest.js +356 -0
- package/dist/capability/secretOrigin.d.ts +121 -0
- package/dist/capability/secretOrigin.d.ts.map +1 -0
- package/dist/capability/secretOrigin.js +134 -0
- package/dist/capability/settings.d.ts +119 -0
- package/dist/capability/settings.d.ts.map +1 -0
- package/dist/capability/settings.js +66 -0
- package/dist/capability/validateBindings.d.ts +4 -0
- package/dist/capability/validateBindings.d.ts.map +1 -0
- package/dist/capability/validateBindings.js +34 -0
- package/dist/capability/vanishingKey.d.ts +31 -0
- package/dist/capability/vanishingKey.d.ts.map +1 -0
- package/dist/capability/vanishingKey.js +79 -0
- package/dist/controlPlane/audit/actions.d.ts +70 -0
- package/dist/controlPlane/audit/actions.d.ts.map +1 -0
- package/dist/controlPlane/audit/actions.js +76 -0
- package/dist/controlPlane/capability.d.ts +47 -0
- package/dist/controlPlane/capability.d.ts.map +1 -0
- package/dist/controlPlane/capability.js +138 -0
- package/dist/controlPlane/config/config.d.ts +19 -0
- package/dist/controlPlane/config/config.d.ts.map +1 -0
- package/dist/controlPlane/config/config.js +85 -0
- package/dist/controlPlane/context.d.ts +27 -0
- package/dist/controlPlane/context.d.ts.map +1 -0
- package/dist/controlPlane/context.js +29 -0
- package/dist/controlPlane/data/connection.d.ts +79 -0
- package/dist/controlPlane/data/connection.d.ts.map +1 -0
- package/dist/controlPlane/data/connection.js +55 -0
- package/dist/controlPlane/data/keyLifecycle.d.ts +62 -0
- package/dist/controlPlane/data/keyLifecycle.d.ts.map +1 -0
- package/dist/controlPlane/data/keyLifecycle.js +126 -0
- package/dist/controlPlane/data/replay.d.ts +18 -0
- package/dist/controlPlane/data/replay.d.ts.map +1 -0
- package/dist/controlPlane/data/replay.js +20 -0
- package/dist/controlPlane/data/tables.d.ts +34 -0
- package/dist/controlPlane/data/tables.d.ts.map +1 -0
- package/dist/controlPlane/data/tables.js +37 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts +313 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -0
- package/dist/controlPlane/discovery/adminRoute.js +136 -0
- package/dist/controlPlane/discovery/configuration.d.ts +108 -0
- package/dist/controlPlane/discovery/configuration.d.ts.map +1 -0
- package/dist/controlPlane/discovery/configuration.js +187 -0
- package/dist/controlPlane/discovery/drift.d.ts +64 -0
- package/dist/controlPlane/discovery/drift.d.ts.map +1 -0
- package/dist/controlPlane/discovery/drift.js +58 -0
- package/dist/controlPlane/discovery/health.d.ts +99 -0
- package/dist/controlPlane/discovery/health.d.ts.map +1 -0
- package/dist/controlPlane/discovery/health.js +151 -0
- package/dist/controlPlane/discovery/healthSummary.d.ts +236 -0
- package/dist/controlPlane/discovery/healthSummary.d.ts.map +1 -0
- package/dist/controlPlane/discovery/healthSummary.js +278 -0
- package/dist/controlPlane/error/errors.d.ts +67 -0
- package/dist/controlPlane/error/errors.d.ts.map +1 -0
- package/dist/controlPlane/error/errors.js +79 -0
- package/dist/controlPlane/http/cors.d.ts +112 -0
- package/dist/controlPlane/http/cors.d.ts.map +1 -0
- package/dist/controlPlane/http/cors.js +192 -0
- package/dist/controlPlane/http/guard.d.ts +77 -0
- package/dist/controlPlane/http/guard.d.ts.map +1 -0
- package/dist/controlPlane/http/guard.js +142 -0
- package/dist/controlPlane/http/handlers.d.ts +179 -0
- package/dist/controlPlane/http/handlers.d.ts.map +1 -0
- package/dist/controlPlane/http/handlers.js +245 -0
- package/dist/controlPlane/http/responses.d.ts +99 -0
- package/dist/controlPlane/http/responses.d.ts.map +1 -0
- package/dist/controlPlane/http/responses.js +63 -0
- package/dist/controlPlane/http/routes.d.ts +40 -0
- package/dist/controlPlane/http/routes.d.ts.map +1 -0
- package/dist/controlPlane/http/routes.js +83 -0
- package/dist/controlPlane/http/schemas.d.ts +39 -0
- package/dist/controlPlane/http/schemas.d.ts.map +1 -0
- package/dist/controlPlane/http/schemas.js +31 -0
- package/dist/controlPlane/http/verify.d.ts +84 -0
- package/dist/controlPlane/http/verify.d.ts.map +1 -0
- package/dist/controlPlane/http/verify.js +65 -0
- package/dist/controlPlane/migrations/0001_init.d.ts +51 -0
- package/dist/controlPlane/migrations/0001_init.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0001_init.js +65 -0
- package/dist/controlPlane/replay/d1Guard.d.ts +29 -0
- package/dist/controlPlane/replay/d1Guard.d.ts.map +1 -0
- package/dist/controlPlane/replay/d1Guard.js +39 -0
- package/dist/controlPlane/replay/guard.d.ts +52 -0
- package/dist/controlPlane/replay/guard.d.ts.map +1 -0
- package/dist/controlPlane/replay/guard.js +0 -0
- package/dist/controlPlane/replay/kvGuard.d.ts +99 -0
- package/dist/controlPlane/replay/kvGuard.d.ts.map +1 -0
- package/dist/controlPlane/replay/kvGuard.js +101 -0
- package/dist/controlPlane/scope/scope.d.ts +68 -0
- package/dist/controlPlane/scope/scope.d.ts.map +1 -0
- package/dist/controlPlane/scope/scope.js +63 -0
- package/dist/controlPlane/token/base64url.d.ts +10 -0
- package/dist/controlPlane/token/base64url.d.ts.map +1 -0
- package/dist/controlPlane/token/base64url.js +48 -0
- package/dist/controlPlane/token/claims.d.ts +83 -0
- package/dist/controlPlane/token/claims.d.ts.map +1 -0
- package/dist/controlPlane/token/claims.js +86 -0
- package/dist/controlPlane/token/digest.d.ts +44 -0
- package/dist/controlPlane/token/digest.d.ts.map +1 -0
- package/dist/controlPlane/token/digest.js +56 -0
- package/dist/controlPlane/token/jws.d.ts +52 -0
- package/dist/controlPlane/token/jws.d.ts.map +1 -0
- package/dist/controlPlane/token/jws.js +65 -0
- package/dist/controlPlane/token/mint.d.ts +56 -0
- package/dist/controlPlane/token/mint.d.ts.map +1 -0
- package/dist/controlPlane/token/mint.js +55 -0
- package/dist/controlPlane/wire.d.ts +122 -0
- package/dist/controlPlane/wire.d.ts.map +1 -0
- package/dist/controlPlane/wire.js +123 -0
- package/dist/createBackend.d.ts +58 -0
- package/dist/createBackend.d.ts.map +1 -0
- package/dist/createBackend.js +170 -0
- package/dist/createEntrypoint.d.ts +53 -0
- package/dist/createEntrypoint.d.ts.map +1 -0
- package/dist/createEntrypoint.js +52 -0
- package/dist/data/boundParameters.d.ts +83 -0
- package/dist/data/boundParameters.d.ts.map +1 -0
- package/dist/data/boundParameters.js +162 -0
- package/dist/data/codecs.d.ts +34 -0
- package/dist/data/codecs.d.ts.map +1 -0
- package/dist/data/codecs.js +135 -0
- package/dist/data/cursor.d.ts +81 -0
- package/dist/data/cursor.d.ts.map +1 -0
- package/dist/data/cursor.js +104 -0
- package/dist/data/databases.d.ts +55 -0
- package/dist/data/databases.d.ts.map +1 -0
- package/dist/data/databases.js +32 -0
- package/dist/data/db.d.ts +42 -0
- package/dist/data/db.d.ts.map +1 -0
- package/dist/data/db.js +24 -0
- package/dist/data/withD1Retry.d.ts +91 -0
- package/dist/data/withD1Retry.d.ts.map +1 -0
- package/dist/data/withD1Retry.js +152 -0
- package/dist/entitlement/entitlement.d.ts +118 -0
- package/dist/entitlement/entitlement.d.ts.map +1 -0
- package/dist/entitlement/entitlement.js +59 -0
- package/dist/entitlement/gateScan.d.ts +26 -0
- package/dist/entitlement/gateScan.d.ts.map +1 -0
- package/dist/entitlement/gateScan.js +96 -0
- package/dist/entitlement/require.d.ts +12 -0
- package/dist/entitlement/require.d.ts.map +1 -0
- package/dist/entitlement/require.js +123 -0
- package/dist/env/ambient.d.ts +52 -0
- package/dist/env/ambient.d.ts.map +1 -0
- package/dist/env/ambient.js +35 -0
- package/dist/env/ci.d.ts +36 -0
- package/dist/env/ci.d.ts.map +1 -0
- package/dist/env/ci.js +40 -0
- package/dist/env/stem.d.ts +23 -0
- package/dist/env/stem.d.ts.map +1 -0
- package/dist/env/stem.js +29 -0
- package/dist/error/cause.d.ts +143 -0
- package/dist/error/cause.d.ts.map +1 -0
- package/dist/error/cause.js +192 -0
- package/dist/error/client.d.ts +35 -0
- package/dist/error/client.d.ts.map +1 -0
- package/dist/error/client.js +38 -0
- package/dist/error/extend.d.ts +101 -0
- package/dist/error/extend.d.ts.map +1 -0
- package/dist/error/extend.js +76 -0
- package/dist/error/http.d.ts +1516 -0
- package/dist/error/http.d.ts.map +1 -0
- package/dist/error/http.js +72 -0
- package/dist/error/payload.d.ts +3034 -0
- package/dist/error/payload.d.ts.map +1 -0
- package/dist/error/payload.js +1110 -0
- package/dist/error/pithyError.d.ts +154 -0
- package/dist/error/pithyError.d.ts.map +1 -0
- package/dist/error/pithyError.js +197 -0
- package/dist/error/terminal.d.ts +28 -0
- package/dist/error/terminal.d.ts.map +1 -0
- package/dist/error/terminal.js +33 -0
- package/dist/http/authContext.d.ts +21 -0
- package/dist/http/authContext.d.ts.map +1 -0
- package/dist/http/authContext.js +23 -0
- package/dist/http/routeContract.d.ts +39 -0
- package/dist/http/routeContract.d.ts.map +1 -0
- package/dist/http/routeContract.js +82 -0
- package/dist/http/sameOrigin.d.ts +47 -0
- package/dist/http/sameOrigin.d.ts.map +1 -0
- package/dist/http/sameOrigin.js +24 -0
- package/dist/http/signedWebhook.d.ts +225 -0
- package/dist/http/signedWebhook.d.ts.map +1 -0
- package/dist/http/signedWebhook.js +247 -0
- package/dist/http/validation.d.ts +37 -0
- package/dist/http/validation.d.ts.map +1 -0
- package/dist/http/validation.js +14 -0
- package/dist/http/verification.d.ts +10 -0
- package/dist/http/verification.d.ts.map +1 -0
- package/dist/http/verification.js +17 -0
- package/dist/i18n/acceptLanguage.d.ts +22 -0
- package/dist/i18n/acceptLanguage.d.ts.map +1 -0
- package/dist/i18n/acceptLanguage.js +55 -0
- package/dist/i18n/catalog.d.ts +39 -0
- package/dist/i18n/catalog.d.ts.map +1 -0
- package/dist/i18n/catalog.js +57 -0
- package/dist/i18n/locale.d.ts +74 -0
- package/dist/i18n/locale.d.ts.map +1 -0
- package/dist/i18n/locale.js +109 -0
- package/dist/i18n/localeMarker.d.ts +29 -0
- package/dist/i18n/localeMarker.d.ts.map +1 -0
- package/dist/i18n/localeMarker.js +112 -0
- package/dist/i18n/match.d.ts +26 -0
- package/dist/i18n/match.d.ts.map +1 -0
- package/dist/i18n/match.js +88 -0
- package/dist/i18n/registry.d.ts +25 -0
- package/dist/i18n/registry.d.ts.map +1 -0
- package/dist/i18n/registry.js +51 -0
- package/dist/i18n/translator.d.ts +107 -0
- package/dist/i18n/translator.d.ts.map +1 -0
- package/dist/i18n/translator.js +80 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/kv/kv.d.ts +166 -0
- package/dist/kv/kv.d.ts.map +1 -0
- package/dist/kv/kv.js +261 -0
- package/dist/kv/namespaces.d.ts +58 -0
- package/dist/kv/namespaces.d.ts.map +1 -0
- package/dist/kv/namespaces.js +40 -0
- package/dist/logger/local.d.ts +41 -0
- package/dist/logger/local.d.ts.map +1 -0
- package/dist/logger/local.js +45 -0
- package/dist/logger/logger.d.ts +56 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +83 -0
- package/dist/logger/record.d.ts +919 -0
- package/dist/logger/record.d.ts.map +1 -0
- package/dist/logger/record.js +79 -0
- package/dist/logger/worker.d.ts +74 -0
- package/dist/logger/worker.d.ts.map +1 -0
- package/dist/logger/worker.js +54 -0
- package/dist/migrations/batch.d.ts +10 -0
- package/dist/migrations/batch.d.ts.map +1 -0
- package/dist/migrations/batch.js +193 -0
- package/dist/migrations/bookkeeping.d.ts +24 -0
- package/dist/migrations/bookkeeping.d.ts.map +1 -0
- package/dist/migrations/bookkeeping.js +57 -0
- package/dist/migrations/owner.d.ts +41 -0
- package/dist/migrations/owner.d.ts.map +1 -0
- package/dist/migrations/owner.js +105 -0
- package/dist/migrations/registry.d.ts +36 -0
- package/dist/migrations/registry.d.ts.map +1 -0
- package/dist/migrations/registry.js +70 -0
- package/dist/migrations/runner.d.ts +67 -0
- package/dist/migrations/runner.d.ts.map +1 -0
- package/dist/migrations/runner.js +218 -0
- package/dist/naming/domains.d.ts +108 -0
- package/dist/naming/domains.d.ts.map +1 -0
- package/dist/naming/domains.js +156 -0
- package/dist/naming/environment.d.ts +118 -0
- package/dist/naming/environment.d.ts.map +1 -0
- package/dist/naming/environment.js +190 -0
- package/dist/naming/feature.d.ts +68 -0
- package/dist/naming/feature.d.ts.map +1 -0
- package/dist/naming/feature.js +106 -0
- package/dist/naming/limits.d.ts +229 -0
- package/dist/naming/limits.d.ts.map +1 -0
- package/dist/naming/limits.js +187 -0
- package/dist/naming/provisionScope.d.ts +91 -0
- package/dist/naming/provisionScope.d.ts.map +1 -0
- package/dist/naming/provisionScope.js +65 -0
- package/dist/naming/resource.d.ts +169 -0
- package/dist/naming/resource.d.ts.map +1 -0
- package/dist/naming/resource.js +220 -0
- package/dist/naming/resourceNames.d.ts +83 -0
- package/dist/naming/resourceNames.d.ts.map +1 -0
- package/dist/naming/resourceNames.js +102 -0
- package/dist/naming/segment.d.ts +29 -0
- package/dist/naming/segment.d.ts.map +1 -0
- package/dist/naming/segment.js +31 -0
- package/dist/projection/asRead.d.ts +72 -0
- package/dist/projection/asRead.d.ts.map +1 -0
- package/dist/projection/asRead.js +169 -0
- package/dist/projection/published.d.ts +95 -0
- package/dist/projection/published.d.ts.map +1 -0
- package/dist/projection/published.js +114 -0
- package/dist/schema/describedness.d.ts +55 -0
- package/dist/schema/describedness.d.ts.map +1 -0
- package/dist/schema/describedness.js +180 -0
- package/dist/seed/compose.d.ts +41 -0
- package/dist/seed/compose.d.ts.map +1 -0
- package/dist/seed/compose.js +51 -0
- package/dist/seed/devLogin.d.ts +50 -0
- package/dist/seed/devLogin.d.ts.map +1 -0
- package/dist/seed/devLogin.js +52 -0
- package/dist/seed/exampleIdentities.d.ts +58 -0
- package/dist/seed/exampleIdentities.d.ts.map +1 -0
- package/dist/seed/exampleIdentities.js +26 -0
- package/dist/seed/metadata.d.ts +20 -0
- package/dist/seed/metadata.d.ts.map +1 -0
- package/dist/seed/metadata.js +27 -0
- package/dist/seed/seed.d.ts +264 -0
- package/dist/seed/seed.d.ts.map +1 -0
- package/dist/seed/seed.js +37 -0
- package/dist/seed/seededRows.d.ts +26 -0
- package/dist/seed/seededRows.d.ts.map +1 -0
- package/dist/seed/seededRows.js +20 -0
- package/dist/seed/writeD1.d.ts +43 -0
- package/dist/seed/writeD1.d.ts.map +1 -0
- package/dist/seed/writeD1.js +73 -0
- package/dist/seed/writeKv.d.ts +29 -0
- package/dist/seed/writeKv.d.ts.map +1 -0
- package/dist/seed/writeKv.js +63 -0
- package/dist/semver/semver.d.ts +62 -0
- package/dist/semver/semver.d.ts.map +1 -0
- package/dist/semver/semver.js +137 -0
- package/dist/text/comments.d.ts +62 -0
- package/dist/text/comments.d.ts.map +1 -0
- package/dist/text/comments.js +161 -0
- package/dist/version.generated.d.ts +5 -0
- package/dist/version.generated.d.ts.map +1 -0
- package/dist/version.generated.js +7 -0
- package/dist/worker/health.d.ts +39 -0
- package/dist/worker/health.d.ts.map +1 -0
- package/dist/worker/health.js +41 -0
- package/dist/worker/identity.d.ts +191 -0
- package/dist/worker/identity.d.ts.map +1 -0
- package/dist/worker/identity.js +162 -0
- package/dist/workflow/bindings.d.ts +41 -0
- package/dist/workflow/bindings.d.ts.map +1 -0
- package/dist/workflow/bindings.js +53 -0
- package/dist/workflow/dispatch.d.ts +98 -0
- package/dist/workflow/dispatch.d.ts.map +1 -0
- package/dist/workflow/dispatch.js +148 -0
- package/dist/workflow/dispatchRoute.d.ts +80 -0
- package/dist/workflow/dispatchRoute.d.ts.map +1 -0
- package/dist/workflow/dispatchRoute.js +134 -0
- package/dist/workflow/faults.d.ts +97 -0
- package/dist/workflow/faults.d.ts.map +1 -0
- package/dist/workflow/faults.js +111 -0
- package/dist/workflow/host.d.ts +176 -0
- package/dist/workflow/host.d.ts.map +1 -0
- package/dist/workflow/host.js +116 -0
- package/dist/workflow/hostEntry.d.ts +50 -0
- package/dist/workflow/hostEntry.d.ts.map +1 -0
- package/dist/workflow/hostEntry.js +24 -0
- package/dist/workflow/hostEnv.d.ts +143 -0
- package/dist/workflow/hostEnv.d.ts.map +1 -0
- package/dist/workflow/hostEnv.js +170 -0
- package/dist/workflow/loopback.d.ts +52 -0
- package/dist/workflow/loopback.d.ts.map +1 -0
- package/dist/workflow/loopback.js +76 -0
- package/dist/workflow/naming.d.ts +79 -0
- package/dist/workflow/naming.d.ts.map +1 -0
- package/dist/workflow/naming.js +138 -0
- package/dist/workflow/register.d.ts +19 -0
- package/dist/workflow/register.d.ts.map +1 -0
- package/dist/workflow/register.js +40 -0
- package/dist/workflow/schemas.d.ts +44 -0
- package/dist/workflow/schemas.d.ts.map +1 -0
- package/dist/workflow/schemas.js +43 -0
- package/dist/workflow/spec.d.ts +81 -0
- package/dist/workflow/spec.d.ts.map +1 -0
- package/dist/workflow/spec.js +0 -0
- package/dist/workflow/stepMessage.d.ts +122 -0
- package/dist/workflow/stepMessage.d.ts.map +1 -0
- package/dist/workflow/stepMessage.js +152 -0
- package/package.json +20 -9
- package/src/capability/manifest.ts +21 -0
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { ControlPlaneScope } from "../scope/scope.js";
|
|
2
|
+
import { CapabilityHealthReport, HealthSummary, HealthSummaryKey, healthReport, healthWire } from "./healthSummary.js";
|
|
3
|
+
import { ManifestConfigKey, ManifestConfigValues } from "./configuration.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/controlPlane/discovery/adminRoute.ts
|
|
6
|
+
/**
|
|
7
|
+
* What a capability tells a management client about its own admin surface.
|
|
8
|
+
*
|
|
9
|
+
* **This exists because knowing a capability is installed is not enough to call it.** A management
|
|
10
|
+
* client that learns only names still has to hold a route table for every capability, guess that
|
|
11
|
+
* payments is mounted at `/payments`, and know out-of-band which scope each operation needs. Every one
|
|
12
|
+
* of those is a thing that can be wrong, and `basePath` is configurable — an adopter who mounts
|
|
13
|
+
* payments at `/billing` would silently 404 a client that hardcoded the default.
|
|
14
|
+
*
|
|
15
|
+
* So the capability declares the routes it actually registered, with the paths it actually used, and
|
|
16
|
+
* `GET /control-plane/manifest` reports them. That is what makes discovery-over-configuration real
|
|
17
|
+
* rather than aspirational: a client composes its navigation *and* its calls from what the Worker says
|
|
18
|
+
* about itself, and an adopter who moves a mount point or upgrades a capability changes what the client
|
|
19
|
+
* does without either side coordinating.
|
|
20
|
+
*
|
|
21
|
+
* **The declaration is checked against the router**, not trusted — see `missingAdminRoutes`. A manifest
|
|
22
|
+
* that drifts from the routes is worse than no manifest, because a client would believe it.
|
|
23
|
+
*/
|
|
24
|
+
/** The HTTP methods an admin route may use. */
|
|
25
|
+
const AdminRouteMethod = z.enum([
|
|
26
|
+
"GET",
|
|
27
|
+
"POST",
|
|
28
|
+
"PUT",
|
|
29
|
+
"PATCH",
|
|
30
|
+
"DELETE"
|
|
31
|
+
]).describe("The HTTP method this admin route answers on.");
|
|
32
|
+
/** One admin route a capability contributes behind the `control-plane` strategy. */
|
|
33
|
+
const AdminRoute = z.object({
|
|
34
|
+
method: AdminRouteMethod.describe("The method to call this route with."),
|
|
35
|
+
path: z.string().min(1).describe("The **fully mounted** path, with the capability's configured `basePath` already applied — `/billing/entitlements/grant`, not `/entitlements/grant`. Built by the capability at assembly, so an adopter who moved the mount point is reflected here rather than silently breaking a client that assumed the default."),
|
|
36
|
+
scope: ControlPlaneScope.nullable().describe("The scope this route requires, or null when it needs only a verified caller (the seam's `ping`). A client compares it against the connection's granted scopes to know which operations it may actually offer, instead of discovering a 403 by trying."),
|
|
37
|
+
summary: z.string().min(1).describe("One line describing what the route does, for a management client to render beside it.")
|
|
38
|
+
}).describe("One admin route, described well enough for a management client to call it without holding any capability-specific knowledge.");
|
|
39
|
+
/**
|
|
40
|
+
* One composed capability's declaration — everything about it that is the same for every caller.
|
|
41
|
+
*
|
|
42
|
+
* Split from {@link CapabilityDescriptor} because the health *values* are the one part of a manifest
|
|
43
|
+
* entry that depends on who is asking, and the seam holds this half from assembly (`compose`) while the
|
|
44
|
+
* other half is resolved per request.
|
|
45
|
+
*/
|
|
46
|
+
const CapabilityDeclaration = z.object({
|
|
47
|
+
name: z.string().min(1).describe("The capability's name — the same token used as its migration namespace and error domain."),
|
|
48
|
+
version: z.string().nullable().describe("The npm version of the package supplying this capability, or null where there is none — the adopter's own `app` capability has a name and no package. Reported per capability and never aggregated: the package name is the join key against a release feed, and a project composes some capabilities and not others, so only the intersection of what it composes and what changed is worth reporting."),
|
|
49
|
+
adminRoutes: z.array(AdminRoute).describe("Every admin route this capability contributes, or empty when it contributes none. Most capabilities are empty: having no management surface is the normal case, and saying so explicitly is what lets a client render a capability it cannot act on."),
|
|
50
|
+
healthKeys: z.array(HealthSummaryKey).default([]).describe("The closed vocabulary of scalars this capability may report about its own state, or empty when it reports none — including when the Worker predates this field and says nothing at all. Declared alongside the routes so a client can render a key it has never heard of from what the Worker says about it — and so a *withheld* number is visible as a key with no value, rather than as silence."),
|
|
51
|
+
configKeys: z.array(ManifestConfigKey).default([]).describe("The configured facts this capability states, or empty when it states none — including when the Worker predates this field. Declared alongside the routes, because a fact is what a client needs to *call* one: `POST /billing/entitlements/grant` names a holder and never assumes it, so a client that cannot learn what this project bills can only guess — and a guess writes a row nothing reads."),
|
|
52
|
+
config: ManifestConfigValues.default({}).describe("What each declared fact resolved to for this deployment. The same answer for every caller, read off the capability's parsed config at assembly: there is no producer here, so no failure state, no per-request cost, and no staleness beyond the manifest itself.")
|
|
53
|
+
}).describe("One capability this Worker composes, the version it is at, the admin surface it exposes, the summary it may report, and the configured facts a client must respect to call any of it.");
|
|
54
|
+
/**
|
|
55
|
+
* One composed capability's manifest entry **as it goes over the wire**.
|
|
56
|
+
*
|
|
57
|
+
* Two flat fields, and neither is what a consumer reads: {@link CapabilityDescriptor} decodes them into
|
|
58
|
+
* one value that says which of the four states it is in. They are flat here because the wire has an
|
|
59
|
+
* older half to keep working. A Worker deployed before #350 sends no `healthUnavailable` and a Worker
|
|
60
|
+
* deployed before #317 sends neither field, and both must still parse; and a client pinned to a build
|
|
61
|
+
* older than the Worker it is reading strips the field it has never heard of and lands on `health:
|
|
62
|
+
* null`, which renders as silence rather than as a zero. Putting the fourth state *inside* `health`
|
|
63
|
+
* would have cost that client the whole manifest for one capability's bad afternoon, which is the
|
|
64
|
+
* failure #352 is about.
|
|
65
|
+
*/
|
|
66
|
+
const CapabilityDescriptorWire = CapabilityDeclaration.extend({
|
|
67
|
+
health: HealthSummary.nullable().default(null).describe("Every declared value this caller may see, or null when there is none to give — either nothing is declared, or this connection lacks the scope the value is behind, or producing it failed, or the Worker predates this field. **Null is never zero.**"),
|
|
68
|
+
healthUnavailable: z.boolean().default(false).describe("Whether producing this capability's summary failed on this read — the fourth state (#350), and the reason `health` being null is not enough on its own. Carries no message, no code and no error: what the producer threw may name a row or a key, so nothing derived from it travels. False when the Worker predates this field.")
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* One composed capability, as `GET /control-plane/manifest` reports it to **this** caller.
|
|
72
|
+
*
|
|
73
|
+
* `health` is the only per-caller part of a manifest entry, and it is a four-state value rather than a
|
|
74
|
+
* nullable record: a capability that declares nothing, one that declares a number this connection was
|
|
75
|
+
* not granted, one that reports a number — zero included — and one whose producer failed. A management
|
|
76
|
+
* client that collapsed any two of those tells an adopter something untrue. The first two collapsed
|
|
77
|
+
* says everything is fine when it has simply not been allowed to look; the last two collapsed says
|
|
78
|
+
* nothing is pending when the store is down.
|
|
79
|
+
*
|
|
80
|
+
* The state travels **on** the value, so the scalars are unreachable without narrowing and a consumer
|
|
81
|
+
* that forgets the sick case gets a type error rather than a zero.
|
|
82
|
+
*/
|
|
83
|
+
const CapabilityDescriptorRead = CapabilityDeclaration.extend({ health: CapabilityHealthReport.describe("What this caller is told about the capability's own state: nothing declared, a number withheld, the numbers themselves, or a producer that failed. Four states on one value, so none of them is reachable by forgetting to check another.") });
|
|
84
|
+
const CapabilityDescriptor = z.codec(CapabilityDescriptorWire, CapabilityDescriptorRead, {
|
|
85
|
+
decode: ({ health, healthUnavailable, ...declaration }) => ({
|
|
86
|
+
...declaration,
|
|
87
|
+
health: healthReport({
|
|
88
|
+
healthKeys: declaration.healthKeys,
|
|
89
|
+
health,
|
|
90
|
+
healthUnavailable
|
|
91
|
+
})
|
|
92
|
+
}),
|
|
93
|
+
encode: ({ health, healthKeys, configKeys, config, ...declaration }) => ({
|
|
94
|
+
...declaration,
|
|
95
|
+
healthKeys: (healthKeys ?? []).map((key) => ({
|
|
96
|
+
...key,
|
|
97
|
+
nominal: key.nominal ?? null
|
|
98
|
+
})),
|
|
99
|
+
configKeys: configKeys ?? [],
|
|
100
|
+
config: config ?? {},
|
|
101
|
+
...healthWire(health)
|
|
102
|
+
})
|
|
103
|
+
}).describe("One capability this Worker composes, as reported to one caller: its version, its admin surface, and the summary that caller is entitled to — or the named reason there is none.");
|
|
104
|
+
/**
|
|
105
|
+
* The manifest a management client reads to build itself.
|
|
106
|
+
*
|
|
107
|
+
* **There is deliberately no manifest schema version, and that is a different thing from the two build
|
|
108
|
+
* identities below.** A client dispatches on the routes described here — what this Worker declares right
|
|
109
|
+
* now — so a capability that changed its paths or its scopes reports the change directly, and a schema
|
|
110
|
+
* version would be a second source of truth to keep in sync with the first. That reasoning is unchanged.
|
|
111
|
+
*
|
|
112
|
+
* What the manifest does carry is **identity**, not schema, and it carries two of them because they
|
|
113
|
+
* answer questions neither can answer alone. `version` is Cloudflare's opaque per-deploy id: it says
|
|
114
|
+
* *exactly which build* is running, which is what forensics needs, what reproduces a report, and what
|
|
115
|
+
* pins the code an audited action ran against. It carries no version semantics, so it says nothing about
|
|
116
|
+
* features. `capabilities[].version` is the npm version of each composed package: it says *which
|
|
117
|
+
* features*, which is what answers "should this customer upgrade", "which customers are exposed to what
|
|
118
|
+
* we just fixed", and "does this project predate the capability a pane needs". Reporting only one leaves
|
|
119
|
+
* half the questions unanswerable, and they are the halves people actually ask.
|
|
120
|
+
*
|
|
121
|
+
* **And each entry carries a third thing: the configured facts that capability states** (#422). Knowing
|
|
122
|
+
* where a route is and which scope it needs is not always enough to call it — `POST
|
|
123
|
+
* {base}/entitlements/grant` names a holder, and whether this project's holders are people or
|
|
124
|
+
* organizations is a decision the adopter made in config. A client that cannot read the decision guesses
|
|
125
|
+
* it, and a guess writes a row nothing reads. See `discovery/configuration.ts` for why a fact is not a
|
|
126
|
+
* health number, despite the two fields sitting side by side.
|
|
127
|
+
*/
|
|
128
|
+
const ControlPlaneManifest = z.object({
|
|
129
|
+
environment: z.string().describe("The environment this connection is bound to, echoed so a client can label what it is looking at."),
|
|
130
|
+
connectionId: z.string().describe("The connection this call authenticated as."),
|
|
131
|
+
version: z.string().nullable().describe("The Cloudflare version id of the build answering this call, from the `CF_VERSION_METADATA` binding, or null where the binding is absent. Opaque and per-deploy: it identifies which build is running, never which features it has. Null is honest — a Worker scaffolded before the binding was declared genuinely cannot say."),
|
|
132
|
+
capabilities: z.array(CapabilityDescriptor).describe("Every capability composed into this Worker, in composition order with the adopter's own app last. A capability absent here has no data to show, which is a fact the client discovers rather than a setting anybody maintains."),
|
|
133
|
+
grantedScopes: z.array(ControlPlaneScope).describe("Every scope this connection holds. Together with each route's own `scope`, this is what tells a client which operations to offer and which to hide.")
|
|
134
|
+
}).describe("What this Worker is, what it composes, and how to call the admin surface it exposes.");
|
|
135
|
+
//#endregion
|
|
136
|
+
export { AdminRoute, AdminRouteMethod, CapabilityDeclaration, CapabilityDescriptor, ControlPlaneManifest };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* One configured fact's value. A string, a number, or a boolean — never anything else.
|
|
4
|
+
*
|
|
5
|
+
* The rule is in the type rather than in a comment: a config object, a product list, or a credentials
|
|
6
|
+
* bag does not compile, so the seam cannot become the route by which an adopter's catalog reaches a
|
|
7
|
+
* discovery read.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ManifestConfigValue: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
10
|
+
export type ManifestConfigValue = z.output<typeof ManifestConfigValue>;
|
|
11
|
+
/** Anything that is not a scalar. Named as a type so the tripwire below is a list to delete from. */
|
|
12
|
+
type NonScalar = object | ((...args: never[]) => unknown);
|
|
13
|
+
/**
|
|
14
|
+
* `true` while `T` is scalars only, `never` the moment it admits anything else — so the assignment
|
|
15
|
+
* below stops compiling if {@link ManifestConfigValue} is ever widened.
|
|
16
|
+
*
|
|
17
|
+
* Declared here rather than imported from `health.ts`: that module reaches `capability.ts` and, through
|
|
18
|
+
* it, hono and kysely, and this one is zod-only so a browser client can read the manifest without them
|
|
19
|
+
* (#430). The tuple wrapper is not decoration — a bare `Extract<...> extends never` distributes over a
|
|
20
|
+
* union and would answer `true` for every member, which is the one way this check could quietly pass.
|
|
21
|
+
*/
|
|
22
|
+
type ScalarOnly<T> = [Extract<T, NonScalar>] extends [never] ? true : never;
|
|
23
|
+
/** The compile-time half of "scalars only". Widen the value type and this line names the file. */
|
|
24
|
+
export declare const MANIFEST_CONFIG_VALUE_IS_SCALAR_ONLY: ScalarOnly<ManifestConfigValue>;
|
|
25
|
+
/**
|
|
26
|
+
* One configured fact a capability may state, declared alongside its routes.
|
|
27
|
+
*
|
|
28
|
+
* `choices` is nullable rather than optional, and a null means "no closed list" rather than "not
|
|
29
|
+
* decided yet": a client renders a fact it has never heard of from the summary, and offers a picker
|
|
30
|
+
* only where the values are enumerable. A base path has no list; what a project bills has exactly two.
|
|
31
|
+
*/
|
|
32
|
+
export declare const ManifestConfigKey: z.ZodObject<{
|
|
33
|
+
key: z.ZodString;
|
|
34
|
+
choices: z.ZodNullable<z.ZodArray<z.ZodString>>;
|
|
35
|
+
summary: z.ZodString;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
export type ManifestConfigKey = z.output<typeof ManifestConfigKey>;
|
|
38
|
+
/**
|
|
39
|
+
* A capability's configured facts as the manifest carries them: declared key → scalar.
|
|
40
|
+
*
|
|
41
|
+
* A record rather than a closed object, because the vocabulary is federated the way scopes and audit
|
|
42
|
+
* actions are — the capability name already namespaces it. **Parsing tolerates a key it has never heard
|
|
43
|
+
* of on purpose**: a client reading a newer Worker must drop the unknown one rather than fail the whole
|
|
44
|
+
* manifest, which is what {@link namedConfigValues} does with it.
|
|
45
|
+
*/
|
|
46
|
+
export declare const ManifestConfigValues: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
47
|
+
export type ManifestConfigValues = z.output<typeof ManifestConfigValues>;
|
|
48
|
+
/**
|
|
49
|
+
* The brand only {@link defineManifestConfig} can produce.
|
|
50
|
+
*
|
|
51
|
+
* Module-local and never exported, so nothing outside this file can write it: the factory becomes the
|
|
52
|
+
* **only** way to build the seam, and a fact therefore cannot reach a manifest without having been
|
|
53
|
+
* checked against a declaration. An inline object literal on `Capability.manifestConfig` is a compile
|
|
54
|
+
* error, which is what stops the next capability from stating a fact nothing validated.
|
|
55
|
+
*/
|
|
56
|
+
declare const manifestConfigSeam: unique symbol;
|
|
57
|
+
/** What a capability declares and what it resolved to. Built only by {@link defineManifestConfig}. */
|
|
58
|
+
export interface CapabilityManifestConfig {
|
|
59
|
+
/** The brand. Present only on a declaration that went through the factory, and never serialized. */
|
|
60
|
+
readonly [manifestConfigSeam]: true;
|
|
61
|
+
/** The closed vocabulary: every fact this capability states, parsed. */
|
|
62
|
+
readonly keys: readonly ManifestConfigKey[];
|
|
63
|
+
/** What each declared fact resolved to for this composition. One answer, for every caller. */
|
|
64
|
+
readonly values: ManifestConfigValues;
|
|
65
|
+
}
|
|
66
|
+
/** The authoring shape — facts as written, before parsing. */
|
|
67
|
+
export interface CapabilityManifestConfigInput {
|
|
68
|
+
/** Every fact this capability states. At least one; an empty vocabulary states nothing. */
|
|
69
|
+
keys: readonly z.input<typeof ManifestConfigKey>[];
|
|
70
|
+
/** The resolved value of each declared fact. Built from the capability's parsed config, never its defaults. */
|
|
71
|
+
values: ManifestConfigValues;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Declare a capability's configured facts. The one constructor, so every declaration in the tree is
|
|
75
|
+
* checked — the key against the name pattern, the values against the keys, and each value against its
|
|
76
|
+
* own closed list.
|
|
77
|
+
*
|
|
78
|
+
* **Nothing downstream re-checks this.** `missingAdminRoutes` keeps a route declaration honest against
|
|
79
|
+
* the router that mounted it, and there is no equivalent here: the declaration and the value come from
|
|
80
|
+
* the same object, so the only defense against a fact that disagrees with the capability is building it
|
|
81
|
+
* from the *resolved* config at the one call site, exactly as `adminRoutes` is built from the resolved
|
|
82
|
+
* `basePath`.
|
|
83
|
+
*
|
|
84
|
+
* Every refusal is an {@link InternalError} rather than a bare `ZodError`, key named, because every one
|
|
85
|
+
* of them is an author's mistake read off a deploy that would not start — and an author reading it
|
|
86
|
+
* wants the problem line and the action line the rest of this kit gives them.
|
|
87
|
+
*/
|
|
88
|
+
export declare function defineManifestConfig(input: CapabilityManifestConfigInput): CapabilityManifestConfig;
|
|
89
|
+
/** One fact with the declaration that says how to read it. */
|
|
90
|
+
export interface NamedConfigValue {
|
|
91
|
+
/** The declaration — its closed list, if it has one, and what it decides. */
|
|
92
|
+
key: ManifestConfigKey;
|
|
93
|
+
/** The value this composition resolved it to. */
|
|
94
|
+
value: ManifestConfigValue;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Pair a capability's configured facts with their declarations, dropping any value nothing declares.
|
|
98
|
+
*
|
|
99
|
+
* This is how "an unknown fact reads as nothing rather than as an error" is real rather than
|
|
100
|
+
* aspirational: a client reads what this returns, so a key from a Worker newer than its build simply is
|
|
101
|
+
* not in the list — and a fact it cannot name is a fact it would otherwise have to guess the meaning of.
|
|
102
|
+
*/
|
|
103
|
+
export declare function namedConfigValues(descriptor: {
|
|
104
|
+
configKeys: readonly ManifestConfigKey[];
|
|
105
|
+
config: ManifestConfigValues;
|
|
106
|
+
}): NamedConfigValue[];
|
|
107
|
+
export {};
|
|
108
|
+
//# sourceMappingURL=configuration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configuration.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/discovery/configuration.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAuDxB;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,+DAI7B,CAAC;AACJ,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEvE,qGAAqG;AACrG,KAAK,SAAS,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,CAAC;AAE1D;;;;;;;;GAQG;AACH,KAAK,UAAU,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC;AAE5E,kGAAkG;AAClG,eAAO,MAAM,oCAAoC,EAAE,UAAU,CAAC,mBAAmB,CAAQ,CAAC;AAE1F;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB;;;;iBAoB3B,CAAC;AACJ,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,yFAI9B,CAAC;AACJ,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAEzE;;;;;;;GAOG;AACH,QAAA,MAAM,kBAAkB,EAAE,OAAO,MAAoD,CAAC;AAEtF,sGAAsG;AACtG,MAAM,WAAW,wBAAwB;IACvC,oGAAoG;IACpG,QAAQ,CAAC,CAAC,kBAAkB,CAAC,EAAE,IAAI,CAAC;IACpC,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC5C,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;CACvC;AAED,8DAA8D;AAC9D,MAAM,WAAW,6BAA6B;IAC5C,2FAA2F;IAC3F,IAAI,EAAE,SAAS,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,EAAE,CAAC;IACnD,+GAA+G;IAC/G,MAAM,EAAE,oBAAoB,CAAC;CAC9B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,6BAA6B,GAAG,wBAAwB,CAuEnG;AAED,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,GAAG,EAAE,iBAAiB,CAAC;IACvB,iDAAiD;IACjD,KAAK,EAAE,mBAAmB,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE;IAC5C,UAAU,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACzC,MAAM,EAAE,oBAAoB,CAAC;CAC9B,GAAG,gBAAgB,EAAE,CAWrB"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { InternalError } from "../../error/pithyError.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/controlPlane/discovery/configuration.ts
|
|
4
|
+
/**
|
|
5
|
+
* The configured facts a capability states into its own manifest entry (#422).
|
|
6
|
+
*
|
|
7
|
+
* **A management client cannot call an operation it has to guess an argument for.**
|
|
8
|
+
* `POST {base}/entitlements/grant` names the holder and never assumes it, which is right — but
|
|
9
|
+
* `PaymentsConfig.billingSubject` decides whether this project's holders are people or organizations,
|
|
10
|
+
* it is required with no default, and nothing on the wire said so. A client guessing `user` against an
|
|
11
|
+
* organization-billed project writes a row nothing reads: the call succeeds, the audit line is real,
|
|
12
|
+
* the screen says `Done.`, and the person still cannot use what somebody just gave them. That is worse
|
|
13
|
+
* than a refusal, because it looks like it worked.
|
|
14
|
+
*
|
|
15
|
+
* So a capability declares the configured facts a client must respect, and the manifest carries them
|
|
16
|
+
* beside the routes they govern. `billingSubject` is the first and will not be the last, which is why
|
|
17
|
+
* this is a vocabulary rather than one field bolted onto the manifest.
|
|
18
|
+
*
|
|
19
|
+
* ## This is not health, and the difference is the whole design
|
|
20
|
+
*
|
|
21
|
+
* The shapes rhyme — `configKeys`/`config` beside `healthKeys`/`health` — and a reader who folds them
|
|
22
|
+
* together gets both wrong. They differ on every axis that matters:
|
|
23
|
+
*
|
|
24
|
+
* - A health value is **per caller**; a configured fact is the same for everybody.
|
|
25
|
+
* - A health value is **produced**, so it may fail, and `unavailable` is one of its four states. A fact
|
|
26
|
+
* is read off resolved config at assembly, so there is no producer, no query, and no failure state to
|
|
27
|
+
* collapse into a zero.
|
|
28
|
+
* - A health value is **as old as the manifest a client cached** and must never be shown as live. A
|
|
29
|
+
* fact changes only when the adopter redeploys, which is when the manifest changes anyway.
|
|
30
|
+
* - A health value is a number to **render**. A fact is a decision to **respect** — it goes into the
|
|
31
|
+
* next request a client makes, not onto a rail.
|
|
32
|
+
*
|
|
33
|
+
* ## No per-key scope, deliberately
|
|
34
|
+
*
|
|
35
|
+
* A health key's `scope` is required, on the stated ground that a count is still a fact
|
|
36
|
+
* about somebody's security posture. A configured fact is not that. `billingSubject` names no account,
|
|
37
|
+
* no transaction and no amount, and reads identically against an empty database — it is a fact about
|
|
38
|
+
* the adopter's own configuration, and the whole manifest already sits behind `manifest:read`, the same
|
|
39
|
+
* gate that discloses every route, every scope and every composed version.
|
|
40
|
+
*
|
|
41
|
+
* Withholding stays addable with no wire change if a later fact wants it — a from-address, a region. A
|
|
42
|
+
* value absent from `config` while its key is present in `configKeys` already means "declared and not
|
|
43
|
+
* shown", which is the same pairing that tells `undeclared` from `withheld` for health, and
|
|
44
|
+
* {@link namedConfigValues} already survives it.
|
|
45
|
+
*
|
|
46
|
+
* ## What keeps it from becoming a config dump
|
|
47
|
+
*
|
|
48
|
+
* A capability's resolved config holds provider secrets, price ids and an adopter's whole catalog. The
|
|
49
|
+
* factory is the only door, and it is narrow on purpose: scalars only, a closed declared vocabulary,
|
|
50
|
+
* and a value refused unless something declared it. Nobody can write `manifestConfig: resolved`.
|
|
51
|
+
*/
|
|
52
|
+
/** A fact's name: one camelCase token, so it reads as a field and never as a path or an id. */
|
|
53
|
+
const MANIFEST_CONFIG_KEY_PATTERN = /^[a-z][A-Za-z0-9]*$/;
|
|
54
|
+
/**
|
|
55
|
+
* One configured fact's value. A string, a number, or a boolean — never anything else.
|
|
56
|
+
*
|
|
57
|
+
* The rule is in the type rather than in a comment: a config object, a product list, or a credentials
|
|
58
|
+
* bag does not compile, so the seam cannot become the route by which an adopter's catalog reaches a
|
|
59
|
+
* discovery read.
|
|
60
|
+
*/
|
|
61
|
+
const ManifestConfigValue = z.union([
|
|
62
|
+
z.string(),
|
|
63
|
+
z.number(),
|
|
64
|
+
z.boolean()
|
|
65
|
+
]).describe("One scalar: a word, a number, or a flag. Nothing else is representable — no nested objects, no lists, no credentials.");
|
|
66
|
+
/** The compile-time half of "scalars only". Widen the value type and this line names the file. */
|
|
67
|
+
const MANIFEST_CONFIG_VALUE_IS_SCALAR_ONLY = true;
|
|
68
|
+
/**
|
|
69
|
+
* One configured fact a capability may state, declared alongside its routes.
|
|
70
|
+
*
|
|
71
|
+
* `choices` is nullable rather than optional, and a null means "no closed list" rather than "not
|
|
72
|
+
* decided yet": a client renders a fact it has never heard of from the summary, and offers a picker
|
|
73
|
+
* only where the values are enumerable. A base path has no list; what a project bills has exactly two.
|
|
74
|
+
*/
|
|
75
|
+
const ManifestConfigKey = z.object({
|
|
76
|
+
key: z.string().regex(MANIFEST_CONFIG_KEY_PATTERN, "A config key is one camelCase token — `billingSubject`.").describe("The key this fact appears under in the capability's config. One camelCase token."),
|
|
77
|
+
choices: z.array(z.string().min(1)).min(1, "A closed list needs a value in it. Write null where the values are not enumerable.").nullable().describe("Every value this fact may take, so a client can offer the same choice the adopter made, or null where the values are not enumerable. At least one where it is a list: an empty one is not `null` spelled differently, it is a list nothing satisfies, so the capability could never boot and the refusal would name no permitted value at all. Read off the capability's own enum rather than retyped, so a new member cannot land without the manifest learning it."),
|
|
78
|
+
summary: z.string().min(1).describe("One line saying what the fact governs, for a client to render beside it without knowing the key.")
|
|
79
|
+
}).describe("One configured fact a capability states: what it is called, what it may be, and what it decides — enough for a client to respect a decision it did not make.");
|
|
80
|
+
/**
|
|
81
|
+
* A capability's configured facts as the manifest carries them: declared key → scalar.
|
|
82
|
+
*
|
|
83
|
+
* A record rather than a closed object, because the vocabulary is federated the way scopes and audit
|
|
84
|
+
* actions are — the capability name already namespaces it. **Parsing tolerates a key it has never heard
|
|
85
|
+
* of on purpose**: a client reading a newer Worker must drop the unknown one rather than fail the whole
|
|
86
|
+
* manifest, which is what {@link namedConfigValues} does with it.
|
|
87
|
+
*/
|
|
88
|
+
const ManifestConfigValues = z.record(z.string(), ManifestConfigValue).describe("One capability's configured facts: each declared key to its scalar. Unknown keys parse rather than throw, so a client of an older build reads what it knows and nothing for the rest.");
|
|
89
|
+
/**
|
|
90
|
+
* The brand only {@link defineManifestConfig} can produce.
|
|
91
|
+
*
|
|
92
|
+
* Module-local and never exported, so nothing outside this file can write it: the factory becomes the
|
|
93
|
+
* **only** way to build the seam, and a fact therefore cannot reach a manifest without having been
|
|
94
|
+
* checked against a declaration. An inline object literal on `Capability.manifestConfig` is a compile
|
|
95
|
+
* error, which is what stops the next capability from stating a fact nothing validated.
|
|
96
|
+
*/
|
|
97
|
+
const manifestConfigSeam = Symbol("pithy.controlPlane.manifestConfig");
|
|
98
|
+
/**
|
|
99
|
+
* Declare a capability's configured facts. The one constructor, so every declaration in the tree is
|
|
100
|
+
* checked — the key against the name pattern, the values against the keys, and each value against its
|
|
101
|
+
* own closed list.
|
|
102
|
+
*
|
|
103
|
+
* **Nothing downstream re-checks this.** `missingAdminRoutes` keeps a route declaration honest against
|
|
104
|
+
* the router that mounted it, and there is no equivalent here: the declaration and the value come from
|
|
105
|
+
* the same object, so the only defense against a fact that disagrees with the capability is building it
|
|
106
|
+
* from the *resolved* config at the one call site, exactly as `adminRoutes` is built from the resolved
|
|
107
|
+
* `basePath`.
|
|
108
|
+
*
|
|
109
|
+
* Every refusal is an {@link InternalError} rather than a bare `ZodError`, key named, because every one
|
|
110
|
+
* of them is an author's mistake read off a deploy that would not start — and an author reading it
|
|
111
|
+
* wants the problem line and the action line the rest of this kit gives them.
|
|
112
|
+
*/
|
|
113
|
+
function defineManifestConfig(input) {
|
|
114
|
+
if (input.keys.length === 0) throw new InternalError({
|
|
115
|
+
message: "A capability declared configured facts with no keys.",
|
|
116
|
+
action: "Declare at least one key, or omit the manifest config entirely.",
|
|
117
|
+
detail: "defineManifestConfig received an empty key list; an empty vocabulary states no fact."
|
|
118
|
+
});
|
|
119
|
+
const keys = [];
|
|
120
|
+
const declared = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const written of input.keys) {
|
|
122
|
+
const parsed = ManifestConfigKey.safeParse(written);
|
|
123
|
+
if (!parsed.success) throw new InternalError({
|
|
124
|
+
message: "A capability declared a configured fact its own schema refuses.",
|
|
125
|
+
action: "Name the fact with one camelCase token, list its values or null, and say what it decides.",
|
|
126
|
+
detail: `manifest config key ${JSON.stringify(written)} is invalid: ${parsed.error.message}`
|
|
127
|
+
});
|
|
128
|
+
if (declared.has(parsed.data.key)) throw new InternalError({
|
|
129
|
+
message: "A capability declared the same configured fact twice.",
|
|
130
|
+
action: "Give each fact one declaration — the second silently decides what the first meant.",
|
|
131
|
+
detail: `manifest config key ${parsed.data.key} is declared more than once`
|
|
132
|
+
});
|
|
133
|
+
declared.set(parsed.data.key, parsed.data);
|
|
134
|
+
keys.push(parsed.data);
|
|
135
|
+
}
|
|
136
|
+
const parsed = ManifestConfigValues.safeParse(input.values);
|
|
137
|
+
if (!parsed.success) throw new InternalError({
|
|
138
|
+
message: "A capability stated a configured fact that is not a scalar.",
|
|
139
|
+
action: "State a word, a number or a flag. A capability's own config never crosses whole.",
|
|
140
|
+
detail: `manifest config values are invalid: ${parsed.error.message}`
|
|
141
|
+
});
|
|
142
|
+
const values = parsed.data;
|
|
143
|
+
for (const key of Object.keys(values)) if (!declared.has(key)) throw new InternalError({
|
|
144
|
+
message: "A capability stated a configured fact it never declared.",
|
|
145
|
+
action: "Declare the fact alongside the capability's routes, or stop stating it.",
|
|
146
|
+
detail: `manifest config value ${key} has no declaration, so a client could only guess at what it means`
|
|
147
|
+
});
|
|
148
|
+
for (const key of keys) {
|
|
149
|
+
const value = Object.hasOwn(values, key.key) ? values[key.key] : void 0;
|
|
150
|
+
if (value === void 0) throw new InternalError({
|
|
151
|
+
message: "A capability declared a configured fact and stated no value for it.",
|
|
152
|
+
action: "State every declared fact, or remove the declaration.",
|
|
153
|
+
detail: `manifest config key ${key.key} is declared and has no value`
|
|
154
|
+
});
|
|
155
|
+
if (key.choices !== null && !(typeof value === "string" && key.choices.includes(value))) throw new InternalError({
|
|
156
|
+
message: "A capability stated a configured fact its own declaration does not permit.",
|
|
157
|
+
action: "State one of the declared choices, or widen the declaration to the enum it came from.",
|
|
158
|
+
detail: `manifest config key ${key.key} was given ${JSON.stringify(value)}, which is not one of ${key.choices.join(", ")}`
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
[manifestConfigSeam]: true,
|
|
163
|
+
keys,
|
|
164
|
+
values
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Pair a capability's configured facts with their declarations, dropping any value nothing declares.
|
|
169
|
+
*
|
|
170
|
+
* This is how "an unknown fact reads as nothing rather than as an error" is real rather than
|
|
171
|
+
* aspirational: a client reads what this returns, so a key from a Worker newer than its build simply is
|
|
172
|
+
* not in the list — and a fact it cannot name is a fact it would otherwise have to guess the meaning of.
|
|
173
|
+
*/
|
|
174
|
+
function namedConfigValues(descriptor) {
|
|
175
|
+
const named = [];
|
|
176
|
+
for (const key of descriptor.configKeys) {
|
|
177
|
+
if (!Object.hasOwn(descriptor.config, key.key)) continue;
|
|
178
|
+
const value = descriptor.config[key.key];
|
|
179
|
+
if (value !== void 0) named.push({
|
|
180
|
+
key,
|
|
181
|
+
value
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return named;
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { MANIFEST_CONFIG_VALUE_IS_SCALAR_ONLY, ManifestConfigKey, ManifestConfigValue, ManifestConfigValues, defineManifestConfig, namedConfigValues };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import type { Capability } from "../../capability/capability";
|
|
3
|
+
import type { AdminRoute } from "./adminRoute";
|
|
4
|
+
/**
|
|
5
|
+
* The gate that keeps `GET /control-plane/manifest` honest.
|
|
6
|
+
*
|
|
7
|
+
* A capability declares its admin routes by hand, and a hand-maintained list beside generated
|
|
8
|
+
* behavior is a list that rots: someone renames a path, changes a `basePath` default, moves an
|
|
9
|
+
* operation behind a different scope, and the declaration keeps confidently describing the old shape.
|
|
10
|
+
*
|
|
11
|
+
* **That failure is worse than having no manifest at all.** With no manifest a client knows it is
|
|
12
|
+
* guessing. With a stale one it believes a route exists, calls it, and gets a 404 it has no way to
|
|
13
|
+
* interpret — and the adopter sees a management client that is broken for reasons entirely inside
|
|
14
|
+
* somebody else's package.
|
|
15
|
+
*
|
|
16
|
+
* So the declaration is checked against the router that actually mounted, the same way
|
|
17
|
+
* `uncoveredParamRoutes` checks that every `:segment` carries a validator. Each capability asserts this
|
|
18
|
+
* in its own `routeContract.test.ts`, so a drifting declaration fails that package's CI rather than
|
|
19
|
+
* some integration run much later.
|
|
20
|
+
*/
|
|
21
|
+
/** One declared route that no composed route answers, and which capability claimed it. */
|
|
22
|
+
export interface AdminRouteDrift {
|
|
23
|
+
/** The capability whose declaration is wrong. */
|
|
24
|
+
capability: string;
|
|
25
|
+
/** The route it claims to expose. */
|
|
26
|
+
route: AdminRoute;
|
|
27
|
+
}
|
|
28
|
+
/** One mounted control-plane route that no capability declares. */
|
|
29
|
+
export interface UndeclaredAdminRoute {
|
|
30
|
+
/** The method it answers on. */
|
|
31
|
+
method: string;
|
|
32
|
+
/** The path it is mounted at. */
|
|
33
|
+
path: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every admin route a capability declares that the composed app does not actually serve.
|
|
37
|
+
*
|
|
38
|
+
* Empty is the passing state. A non-empty result names the capability and the route, because the fix
|
|
39
|
+
* is always in one of two places — the declaration or the registration — and knowing which capability
|
|
40
|
+
* owns both is most of the diagnosis.
|
|
41
|
+
*
|
|
42
|
+
* The comparison is on method and path only. Whether the route carries the right *scope* is not
|
|
43
|
+
* knowable from the router (a middleware is an opaque function to Hono), so that half stays the
|
|
44
|
+
* capability's responsibility and is covered by its own route tests.
|
|
45
|
+
*/
|
|
46
|
+
export declare function missingAdminRoutes(app: Hono<never>, capabilities: readonly Capability[]): AdminRouteDrift[];
|
|
47
|
+
/**
|
|
48
|
+
* Every control-plane route the app actually serves that no capability declares — the other direction.
|
|
49
|
+
*
|
|
50
|
+
* {@link missingAdminRoutes} walks declared → mounted and catches a declaration that lies. This walks
|
|
51
|
+
* mounted → declared and catches the opposite: a route that is guarded, reachable, and invisible.
|
|
52
|
+
*
|
|
53
|
+
* **That gap stopped being cosmetic when CORS started deriving from this table** (#468). An undeclared
|
|
54
|
+
* admin route was previously discoverable-by-trying — the manifest omitted it, a client that knew the
|
|
55
|
+
* path could still call it. Now it also gets no `app.use`, so its preflight 404s and no browser can
|
|
56
|
+
* reach it at all, while every test in the capability that owns it keeps passing. The failure is
|
|
57
|
+
* invisible from the capability's own suite and shows up as a pane that will not load.
|
|
58
|
+
*
|
|
59
|
+
* A route is control-plane if its handler chain carries a {@link isControlPlaneGuard} gate, which is
|
|
60
|
+
* the only thing that distinguishes one — a middleware is otherwise an opaque function to Hono, and
|
|
61
|
+
* inferring it from the path would be guessing about the one table this is meant to check.
|
|
62
|
+
*/
|
|
63
|
+
export declare function undeclaredAdminRoutes(app: Hono<never>, capabilities: readonly Capability[]): UndeclaredAdminRoute[];
|
|
64
|
+
//# sourceMappingURL=drift.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drift.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/discovery/drift.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;;;;;;;;;;;GAgBG;AAEH,0FAA0F;AAC1F,MAAM,WAAW,eAAe;IAC9B,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,mEAAmE;AACnE,MAAM,WAAW,oBAAoB;IACnC,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAOD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,SAAS,UAAU,EAAE,GAAG,eAAe,EAAE,CAS3G;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,SAAS,UAAU,EAAE,GAAG,oBAAoB,EAAE,CAcnH"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isControlPlaneGuard } from "../http/guard.js";
|
|
2
|
+
//#region src/controlPlane/discovery/drift.ts
|
|
3
|
+
/** `app.routes` entries, reduced to the pairs worth comparing. */
|
|
4
|
+
function mounted(app) {
|
|
5
|
+
return new Set(app.routes.map((route) => `${route.method.toUpperCase()} ${route.path}`));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Every admin route a capability declares that the composed app does not actually serve.
|
|
9
|
+
*
|
|
10
|
+
* Empty is the passing state. A non-empty result names the capability and the route, because the fix
|
|
11
|
+
* is always in one of two places — the declaration or the registration — and knowing which capability
|
|
12
|
+
* owns both is most of the diagnosis.
|
|
13
|
+
*
|
|
14
|
+
* The comparison is on method and path only. Whether the route carries the right *scope* is not
|
|
15
|
+
* knowable from the router (a middleware is an opaque function to Hono), so that half stays the
|
|
16
|
+
* capability's responsibility and is covered by its own route tests.
|
|
17
|
+
*/
|
|
18
|
+
function missingAdminRoutes(app, capabilities) {
|
|
19
|
+
const served = mounted(app);
|
|
20
|
+
const drift = [];
|
|
21
|
+
for (const capability of capabilities) for (const route of capability.adminRoutes ?? []) if (!served.has(`${route.method} ${route.path}`)) drift.push({
|
|
22
|
+
capability: capability.name,
|
|
23
|
+
route
|
|
24
|
+
});
|
|
25
|
+
return drift;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Every control-plane route the app actually serves that no capability declares — the other direction.
|
|
29
|
+
*
|
|
30
|
+
* {@link missingAdminRoutes} walks declared → mounted and catches a declaration that lies. This walks
|
|
31
|
+
* mounted → declared and catches the opposite: a route that is guarded, reachable, and invisible.
|
|
32
|
+
*
|
|
33
|
+
* **That gap stopped being cosmetic when CORS started deriving from this table** (#468). An undeclared
|
|
34
|
+
* admin route was previously discoverable-by-trying — the manifest omitted it, a client that knew the
|
|
35
|
+
* path could still call it. Now it also gets no `app.use`, so its preflight 404s and no browser can
|
|
36
|
+
* reach it at all, while every test in the capability that owns it keeps passing. The failure is
|
|
37
|
+
* invisible from the capability's own suite and shows up as a pane that will not load.
|
|
38
|
+
*
|
|
39
|
+
* A route is control-plane if its handler chain carries a {@link isControlPlaneGuard} gate, which is
|
|
40
|
+
* the only thing that distinguishes one — a middleware is otherwise an opaque function to Hono, and
|
|
41
|
+
* inferring it from the path would be guessing about the one table this is meant to check.
|
|
42
|
+
*/
|
|
43
|
+
function undeclaredAdminRoutes(app, capabilities) {
|
|
44
|
+
const declared = /* @__PURE__ */ new Set();
|
|
45
|
+
for (const capability of capabilities) for (const route of capability.adminRoutes ?? []) declared.add(`${route.method} ${route.path}`);
|
|
46
|
+
const guarded = /* @__PURE__ */ new Map();
|
|
47
|
+
for (const route of app.routes) {
|
|
48
|
+
if (!isControlPlaneGuard(route.handler)) continue;
|
|
49
|
+
const method = route.method.toUpperCase();
|
|
50
|
+
guarded.set(`${method} ${route.path}`, {
|
|
51
|
+
method,
|
|
52
|
+
path: route.path
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return [...guarded.entries()].filter(([key]) => !declared.has(key)).map(([, route]) => route);
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { missingAdminRoutes, undeclaredAdminRoutes };
|