@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,148 @@
|
|
|
1
|
+
import { PithyError, messageOf } from "../error/pithyError.js";
|
|
2
|
+
import "../worker/identity.js";
|
|
3
|
+
import { noopLogger } from "../logger/logger.js";
|
|
4
|
+
import { originVarName } from "../env/stem.js";
|
|
5
|
+
import { loopbackWorkflowBinding } from "./loopback.js";
|
|
6
|
+
//#region src/workflow/dispatch.ts
|
|
7
|
+
/**
|
|
8
|
+
* Read a binding off the per-request env, narrowed to the one method dispatch needs.
|
|
9
|
+
*
|
|
10
|
+
* Exported because the host's dispatch route ({@link ./dispatchRoute.ts}) asks the same question of
|
|
11
|
+
* the same env, and a second duck-type would be a second answer to "is this a Workflow binding" the
|
|
12
|
+
* first day one of them learned about a new shape.
|
|
13
|
+
*/
|
|
14
|
+
function workflowBindingFor(env, name) {
|
|
15
|
+
const value = env[name];
|
|
16
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
17
|
+
return typeof value.create === "function" ? value : void 0;
|
|
18
|
+
}
|
|
19
|
+
/** The one environment a loopback substitution is made in. Verbatim, and the only one. */
|
|
20
|
+
const LOOPBACK_ENVIRONMENT = "dev";
|
|
21
|
+
/** A non-blank string off the env, or `undefined`. Anything else on that key is not an address. */
|
|
22
|
+
function stringVar(env, name) {
|
|
23
|
+
const value = env[name];
|
|
24
|
+
if (typeof value !== "string") return void 0;
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The binding to dispatch on: the real one, or — under `pithy dev` — a loopback stand-in for it.
|
|
30
|
+
*
|
|
31
|
+
* ## Why a stand-in exists at all
|
|
32
|
+
*
|
|
33
|
+
* A deployed app Worker starts a capability's Workflow through a **cross-script** binding pointing at
|
|
34
|
+
* `<project>-<env>-email`. `pithy dev` runs no script under that name, so locally that binding cannot
|
|
35
|
+
* work — it is absent, or it is present and every `create` on it fails — and every dispatch it carried
|
|
36
|
+
* went nowhere (pithy-sh/pithy#410). What *does* exist locally is the capability host itself, on a
|
|
37
|
+
* pinned port, with its address in this Worker's own vars as `<STEM>_ORIGIN`. So the seam is filled
|
|
38
|
+
* rather than left empty, and the call site — `enqueueEmail`, the cron handler, `c.var.workflows` — is
|
|
39
|
+
* byte-identical either way.
|
|
40
|
+
*
|
|
41
|
+
* ## Three rules, and each of them is a refusal
|
|
42
|
+
*
|
|
43
|
+
* **In `dev`, a published origin wins over the binding.** The deliberate order, and the one that
|
|
44
|
+
* closes #410 whatever wrangler decides to hand a local Worker for a script it is not running: a
|
|
45
|
+
* binding that is present but cannot reach anything is indistinguishable at runtime from one that
|
|
46
|
+
* works, so preferring it would leave the silence in place and depend on a wrangler behavior nothing
|
|
47
|
+
* here controls. Preferring the origin costs nothing, because a published origin is not something a
|
|
48
|
+
* composition can have by accident — see the next rule.
|
|
49
|
+
*
|
|
50
|
+
* **A published origin names a host `pithy dev` is running.** The orchestrator writes one `--var` per
|
|
51
|
+
* *capability host* in the dev set, and never hands a host its own address. So `EMAIL_ORIGIN` on a
|
|
52
|
+
* Worker's env means exactly one thing: the email host is up, locally, there. An adopter's own
|
|
53
|
+
* app-owned Workflow — the same-script shape, which `wrangler dev` implements unchanged — is never
|
|
54
|
+
* published an origin and so is never diverted.
|
|
55
|
+
*
|
|
56
|
+
* **Only `dev`.** Not `staging`, not `prod`, and — the case that matters — not a composition that
|
|
57
|
+
* stamped no environment at all. A gate that reads silence as `dev` opens itself in exactly the
|
|
58
|
+
* deployment whose `wrangler.jsonc` lost the var.
|
|
59
|
+
*
|
|
60
|
+
* **The environment comes off the request env, not off the host's shell.** `--var ENVIRONMENT` is
|
|
61
|
+
* what puts it on a Worker, and the shell that ran `wrangler dev` does not cross into workerd. The
|
|
62
|
+
* dispatch *route* reads the ambient env instead, because it is registered before any request exists;
|
|
63
|
+
* here there is a request env in hand, and it is the truthful one.
|
|
64
|
+
*/
|
|
65
|
+
function resolveWorkflowBinding(env, request) {
|
|
66
|
+
const origin = stringVar(env, "ENVIRONMENT") === LOOPBACK_ENVIRONMENT ? stringVar(env, originVarName(request.capability)) : void 0;
|
|
67
|
+
if (origin) {
|
|
68
|
+
request.log?.debug("workflow dispatch over loopback", {
|
|
69
|
+
binding: request.binding,
|
|
70
|
+
capability: request.capability,
|
|
71
|
+
origin
|
|
72
|
+
});
|
|
73
|
+
return loopbackWorkflowBinding({
|
|
74
|
+
origin,
|
|
75
|
+
binding: request.binding,
|
|
76
|
+
capability: request.capability,
|
|
77
|
+
fetch: request.fetch
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return workflowBindingFor(env, request.binding);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Start one registered job. The single dispatch path — `createBackend`'s per-request dispatcher and
|
|
84
|
+
* `createEntrypoint`'s cron handler both route through here, so a scheduled run and a manual one
|
|
85
|
+
* validate identically.
|
|
86
|
+
*/
|
|
87
|
+
async function triggerWorkflow(env, registry, key, params, log = noopLogger) {
|
|
88
|
+
const entry = registry[key];
|
|
89
|
+
if (!entry) {
|
|
90
|
+
const known = Object.keys(registry).sort().join(", ");
|
|
91
|
+
throw new PithyError({
|
|
92
|
+
code: "core/unknown_workflow",
|
|
93
|
+
status: 500,
|
|
94
|
+
message: `No workflow is registered as "${key}".`,
|
|
95
|
+
action: "Check the key, and compose the capability that declares the job.",
|
|
96
|
+
detail: known ? `Registered workflows: ${known}.` : "No capability registered any workflow."
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const parsed = entry.spec.params.safeParse(params);
|
|
100
|
+
if (!parsed.success) throw new PithyError({
|
|
101
|
+
code: "core/invalid_workflow_params",
|
|
102
|
+
status: 400,
|
|
103
|
+
message: `The parameters for workflow "${key}" are not valid.`,
|
|
104
|
+
action: "Correct the parameters to match the workflow's schema.",
|
|
105
|
+
detail: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
106
|
+
});
|
|
107
|
+
const binding = resolveWorkflowBinding(env, {
|
|
108
|
+
binding: entry.spec.binding,
|
|
109
|
+
capability: entry.capability,
|
|
110
|
+
log
|
|
111
|
+
});
|
|
112
|
+
if (!binding) {
|
|
113
|
+
if (entry.spec.optional) {
|
|
114
|
+
log.warn("workflow skipped", {
|
|
115
|
+
workflow: key,
|
|
116
|
+
binding: entry.spec.binding,
|
|
117
|
+
reason: "binding absent — the workflow host is not deployed or not bound"
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
throw new PithyError({
|
|
122
|
+
code: "core/missing_workflow_binding",
|
|
123
|
+
status: 500,
|
|
124
|
+
message: `The workflow binding "${entry.spec.binding}" is not available.`,
|
|
125
|
+
action: `Deploy the ${entry.capability} workflow host, then bind ${entry.spec.binding} in wrangler.jsonc.`,
|
|
126
|
+
detail: `Dispatching "${key}" needs a Workflow binding named ${entry.spec.binding} on the Worker env.`
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await binding.create({ params: parsed.data });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (!entry.spec.optional) throw error;
|
|
133
|
+
log.warn("workflow skipped", {
|
|
134
|
+
workflow: key,
|
|
135
|
+
binding: entry.spec.binding,
|
|
136
|
+
reason: messageOf(error)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Build the per-request dispatcher over one registry. A closure rather than a lazily-built registry
|
|
142
|
+
* object: there is one method and no per-job resource to construct, so there is nothing to defer.
|
|
143
|
+
*/
|
|
144
|
+
function buildWorkflowDispatcher(env, registry, log = noopLogger) {
|
|
145
|
+
return { trigger: (key, params) => triggerWorkflow(env, registry, key, params, log) };
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { buildWorkflowDispatcher, resolveWorkflowBinding, triggerWorkflow, workflowBindingFor };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import type { PithyHonoEnv } from "../capability/capability";
|
|
3
|
+
import { type AmbientEnv } from "../env/ambient";
|
|
4
|
+
import type { VerificationStrategy } from "../http/verification";
|
|
5
|
+
import type { WorkflowRegistry } from "./spec";
|
|
6
|
+
/**
|
|
7
|
+
* The host's dispatch route — the loopback door `pithy dev` starts an instance through.
|
|
8
|
+
*
|
|
9
|
+
* ## Why a host needs an HTTP door at all
|
|
10
|
+
*
|
|
11
|
+
* A deployed app Worker starts a capability's Workflow through a **cross-script** binding:
|
|
12
|
+
* `{ binding: "EMAIL_SENDER", script_name: "<project>-<env>-email" }`. `pithy dev` does not run that
|
|
13
|
+
* second Worker's script under that name, and CLAUDE.md rules out wrangler's cross-`wrangler dev`
|
|
14
|
+
* registry, so locally the binding names nothing and the call throws (pithy-sh/pithy#410). What
|
|
15
|
+
* *does* exist locally is loopback: every worker in the dev set has a pinned port and an
|
|
16
|
+
* `<STEM>_ORIGIN` in every sibling's env. This route is the other end of that wire.
|
|
17
|
+
*
|
|
18
|
+
* The instance is started on the **host's own same-script binding** — the one with no `script_name`,
|
|
19
|
+
* which `wrangler dev` implements unchanged. So the local path and the deployed path run the same
|
|
20
|
+
* Workflow class, from the same registry, with the same params schema. Only the hop differs.
|
|
21
|
+
*
|
|
22
|
+
* ## The environment gate is the security boundary
|
|
23
|
+
*
|
|
24
|
+
* An HTTP door into another Worker's Workflows is not something the kit leaves open. The route is
|
|
25
|
+
* served in a `dev` composition and refused in every other, by a guard that runs before anything
|
|
26
|
+
* reads the request.
|
|
27
|
+
*
|
|
28
|
+
* **It is a mounted refusal, not an unmounted route** — deliberately the opposite of
|
|
29
|
+
* `@pithy-sh/auth`'s dev-login, and the difference is what the two routes do. Dev-login mints an
|
|
30
|
+
* authenticated session with no credential presented, so its existence in a production route table is
|
|
31
|
+
* itself the finding, and it registers nothing outside `dev`. This route starts a job the host
|
|
32
|
+
* already runs on its own cron, on a binding the caller must already have, and it starts nothing at
|
|
33
|
+
* all before the guard has run. What an operator gains from mounting it is the answer: a loopback
|
|
34
|
+
* dispatcher wired into `staging` by mistake gets `auth/forbidden` naming the binding as the path,
|
|
35
|
+
* rather than a 404 that reads as a typo and sends them looking at the URL.
|
|
36
|
+
*
|
|
37
|
+
* **There is no second CI gate**, and that too is the opposite of dev-login's, on purpose. CI is
|
|
38
|
+
* precisely where the local loop has to work — `pithy dev` under an integration suite is a `dev`
|
|
39
|
+
* composition dispatching over loopback — and a gate that shut it there would turn the silence #410
|
|
40
|
+
* repairs back on in the one place nobody is watching for it.
|
|
41
|
+
*
|
|
42
|
+
* ## Contract
|
|
43
|
+
*
|
|
44
|
+
* `public` verification and a declared request contract, with the guard ahead of both validators so
|
|
45
|
+
* a refusal is never downgraded to a 400 that tells a caller which request shapes are well-formed.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* How the route verifies its caller: it does not.
|
|
49
|
+
*
|
|
50
|
+
* Stated rather than omitted, because every route declares one (CLAUDE.md §HTTP) and `public` is the
|
|
51
|
+
* honest answer — there is no credential on this wire. What stands in for one is the environment
|
|
52
|
+
* gate plus the address: the route answers only in a `dev` composition, and a `dev` composition is a
|
|
53
|
+
* `wrangler dev` bound to loopback on a port `pithy dev` pinned. Deployed environments reach the same
|
|
54
|
+
* Workflow through the cross-script binding and never through this.
|
|
55
|
+
*/
|
|
56
|
+
export declare const WORKFLOW_DISPATCH_VERIFICATION: VerificationStrategy;
|
|
57
|
+
/** What the route needs to serve: whose host this is, what it hosts, and where it is running. */
|
|
58
|
+
export interface WorkflowDispatchOptions {
|
|
59
|
+
/** The capability that owns the host. Named in refusals, so an operator knows which Worker answered. */
|
|
60
|
+
capability: string;
|
|
61
|
+
/**
|
|
62
|
+
* The composed registry this host runs. The route resolves `:binding` against it, so a binding the
|
|
63
|
+
* host hosts no Workflow for is refused by name — and the params validate against the declaring
|
|
64
|
+
* spec's own schema rather than against anything restated here.
|
|
65
|
+
*/
|
|
66
|
+
registry: WorkflowRegistry;
|
|
67
|
+
/**
|
|
68
|
+
* The ambient environment the gate reads. Defaults to the process env, which in a Worker is the
|
|
69
|
+
* script's own vars — where `pithy init` stamps `ENVIRONMENT`. Injectable for tests.
|
|
70
|
+
*/
|
|
71
|
+
env?: AmbientEnv;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Mount the dispatch route on a host worker's app.
|
|
75
|
+
*
|
|
76
|
+
* The guard is registered as middleware ahead of the validators so the order is structural rather
|
|
77
|
+
* than remembered: outside `dev` nothing reads the path, the body, or the env.
|
|
78
|
+
*/
|
|
79
|
+
export declare function registerWorkflowDispatchRoute(app: Hono<PithyHonoEnv>, options: WorkflowDispatchOptions): void;
|
|
80
|
+
//# sourceMappingURL=dispatchRoute.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatchRoute.d.ts","sourceRoot":"","sources":["../../src/workflow/dispatchRoute.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAW,IAAI,EAAQ,MAAM,MAAM,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,KAAK,UAAU,EAAsC,MAAM,gBAAgB,CAAC;AAGrF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAQjE,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,EAAE,oBAA+B,CAAC;AAK7E,iGAAiG;AACjG,MAAM,WAAW,uBAAuB;IACtC,wGAAwG;IACxG,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,EAAE,gBAAgB,CAAC;IAC3B;;;OAGG;IACH,GAAG,CAAC,EAAE,UAAU,CAAC;CAClB;AAqBD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAkE7G"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ForbiddenError, PithyError } from "../error/pithyError.js";
|
|
2
|
+
import { validationHook } from "../http/validation.js";
|
|
3
|
+
import { WORKFLOW_DISPATCH_ROUTE, WorkflowDispatchParams, WorkflowDispatchRequest } from "./schemas.js";
|
|
4
|
+
import { workflowBindingFor } from "./dispatch.js";
|
|
5
|
+
import { ambientEnv, compositionEnvironment } from "../env/ambient.js";
|
|
6
|
+
import { zValidator } from "@hono/zod-validator";
|
|
7
|
+
//#region src/workflow/dispatchRoute.ts
|
|
8
|
+
/**
|
|
9
|
+
* The host's dispatch route — the loopback door `pithy dev` starts an instance through.
|
|
10
|
+
*
|
|
11
|
+
* ## Why a host needs an HTTP door at all
|
|
12
|
+
*
|
|
13
|
+
* A deployed app Worker starts a capability's Workflow through a **cross-script** binding:
|
|
14
|
+
* `{ binding: "EMAIL_SENDER", script_name: "<project>-<env>-email" }`. `pithy dev` does not run that
|
|
15
|
+
* second Worker's script under that name, and CLAUDE.md rules out wrangler's cross-`wrangler dev`
|
|
16
|
+
* registry, so locally the binding names nothing and the call throws (pithy-sh/pithy#410). What
|
|
17
|
+
* *does* exist locally is loopback: every worker in the dev set has a pinned port and an
|
|
18
|
+
* `<STEM>_ORIGIN` in every sibling's env. This route is the other end of that wire.
|
|
19
|
+
*
|
|
20
|
+
* The instance is started on the **host's own same-script binding** — the one with no `script_name`,
|
|
21
|
+
* which `wrangler dev` implements unchanged. So the local path and the deployed path run the same
|
|
22
|
+
* Workflow class, from the same registry, with the same params schema. Only the hop differs.
|
|
23
|
+
*
|
|
24
|
+
* ## The environment gate is the security boundary
|
|
25
|
+
*
|
|
26
|
+
* An HTTP door into another Worker's Workflows is not something the kit leaves open. The route is
|
|
27
|
+
* served in a `dev` composition and refused in every other, by a guard that runs before anything
|
|
28
|
+
* reads the request.
|
|
29
|
+
*
|
|
30
|
+
* **It is a mounted refusal, not an unmounted route** — deliberately the opposite of
|
|
31
|
+
* `@pithy-sh/auth`'s dev-login, and the difference is what the two routes do. Dev-login mints an
|
|
32
|
+
* authenticated session with no credential presented, so its existence in a production route table is
|
|
33
|
+
* itself the finding, and it registers nothing outside `dev`. This route starts a job the host
|
|
34
|
+
* already runs on its own cron, on a binding the caller must already have, and it starts nothing at
|
|
35
|
+
* all before the guard has run. What an operator gains from mounting it is the answer: a loopback
|
|
36
|
+
* dispatcher wired into `staging` by mistake gets `auth/forbidden` naming the binding as the path,
|
|
37
|
+
* rather than a 404 that reads as a typo and sends them looking at the URL.
|
|
38
|
+
*
|
|
39
|
+
* **There is no second CI gate**, and that too is the opposite of dev-login's, on purpose. CI is
|
|
40
|
+
* precisely where the local loop has to work — `pithy dev` under an integration suite is a `dev`
|
|
41
|
+
* composition dispatching over loopback — and a gate that shut it there would turn the silence #410
|
|
42
|
+
* repairs back on in the one place nobody is watching for it.
|
|
43
|
+
*
|
|
44
|
+
* ## Contract
|
|
45
|
+
*
|
|
46
|
+
* `public` verification and a declared request contract, with the guard ahead of both validators so
|
|
47
|
+
* a refusal is never downgraded to a 400 that tells a caller which request shapes are well-formed.
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* How the route verifies its caller: it does not.
|
|
51
|
+
*
|
|
52
|
+
* Stated rather than omitted, because every route declares one (CLAUDE.md §HTTP) and `public` is the
|
|
53
|
+
* honest answer — there is no credential on this wire. What stands in for one is the environment
|
|
54
|
+
* gate plus the address: the route answers only in a `dev` composition, and a `dev` composition is a
|
|
55
|
+
* `wrangler dev` bound to loopback on a port `pithy dev` pinned. Deployed environments reach the same
|
|
56
|
+
* Workflow through the cross-script binding and never through this.
|
|
57
|
+
*/
|
|
58
|
+
const WORKFLOW_DISPATCH_VERIFICATION = "public";
|
|
59
|
+
/** The environment this route serves in. Verbatim, and the only one. */
|
|
60
|
+
const DISPATCH_ENVIRONMENT = "dev";
|
|
61
|
+
/** The job this host hosts on a given binding, or `undefined`. */
|
|
62
|
+
function specForBinding(registry, binding) {
|
|
63
|
+
return Object.values(registry).find((entry) => entry.spec.binding === binding);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The refusal. `auth/forbidden` (403) rather than a 404: the door exists, it is shut because of
|
|
67
|
+
* *where* this Worker is running, and pretending otherwise costs the operator the sentence that ends
|
|
68
|
+
* their search. `action` names the real path and is stripped before it reaches the caller, like every
|
|
69
|
+
* other action.
|
|
70
|
+
*/
|
|
71
|
+
function refuseOutsideDev(capability, environment) {
|
|
72
|
+
return new ForbiddenError({
|
|
73
|
+
message: `Workflow dispatch over HTTP is served only in the ${DISPATCH_ENVIRONMENT} environment.`,
|
|
74
|
+
action: `Dispatch through the cross-script ${capability} Workflow binding, or run this host under pithy dev.`,
|
|
75
|
+
detail: `The composition's ENVIRONMENT is ${environment ?? "unstamped"}.`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Mount the dispatch route on a host worker's app.
|
|
80
|
+
*
|
|
81
|
+
* The guard is registered as middleware ahead of the validators so the order is structural rather
|
|
82
|
+
* than remembered: outside `dev` nothing reads the path, the body, or the env.
|
|
83
|
+
*/
|
|
84
|
+
function registerWorkflowDispatchRoute(app, options) {
|
|
85
|
+
const { capability, registry } = options;
|
|
86
|
+
const ambient = options.env;
|
|
87
|
+
const guard = async (_c, next) => {
|
|
88
|
+
const environment = compositionEnvironment(ambient ?? ambientEnv());
|
|
89
|
+
if (environment !== DISPATCH_ENVIRONMENT) throw refuseOutsideDev(capability, environment);
|
|
90
|
+
await next();
|
|
91
|
+
};
|
|
92
|
+
app.post(WORKFLOW_DISPATCH_ROUTE, guard, zValidator("param", WorkflowDispatchParams, validationHook), zValidator("json", WorkflowDispatchRequest, validationHook), async (c) => {
|
|
93
|
+
const { binding } = c.req.valid("param");
|
|
94
|
+
const { id, params } = c.req.valid("json");
|
|
95
|
+
const entry = specForBinding(registry, binding);
|
|
96
|
+
if (!entry) {
|
|
97
|
+
const known = Object.values(registry).map((registered) => registered.spec.binding).sort().join(", ");
|
|
98
|
+
throw new PithyError({
|
|
99
|
+
code: "core/unknown_workflow",
|
|
100
|
+
status: 500,
|
|
101
|
+
message: `No workflow on this host is bound to "${binding}".`,
|
|
102
|
+
action: `Check the binding name against the ${capability} capability's declared workflows.`,
|
|
103
|
+
detail: known ? `This host hosts: ${known}.` : "This host hosts no workflows at all."
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const parsed = entry.spec.params.safeParse(params);
|
|
107
|
+
if (!parsed.success) throw new PithyError({
|
|
108
|
+
code: "core/invalid_workflow_params",
|
|
109
|
+
status: 400,
|
|
110
|
+
message: `The parameters for workflow "${entry.key}" are not valid.`,
|
|
111
|
+
action: "Correct the parameters to match the workflow's schema.",
|
|
112
|
+
detail: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
|
|
113
|
+
});
|
|
114
|
+
const target = workflowBindingFor(c.env, binding);
|
|
115
|
+
if (!target) throw new PithyError({
|
|
116
|
+
code: "core/missing_workflow_binding",
|
|
117
|
+
status: 500,
|
|
118
|
+
message: `The workflow binding "${binding}" is not available on this host.`,
|
|
119
|
+
action: `Bind ${binding} in the ${capability} host's wrangler.jsonc, then restart it.`,
|
|
120
|
+
detail: `Dispatching "${entry.key}" needs a same-script Workflow binding named ${binding}.`
|
|
121
|
+
});
|
|
122
|
+
await target.create({
|
|
123
|
+
id,
|
|
124
|
+
params: parsed.data
|
|
125
|
+
});
|
|
126
|
+
return c.json({
|
|
127
|
+
binding,
|
|
128
|
+
id,
|
|
129
|
+
started: true
|
|
130
|
+
}, 202);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
export { WORKFLOW_DISPATCH_VERIFICATION, registerWorkflowDispatchRoute };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Every fault that reaches a durable step is classified — retryable or terminal — at a stated site.**
|
|
3
|
+
*
|
|
4
|
+
* A Workflow is chosen *because of* its retry semantics: durable execution, a journal, steps that resume,
|
|
5
|
+
* backoff that survives an eviction. A step that inherits the platform default has not decided; it has
|
|
6
|
+
* deferred, and the default is retry-everything. So a deterministic refusal — a name that already exists,
|
|
7
|
+
* a value too large, a payload that will not parse — is answered slowly instead of immediately, the retry
|
|
8
|
+
* budget burns down on an answer that cannot change, and an instant refusal looks like an outage
|
|
9
|
+
* (pithy-sh/pithy#338).
|
|
10
|
+
*
|
|
11
|
+
* ## The three layers, and which owns what
|
|
12
|
+
*
|
|
13
|
+
* 1. **The capability states its own codes.** A {@link WorkflowRetryPolicy} names the `PithyError` codes
|
|
14
|
+
* that capability retries, each with the reason a second attempt can answer differently. One statement
|
|
15
|
+
* per capability, in one file, the way `@pithy-sh/email`'s `errorMapping.ts` already states the Email
|
|
16
|
+
* Service's `E_*` codes.
|
|
17
|
+
* 2. **Core owns D1.** `withD1Retry` already classifies D1's fault vocabulary, so this asks it rather than
|
|
18
|
+
* growing a second copy — and that is also the no-double-retry rule: a fault the inner layer refused to
|
|
19
|
+
* retry (a constraint violation, a SQL error) is terminal here too, while a transient one stays
|
|
20
|
+
* retryable so the step re-drives it with backoff far longer than the inner loop's.
|
|
21
|
+
* 3. **Everything else is terminal.** Retry is opted into, never inherited. The platform default is the
|
|
22
|
+
* opposite, and it is the wrong way round: a terminal fault wrongly called transient burns the whole
|
|
23
|
+
* budget before surfacing, while a transient one wrongly called terminal surfaces immediately with the
|
|
24
|
+
* fault written on it. One of those is a diagnosis; the other is a hang.
|
|
25
|
+
*
|
|
26
|
+
* ## Where the wrapping happens, and why it cannot be anywhere else
|
|
27
|
+
*
|
|
28
|
+
* {@link classifiedSteps} converts a terminal fault **inside** the step callback. The platform's retry loop
|
|
29
|
+
* lives inside `step.do`: by the time a `step.do` promise rejects, every retry has already been spent. A
|
|
30
|
+
* wrapper around the call would classify a fault the engine had already re-driven five times — correct
|
|
31
|
+
* looking, and exactly as slow as doing nothing.
|
|
32
|
+
*/
|
|
33
|
+
/** Whether the platform should try a step's body again, or stop with the answer it has. */
|
|
34
|
+
export type WorkflowFaultDisposition = "retry" | "terminal";
|
|
35
|
+
/**
|
|
36
|
+
* One capability's stated retry classification — the whole of what it retries, and why.
|
|
37
|
+
*
|
|
38
|
+
* A record rather than a set, because the value is the argument: "what would a second attempt see that
|
|
39
|
+
* this one did not" is the question that decides, and writing the answer down is what keeps the next code
|
|
40
|
+
* from being added to the list by reflex.
|
|
41
|
+
*/
|
|
42
|
+
export interface WorkflowRetryPolicy {
|
|
43
|
+
/** The capability whose Workflows this governs — `secrets`, `payments`, `email`. Named in the fault's reason. */
|
|
44
|
+
capability: string;
|
|
45
|
+
/**
|
|
46
|
+
* `PithyError` code → why a retry can answer differently. **Everything absent is terminal**, including
|
|
47
|
+
* codes another capability retries: a 502 from a payment rail is worth re-driving, and a 502 from a
|
|
48
|
+
* store that has already taken the write is not.
|
|
49
|
+
*/
|
|
50
|
+
retryable: Readonly<Record<string, string>>;
|
|
51
|
+
}
|
|
52
|
+
/** One fault, classified: what it was, what happens next, and the stated reason for it. */
|
|
53
|
+
export interface WorkflowFault {
|
|
54
|
+
/** Retry, or stop. */
|
|
55
|
+
disposition: WorkflowFaultDisposition;
|
|
56
|
+
/** The `PithyError` code, `d1/<fault>` for a D1 fault class, or `unclassified`. */
|
|
57
|
+
code: string;
|
|
58
|
+
/** Why — the policy's own sentence for a retry, or what made this terminal. */
|
|
59
|
+
reason: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Classify one fault against a capability's policy. Pure, so a capability's classification is a unit
|
|
63
|
+
* test rather than a claim — and so the same function decides in a test, in Miniflare, and in production.
|
|
64
|
+
*/
|
|
65
|
+
export declare function classifyWorkflowFault(error: unknown, policy: WorkflowRetryPolicy): WorkflowFault;
|
|
66
|
+
/** The half of a Workflow step this kit uses: run a named body, or serve its journalled result. */
|
|
67
|
+
export interface WorkflowStepLike {
|
|
68
|
+
/** Run a named step, or return its journalled result if this instance already completed it. */
|
|
69
|
+
do<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The platform's terminal-error class — `NonRetryableError` from `cloudflare:workflows`, taken as a
|
|
73
|
+
* parameter.
|
|
74
|
+
*
|
|
75
|
+
* Injected rather than imported because `@pithy-sh/core` is bundled into every adopter's Worker *and*
|
|
76
|
+
* imported by the CLI under node, so it may not import a `cloudflare:` module. The Workflow class is
|
|
77
|
+
* already inside workerd and already imports one; handing the class in costs it a word and keeps the
|
|
78
|
+
* classification itself testable anywhere.
|
|
79
|
+
*
|
|
80
|
+
* **One argument, deliberately.** `NonRetryableError`'s constructor also takes a `name`, and the engine
|
|
81
|
+
* recognizes the error *by* that name: measured against a real Workflow in `wrangler dev`, passing any
|
|
82
|
+
* name but the default turned an instance that failed in 0.7s back into one that retried for 32s and
|
|
83
|
+
* then failed. The signature is narrowed so nobody can reintroduce that by being helpful.
|
|
84
|
+
*/
|
|
85
|
+
export interface TerminalErrorConstructor {
|
|
86
|
+
new (message: string): Error;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Wrap a Workflow's step runner so every fault it raises is classified against `policy`, and a terminal
|
|
90
|
+
* one is re-thrown as the platform's terminal error — **inside the step**, before the retry loop reads it.
|
|
91
|
+
*
|
|
92
|
+
* The result is structurally a step runner, so a body that already takes one (`reconcilePayments`,
|
|
93
|
+
* `runSendBatch`, `runAtRestKeyRotation`) takes this instead with no change: the classification is
|
|
94
|
+
* declared once, at the entrypoint, and every step below it inherits the decision.
|
|
95
|
+
*/
|
|
96
|
+
export declare function classifiedSteps(step: WorkflowStepLike, policy: WorkflowRetryPolicy, terminalError: TerminalErrorConstructor): WorkflowStepLike;
|
|
97
|
+
//# sourceMappingURL=faults.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"faults.d.ts","sourceRoot":"","sources":["../../src/workflow/faults.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,2FAA2F;AAC3F,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,UAAU,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,iHAAiH;IACjH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC7C;AAED,2FAA2F;AAC3F,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,WAAW,EAAE,wBAAwB,CAAC;IACtC,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB;AAsDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,GAAG,aAAa,CA0BhG;AAED,mGAAmG;AACnG,MAAM,WAAW,gBAAgB;IAC/B,+FAA+F;IAC/F,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,wBAAwB;IACvC,KAAK,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,mBAAmB,EAC3B,aAAa,EAAE,wBAAwB,GACtC,gBAAgB,CA2BlB"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { PithyError } from "../error/pithyError.js";
|
|
2
|
+
import { d1TransientFault } from "../data/withD1Retry.js";
|
|
3
|
+
import { encodeWorkflowStepMessage } from "./stepMessage.js";
|
|
4
|
+
//#region src/workflow/faults.ts
|
|
5
|
+
/** The `code` reported for a throw no layer recognizes. */
|
|
6
|
+
const UNCLASSIFIED = "unclassified";
|
|
7
|
+
/**
|
|
8
|
+
* The code a thrown value carries, or `null` when it is not a `PithyError`.
|
|
9
|
+
*
|
|
10
|
+
* The structural read is not belt-and-braces: `instanceof` is per module instance, and a bundler that
|
|
11
|
+
* gives a host worker two copies of core would silently answer `false` for a perfectly good
|
|
12
|
+
* `PithyError` — which here means classifying a stated retryable fault as terminal. The shape is
|
|
13
|
+
* checked, not assumed.
|
|
14
|
+
*/
|
|
15
|
+
function pithyCodeOf(error) {
|
|
16
|
+
if (error instanceof PithyError) return error.payload.code;
|
|
17
|
+
if (error && typeof error === "object" && "payload" in error) {
|
|
18
|
+
const payload = error.payload;
|
|
19
|
+
if (payload && typeof payload === "object" && "code" in payload) {
|
|
20
|
+
const code = payload.code;
|
|
21
|
+
if (typeof code === "string") return code;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/** The public message of a thrown value, for the terminal error's own text. */
|
|
27
|
+
function messageOf(error) {
|
|
28
|
+
return error instanceof Error ? error.message : String(error);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The operator's remedy a thrown value carries, or `undefined` when it states none.
|
|
32
|
+
*
|
|
33
|
+
* Read structurally for the same reason {@link pithyCodeOf} is: `instanceof` is per module instance,
|
|
34
|
+
* and two copies of core in one bundle would answer `false` for a perfectly good `PithyError` — here
|
|
35
|
+
* meaning a remedy the raising error stated is silently dropped at the boundary. That is the bug this
|
|
36
|
+
* function exists to end, so it is not reintroduced by the check.
|
|
37
|
+
*
|
|
38
|
+
* **`detail` is deliberately not read here, and there is no sibling for it.** `action` is already
|
|
39
|
+
* operator-facing — the CLI prints it, `operatorError` includes it. `detail` is throw-site context and
|
|
40
|
+
* stays at the throw site.
|
|
41
|
+
*/
|
|
42
|
+
function pithyActionOf(error) {
|
|
43
|
+
if (error instanceof PithyError) return error.payload.action;
|
|
44
|
+
if (error && typeof error === "object" && "payload" in error) {
|
|
45
|
+
const payload = error.payload;
|
|
46
|
+
if (payload && typeof payload === "object" && "action" in payload) {
|
|
47
|
+
const action = payload.action;
|
|
48
|
+
if (typeof action === "string") return action;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Classify one fault against a capability's policy. Pure, so a capability's classification is a unit
|
|
54
|
+
* test rather than a claim — and so the same function decides in a test, in Miniflare, and in production.
|
|
55
|
+
*/
|
|
56
|
+
function classifyWorkflowFault(error, policy) {
|
|
57
|
+
const code = pithyCodeOf(error);
|
|
58
|
+
if (code !== null) {
|
|
59
|
+
const reason = policy.retryable[code];
|
|
60
|
+
if (reason !== void 0) return {
|
|
61
|
+
disposition: "retry",
|
|
62
|
+
code,
|
|
63
|
+
reason
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
disposition: "terminal",
|
|
67
|
+
code,
|
|
68
|
+
reason: `${policy.capability} does not retry ${code}: a second attempt reaches the same answer.`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const d1 = d1TransientFault(error);
|
|
72
|
+
if (d1 !== null) return {
|
|
73
|
+
disposition: "retry",
|
|
74
|
+
code: `d1/${d1}`,
|
|
75
|
+
reason: `D1 reported a transient ${d1} fault; withD1Retry classifies it retryable.`
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
disposition: "terminal",
|
|
79
|
+
code: UNCLASSIFIED,
|
|
80
|
+
reason: `Nothing classifies this fault, and ${policy.capability} retries only what it states.`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Wrap a Workflow's step runner so every fault it raises is classified against `policy`, and a terminal
|
|
85
|
+
* one is re-thrown as the platform's terminal error — **inside the step**, before the retry loop reads it.
|
|
86
|
+
*
|
|
87
|
+
* The result is structurally a step runner, so a body that already takes one (`reconcilePayments`,
|
|
88
|
+
* `runSendBatch`, `runAtRestKeyRotation`) takes this instead with no change: the classification is
|
|
89
|
+
* declared once, at the entrypoint, and every step below it inherits the decision.
|
|
90
|
+
*/
|
|
91
|
+
function classifiedSteps(step, policy, terminalError) {
|
|
92
|
+
return { do(name, fn) {
|
|
93
|
+
return step.do(name, async () => {
|
|
94
|
+
try {
|
|
95
|
+
return await fn();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const fault = classifyWorkflowFault(error, policy);
|
|
98
|
+
if (fault.disposition === "retry") throw error;
|
|
99
|
+
const terminal = new terminalError(encodeWorkflowStepMessage({
|
|
100
|
+
code: fault.code,
|
|
101
|
+
message: messageOf(error),
|
|
102
|
+
action: pithyActionOf(error)
|
|
103
|
+
}));
|
|
104
|
+
terminal.cause = error;
|
|
105
|
+
throw terminal;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} };
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { classifiedSteps, classifyWorkflowFault };
|