@pithy-sh/core 0.1.1 → 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,56 @@
|
|
|
1
|
+
import type { Ed25519PublicJwk } from "../data/connection";
|
|
2
|
+
/**
|
|
3
|
+
* Mint a control-plane token — the management client's half of the seam.
|
|
4
|
+
*
|
|
5
|
+
* **This ships because the seam is MIT and is never gated by tier.** "You can build your own client
|
|
6
|
+
* against your own Worker" has to be true, and it is not true if the only way to produce a token the
|
|
7
|
+
* Worker accepts is to reverse-engineer the verifier. So the minting side lives here, beside the
|
|
8
|
+
* verifying side, and the two are tested against each other.
|
|
9
|
+
*
|
|
10
|
+
* It is also what the hosted dashboard would use, and what every test in this package uses to sign a
|
|
11
|
+
* real token rather than assert against a fixture.
|
|
12
|
+
*
|
|
13
|
+
* **Nothing here touches a private key store.** The caller supplies an already-imported `CryptoKey`.
|
|
14
|
+
* Core holds no key material, has no opinion about where a private key lives, and this file would be
|
|
15
|
+
* the wrong place to acquire one.
|
|
16
|
+
*/
|
|
17
|
+
/** One token's worth of claims, minus the parts {@link mintControlPlaneToken} computes. */
|
|
18
|
+
export interface MintControlPlaneToken {
|
|
19
|
+
/** The signing key, imported for `Ed25519` with `["sign"]` usage. */
|
|
20
|
+
privateKey: CryptoKey;
|
|
21
|
+
/** The `kid` naming which registered public key verifies this token. */
|
|
22
|
+
keyId: string;
|
|
23
|
+
/** The management client's origin — must equal the `issuer` stored on the connection. */
|
|
24
|
+
issuer: string;
|
|
25
|
+
/** The connection this token addresses. */
|
|
26
|
+
connectionId: string;
|
|
27
|
+
/** Who, in the management client's own id space, is acting. */
|
|
28
|
+
subject: string;
|
|
29
|
+
/** The single operation this token authorizes. One call, one scope. */
|
|
30
|
+
scope: string;
|
|
31
|
+
/** The raw request body these claims are bound to. Empty for a request that carries none. */
|
|
32
|
+
body?: Uint8Array;
|
|
33
|
+
/** Seconds this token is good for. The Worker caps it, so asking for more only wastes the request. */
|
|
34
|
+
lifetimeSeconds?: number;
|
|
35
|
+
/** The clock, injected so a caller can mint a token at a chosen instant. */
|
|
36
|
+
now?: () => Date;
|
|
37
|
+
/** The token's unique id. Generated when absent; supply one only to test a replay. */
|
|
38
|
+
tokenId?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build and sign a compact JWS the seam will accept.
|
|
42
|
+
*
|
|
43
|
+
* The body digest is computed here rather than accepted from the caller, so a client cannot mint a
|
|
44
|
+
* token whose digest disagrees with the bytes it is about to send. `bodySha256` is null exactly when
|
|
45
|
+
* the body is empty, which is the same bijection the verifier enforces.
|
|
46
|
+
*/
|
|
47
|
+
export declare function mintControlPlaneToken(input: MintControlPlaneToken): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Export a generated key pair's public half as the JWK the adopter registers.
|
|
50
|
+
*
|
|
51
|
+
* `crv` and `kty` are re-asserted from our own constants rather than trusted from the export, so a
|
|
52
|
+
* runtime that spelled them differently produces a key this seam still recognizes — or fails here,
|
|
53
|
+
* where it is obvious, rather than at the first verification.
|
|
54
|
+
*/
|
|
55
|
+
export declare function exportPublicJwk(publicKey: CryptoKey): Promise<Ed25519PublicJwk>;
|
|
56
|
+
//# sourceMappingURL=mint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mint.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/token/mint.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAK3D;;;;;;;;;;;;;;GAcG;AAEH,2FAA2F;AAC3F,MAAM,WAAW,qBAAqB;IACpC,qEAAqE;IACrE,UAAU,EAAE,SAAS,CAAC;IACtB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,yFAAyF;IACzF,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,6FAA6F;IAC7F,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sGAAsG;IACtG,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,sFAAsF;IACtF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAOD;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBzF;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAIrF"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ControlPlaneClaims } from "./claims.js";
|
|
2
|
+
import { base64UrlEncode } from "./base64url.js";
|
|
3
|
+
import { sha256Base64Url } from "./digest.js";
|
|
4
|
+
//#region src/controlPlane/token/mint.ts
|
|
5
|
+
/** Base64url a UTF-8 JSON serialization — the encoding both JWS segments use. */
|
|
6
|
+
function encodeSegment(value) {
|
|
7
|
+
return base64UrlEncode(new TextEncoder().encode(JSON.stringify(value)));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Build and sign a compact JWS the seam will accept.
|
|
11
|
+
*
|
|
12
|
+
* The body digest is computed here rather than accepted from the caller, so a client cannot mint a
|
|
13
|
+
* token whose digest disagrees with the bytes it is about to send. `bodySha256` is null exactly when
|
|
14
|
+
* the body is empty, which is the same bijection the verifier enforces.
|
|
15
|
+
*/
|
|
16
|
+
async function mintControlPlaneToken(input) {
|
|
17
|
+
const body = input.body ?? /* @__PURE__ */ new Uint8Array(0);
|
|
18
|
+
const issuedAt = Math.floor((input.now?.() ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
|
|
19
|
+
const header = {
|
|
20
|
+
alg: "EdDSA",
|
|
21
|
+
typ: "JWT",
|
|
22
|
+
kid: input.keyId
|
|
23
|
+
};
|
|
24
|
+
const claims = ControlPlaneClaims.parse({
|
|
25
|
+
iss: input.issuer,
|
|
26
|
+
aud: input.connectionId,
|
|
27
|
+
sub: input.subject,
|
|
28
|
+
scope: input.scope,
|
|
29
|
+
jti: input.tokenId ?? crypto.randomUUID(),
|
|
30
|
+
iat: issuedAt,
|
|
31
|
+
exp: issuedAt + (input.lifetimeSeconds ?? 60),
|
|
32
|
+
bodySha256: body.byteLength === 0 ? null : await sha256Base64Url(body)
|
|
33
|
+
});
|
|
34
|
+
const signingInput = `${encodeSegment(header)}.${encodeSegment(claims)}`;
|
|
35
|
+
const signature = await crypto.subtle.sign("Ed25519", input.privateKey, new TextEncoder().encode(signingInput));
|
|
36
|
+
return `${signingInput}.${base64UrlEncode(new Uint8Array(signature))}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Export a generated key pair's public half as the JWK the adopter registers.
|
|
40
|
+
*
|
|
41
|
+
* `crv` and `kty` are re-asserted from our own constants rather than trusted from the export, so a
|
|
42
|
+
* runtime that spelled them differently produces a key this seam still recognizes — or fails here,
|
|
43
|
+
* where it is obvious, rather than at the first verification.
|
|
44
|
+
*/
|
|
45
|
+
async function exportPublicJwk(publicKey) {
|
|
46
|
+
const exported = await crypto.subtle.exportKey("jwk", publicKey);
|
|
47
|
+
if (!exported.x) throw new TypeError("Ed25519 public key export carried no `x` coordinate.");
|
|
48
|
+
return {
|
|
49
|
+
kty: "OKP",
|
|
50
|
+
crv: "Ed25519",
|
|
51
|
+
x: exported.x
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { exportPublicJwk, mintControlPlaneToken };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The control-plane wire contract: the names both ends of the seam must spell identically, and the one
|
|
3
|
+
* rule for reading what they carry.
|
|
4
|
+
*
|
|
5
|
+
* **This module imports nothing, and that is its whole job.** Per `docs/CONTROL-PLANE.md` §4 a browser
|
|
6
|
+
* calls the adopter's Worker *directly* — the management client's server mints the token, the browser
|
|
7
|
+
* sends it — so a DOM-typed module has to name the request header, and has to read the version header
|
|
8
|
+
* off the response to notice a customer deploying mid-session. Reaching these constants through
|
|
9
|
+
* `http/verify` drags the verifier in with them: WebCrypto, the replay guard, the seam config. A
|
|
10
|
+
* program compiled against `lib.dom` then fails on `token/digest.ts` and `token/jws.ts`, where a
|
|
11
|
+
* `Uint8Array<ArrayBufferLike>` is not the `BufferSource` the DOM lib declares — the Workers and DOM
|
|
12
|
+
* type libraries meeting, which no amount of care at the call site resolves.
|
|
13
|
+
*
|
|
14
|
+
* The alternative was to let each client re-declare the strings. That is what the dashboard did, with
|
|
15
|
+
* a comment and an assertion holding the copy honest, and two copies of a wire constant is exactly the
|
|
16
|
+
* drift a constant exists to prevent. `wire.test.ts` asserts the module graph here stays empty, because
|
|
17
|
+
* the property is only true until someone adds the first convenient import.
|
|
18
|
+
*
|
|
19
|
+
* {@link workerBuildChanged} is here for the same reason one level up. The dashboard did not only copy
|
|
20
|
+
* the header name — it wrote its own rule for reading the value, "invalidate when the id differs", and
|
|
21
|
+
* that rule was wrong in a way a copied constant could never have caught. A rule both ends must agree on
|
|
22
|
+
* belongs beside the names both ends must spell.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The header a control-plane token is presented on.
|
|
26
|
+
*
|
|
27
|
+
* Not `Authorization`, deliberately: `@pithy-sh/auth` builds a Better Auth instance for any request
|
|
28
|
+
* carrying that header, so a management call would drag the whole auth stack in. See `http/verify.ts`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const CONTROL_PLANE_HEADER = "pithy-control-plane";
|
|
31
|
+
/**
|
|
32
|
+
* The response header carrying the build that answered a control-plane call.
|
|
33
|
+
*
|
|
34
|
+
* Set on every control-plane response — allowed and denied alike, and on every capability's admin
|
|
35
|
+
* routes rather than only the seam's own. A management client reads it to pin each recorded action to
|
|
36
|
+
* the exact build it hit, and to notice a version changing mid-session, which is the moment a rendered
|
|
37
|
+
* pane has quietly gone out of date. Absent where the `CF_VERSION_METADATA` binding is, which reads as
|
|
38
|
+
* "this Worker cannot say" rather than as a value to trust.
|
|
39
|
+
*
|
|
40
|
+
* **Carries the id and nothing else, permanently.** Its companion below is a second header rather than
|
|
41
|
+
* a second field in this one for one reason: a client already deployed compares this whole string. Fold
|
|
42
|
+
* anything into it and the day the adopter upgrades the kit reads as a version change that never
|
|
43
|
+
* happened — a false invalidation, which is the failure the pair exists to prevent, arriving from the
|
|
44
|
+
* fix for it.
|
|
45
|
+
*/
|
|
46
|
+
export declare const CONTROL_PLANE_VERSION_HEADER = "pithy-worker-version";
|
|
47
|
+
/**
|
|
48
|
+
* The response header carrying **when the running version was uploaded** — ISO-8601, verbatim, off the
|
|
49
|
+
* same `CF_VERSION_METADATA` binding as the id.
|
|
50
|
+
*
|
|
51
|
+
* Read beside {@link CONTROL_PLANE_VERSION_HEADER}, never folded into it, so a client can compare two
|
|
52
|
+
* values separately. {@link workerBuildChanged} is the rule; do not hand-write one.
|
|
53
|
+
*
|
|
54
|
+
* **Cloudflare has two objects, and this is the first of them.** A *version* is an immutable upload of
|
|
55
|
+
* code and config — id, created timestamp, tag, fixed at upload and never again. A *deployment* points
|
|
56
|
+
* at one or more versions with traffic percentages, and is its own object with its own id and time. The
|
|
57
|
+
* `CF_VERSION_METADATA` binding reports the version, and **the runtime hands a Worker no binding for the
|
|
58
|
+
* deployment**. So this header is the upload moment, and it is fixed for as long as that version exists.
|
|
59
|
+
*
|
|
60
|
+
* That mechanism predicts the behavior rather than merely recording it: **a rollback creates a new
|
|
61
|
+
* deployment aimed at an existing version, so nothing here moves** — the binding is not stale and not
|
|
62
|
+
* ambiguous, the thing that changed was a different object. It answers the next question too. A traffic
|
|
63
|
+
* split, a gradual rollout, which deployment is serving: none of it is visible from inside a Worker.
|
|
64
|
+
*
|
|
65
|
+
* Measured on a real account, 2026-08-10, which is what made it a fact rather than a reading of the
|
|
66
|
+
* docs: version `A` uploaded at `22:28:56.762349Z`, `B` four seconds later, `wrangler rollback` to `A`
|
|
67
|
+
* at ~`22:29:20`. Reading at `22:31:37` returned `A`'s id **and `A`'s original timestamp**, unmoved,
|
|
68
|
+
* while wrangler's own output named the new deployment's version list. Two details from the same run
|
|
69
|
+
* that a client will meet:
|
|
70
|
+
*
|
|
71
|
+
* - **Every `wrangler deploy` mints a new version**, even for a one-character change — that is why `B`
|
|
72
|
+
* exists. An ordinary redeploy therefore always moves the id, and is never invisible.
|
|
73
|
+
* - **Propagation lags.** Twenty seconds after wrangler reported the rollback at 100%, the URL still
|
|
74
|
+
* answered from the older version. A client watching closely sees the pair flip and settle; that is
|
|
75
|
+
* the platform converging, not two deploys.
|
|
76
|
+
*/
|
|
77
|
+
export declare const CONTROL_PLANE_VERSION_CREATED_HEADER = "pithy-worker-version-created";
|
|
78
|
+
/**
|
|
79
|
+
* The pair a client reads off one control-plane response: two `headers.get()` calls, each `string | null`.
|
|
80
|
+
*/
|
|
81
|
+
export interface WorkerBuild {
|
|
82
|
+
/** {@link CONTROL_PLANE_VERSION_HEADER}, or `null` where the response carried none. */
|
|
83
|
+
version: string | null;
|
|
84
|
+
/** {@link CONTROL_PLANE_VERSION_CREATED_HEADER}, or `null` where the response carried none. */
|
|
85
|
+
createdAt: string | null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Did the Worker answering now differ from the Worker that answered before?
|
|
89
|
+
*
|
|
90
|
+
* **The rule, total over the pair.** A client compares field by field and only where both sides carried
|
|
91
|
+
* a value; anything that differs is a change. Four states:
|
|
92
|
+
*
|
|
93
|
+
* 1. nothing differs — the same build, still answering. Say nothing.
|
|
94
|
+
* 2. `version` differs — a different build is live, and what is rendered came from one that no longer
|
|
95
|
+
* serves.
|
|
96
|
+
* 3. `version` the same, `createdAt` differs — **the same build was deployed again.** Same consequence.
|
|
97
|
+
* 4. either side silent on a field — that field says nothing.
|
|
98
|
+
*
|
|
99
|
+
* **State 3 is not reachable on today's platform, and it stays anyway.** A version's timestamp
|
|
100
|
+
* is fixed at upload and a rollback only makes a new deployment, so the pair cannot show a moved time
|
|
101
|
+
* against an unmoved id (see {@link CONTROL_PLANE_VERSION_CREATED_HEADER}). It is here because a total
|
|
102
|
+
* comparison over the pair is the correct shape, not because a rollback was expected to trip it — **a
|
|
103
|
+
* comparison that enumerates which fields are allowed to move is wrong the day the platform moves a
|
|
104
|
+
* different one**, and it is wrong silently, as "nothing changed". It costs one branch.
|
|
105
|
+
*
|
|
106
|
+
* Direction is *not* interpreted. A time that moved is a fact; "it moved backwards, therefore a
|
|
107
|
+
* rollback" is not, and would be wrong twice over — a rollback moves neither field, and propagation lag
|
|
108
|
+
* makes an older version answer for a while after a deploy.
|
|
109
|
+
*
|
|
110
|
+
* **What no state here reaches: a deployment.** Rollbacks, traffic splits, gradual rollouts — a Worker
|
|
111
|
+
* has no binding for the object that carries them, so this is a boundary of what the runtime exposes
|
|
112
|
+
* rather than a gap in the seam. A client that needs a deployment reads Cloudflare's deployments API.
|
|
113
|
+
*
|
|
114
|
+
* **Absence is never change**, in every direction: a Worker that declares no `version_metadata` sends
|
|
115
|
+
* neither header, a call that failed before its headers were read has none, and a client that has not
|
|
116
|
+
* looked yet holds nulls. A blank value counts as absent too — the seam never sends one, but a proxy in
|
|
117
|
+
* between can blank a header it does not understand, and an empty string differs from every real id. The
|
|
118
|
+
* failure this shape refuses is the one that costs an adopter trust: invalidating a rendered pane on a
|
|
119
|
+
* deploy that never happened.
|
|
120
|
+
*/
|
|
121
|
+
export declare function workerBuildChanged(before: WorkerBuild, after: WorkerBuild): boolean;
|
|
122
|
+
//# sourceMappingURL=wire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/controlPlane/wire.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,wBAAwB,CAAC;AAE1D;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,4BAA4B,yBAAyB,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,oCAAoC,iCAAiC,CAAC;AAEnF;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,uFAAuF;IACvF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,+FAA+F;IAC/F,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAEnF"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//#region src/controlPlane/wire.ts
|
|
2
|
+
/**
|
|
3
|
+
* The control-plane wire contract: the names both ends of the seam must spell identically, and the one
|
|
4
|
+
* rule for reading what they carry.
|
|
5
|
+
*
|
|
6
|
+
* **This module imports nothing, and that is its whole job.** Per `docs/CONTROL-PLANE.md` §4 a browser
|
|
7
|
+
* calls the adopter's Worker *directly* — the management client's server mints the token, the browser
|
|
8
|
+
* sends it — so a DOM-typed module has to name the request header, and has to read the version header
|
|
9
|
+
* off the response to notice a customer deploying mid-session. Reaching these constants through
|
|
10
|
+
* `http/verify` drags the verifier in with them: WebCrypto, the replay guard, the seam config. A
|
|
11
|
+
* program compiled against `lib.dom` then fails on `token/digest.ts` and `token/jws.ts`, where a
|
|
12
|
+
* `Uint8Array<ArrayBufferLike>` is not the `BufferSource` the DOM lib declares — the Workers and DOM
|
|
13
|
+
* type libraries meeting, which no amount of care at the call site resolves.
|
|
14
|
+
*
|
|
15
|
+
* The alternative was to let each client re-declare the strings. That is what the dashboard did, with
|
|
16
|
+
* a comment and an assertion holding the copy honest, and two copies of a wire constant is exactly the
|
|
17
|
+
* drift a constant exists to prevent. `wire.test.ts` asserts the module graph here stays empty, because
|
|
18
|
+
* the property is only true until someone adds the first convenient import.
|
|
19
|
+
*
|
|
20
|
+
* {@link workerBuildChanged} is here for the same reason one level up. The dashboard did not only copy
|
|
21
|
+
* the header name — it wrote its own rule for reading the value, "invalidate when the id differs", and
|
|
22
|
+
* that rule was wrong in a way a copied constant could never have caught. A rule both ends must agree on
|
|
23
|
+
* belongs beside the names both ends must spell.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The header a control-plane token is presented on.
|
|
27
|
+
*
|
|
28
|
+
* Not `Authorization`, deliberately: `@pithy-sh/auth` builds a Better Auth instance for any request
|
|
29
|
+
* carrying that header, so a management call would drag the whole auth stack in. See `http/verify.ts`.
|
|
30
|
+
*/
|
|
31
|
+
const CONTROL_PLANE_HEADER = "pithy-control-plane";
|
|
32
|
+
/**
|
|
33
|
+
* The response header carrying the build that answered a control-plane call.
|
|
34
|
+
*
|
|
35
|
+
* Set on every control-plane response — allowed and denied alike, and on every capability's admin
|
|
36
|
+
* routes rather than only the seam's own. A management client reads it to pin each recorded action to
|
|
37
|
+
* the exact build it hit, and to notice a version changing mid-session, which is the moment a rendered
|
|
38
|
+
* pane has quietly gone out of date. Absent where the `CF_VERSION_METADATA` binding is, which reads as
|
|
39
|
+
* "this Worker cannot say" rather than as a value to trust.
|
|
40
|
+
*
|
|
41
|
+
* **Carries the id and nothing else, permanently.** Its companion below is a second header rather than
|
|
42
|
+
* a second field in this one for one reason: a client already deployed compares this whole string. Fold
|
|
43
|
+
* anything into it and the day the adopter upgrades the kit reads as a version change that never
|
|
44
|
+
* happened — a false invalidation, which is the failure the pair exists to prevent, arriving from the
|
|
45
|
+
* fix for it.
|
|
46
|
+
*/
|
|
47
|
+
const CONTROL_PLANE_VERSION_HEADER = "pithy-worker-version";
|
|
48
|
+
/**
|
|
49
|
+
* The response header carrying **when the running version was uploaded** — ISO-8601, verbatim, off the
|
|
50
|
+
* same `CF_VERSION_METADATA` binding as the id.
|
|
51
|
+
*
|
|
52
|
+
* Read beside {@link CONTROL_PLANE_VERSION_HEADER}, never folded into it, so a client can compare two
|
|
53
|
+
* values separately. {@link workerBuildChanged} is the rule; do not hand-write one.
|
|
54
|
+
*
|
|
55
|
+
* **Cloudflare has two objects, and this is the first of them.** A *version* is an immutable upload of
|
|
56
|
+
* code and config — id, created timestamp, tag, fixed at upload and never again. A *deployment* points
|
|
57
|
+
* at one or more versions with traffic percentages, and is its own object with its own id and time. The
|
|
58
|
+
* `CF_VERSION_METADATA` binding reports the version, and **the runtime hands a Worker no binding for the
|
|
59
|
+
* deployment**. So this header is the upload moment, and it is fixed for as long as that version exists.
|
|
60
|
+
*
|
|
61
|
+
* That mechanism predicts the behavior rather than merely recording it: **a rollback creates a new
|
|
62
|
+
* deployment aimed at an existing version, so nothing here moves** — the binding is not stale and not
|
|
63
|
+
* ambiguous, the thing that changed was a different object. It answers the next question too. A traffic
|
|
64
|
+
* split, a gradual rollout, which deployment is serving: none of it is visible from inside a Worker.
|
|
65
|
+
*
|
|
66
|
+
* Measured on a real account, 2026-08-10, which is what made it a fact rather than a reading of the
|
|
67
|
+
* docs: version `A` uploaded at `22:28:56.762349Z`, `B` four seconds later, `wrangler rollback` to `A`
|
|
68
|
+
* at ~`22:29:20`. Reading at `22:31:37` returned `A`'s id **and `A`'s original timestamp**, unmoved,
|
|
69
|
+
* while wrangler's own output named the new deployment's version list. Two details from the same run
|
|
70
|
+
* that a client will meet:
|
|
71
|
+
*
|
|
72
|
+
* - **Every `wrangler deploy` mints a new version**, even for a one-character change — that is why `B`
|
|
73
|
+
* exists. An ordinary redeploy therefore always moves the id, and is never invisible.
|
|
74
|
+
* - **Propagation lags.** Twenty seconds after wrangler reported the rollback at 100%, the URL still
|
|
75
|
+
* answered from the older version. A client watching closely sees the pair flip and settle; that is
|
|
76
|
+
* the platform converging, not two deploys.
|
|
77
|
+
*/
|
|
78
|
+
const CONTROL_PLANE_VERSION_CREATED_HEADER = "pithy-worker-version-created";
|
|
79
|
+
/**
|
|
80
|
+
* Did the Worker answering now differ from the Worker that answered before?
|
|
81
|
+
*
|
|
82
|
+
* **The rule, total over the pair.** A client compares field by field and only where both sides carried
|
|
83
|
+
* a value; anything that differs is a change. Four states:
|
|
84
|
+
*
|
|
85
|
+
* 1. nothing differs — the same build, still answering. Say nothing.
|
|
86
|
+
* 2. `version` differs — a different build is live, and what is rendered came from one that no longer
|
|
87
|
+
* serves.
|
|
88
|
+
* 3. `version` the same, `createdAt` differs — **the same build was deployed again.** Same consequence.
|
|
89
|
+
* 4. either side silent on a field — that field says nothing.
|
|
90
|
+
*
|
|
91
|
+
* **State 3 is not reachable on today's platform, and it stays anyway.** A version's timestamp
|
|
92
|
+
* is fixed at upload and a rollback only makes a new deployment, so the pair cannot show a moved time
|
|
93
|
+
* against an unmoved id (see {@link CONTROL_PLANE_VERSION_CREATED_HEADER}). It is here because a total
|
|
94
|
+
* comparison over the pair is the correct shape, not because a rollback was expected to trip it — **a
|
|
95
|
+
* comparison that enumerates which fields are allowed to move is wrong the day the platform moves a
|
|
96
|
+
* different one**, and it is wrong silently, as "nothing changed". It costs one branch.
|
|
97
|
+
*
|
|
98
|
+
* Direction is *not* interpreted. A time that moved is a fact; "it moved backwards, therefore a
|
|
99
|
+
* rollback" is not, and would be wrong twice over — a rollback moves neither field, and propagation lag
|
|
100
|
+
* makes an older version answer for a while after a deploy.
|
|
101
|
+
*
|
|
102
|
+
* **What no state here reaches: a deployment.** Rollbacks, traffic splits, gradual rollouts — a Worker
|
|
103
|
+
* has no binding for the object that carries them, so this is a boundary of what the runtime exposes
|
|
104
|
+
* rather than a gap in the seam. A client that needs a deployment reads Cloudflare's deployments API.
|
|
105
|
+
*
|
|
106
|
+
* **Absence is never change**, in every direction: a Worker that declares no `version_metadata` sends
|
|
107
|
+
* neither header, a call that failed before its headers were read has none, and a client that has not
|
|
108
|
+
* looked yet holds nulls. A blank value counts as absent too — the seam never sends one, but a proxy in
|
|
109
|
+
* between can blank a header it does not understand, and an empty string differs from every real id. The
|
|
110
|
+
* failure this shape refuses is the one that costs an adopter trust: invalidating a rendered pane on a
|
|
111
|
+
* deploy that never happened.
|
|
112
|
+
*/
|
|
113
|
+
function workerBuildChanged(before, after) {
|
|
114
|
+
return differs(before.version, after.version) || differs(before.createdAt, after.createdAt);
|
|
115
|
+
}
|
|
116
|
+
/** One field of the pair: a difference only where both sides actually said something. */
|
|
117
|
+
function differs(before, after) {
|
|
118
|
+
if (typeof before !== "string" || before.trim() === "") return false;
|
|
119
|
+
if (typeof after !== "string" || after.trim() === "") return false;
|
|
120
|
+
return before !== after;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { CONTROL_PLANE_HEADER, CONTROL_PLANE_VERSION_CREATED_HEADER, CONTROL_PLANE_VERSION_HEADER, workerBuildChanged };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Capability, MergedDatabases, MergedKvNamespaces, MergedWorkflowParams, PithyVars } from "./capability/capability";
|
|
3
|
+
import { type DbRegistry } from "./data/databases";
|
|
4
|
+
import { type KvRegistry } from "./kv/namespaces";
|
|
5
|
+
import type { Logger } from "./logger/logger";
|
|
6
|
+
import { type WorkflowDispatcher } from "./workflow/dispatch";
|
|
7
|
+
/** Inputs to {@link createBackend}: the capabilities to assemble, plus the app's own capability. */
|
|
8
|
+
export interface CreateBackendOptions<Caps extends readonly Capability[], App extends Capability> {
|
|
9
|
+
/** Library capabilities (core + each `@pithy-sh/*`), composed in order. */
|
|
10
|
+
capabilities: Caps;
|
|
11
|
+
/**
|
|
12
|
+
* The app itself, as a `Capability` — principle 4: the app is just another capability. Its
|
|
13
|
+
* databases, KV namespaces, middleware, routes, and bindings compose with the rest; routes last.
|
|
14
|
+
*/
|
|
15
|
+
app?: App;
|
|
16
|
+
/**
|
|
17
|
+
* The base logger bound to `c.var.log`, set **once** here at assembly (the per-request `env`
|
|
18
|
+
* constraint is honored: correlation fields are bound per request, not at module load). Defaults to
|
|
19
|
+
* the CF-native Mode 2 worker logger (`info`, structured records to Workers Logs). Pass a configured
|
|
20
|
+
* one to change the level or attach a tail/Logpush transport.
|
|
21
|
+
*/
|
|
22
|
+
logger?: Logger;
|
|
23
|
+
/**
|
|
24
|
+
* Fallback environment name (`dev` | `staging` | `production`) for the log correlation `env` field.
|
|
25
|
+
* The primary signal is the per-request `ENVIRONMENT` var (the convention `@pithy-sh/secrets` and
|
|
26
|
+
* `@pithy-sh/email` already stamp into each deployed Worker); this is only used where that var is
|
|
27
|
+
* absent — e.g. a non-provisioned local run. Falls through to `unknown` when neither is present.
|
|
28
|
+
*/
|
|
29
|
+
env?: string;
|
|
30
|
+
}
|
|
31
|
+
/** The Hono env `createBackend` returns — `db`/`kv`/`workflows` typed precisely from the merged capabilities. */
|
|
32
|
+
type BackendEnv<Caps extends readonly Capability[]> = {
|
|
33
|
+
Bindings: Record<string, unknown>;
|
|
34
|
+
Variables: Omit<PithyVars, "db" | "kv" | "workflows"> & {
|
|
35
|
+
db: DbRegistry<MergedDatabases<Caps>>;
|
|
36
|
+
kv: KvRegistry<MergedKvNamespaces<Caps>>;
|
|
37
|
+
workflows: WorkflowDispatcher<MergedWorkflowParams<Caps>>;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Assemble capabilities into a deployable Hono app — a valid Worker `fetch` handler.
|
|
42
|
+
*
|
|
43
|
+
* Serves `GET /health`; merges every capability's `databases` and `kvNamespaces` into registries
|
|
44
|
+
* (tables/stores unioned per group); validates that every required binding is present; composes
|
|
45
|
+
* each capability's middleware; and mounts the capability (then app) routes. On every request
|
|
46
|
+
* `c.var.db` is the typed database registry — one `Kysely` per named database (`c.var.db.app`) —
|
|
47
|
+
* and `c.var.kv` the typed namespace registry — one `TypedKv` per store (`c.var.kv.cms.pages`).
|
|
48
|
+
* `PithyError`s become their declared HTTP status via {@link pithyErrorHandler}.
|
|
49
|
+
*
|
|
50
|
+
* Binding validation runs **once on the first request**, not at module load: in Workers `env` is
|
|
51
|
+
* per-request, so there is no env to check until a request arrives. The result is memoized.
|
|
52
|
+
*
|
|
53
|
+
* The return is typed precisely from the capabilities array — `createBackend({ capabilities }).get(
|
|
54
|
+
* "/x", (c) => c.var.db.app.selectFrom("…"))` gets autocomplete on every registered table.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createBackend<const Caps extends readonly Capability[], const App extends Capability = Capability<Record<never, never>, Record<never, never>>>(options: CreateBackendOptions<Caps, App>): Hono<BackendEnv<readonly [...Caps, App]>>;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=createBackend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createBackend.d.ts","sourceRoot":"","sources":["../src/createBackend.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EAEpB,SAAS,EACV,MAAM,yBAAyB,CAAC;AAKjC,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMtF,OAAO,EAA8B,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAK9C,OAAO,EAA2B,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,oGAAoG;AACpG,MAAM,WAAW,oBAAoB,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,EAAE,GAAG,SAAS,UAAU;IAC9F,2EAA2E;IAC3E,YAAY,EAAE,IAAI,CAAC;IACnB;;;OAGG;IACH,GAAG,CAAC,EAAE,GAAG,CAAC;IACV;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,iHAAiH;AACjH,KAAK,UAAU,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,IAAI;IACpD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,WAAW,CAAC,GAAG;QACtD,EAAE,EAAE,UAAU,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC,EAAE,EAAE,UAAU,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;QACzC,SAAS,EAAE,kBAAkB,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC;KAC3D,CAAC;CACH,CAAC;AAuCF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAC3B,KAAK,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,EACxC,KAAK,CAAC,GAAG,SAAS,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,EACrF,OAAO,EAAE,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAuKrF"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { noopEmit } from "./audit/recorder.js";
|
|
2
|
+
import { BindingSpec } from "./capability/bindings.js";
|
|
3
|
+
import { ValidationError } from "./error/pithyError.js";
|
|
4
|
+
import { recordComposition } from "./capability/composition.js";
|
|
5
|
+
import { validateBindings } from "./capability/validateBindings.js";
|
|
6
|
+
import { workerVersion } from "./worker/identity.js";
|
|
7
|
+
import { isControlPlaneCapability } from "./controlPlane/capability.js";
|
|
8
|
+
import { allowedOriginSet, corsSurface, registerControlPlaneCors } from "./controlPlane/http/cors.js";
|
|
9
|
+
import { buildDbRegistry, composeDatabases } from "./data/databases.js";
|
|
10
|
+
import { noEntitlementProvider } from "./entitlement/entitlement.js";
|
|
11
|
+
import { pithyErrorHandler } from "./error/http.js";
|
|
12
|
+
import { bakedTranslator } from "./i18n/translator.js";
|
|
13
|
+
import { catalogFor, composeMessages } from "./i18n/registry.js";
|
|
14
|
+
import { buildKvRegistry, composeKv } from "./kv/namespaces.js";
|
|
15
|
+
import { bindRequestContext, createWorkerLogger } from "./logger/worker.js";
|
|
16
|
+
import { HEALTH_PATH } from "./worker/health.js";
|
|
17
|
+
import { registeredWorkflowBinding } from "./workflow/bindings.js";
|
|
18
|
+
import { buildWorkflowDispatcher } from "./workflow/dispatch.js";
|
|
19
|
+
import { composeWorkflows } from "./workflow/register.js";
|
|
20
|
+
import { Hono } from "hono";
|
|
21
|
+
//#region src/createBackend.ts
|
|
22
|
+
/**
|
|
23
|
+
* Collapse duplicate bindings (same `type:name`) — capability and derived specs overlap. A binding
|
|
24
|
+
* required by **any** source stays required: a derived database/namespace binding (structurally
|
|
25
|
+
* required — the request context builds a handle on it) must not be masked by an author marking the
|
|
26
|
+
* same binding `optional`. So `optional` is the AND of every occurrence.
|
|
27
|
+
*/
|
|
28
|
+
function dedupeBindings(specs) {
|
|
29
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const spec of specs) {
|
|
31
|
+
const key = `${spec.type}:${spec.name}`;
|
|
32
|
+
const existing = byKey.get(key);
|
|
33
|
+
if (!existing) byKey.set(key, { ...spec });
|
|
34
|
+
else if (!spec.optional) existing.optional = false;
|
|
35
|
+
}
|
|
36
|
+
return [...byKey.values()];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The deployed Worker version id, for the log record's `version` correlation field.
|
|
40
|
+
*
|
|
41
|
+
* Delegates to `workerVersion` rather than re-reading the binding: this used to be a private copy here,
|
|
42
|
+
* and a second reader of one binding is how the name and the declaration drift apart. `undefined` rather
|
|
43
|
+
* than `null` because the logger omits an undefined field.
|
|
44
|
+
*/
|
|
45
|
+
function versionOf(env) {
|
|
46
|
+
return workerVersion(env) ?? void 0;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The environment name for log correlation, resolved from context: the per-request `ENVIRONMENT` var
|
|
50
|
+
* (the signal `@pithy-sh/secrets`/`@pithy-sh/email` stamp into each deployed Worker), else the explicit
|
|
51
|
+
* `createBackend({ env })` fallback, else `unknown`. No caller effort where `ENVIRONMENT` is present.
|
|
52
|
+
*/
|
|
53
|
+
function envNameOf(env, fallback) {
|
|
54
|
+
return typeof env.ENVIRONMENT === "string" && env.ENVIRONMENT.length > 0 ? env.ENVIRONMENT : fallback ?? "unknown";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Assemble capabilities into a deployable Hono app — a valid Worker `fetch` handler.
|
|
58
|
+
*
|
|
59
|
+
* Serves `GET /health`; merges every capability's `databases` and `kvNamespaces` into registries
|
|
60
|
+
* (tables/stores unioned per group); validates that every required binding is present; composes
|
|
61
|
+
* each capability's middleware; and mounts the capability (then app) routes. On every request
|
|
62
|
+
* `c.var.db` is the typed database registry — one `Kysely` per named database (`c.var.db.app`) —
|
|
63
|
+
* and `c.var.kv` the typed namespace registry — one `TypedKv` per store (`c.var.kv.cms.pages`).
|
|
64
|
+
* `PithyError`s become their declared HTTP status via {@link pithyErrorHandler}.
|
|
65
|
+
*
|
|
66
|
+
* Binding validation runs **once on the first request**, not at module load: in Workers `env` is
|
|
67
|
+
* per-request, so there is no env to check until a request arrives. The result is memoized.
|
|
68
|
+
*
|
|
69
|
+
* The return is typed precisely from the capabilities array — `createBackend({ capabilities }).get(
|
|
70
|
+
* "/x", (c) => c.var.db.app.selectFrom("…"))` gets autocomplete on every registered table.
|
|
71
|
+
*/
|
|
72
|
+
function createBackend(options) {
|
|
73
|
+
const all = options.app ? [...options.capabilities, options.app] : [...options.capabilities];
|
|
74
|
+
const present = new Set(all.map((cap) => cap.name));
|
|
75
|
+
for (const cap of all) for (const dep of cap.dependsOn ?? []) if (!present.has(dep)) throw new ValidationError({
|
|
76
|
+
message: `Capability "${cap.name}" requires the "${dep}" capability, which is not composed.`,
|
|
77
|
+
action: `Add ${dep}() to createBackend's capabilities (run \`pithy add ${dep}\`).`
|
|
78
|
+
});
|
|
79
|
+
for (const cap of all) cap.compose?.({ capabilities: all });
|
|
80
|
+
recordComposition(all);
|
|
81
|
+
const databases = composeDatabases(all);
|
|
82
|
+
const messages = composeMessages(all);
|
|
83
|
+
const defaultTranslator = bakedTranslator(catalogFor(messages, "en"));
|
|
84
|
+
const namespaces = composeKv(all);
|
|
85
|
+
const workflows = composeWorkflows(all);
|
|
86
|
+
const required = dedupeBindings([
|
|
87
|
+
...all.flatMap((cap) => cap.requiredBindings),
|
|
88
|
+
...Object.values(databases).map((db) => BindingSpec.parse({
|
|
89
|
+
type: "d1",
|
|
90
|
+
name: db.binding
|
|
91
|
+
})),
|
|
92
|
+
...Object.values(namespaces).map((ns) => BindingSpec.parse({
|
|
93
|
+
type: "kv",
|
|
94
|
+
name: ns.binding
|
|
95
|
+
})),
|
|
96
|
+
...Object.values(workflows).map((entry) => BindingSpec.parse(registeredWorkflowBinding(entry)))
|
|
97
|
+
]);
|
|
98
|
+
const app = new Hono();
|
|
99
|
+
app.onError(pithyErrorHandler);
|
|
100
|
+
const baseLogger = options.logger ?? createWorkerLogger();
|
|
101
|
+
const controlPlane = all.find(isControlPlaneCapability);
|
|
102
|
+
if (controlPlane) {
|
|
103
|
+
const cors = controlPlane.controlPlaneConfig;
|
|
104
|
+
registerControlPlaneCors(app, corsSurface(all), allowedOriginSet(cors), cors.corsMaxAgeSeconds);
|
|
105
|
+
}
|
|
106
|
+
let validated = false;
|
|
107
|
+
app.use("*", async (c, next) => {
|
|
108
|
+
const env = c.env;
|
|
109
|
+
if (!validated) {
|
|
110
|
+
validateBindings(env, required);
|
|
111
|
+
validated = true;
|
|
112
|
+
}
|
|
113
|
+
if (c.get("auth") === void 0) c.set("auth", null);
|
|
114
|
+
if (c.get("locale") === void 0) c.set("locale", null);
|
|
115
|
+
if (c.get("t") === void 0) c.set("t", defaultTranslator);
|
|
116
|
+
if (c.get("controlPlane") === void 0) c.set("controlPlane", null);
|
|
117
|
+
if (c.get("controlPlaneVerifier") === void 0) c.set("controlPlaneVerifier", null);
|
|
118
|
+
if (c.get("sameOrigin") === void 0) c.set("sameOrigin", null);
|
|
119
|
+
if (c.get("emit") === void 0) c.set("emit", noopEmit);
|
|
120
|
+
if (c.get("entitlements") === void 0) c.set("entitlements", noEntitlementProvider);
|
|
121
|
+
if (c.get("log") === void 0) c.set("log", bindRequestContext(baseLogger, {
|
|
122
|
+
request: c.req.header("cf-ray") ?? crypto.randomUUID(),
|
|
123
|
+
method: c.req.method,
|
|
124
|
+
path: c.req.path,
|
|
125
|
+
env: envNameOf(env, options.env),
|
|
126
|
+
version: versionOf(env)
|
|
127
|
+
}));
|
|
128
|
+
if (c.get("db") === void 0) c.set("db", buildDbRegistry(env, databases));
|
|
129
|
+
if (c.get("kv") === void 0) c.set("kv", buildKvRegistry(env, namespaces, c.var.log));
|
|
130
|
+
if (c.get("workflows") === void 0) c.set("workflows", buildWorkflowDispatcher(env, workflows, c.var.log));
|
|
131
|
+
const start = Date.now();
|
|
132
|
+
await next();
|
|
133
|
+
c.var.log.info("request", {
|
|
134
|
+
status: c.res.status,
|
|
135
|
+
elapsed: Date.now() - start
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
/**
|
|
139
|
+
* `GET {@link HEALTH_PATH}` — liveness, and **which build is answering**.
|
|
140
|
+
*
|
|
141
|
+
* The path is `worker/health.ts`'s, not a literal here. Three other sites need the same string — the
|
|
142
|
+
* CLI's route allowlist, `pithy deploy`'s post-deploy probe, and the bare home screen a no-auth
|
|
143
|
+
* scaffold seeds — and that last one is in somebody else's repository, rendering nothing but this
|
|
144
|
+
* request's answer (#400).
|
|
145
|
+
*
|
|
146
|
+
* The version is what turns `pithy deploy`'s post-deploy check from a liveness probe into an
|
|
147
|
+
* assertion. `status: "ok"` at the declared domain proves *a* Worker is there; it does not prove it is
|
|
148
|
+
* the one just shipped, and the old version answering happily is exactly the failure worth catching —
|
|
149
|
+
* a deploy that landed somewhere else while the declared domain kept serving what was already on it.
|
|
150
|
+
* With the id here, `deploy` compares what it shipped against what replies.
|
|
151
|
+
*
|
|
152
|
+
* **Public, deliberately.** A Cloudflare version id is an opaque UUID carrying no version semantics
|
|
153
|
+
* and no exploitable detail, and most platforms expose a build identifier. The alternative — reporting
|
|
154
|
+
* it only through the authenticated control-plane manifest — is better for privacy and useless for
|
|
155
|
+
* deploy verification, since `deploy` holds no control-plane credential.
|
|
156
|
+
*
|
|
157
|
+
* `null` where the binding is absent, which is honest rather than misleading: a project scaffolded
|
|
158
|
+
* before `version_metadata` was declared reports "I cannot tell you", and `deploy` reports the check
|
|
159
|
+
* as inconclusive instead of failing it.
|
|
160
|
+
*/
|
|
161
|
+
app.get(HEALTH_PATH, (c) => c.json({
|
|
162
|
+
status: "ok",
|
|
163
|
+
version: workerVersion(c.env)
|
|
164
|
+
}));
|
|
165
|
+
for (const cap of all) for (const middleware of cap.middleware ?? []) middleware(app);
|
|
166
|
+
for (const cap of all) cap.routes?.(app);
|
|
167
|
+
return app;
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
export { createBackend };
|