@pithy-sh/core 0.1.2 → 0.1.4
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,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { HostEnvReport } from "../workflow/hostEnv";
|
|
3
|
+
/**
|
|
4
|
+
* How a capability checks that its own settings **work** — the seam `pithy doctor` runs (#411).
|
|
5
|
+
*
|
|
6
|
+
* Everything doctor asked before this was a question about *presence*: is the option key written, is the
|
|
7
|
+
* binding declared, is the ledger level. So a project could be entirely green while `fromAddress` named a
|
|
8
|
+
* domain nobody onboarded, the signing key was never created, `BASE_URL` was staging's URL in production's
|
|
9
|
+
* config, and no mail arrived. Presence is not the same question as correctness, and only the capability
|
|
10
|
+
* knows the second one.
|
|
11
|
+
*
|
|
12
|
+
* ## It hangs off the capability, never off the manifest
|
|
13
|
+
*
|
|
14
|
+
* The declaration sits on the {@link import("./capability").Capability} object, beside `health`, because
|
|
15
|
+
* `pithy.manifest.json` looks like the obvious signal and is the wrong one: `@pithy-sh/matchmaking` and
|
|
16
|
+
* `@pithy-sh/rating` are published capability packages that ship no manifest, and a manifest-keyed rule
|
|
17
|
+
* skips both in silence. The CLI already holds every composed instance (`project/workerScope.ts`), so
|
|
18
|
+
* discovery costs it nothing and misses nothing.
|
|
19
|
+
*
|
|
20
|
+
* A capability with nothing worth checking declares nothing. That is ordinary, silent, and not a fault.
|
|
21
|
+
*
|
|
22
|
+
* ## Two tiers, because they cost different things
|
|
23
|
+
*
|
|
24
|
+
* - **local** — does the value parse, is it the right shape, is it right for this environment. Free,
|
|
25
|
+
* offline, and always run. It validates through the very Zod object the capability's host Worker
|
|
26
|
+
* validates at boot ({@link hostEnvFindings} is the bridge), so the check an operator runs and the
|
|
27
|
+
* check the host runs cannot come to two answers.
|
|
28
|
+
* - **account** — is the domain a zone here, does the secret exist, is the database there. One Cloudflare
|
|
29
|
+
* call, so it is opt-in and it is skipped whenever the account cannot be reached.
|
|
30
|
+
*
|
|
31
|
+
* Both are faults. A local finding fails `pithy doctor`'s exit; an account finding does too, **but only
|
|
32
|
+
* when the account was reached** — an unreachable account is reported as *skipped*, never as a pass, and
|
|
33
|
+
* gates nothing. Three answers, and a boolean can hold two of them.
|
|
34
|
+
*
|
|
35
|
+
* ## Nothing here writes
|
|
36
|
+
*
|
|
37
|
+
* A check reports. `pithy upgrade`, `pithy <capability> provision` and the commands each finding names are
|
|
38
|
+
* what change anything, which is why every finding carries the action that resolves it.
|
|
39
|
+
*/
|
|
40
|
+
/** One setting that does not work, and the one thing an operator does about it. */
|
|
41
|
+
export declare const SettingsFinding: z.ZodObject<{
|
|
42
|
+
setting: z.ZodString;
|
|
43
|
+
environment: z.ZodNullable<z.ZodString>;
|
|
44
|
+
problem: z.ZodString;
|
|
45
|
+
action: z.ZodString;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
export type SettingsFinding = z.infer<typeof SettingsFinding>;
|
|
48
|
+
/** One environment the project declares, and what this Worker's own config says about it. */
|
|
49
|
+
export interface SettingsEnvironment {
|
|
50
|
+
/** The environment id — `dev`, `staging`, `prod`. Verbatim, never `production`. */
|
|
51
|
+
name: string;
|
|
52
|
+
/**
|
|
53
|
+
* The origin this Worker answers on in that environment, or `null` where its config names none.
|
|
54
|
+
*
|
|
55
|
+
* Supplied by the CLI rather than resolved by the capability: which hostname an environment serves is
|
|
56
|
+
* the adopter's declaration to make, and `project/domains.ts` is the one reader of it.
|
|
57
|
+
*/
|
|
58
|
+
origin: string | null;
|
|
59
|
+
}
|
|
60
|
+
/** What a check is told about the project it is checking. Both tiers get it; the account tier gets more. */
|
|
61
|
+
export interface SettingsCheckContext {
|
|
62
|
+
/** The root config's `name` — the leading segment of every resource this project provisions. */
|
|
63
|
+
project: string;
|
|
64
|
+
/** The Worker whose `pithy.config.ts` composed this capability. */
|
|
65
|
+
worker: string;
|
|
66
|
+
/** Every environment the project declares, in declaration order. */
|
|
67
|
+
environments: readonly SettingsEnvironment[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The account questions a check may ask — a closed vocabulary, on purpose.
|
|
71
|
+
*
|
|
72
|
+
* Closed for the reason `CapabilityHealth` is: handing a capability a raw Cloudflare client would make
|
|
73
|
+
* every capability a place a network call can be invented, and would put `@pithy-sh/cloudflare` in the
|
|
74
|
+
* dependency list of packages that need none of it. Three questions cover what the first checks ask; a
|
|
75
|
+
* fourth lands here, once, when a capability actually needs it.
|
|
76
|
+
*
|
|
77
|
+
* Every method may throw. A throw is the account failing to answer, and the runner reports that
|
|
78
|
+
* capability's account tier as unchecked rather than as a pass.
|
|
79
|
+
*/
|
|
80
|
+
export interface SettingsAccountReader {
|
|
81
|
+
/** Every D1 database name this account holds. */
|
|
82
|
+
d1Databases(): Promise<readonly string[]>;
|
|
83
|
+
/** Whether this account holds a Cloudflare zone covering the hostname — the prerequisite for onboarding it. */
|
|
84
|
+
zone(hostname: string): Promise<boolean>;
|
|
85
|
+
/** Whether a declared secret has a value in that environment. The name is the registry's, not a binding's. */
|
|
86
|
+
secret(request: {
|
|
87
|
+
name: string;
|
|
88
|
+
environment: string;
|
|
89
|
+
}): Promise<boolean>;
|
|
90
|
+
}
|
|
91
|
+
/** What the account tier is told: everything the local tier gets, plus the account it may ask. */
|
|
92
|
+
export interface SettingsAccountContext extends SettingsCheckContext {
|
|
93
|
+
/** The account, as the closed set of questions above. */
|
|
94
|
+
account: SettingsAccountReader;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A capability's settings check, as it hangs off the capability.
|
|
98
|
+
*
|
|
99
|
+
* `local` is required — a capability that declares this seam declares at least the free half of it. A
|
|
100
|
+
* check that would only ever reach the account is a check that says nothing offline, and offline is where
|
|
101
|
+
* most of these faults are cheapest to find.
|
|
102
|
+
*/
|
|
103
|
+
export interface CapabilitySettings {
|
|
104
|
+
/** The free half. Pure, offline, and always run. */
|
|
105
|
+
local(context: SettingsCheckContext): SettingsFinding[] | Promise<SettingsFinding[]>;
|
|
106
|
+
/** The half that costs a Cloudflare call. Omitted where a capability has nothing to ask the account. */
|
|
107
|
+
account?(context: SettingsAccountContext): Promise<SettingsFinding[]>;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Turn a host env's own parse report into findings — the bridge that makes "one schema, two readers" real.
|
|
111
|
+
*
|
|
112
|
+
* A capability's local tier builds the env its host *would* be handed, runs {@link
|
|
113
|
+
* import("../workflow/hostEnv").checkHostEnv} against its own declaration, and hands the report here. The
|
|
114
|
+
* problem line is Zod's own message and the action line is {@link hostEnvProviderSentence} — the same
|
|
115
|
+
* words the host writes into its log before it refuses to start. Two readers of one declaration, and no
|
|
116
|
+
* second wording to drift.
|
|
117
|
+
*/
|
|
118
|
+
export declare function hostEnvFindings(report: HostEnvReport<unknown>, environment: string | null): SettingsFinding[];
|
|
119
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/capability/settings.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,mFAAmF;AACnF,eAAO,MAAM,eAAe;;;;;iBA0B+D,CAAC;AAC5F,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,6FAA6F;AAC7F,MAAM,WAAW,mBAAmB;IAClC,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,4GAA4G;AAC5G,MAAM,WAAW,oBAAoB;IACnC,gGAAgG;IAChG,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,YAAY,EAAE,SAAS,mBAAmB,EAAE,CAAC;CAC9C;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,qBAAqB;IACpC,iDAAiD;IACjD,WAAW,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IAC1C,+GAA+G;IAC/G,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,8GAA8G;IAC9G,MAAM,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1E;AAED,kGAAkG;AAClG,MAAM,WAAW,sBAAuB,SAAQ,oBAAoB;IAClE,yDAAyD;IACzD,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,oDAAoD;IACpD,KAAK,CAAC,OAAO,EAAE,oBAAoB,GAAG,eAAe,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IACrF,wGAAwG;IACxG,OAAO,CAAC,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,eAAe,EAAE,CAO7G"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { hostEnvProviderSentence } from "../workflow/hostEnv.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/capability/settings.ts
|
|
4
|
+
/**
|
|
5
|
+
* How a capability checks that its own settings **work** — the seam `pithy doctor` runs (#411).
|
|
6
|
+
*
|
|
7
|
+
* Everything doctor asked before this was a question about *presence*: is the option key written, is the
|
|
8
|
+
* binding declared, is the ledger level. So a project could be entirely green while `fromAddress` named a
|
|
9
|
+
* domain nobody onboarded, the signing key was never created, `BASE_URL` was staging's URL in production's
|
|
10
|
+
* config, and no mail arrived. Presence is not the same question as correctness, and only the capability
|
|
11
|
+
* knows the second one.
|
|
12
|
+
*
|
|
13
|
+
* ## It hangs off the capability, never off the manifest
|
|
14
|
+
*
|
|
15
|
+
* The declaration sits on the {@link import("./capability").Capability} object, beside `health`, because
|
|
16
|
+
* `pithy.manifest.json` looks like the obvious signal and is the wrong one: `@pithy-sh/matchmaking` and
|
|
17
|
+
* `@pithy-sh/rating` are published capability packages that ship no manifest, and a manifest-keyed rule
|
|
18
|
+
* skips both in silence. The CLI already holds every composed instance (`project/workerScope.ts`), so
|
|
19
|
+
* discovery costs it nothing and misses nothing.
|
|
20
|
+
*
|
|
21
|
+
* A capability with nothing worth checking declares nothing. That is ordinary, silent, and not a fault.
|
|
22
|
+
*
|
|
23
|
+
* ## Two tiers, because they cost different things
|
|
24
|
+
*
|
|
25
|
+
* - **local** — does the value parse, is it the right shape, is it right for this environment. Free,
|
|
26
|
+
* offline, and always run. It validates through the very Zod object the capability's host Worker
|
|
27
|
+
* validates at boot ({@link hostEnvFindings} is the bridge), so the check an operator runs and the
|
|
28
|
+
* check the host runs cannot come to two answers.
|
|
29
|
+
* - **account** — is the domain a zone here, does the secret exist, is the database there. One Cloudflare
|
|
30
|
+
* call, so it is opt-in and it is skipped whenever the account cannot be reached.
|
|
31
|
+
*
|
|
32
|
+
* Both are faults. A local finding fails `pithy doctor`'s exit; an account finding does too, **but only
|
|
33
|
+
* when the account was reached** — an unreachable account is reported as *skipped*, never as a pass, and
|
|
34
|
+
* gates nothing. Three answers, and a boolean can hold two of them.
|
|
35
|
+
*
|
|
36
|
+
* ## Nothing here writes
|
|
37
|
+
*
|
|
38
|
+
* A check reports. `pithy upgrade`, `pithy <capability> provision` and the commands each finding names are
|
|
39
|
+
* what change anything, which is why every finding carries the action that resolves it.
|
|
40
|
+
*/
|
|
41
|
+
/** One setting that does not work, and the one thing an operator does about it. */
|
|
42
|
+
const SettingsFinding = z.object({
|
|
43
|
+
setting: z.string().min(1).describe("What is wrong — the env field, config key, or account resource, spelled as the operator would find it. It leads the line, so it is the name they search for."),
|
|
44
|
+
environment: z.string().min(1).nullable().describe("The environment this is about, or `null` where the setting is the same in all of them. A per-environment fault is reported once per environment, because that is how many edits it takes."),
|
|
45
|
+
problem: z.string().min(1).describe("Why it does not work, in one sentence. The problem line, in the same voice every PithyError uses."),
|
|
46
|
+
action: z.string().min(1).describe("What resolves it: the `pithy` command, the `pithy.config.ts` key, or the one-time account action. Never optional — a finding nobody can act on is a complaint.")
|
|
47
|
+
}).describe("One capability setting that does not work, with the action that resolves it.");
|
|
48
|
+
/**
|
|
49
|
+
* Turn a host env's own parse report into findings — the bridge that makes "one schema, two readers" real.
|
|
50
|
+
*
|
|
51
|
+
* A capability's local tier builds the env its host *would* be handed, runs {@link
|
|
52
|
+
* import("../workflow/hostEnv").checkHostEnv} against its own declaration, and hands the report here. The
|
|
53
|
+
* problem line is Zod's own message and the action line is {@link hostEnvProviderSentence} — the same
|
|
54
|
+
* words the host writes into its log before it refuses to start. Two readers of one declaration, and no
|
|
55
|
+
* second wording to drift.
|
|
56
|
+
*/
|
|
57
|
+
function hostEnvFindings(report, environment) {
|
|
58
|
+
return report.problems.map((problem) => ({
|
|
59
|
+
setting: problem.field,
|
|
60
|
+
environment,
|
|
61
|
+
problem: problem.reason,
|
|
62
|
+
action: hostEnvProviderSentence(problem.provider)
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { SettingsFinding, hostEnvFindings };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type BindingSpec } from "./bindings";
|
|
2
|
+
/** Throw a typed error if any non-optional binding is absent from `env`. */
|
|
3
|
+
export declare function validateBindings(env: Record<string, unknown>, required: BindingSpec[]): void;
|
|
4
|
+
//# sourceMappingURL=validateBindings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateBindings.d.ts","sourceRoot":"","sources":["../../src/capability/validateBindings.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,WAAW,EAAwB,MAAM,YAAY,CAAC;AA2BpE,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI,CAU5F"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isProvisionedBinding } from "./bindings.js";
|
|
2
|
+
import { InternalError } from "../error/pithyError.js";
|
|
3
|
+
//#region src/capability/validateBindings.ts
|
|
4
|
+
/** How a missing binding is named in both halves of the error: `kv:SESSIONS`. */
|
|
5
|
+
function label(binding) {
|
|
6
|
+
return `${binding.type}:${binding.name}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* What to do about the bindings that are missing — and there are two answers, not one.
|
|
10
|
+
*
|
|
11
|
+
* A `kv` or `d1` binding is a line the adopter adds to `wrangler.jsonc`. A `secret`, `workflow`, or
|
|
12
|
+
* `vectorize` binding is not: its entry carries a provisioned value, so the old single line
|
|
13
|
+
* ("Add the binding(s) to wrangler.jsonc, then redeploy.") sent anyone missing a Secrets Store entry
|
|
14
|
+
* to a file that entry never appears in. Each list names only its own bindings, so a mixed failure
|
|
15
|
+
* says which of them is which instead of leaving the reader to sort it out.
|
|
16
|
+
*/
|
|
17
|
+
function bindingAction(missing) {
|
|
18
|
+
const written = missing.filter((binding) => !isProvisionedBinding(binding.type)).map(label);
|
|
19
|
+
const provisioned = missing.filter((binding) => isProvisionedBinding(binding.type)).map(label);
|
|
20
|
+
const sentences = [];
|
|
21
|
+
if (written.length > 0) sentences.push(`Add ${written.join(", ")} to wrangler.jsonc, then redeploy.`);
|
|
22
|
+
if (provisioned.length > 0) sentences.push(`Provision ${provisioned.join(", ")} — a provision command creates those, or .dev.vars in dev.`);
|
|
23
|
+
return sentences.join(" ");
|
|
24
|
+
}
|
|
25
|
+
/** Throw a typed error if any non-optional binding is absent from `env`. */
|
|
26
|
+
function validateBindings(env, required) {
|
|
27
|
+
const missing = required.filter((b) => !b.optional && env[b.name] == null);
|
|
28
|
+
if (missing.length > 0) throw new InternalError({
|
|
29
|
+
message: `Missing required bindings: ${missing.map(label).join(", ")}`,
|
|
30
|
+
action: bindingAction(missing)
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { validateBindings };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Whether a value states no key that would vanish during parsing.
|
|
4
|
+
*
|
|
5
|
+
* Non-objects pass: they hold no keys, and the schema behind this guard is what refuses them for being the
|
|
6
|
+
* wrong type. `Object.hasOwn` and not `in`, because every object inherits `__proto__` and only an own one
|
|
7
|
+
* is data somebody wrote.
|
|
8
|
+
*/
|
|
9
|
+
export declare function statesNoVanishingKey(value: unknown): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* A schema held to keys that survive being read.
|
|
12
|
+
*
|
|
13
|
+
* Wraps rather than replaces: the schema behind it keeps its own key and value rules, and this only
|
|
14
|
+
* guarantees they are given every key the payload wrote. See {@link VANISHING_KEY} for why that is not
|
|
15
|
+
* already true.
|
|
16
|
+
*
|
|
17
|
+
* `subject` names what wrote the record, because the reader of the error is the person who has to go and
|
|
18
|
+
* change it — a capability author for a manifest, an adopter for a projection.
|
|
19
|
+
*
|
|
20
|
+
* Typed on the wrapped schema's own input rather than on `unknown`, so a guard is transparent to whatever
|
|
21
|
+
* it is composed into. Widening every wrapped schema to `unknown` would make the guard's altitude a
|
|
22
|
+
* typing question — it could only go where nothing downstream needed the input type — and altitude is the
|
|
23
|
+
* one thing about this guard that should be decided on the exposure.
|
|
24
|
+
*/
|
|
25
|
+
export declare function refusesVanishingKey<Output, Input>(schema: z.ZodType<Output, Input>, subject: string): z.ZodPipe<z.ZodCustom<Input, Input>, z.ZodType<Output, Input>>;
|
|
26
|
+
/**
|
|
27
|
+
* A record a manifest may state. The manifest's wording of {@link refusesVanishingKey}, kept in one place
|
|
28
|
+
* so its two call sites cannot drift into two sentences for one rule.
|
|
29
|
+
*/
|
|
30
|
+
export declare function manifestRecord<Output, Input>(record: z.ZodType<Output, Input>): z.ZodPipe<z.ZodCustom<Input, Input>, z.ZodType<Output, Input>>;
|
|
31
|
+
//# sourceMappingURL=vanishingKey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vanishingKey.d.ts","sourceRoot":"","sources":["../../src/capability/vanishingKey.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA0CxB;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAG5D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,KAAK,EAC/C,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,EAChC,OAAO,EAAE,MAAM,GACd,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAMhE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,KAAK,EAC1C,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,GAC/B,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAEhE"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/capability/vanishingKey.ts
|
|
3
|
+
/**
|
|
4
|
+
* The one key a parsed object loses on the way through a schema, and the guard that refuses it.
|
|
5
|
+
*
|
|
6
|
+
* `JSON.parse` gives `__proto__` an own property, because it must — the alternative is JSON that can set
|
|
7
|
+
* the prototype of every object built from it. Zod then skips that key while projecting an object or a
|
|
8
|
+
* record, for exactly the same reason: assigning it onto the `{}` it builds into would run the setter on
|
|
9
|
+
* `Object.prototype` instead of creating a property. Neither decision is wrong. Together they mean a key
|
|
10
|
+
* enters the parse, matches no rule, raises no issue, and is not in the result.
|
|
11
|
+
*
|
|
12
|
+
* That is the shape worth naming: **not a value rewritten, a key that was never reported at all.** A key
|
|
13
|
+
* schema would have refused `__proto__` on its first character; it never runs, because the skip happens
|
|
14
|
+
* before the key type does. So this does not add a rule. It gets the rule that is already written back in
|
|
15
|
+
* front of the data.
|
|
16
|
+
*
|
|
17
|
+
* **`Object.create(null)` does not close this, and #331 left that question open.** The idea is sound on
|
|
18
|
+
* its face — a null-prototype target has no `__proto__` setter to run, so the assignment would create a
|
|
19
|
+
* property and the key would survive — and it is wrong here for a reason that is worth writing down once.
|
|
20
|
+
* Zod's skip is a literal `key === "__proto__"` comparison made *before* any assignment
|
|
21
|
+
* (`zod/v4/core/schemas` at the object and record branches), so the prototype of the target has no bearing
|
|
22
|
+
* on it, and neither does the prototype of the input: handing `z.record` a value built with
|
|
23
|
+
* `Object.assign(Object.create(null), parsed)` still returns without the key. Measured, not assumed. The
|
|
24
|
+
* class cannot be closed by changing what the object is; it can only be closed in front of the schema,
|
|
25
|
+
* which is what this is.
|
|
26
|
+
*
|
|
27
|
+
* **Refuse, not degrade.** The surrounding design degrades an unrecognized *issuer* to `other` so a client
|
|
28
|
+
* built today can read a manifest written tomorrow, and that is right for a value. It is wrong here twice
|
|
29
|
+
* over. `__proto__` is not a name a future issuer will be called; it is a mistake or an attack. And
|
|
30
|
+
* degrading a key is a merge — the defect the issuer-key rule was written to close, where two unknown keys
|
|
31
|
+
* rewritten to one overwrite each other's requirements in silence. Refusing is the only answer that leaves
|
|
32
|
+
* nothing to detect after the fact: a payload that will not parse is reported by every client that reads
|
|
33
|
+
* it, and `pithy doctor` already names the package that shipped it.
|
|
34
|
+
*
|
|
35
|
+
* Applied at the schema, not at a call site, because the exposure belongs to the *shape* — any record read
|
|
36
|
+
* from outside this process has it — and a rule that lives at one call site is how this codebase has
|
|
37
|
+
* repeatedly fixed one field and left its siblings. `vanishingKey.test.ts` walks each guarded root and
|
|
38
|
+
* attacks every record it finds, so a record added later without this guard fails on the commit that adds
|
|
39
|
+
* it.
|
|
40
|
+
*/
|
|
41
|
+
const VANISHING_KEY = "__proto__";
|
|
42
|
+
/**
|
|
43
|
+
* Whether a value states no key that would vanish during parsing.
|
|
44
|
+
*
|
|
45
|
+
* Non-objects pass: they hold no keys, and the schema behind this guard is what refuses them for being the
|
|
46
|
+
* wrong type. `Object.hasOwn` and not `in`, because every object inherits `__proto__` and only an own one
|
|
47
|
+
* is data somebody wrote.
|
|
48
|
+
*/
|
|
49
|
+
function statesNoVanishingKey(value) {
|
|
50
|
+
if (typeof value !== "object" || value === null) return true;
|
|
51
|
+
return !Object.hasOwn(value, VANISHING_KEY);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A schema held to keys that survive being read.
|
|
55
|
+
*
|
|
56
|
+
* Wraps rather than replaces: the schema behind it keeps its own key and value rules, and this only
|
|
57
|
+
* guarantees they are given every key the payload wrote. See {@link VANISHING_KEY} for why that is not
|
|
58
|
+
* already true.
|
|
59
|
+
*
|
|
60
|
+
* `subject` names what wrote the record, because the reader of the error is the person who has to go and
|
|
61
|
+
* change it — a capability author for a manifest, an adopter for a projection.
|
|
62
|
+
*
|
|
63
|
+
* Typed on the wrapped schema's own input rather than on `unknown`, so a guard is transparent to whatever
|
|
64
|
+
* it is composed into. Widening every wrapped schema to `unknown` would make the guard's altitude a
|
|
65
|
+
* typing question — it could only go where nothing downstream needed the input type — and altitude is the
|
|
66
|
+
* one thing about this guard that should be decided on the exposure.
|
|
67
|
+
*/
|
|
68
|
+
function refusesVanishingKey(schema, subject) {
|
|
69
|
+
return z.custom(statesNoVanishingKey, { error: `${subject} may not state a "${VANISHING_KEY}" key. It is the one name a parsed object loses without a word, so whatever it keyed would be gone with it.` }).pipe(schema);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A record a manifest may state. The manifest's wording of {@link refusesVanishingKey}, kept in one place
|
|
73
|
+
* so its two call sites cannot drift into two sentences for one rule.
|
|
74
|
+
*/
|
|
75
|
+
function manifestRecord(record) {
|
|
76
|
+
return refusesVanishingKey(record, "A manifest record");
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { manifestRecord, refusesVanishingKey, statesNoVanishingKey };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { AuditEventInput } from "../../audit/auditEvent";
|
|
2
|
+
import type { AuditEmit } from "../../audit/recorder";
|
|
3
|
+
import type { Logger } from "../../logger/logger";
|
|
4
|
+
/**
|
|
5
|
+
* The audit action codes the `control-plane` seam emits, as `domain/reason` strings under the
|
|
6
|
+
* `controlplane` domain.
|
|
7
|
+
*
|
|
8
|
+
* **The domain is one word on purpose.** `AuditAction`'s pattern allows no hyphen, so
|
|
9
|
+
* `control-plane/call_allowed` is not a valid code at all. The route-strategy literal stays
|
|
10
|
+
* `control-plane`; every namespace token — capability, migrations, table prefix, error codes, these
|
|
11
|
+
* actions — is `controlplane`. The audit *actor kind* is the hyphenated `"control-plane"`, because
|
|
12
|
+
* that is an enum member and not a namespace. (None of this is Cloudflare's control plane; that is the
|
|
13
|
+
* outbound provisioning API behind `@pithy-sh/cloudflare`.)
|
|
14
|
+
*
|
|
15
|
+
* These are declared as a plain `as const` map rather than through `@pithy-sh/audit`'s
|
|
16
|
+
* `defineAuditActions`, because audit already depends on core — importing it back would be a cycle.
|
|
17
|
+
* `actions.test.ts` asserts every value against core's `AuditAction` pattern instead, so the map is
|
|
18
|
+
* held to the same contract the helper would have enforced.
|
|
19
|
+
*
|
|
20
|
+
* Emission goes through core's `emit` seam (`c.var.emit`), a no-op when the audit capability is
|
|
21
|
+
* absent, so the seam never imports audit at runtime either (principle 4).
|
|
22
|
+
*
|
|
23
|
+
* **Four of these are emitted by a route; three are emitted by the CLI** (#294). The connection
|
|
24
|
+
* lifecycle — registered, updated, removed — is written by `pithy dashboard` opening the adopter's D1
|
|
25
|
+
* directly, because those operations never reach their Worker: a first connect has no key to sign with
|
|
26
|
+
* and the Worker may not be deployed, an update re-points an address the Worker never reads, and a
|
|
27
|
+
* disconnect withdraws trust, which must not depend on a working connection (docs/CONTROL-PLANE.md §15).
|
|
28
|
+
* So there is no route in a position to record them, and the write records itself instead — in the same
|
|
29
|
+
* D1, from `connectionRegistry`. The `actorType` is what tells the two writers apart: a route records
|
|
30
|
+
* `control-plane`, meaning the management client called in and proved it, while a CLI-side event carries
|
|
31
|
+
* the adopter's own operator and no session at all.
|
|
32
|
+
*
|
|
33
|
+
* A declared code that nothing emits is a surface the trail promises and never delivers, which is what
|
|
34
|
+
* these three were until #294. `packages/cli/src/ci/auditActions.test.ts` fails the build on one.
|
|
35
|
+
*/
|
|
36
|
+
export declare const ControlPlaneAuditActions: {
|
|
37
|
+
/** A verified, in-scope management call was admitted. The trail of what the dashboard actually did. */
|
|
38
|
+
readonly callAllowed: "controlplane/call_allowed";
|
|
39
|
+
/** A management call was refused — no connection, bad credential, or an ungranted scope. Outcome `denied`. */
|
|
40
|
+
readonly callDenied: "controlplane/call_denied";
|
|
41
|
+
/** A new public key was registered on a connection, opening a rotation overlap. */
|
|
42
|
+
readonly keyRegistered: "controlplane/key_registered";
|
|
43
|
+
/** A superseded public key was given an end date and stopped being accepted. */
|
|
44
|
+
readonly keyExpired: "controlplane/key_expired";
|
|
45
|
+
/** A management client was connected to this environment — a new connection, or one started over. */
|
|
46
|
+
readonly connectionRegistered: "controlplane/connection_registered";
|
|
47
|
+
/** An existing connection changed — its granted scopes, the Worker URL it points at, or the keys it trusts. */
|
|
48
|
+
readonly connectionUpdated: "controlplane/connection_updated";
|
|
49
|
+
/** A connection was removed. The seam returns to its shipped state: connected to nothing, denying everything. */
|
|
50
|
+
readonly connectionRemoved: "controlplane/connection_removed";
|
|
51
|
+
};
|
|
52
|
+
/** One of the control-plane audit action codes. */
|
|
53
|
+
export type ControlPlaneAuditAction = (typeof ControlPlaneAuditActions)[keyof typeof ControlPlaneAuditActions];
|
|
54
|
+
/**
|
|
55
|
+
* Emit an audit event and swallow any failure.
|
|
56
|
+
*
|
|
57
|
+
* **Every denial emits an event, and that is exactly why this cannot throw.** By the time the seam
|
|
58
|
+
* records a call, the security-relevant outcome is already decided — the 401 or 403 is going out
|
|
59
|
+
* whatever happens next. An audit write that threw would convert a correct denial into a 500: it would
|
|
60
|
+
* hand the caller a different response for a *failing* store than for a healthy one, and turn a
|
|
61
|
+
* transient D1 problem into an availability bug on the admin surface. The record is evidence of the
|
|
62
|
+
* decision, never part of making it.
|
|
63
|
+
*
|
|
64
|
+
* The real recorder is non-fatal by contract already ({@link AuditEmit}), so this is belt and braces —
|
|
65
|
+
* but the gate is a security boundary and does not get to assume its collaborators behave. The failure
|
|
66
|
+
* is logged when a logger is at hand, so a store that has stopped accepting writes is visible rather
|
|
67
|
+
* than silent.
|
|
68
|
+
*/
|
|
69
|
+
export declare function safeEmit(emit: AuditEmit, event: AuditEventInput, log?: Logger): Promise<void>;
|
|
70
|
+
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/audit/actions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,wBAAwB;IACnC,uGAAuG;aACvG,WAAW,EAAE,2BAA2B;IACxC,8GAA8G;aAC9G,UAAU,EAAE,0BAA0B;IACtC,mFAAmF;aACnF,aAAa,EAAE,6BAA6B;IAC5C,gFAAgF;aAChF,UAAU,EAAE,0BAA0B;IACtC,qGAAqG;aACrG,oBAAoB,EAAE,oCAAoC;IAC1D,+GAA+G;aAC/G,iBAAiB,EAAE,iCAAiC;IACpD,iHAAiH;aACjH,iBAAiB,EAAE,iCAAiC;CAC5C,CAAC;AAEX,mDAAmD;AACnD,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,OAAO,wBAAwB,CAAC,CAAC;AAE/G;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMnG"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region src/controlPlane/audit/actions.ts
|
|
2
|
+
/**
|
|
3
|
+
* The audit action codes the `control-plane` seam emits, as `domain/reason` strings under the
|
|
4
|
+
* `controlplane` domain.
|
|
5
|
+
*
|
|
6
|
+
* **The domain is one word on purpose.** `AuditAction`'s pattern allows no hyphen, so
|
|
7
|
+
* `control-plane/call_allowed` is not a valid code at all. The route-strategy literal stays
|
|
8
|
+
* `control-plane`; every namespace token — capability, migrations, table prefix, error codes, these
|
|
9
|
+
* actions — is `controlplane`. The audit *actor kind* is the hyphenated `"control-plane"`, because
|
|
10
|
+
* that is an enum member and not a namespace. (None of this is Cloudflare's control plane; that is the
|
|
11
|
+
* outbound provisioning API behind `@pithy-sh/cloudflare`.)
|
|
12
|
+
*
|
|
13
|
+
* These are declared as a plain `as const` map rather than through `@pithy-sh/audit`'s
|
|
14
|
+
* `defineAuditActions`, because audit already depends on core — importing it back would be a cycle.
|
|
15
|
+
* `actions.test.ts` asserts every value against core's `AuditAction` pattern instead, so the map is
|
|
16
|
+
* held to the same contract the helper would have enforced.
|
|
17
|
+
*
|
|
18
|
+
* Emission goes through core's `emit` seam (`c.var.emit`), a no-op when the audit capability is
|
|
19
|
+
* absent, so the seam never imports audit at runtime either (principle 4).
|
|
20
|
+
*
|
|
21
|
+
* **Four of these are emitted by a route; three are emitted by the CLI** (#294). The connection
|
|
22
|
+
* lifecycle — registered, updated, removed — is written by `pithy dashboard` opening the adopter's D1
|
|
23
|
+
* directly, because those operations never reach their Worker: a first connect has no key to sign with
|
|
24
|
+
* and the Worker may not be deployed, an update re-points an address the Worker never reads, and a
|
|
25
|
+
* disconnect withdraws trust, which must not depend on a working connection (docs/CONTROL-PLANE.md §15).
|
|
26
|
+
* So there is no route in a position to record them, and the write records itself instead — in the same
|
|
27
|
+
* D1, from `connectionRegistry`. The `actorType` is what tells the two writers apart: a route records
|
|
28
|
+
* `control-plane`, meaning the management client called in and proved it, while a CLI-side event carries
|
|
29
|
+
* the adopter's own operator and no session at all.
|
|
30
|
+
*
|
|
31
|
+
* A declared code that nothing emits is a surface the trail promises and never delivers, which is what
|
|
32
|
+
* these three were until #294. `packages/cli/src/ci/auditActions.test.ts` fails the build on one.
|
|
33
|
+
*/
|
|
34
|
+
const ControlPlaneAuditActions = {
|
|
35
|
+
/** A verified, in-scope management call was admitted. The trail of what the dashboard actually did. */
|
|
36
|
+
callAllowed: "controlplane/call_allowed",
|
|
37
|
+
/** A management call was refused — no connection, bad credential, or an ungranted scope. Outcome `denied`. */
|
|
38
|
+
callDenied: "controlplane/call_denied",
|
|
39
|
+
/** A new public key was registered on a connection, opening a rotation overlap. */
|
|
40
|
+
keyRegistered: "controlplane/key_registered",
|
|
41
|
+
/** A superseded public key was given an end date and stopped being accepted. */
|
|
42
|
+
keyExpired: "controlplane/key_expired",
|
|
43
|
+
/** A management client was connected to this environment — a new connection, or one started over. */
|
|
44
|
+
connectionRegistered: "controlplane/connection_registered",
|
|
45
|
+
/** An existing connection changed — its granted scopes, the Worker URL it points at, or the keys it trusts. */
|
|
46
|
+
connectionUpdated: "controlplane/connection_updated",
|
|
47
|
+
/** A connection was removed. The seam returns to its shipped state: connected to nothing, denying everything. */
|
|
48
|
+
connectionRemoved: "controlplane/connection_removed"
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Emit an audit event and swallow any failure.
|
|
52
|
+
*
|
|
53
|
+
* **Every denial emits an event, and that is exactly why this cannot throw.** By the time the seam
|
|
54
|
+
* records a call, the security-relevant outcome is already decided — the 401 or 403 is going out
|
|
55
|
+
* whatever happens next. An audit write that threw would convert a correct denial into a 500: it would
|
|
56
|
+
* hand the caller a different response for a *failing* store than for a healthy one, and turn a
|
|
57
|
+
* transient D1 problem into an availability bug on the admin surface. The record is evidence of the
|
|
58
|
+
* decision, never part of making it.
|
|
59
|
+
*
|
|
60
|
+
* The real recorder is non-fatal by contract already ({@link AuditEmit}), so this is belt and braces —
|
|
61
|
+
* but the gate is a security boundary and does not get to assume its collaborators behave. The failure
|
|
62
|
+
* is logged when a logger is at hand, so a store that has stopped accepting writes is visible rather
|
|
63
|
+
* than silent.
|
|
64
|
+
*/
|
|
65
|
+
async function safeEmit(emit, event, log) {
|
|
66
|
+
try {
|
|
67
|
+
await emit(event);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
log?.warn("control-plane audit event dropped", {
|
|
70
|
+
action: event.action,
|
|
71
|
+
error
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { ControlPlaneAuditActions, safeEmit };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type Capability } from "../capability/capability";
|
|
2
|
+
import type { DatabaseSpecMap } from "../data/databases";
|
|
3
|
+
import type { KvNamespaceSpecMap } from "../kv/namespaces";
|
|
4
|
+
import { ControlPlaneConfig, type ControlPlaneConfigInput } from "./config/config";
|
|
5
|
+
import { CONTROL_PLANE_KV_BINDING } from "./replay/kvGuard";
|
|
6
|
+
/**
|
|
7
|
+
* Where the control-plane registration sorts in the app database. Composed key:
|
|
8
|
+
* `1100_controlplane_0001_init`.
|
|
9
|
+
*
|
|
10
|
+
* Taken from `NEXT_FREE_ORDER` in `packages/cli/src/migrations/orders.test.ts`, which is the one place
|
|
11
|
+
* an order may be allocated, and registered in that file's `DECLARED` table. Core used to be skipped by
|
|
12
|
+
* that scanner on the reasoning that it owns the ceiling rather than an order; that stopped being true
|
|
13
|
+
* here, and the skip was removed with this change. Stable forever — renumbering would rename the
|
|
14
|
+
* composed key and make Kysely re-run an applied migration.
|
|
15
|
+
*/
|
|
16
|
+
export declare const CONTROLPLANE_MIGRATION_ORDER = 1100;
|
|
17
|
+
export type ControlPlaneOptions = ControlPlaneConfigInput;
|
|
18
|
+
/** The control-plane capability, with its resolved config attached. */
|
|
19
|
+
export interface ControlPlaneCapability extends Capability<DatabaseSpecMap, KvNamespaceSpecMap, "controlplane"> {
|
|
20
|
+
/** The resolved seam config. */
|
|
21
|
+
controlPlaneConfig: ControlPlaneConfig;
|
|
22
|
+
}
|
|
23
|
+
/** Narrow a composed capability to this one — the CLI and tests use it to find the seam's config. */
|
|
24
|
+
export declare function isControlPlaneCapability(capability: Capability): capability is ControlPlaneCapability;
|
|
25
|
+
/**
|
|
26
|
+
* The `control-plane` seam: inbound, adopter-authenticated M2M admin access to this Worker.
|
|
27
|
+
*
|
|
28
|
+
* **Not Cloudflare's control plane.** That is the outbound REST API `@pithy-sh/cloudflare` calls to
|
|
29
|
+
* provision D1, KV, and Workers, and it carries `cloudflare/*` codes. This is the inverse — a
|
|
30
|
+
* management client calling *in*, authenticated by a key the adopter registered. See
|
|
31
|
+
* `docs/CONTROL-PLANE.md`, whose first section exists entirely to keep the two apart.
|
|
32
|
+
*
|
|
33
|
+
* **Composing it grants nobody anything.** The capability ships present-and-denying: with no connection
|
|
34
|
+
* registered, every route it guards answers `controlplane/not_connected`, and there is no flag that
|
|
35
|
+
* changes that. `pithy dashboard connect` is the deliberate second step, and revoking is a row the
|
|
36
|
+
* adopter deletes without asking anyone.
|
|
37
|
+
*
|
|
38
|
+
* It lives in `@pithy-sh/core` rather than its own package because every capability contributing admin
|
|
39
|
+
* routes imports `requireControlPlane`, and a capability may depend on a core seam but never on a
|
|
40
|
+
* sibling package (principle 4). MIT with the rest of core, and never gated by tier: this is the code
|
|
41
|
+
* that runs in the adopter's own Worker, so restricting it would make "build your own client against
|
|
42
|
+
* your own Worker" untrue.
|
|
43
|
+
*/
|
|
44
|
+
export declare function controlplane(options?: ControlPlaneOptions): ControlPlaneCapability;
|
|
45
|
+
/** Re-exported so a Worker's wrangler config and this capability cannot disagree about the binding name. */
|
|
46
|
+
export { CONTROL_PLANE_KV_BINDING };
|
|
47
|
+
//# sourceMappingURL=capability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../../src/controlPlane/capability.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAuC,MAAM,0BAA0B,CAAC;AAChG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAc,MAAM,kBAAkB,CAAC;AAGvE,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAUnF,OAAO,EACL,wBAAwB,EAIzB,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;;GASG;AACH,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG,uBAAuB,CAAC;AAE1D,uEAAuE;AACvE,MAAM,WAAW,sBAAuB,SAAQ,UAAU,CAAC,eAAe,EAAE,kBAAkB,EAAE,cAAc,CAAC;IAC7G,gCAAgC;IAChC,kBAAkB,EAAE,kBAAkB,CAAC;CACxC;AAED,qGAAqG;AACrG,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,IAAI,sBAAsB,CAErG;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,sBAAsB,CAwJtF;AAED,4GAA4G;AAC5G,OAAO,EAAE,wBAAwB,EAAE,CAAC"}
|