@pithy-sh/core 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/address/address.d.ts +57 -0
- package/dist/address/address.d.ts.map +1 -0
- package/dist/address/address.js +71 -0
- package/dist/audit/auditEvent.d.ts +89 -0
- package/dist/audit/auditEvent.d.ts.map +1 -0
- package/dist/audit/auditEvent.js +69 -0
- package/dist/audit/recorder.d.ts +18 -0
- package/dist/audit/recorder.d.ts.map +1 -0
- package/dist/audit/recorder.js +8 -0
- package/dist/capability/bindings.d.ts +62 -0
- package/dist/capability/bindings.d.ts.map +1 -0
- package/dist/capability/bindings.js +97 -0
- package/dist/capability/capability.d.ts +501 -0
- package/dist/capability/capability.d.ts.map +1 -0
- package/dist/capability/capability.js +16 -0
- package/dist/capability/client.d.ts +70 -0
- package/dist/capability/client.d.ts.map +1 -0
- package/dist/capability/client.js +86 -0
- package/dist/capability/compose.d.ts +25 -0
- package/dist/capability/compose.d.ts.map +1 -0
- package/dist/capability/compose.js +54 -0
- package/dist/capability/composition.d.ts +26 -0
- package/dist/capability/composition.d.ts.map +1 -0
- package/dist/capability/composition.js +84 -0
- package/dist/capability/config.d.ts +7 -0
- package/dist/capability/config.d.ts.map +1 -0
- package/dist/capability/config.js +14 -0
- package/dist/capability/devSecret.d.ts +32 -0
- package/dist/capability/devSecret.d.ts.map +1 -0
- package/dist/capability/devSecret.js +28 -0
- package/dist/capability/manifest.d.ts +298 -0
- package/dist/capability/manifest.d.ts.map +1 -0
- package/dist/capability/manifest.js +356 -0
- package/dist/capability/secretOrigin.d.ts +121 -0
- package/dist/capability/secretOrigin.d.ts.map +1 -0
- package/dist/capability/secretOrigin.js +134 -0
- package/dist/capability/settings.d.ts +119 -0
- package/dist/capability/settings.d.ts.map +1 -0
- package/dist/capability/settings.js +66 -0
- package/dist/capability/validateBindings.d.ts +4 -0
- package/dist/capability/validateBindings.d.ts.map +1 -0
- package/dist/capability/validateBindings.js +34 -0
- package/dist/capability/vanishingKey.d.ts +31 -0
- package/dist/capability/vanishingKey.d.ts.map +1 -0
- package/dist/capability/vanishingKey.js +79 -0
- package/dist/controlPlane/audit/actions.d.ts +70 -0
- package/dist/controlPlane/audit/actions.d.ts.map +1 -0
- package/dist/controlPlane/audit/actions.js +76 -0
- package/dist/controlPlane/capability.d.ts +47 -0
- package/dist/controlPlane/capability.d.ts.map +1 -0
- package/dist/controlPlane/capability.js +138 -0
- package/dist/controlPlane/config/config.d.ts +19 -0
- package/dist/controlPlane/config/config.d.ts.map +1 -0
- package/dist/controlPlane/config/config.js +85 -0
- package/dist/controlPlane/context.d.ts +27 -0
- package/dist/controlPlane/context.d.ts.map +1 -0
- package/dist/controlPlane/context.js +29 -0
- package/dist/controlPlane/data/connection.d.ts +79 -0
- package/dist/controlPlane/data/connection.d.ts.map +1 -0
- package/dist/controlPlane/data/connection.js +55 -0
- package/dist/controlPlane/data/keyLifecycle.d.ts +62 -0
- package/dist/controlPlane/data/keyLifecycle.d.ts.map +1 -0
- package/dist/controlPlane/data/keyLifecycle.js +126 -0
- package/dist/controlPlane/data/replay.d.ts +18 -0
- package/dist/controlPlane/data/replay.d.ts.map +1 -0
- package/dist/controlPlane/data/replay.js +20 -0
- package/dist/controlPlane/data/tables.d.ts +34 -0
- package/dist/controlPlane/data/tables.d.ts.map +1 -0
- package/dist/controlPlane/data/tables.js +37 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts +313 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -0
- package/dist/controlPlane/discovery/adminRoute.js +136 -0
- package/dist/controlPlane/discovery/configuration.d.ts +108 -0
- package/dist/controlPlane/discovery/configuration.d.ts.map +1 -0
- package/dist/controlPlane/discovery/configuration.js +187 -0
- package/dist/controlPlane/discovery/drift.d.ts +64 -0
- package/dist/controlPlane/discovery/drift.d.ts.map +1 -0
- package/dist/controlPlane/discovery/drift.js +58 -0
- package/dist/controlPlane/discovery/health.d.ts +99 -0
- package/dist/controlPlane/discovery/health.d.ts.map +1 -0
- package/dist/controlPlane/discovery/health.js +151 -0
- package/dist/controlPlane/discovery/healthSummary.d.ts +236 -0
- package/dist/controlPlane/discovery/healthSummary.d.ts.map +1 -0
- package/dist/controlPlane/discovery/healthSummary.js +278 -0
- package/dist/controlPlane/error/errors.d.ts +67 -0
- package/dist/controlPlane/error/errors.d.ts.map +1 -0
- package/dist/controlPlane/error/errors.js +79 -0
- package/dist/controlPlane/http/cors.d.ts +112 -0
- package/dist/controlPlane/http/cors.d.ts.map +1 -0
- package/dist/controlPlane/http/cors.js +192 -0
- package/dist/controlPlane/http/guard.d.ts +77 -0
- package/dist/controlPlane/http/guard.d.ts.map +1 -0
- package/dist/controlPlane/http/guard.js +142 -0
- package/dist/controlPlane/http/handlers.d.ts +179 -0
- package/dist/controlPlane/http/handlers.d.ts.map +1 -0
- package/dist/controlPlane/http/handlers.js +245 -0
- package/dist/controlPlane/http/responses.d.ts +99 -0
- package/dist/controlPlane/http/responses.d.ts.map +1 -0
- package/dist/controlPlane/http/responses.js +63 -0
- package/dist/controlPlane/http/routes.d.ts +40 -0
- package/dist/controlPlane/http/routes.d.ts.map +1 -0
- package/dist/controlPlane/http/routes.js +83 -0
- package/dist/controlPlane/http/schemas.d.ts +39 -0
- package/dist/controlPlane/http/schemas.d.ts.map +1 -0
- package/dist/controlPlane/http/schemas.js +31 -0
- package/dist/controlPlane/http/verify.d.ts +84 -0
- package/dist/controlPlane/http/verify.d.ts.map +1 -0
- package/dist/controlPlane/http/verify.js +65 -0
- package/dist/controlPlane/migrations/0001_init.d.ts +51 -0
- package/dist/controlPlane/migrations/0001_init.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0001_init.js +65 -0
- package/dist/controlPlane/replay/d1Guard.d.ts +29 -0
- package/dist/controlPlane/replay/d1Guard.d.ts.map +1 -0
- package/dist/controlPlane/replay/d1Guard.js +39 -0
- package/dist/controlPlane/replay/guard.d.ts +52 -0
- package/dist/controlPlane/replay/guard.d.ts.map +1 -0
- package/dist/controlPlane/replay/guard.js +0 -0
- package/dist/controlPlane/replay/kvGuard.d.ts +99 -0
- package/dist/controlPlane/replay/kvGuard.d.ts.map +1 -0
- package/dist/controlPlane/replay/kvGuard.js +101 -0
- package/dist/controlPlane/scope/scope.d.ts +68 -0
- package/dist/controlPlane/scope/scope.d.ts.map +1 -0
- package/dist/controlPlane/scope/scope.js +63 -0
- package/dist/controlPlane/token/base64url.d.ts +10 -0
- package/dist/controlPlane/token/base64url.d.ts.map +1 -0
- package/dist/controlPlane/token/base64url.js +48 -0
- package/dist/controlPlane/token/claims.d.ts +83 -0
- package/dist/controlPlane/token/claims.d.ts.map +1 -0
- package/dist/controlPlane/token/claims.js +86 -0
- package/dist/controlPlane/token/digest.d.ts +44 -0
- package/dist/controlPlane/token/digest.d.ts.map +1 -0
- package/dist/controlPlane/token/digest.js +56 -0
- package/dist/controlPlane/token/jws.d.ts +52 -0
- package/dist/controlPlane/token/jws.d.ts.map +1 -0
- package/dist/controlPlane/token/jws.js +65 -0
- package/dist/controlPlane/token/mint.d.ts +56 -0
- package/dist/controlPlane/token/mint.d.ts.map +1 -0
- package/dist/controlPlane/token/mint.js +55 -0
- package/dist/controlPlane/wire.d.ts +122 -0
- package/dist/controlPlane/wire.d.ts.map +1 -0
- package/dist/controlPlane/wire.js +123 -0
- package/dist/createBackend.d.ts +58 -0
- package/dist/createBackend.d.ts.map +1 -0
- package/dist/createBackend.js +170 -0
- package/dist/createEntrypoint.d.ts +53 -0
- package/dist/createEntrypoint.d.ts.map +1 -0
- package/dist/createEntrypoint.js +52 -0
- package/dist/data/boundParameters.d.ts +83 -0
- package/dist/data/boundParameters.d.ts.map +1 -0
- package/dist/data/boundParameters.js +162 -0
- package/dist/data/codecs.d.ts +34 -0
- package/dist/data/codecs.d.ts.map +1 -0
- package/dist/data/codecs.js +135 -0
- package/dist/data/cursor.d.ts +81 -0
- package/dist/data/cursor.d.ts.map +1 -0
- package/dist/data/cursor.js +104 -0
- package/dist/data/databases.d.ts +55 -0
- package/dist/data/databases.d.ts.map +1 -0
- package/dist/data/databases.js +32 -0
- package/dist/data/db.d.ts +42 -0
- package/dist/data/db.d.ts.map +1 -0
- package/dist/data/db.js +24 -0
- package/dist/data/withD1Retry.d.ts +91 -0
- package/dist/data/withD1Retry.d.ts.map +1 -0
- package/dist/data/withD1Retry.js +152 -0
- package/dist/entitlement/entitlement.d.ts +118 -0
- package/dist/entitlement/entitlement.d.ts.map +1 -0
- package/dist/entitlement/entitlement.js +59 -0
- package/dist/entitlement/gateScan.d.ts +26 -0
- package/dist/entitlement/gateScan.d.ts.map +1 -0
- package/dist/entitlement/gateScan.js +96 -0
- package/dist/entitlement/require.d.ts +12 -0
- package/dist/entitlement/require.d.ts.map +1 -0
- package/dist/entitlement/require.js +123 -0
- package/dist/env/ambient.d.ts +52 -0
- package/dist/env/ambient.d.ts.map +1 -0
- package/dist/env/ambient.js +35 -0
- package/dist/env/ci.d.ts +36 -0
- package/dist/env/ci.d.ts.map +1 -0
- package/dist/env/ci.js +40 -0
- package/dist/env/stem.d.ts +23 -0
- package/dist/env/stem.d.ts.map +1 -0
- package/dist/env/stem.js +29 -0
- package/dist/error/cause.d.ts +143 -0
- package/dist/error/cause.d.ts.map +1 -0
- package/dist/error/cause.js +192 -0
- package/dist/error/client.d.ts +35 -0
- package/dist/error/client.d.ts.map +1 -0
- package/dist/error/client.js +38 -0
- package/dist/error/extend.d.ts +101 -0
- package/dist/error/extend.d.ts.map +1 -0
- package/dist/error/extend.js +76 -0
- package/dist/error/http.d.ts +1516 -0
- package/dist/error/http.d.ts.map +1 -0
- package/dist/error/http.js +72 -0
- package/dist/error/payload.d.ts +3034 -0
- package/dist/error/payload.d.ts.map +1 -0
- package/dist/error/payload.js +1110 -0
- package/dist/error/pithyError.d.ts +154 -0
- package/dist/error/pithyError.d.ts.map +1 -0
- package/dist/error/pithyError.js +197 -0
- package/dist/error/terminal.d.ts +28 -0
- package/dist/error/terminal.d.ts.map +1 -0
- package/dist/error/terminal.js +33 -0
- package/dist/http/authContext.d.ts +21 -0
- package/dist/http/authContext.d.ts.map +1 -0
- package/dist/http/authContext.js +23 -0
- package/dist/http/routeContract.d.ts +39 -0
- package/dist/http/routeContract.d.ts.map +1 -0
- package/dist/http/routeContract.js +82 -0
- package/dist/http/sameOrigin.d.ts +47 -0
- package/dist/http/sameOrigin.d.ts.map +1 -0
- package/dist/http/sameOrigin.js +24 -0
- package/dist/http/signedWebhook.d.ts +225 -0
- package/dist/http/signedWebhook.d.ts.map +1 -0
- package/dist/http/signedWebhook.js +247 -0
- package/dist/http/validation.d.ts +37 -0
- package/dist/http/validation.d.ts.map +1 -0
- package/dist/http/validation.js +14 -0
- package/dist/http/verification.d.ts +10 -0
- package/dist/http/verification.d.ts.map +1 -0
- package/dist/http/verification.js +17 -0
- package/dist/i18n/acceptLanguage.d.ts +22 -0
- package/dist/i18n/acceptLanguage.d.ts.map +1 -0
- package/dist/i18n/acceptLanguage.js +55 -0
- package/dist/i18n/catalog.d.ts +39 -0
- package/dist/i18n/catalog.d.ts.map +1 -0
- package/dist/i18n/catalog.js +57 -0
- package/dist/i18n/locale.d.ts +74 -0
- package/dist/i18n/locale.d.ts.map +1 -0
- package/dist/i18n/locale.js +109 -0
- package/dist/i18n/localeMarker.d.ts +29 -0
- package/dist/i18n/localeMarker.d.ts.map +1 -0
- package/dist/i18n/localeMarker.js +112 -0
- package/dist/i18n/match.d.ts +26 -0
- package/dist/i18n/match.d.ts.map +1 -0
- package/dist/i18n/match.js +88 -0
- package/dist/i18n/registry.d.ts +25 -0
- package/dist/i18n/registry.d.ts.map +1 -0
- package/dist/i18n/registry.js +51 -0
- package/dist/i18n/translator.d.ts +107 -0
- package/dist/i18n/translator.d.ts.map +1 -0
- package/dist/i18n/translator.js +80 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/kv/kv.d.ts +166 -0
- package/dist/kv/kv.d.ts.map +1 -0
- package/dist/kv/kv.js +261 -0
- package/dist/kv/namespaces.d.ts +58 -0
- package/dist/kv/namespaces.d.ts.map +1 -0
- package/dist/kv/namespaces.js +40 -0
- package/dist/logger/local.d.ts +41 -0
- package/dist/logger/local.d.ts.map +1 -0
- package/dist/logger/local.js +45 -0
- package/dist/logger/logger.d.ts +56 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +83 -0
- package/dist/logger/record.d.ts +919 -0
- package/dist/logger/record.d.ts.map +1 -0
- package/dist/logger/record.js +79 -0
- package/dist/logger/worker.d.ts +74 -0
- package/dist/logger/worker.d.ts.map +1 -0
- package/dist/logger/worker.js +54 -0
- package/dist/migrations/batch.d.ts +10 -0
- package/dist/migrations/batch.d.ts.map +1 -0
- package/dist/migrations/batch.js +193 -0
- package/dist/migrations/bookkeeping.d.ts +24 -0
- package/dist/migrations/bookkeeping.d.ts.map +1 -0
- package/dist/migrations/bookkeeping.js +57 -0
- package/dist/migrations/owner.d.ts +41 -0
- package/dist/migrations/owner.d.ts.map +1 -0
- package/dist/migrations/owner.js +105 -0
- package/dist/migrations/registry.d.ts +36 -0
- package/dist/migrations/registry.d.ts.map +1 -0
- package/dist/migrations/registry.js +70 -0
- package/dist/migrations/runner.d.ts +67 -0
- package/dist/migrations/runner.d.ts.map +1 -0
- package/dist/migrations/runner.js +218 -0
- package/dist/naming/domains.d.ts +108 -0
- package/dist/naming/domains.d.ts.map +1 -0
- package/dist/naming/domains.js +156 -0
- package/dist/naming/environment.d.ts +118 -0
- package/dist/naming/environment.d.ts.map +1 -0
- package/dist/naming/environment.js +190 -0
- package/dist/naming/feature.d.ts +68 -0
- package/dist/naming/feature.d.ts.map +1 -0
- package/dist/naming/feature.js +106 -0
- package/dist/naming/limits.d.ts +229 -0
- package/dist/naming/limits.d.ts.map +1 -0
- package/dist/naming/limits.js +187 -0
- package/dist/naming/provisionScope.d.ts +91 -0
- package/dist/naming/provisionScope.d.ts.map +1 -0
- package/dist/naming/provisionScope.js +65 -0
- package/dist/naming/resource.d.ts +169 -0
- package/dist/naming/resource.d.ts.map +1 -0
- package/dist/naming/resource.js +220 -0
- package/dist/naming/resourceNames.d.ts +83 -0
- package/dist/naming/resourceNames.d.ts.map +1 -0
- package/dist/naming/resourceNames.js +102 -0
- package/dist/naming/segment.d.ts +29 -0
- package/dist/naming/segment.d.ts.map +1 -0
- package/dist/naming/segment.js +31 -0
- package/dist/projection/asRead.d.ts +72 -0
- package/dist/projection/asRead.d.ts.map +1 -0
- package/dist/projection/asRead.js +169 -0
- package/dist/projection/published.d.ts +95 -0
- package/dist/projection/published.d.ts.map +1 -0
- package/dist/projection/published.js +114 -0
- package/dist/schema/describedness.d.ts +55 -0
- package/dist/schema/describedness.d.ts.map +1 -0
- package/dist/schema/describedness.js +180 -0
- package/dist/seed/compose.d.ts +41 -0
- package/dist/seed/compose.d.ts.map +1 -0
- package/dist/seed/compose.js +51 -0
- package/dist/seed/devLogin.d.ts +50 -0
- package/dist/seed/devLogin.d.ts.map +1 -0
- package/dist/seed/devLogin.js +52 -0
- package/dist/seed/exampleIdentities.d.ts +58 -0
- package/dist/seed/exampleIdentities.d.ts.map +1 -0
- package/dist/seed/exampleIdentities.js +26 -0
- package/dist/seed/metadata.d.ts +20 -0
- package/dist/seed/metadata.d.ts.map +1 -0
- package/dist/seed/metadata.js +27 -0
- package/dist/seed/seed.d.ts +264 -0
- package/dist/seed/seed.d.ts.map +1 -0
- package/dist/seed/seed.js +37 -0
- package/dist/seed/seededRows.d.ts +26 -0
- package/dist/seed/seededRows.d.ts.map +1 -0
- package/dist/seed/seededRows.js +20 -0
- package/dist/seed/writeD1.d.ts +43 -0
- package/dist/seed/writeD1.d.ts.map +1 -0
- package/dist/seed/writeD1.js +73 -0
- package/dist/seed/writeKv.d.ts +29 -0
- package/dist/seed/writeKv.d.ts.map +1 -0
- package/dist/seed/writeKv.js +63 -0
- package/dist/semver/semver.d.ts +62 -0
- package/dist/semver/semver.d.ts.map +1 -0
- package/dist/semver/semver.js +137 -0
- package/dist/text/comments.d.ts +62 -0
- package/dist/text/comments.d.ts.map +1 -0
- package/dist/text/comments.js +161 -0
- package/dist/version.generated.d.ts +5 -0
- package/dist/version.generated.d.ts.map +1 -0
- package/dist/version.generated.js +7 -0
- package/dist/worker/health.d.ts +39 -0
- package/dist/worker/health.d.ts.map +1 -0
- package/dist/worker/health.js +41 -0
- package/dist/worker/identity.d.ts +191 -0
- package/dist/worker/identity.d.ts.map +1 -0
- package/dist/worker/identity.js +162 -0
- package/dist/workflow/bindings.d.ts +41 -0
- package/dist/workflow/bindings.d.ts.map +1 -0
- package/dist/workflow/bindings.js +53 -0
- package/dist/workflow/dispatch.d.ts +98 -0
- package/dist/workflow/dispatch.d.ts.map +1 -0
- package/dist/workflow/dispatch.js +148 -0
- package/dist/workflow/dispatchRoute.d.ts +80 -0
- package/dist/workflow/dispatchRoute.d.ts.map +1 -0
- package/dist/workflow/dispatchRoute.js +134 -0
- package/dist/workflow/faults.d.ts +97 -0
- package/dist/workflow/faults.d.ts.map +1 -0
- package/dist/workflow/faults.js +111 -0
- package/dist/workflow/host.d.ts +176 -0
- package/dist/workflow/host.d.ts.map +1 -0
- package/dist/workflow/host.js +116 -0
- package/dist/workflow/hostEntry.d.ts +50 -0
- package/dist/workflow/hostEntry.d.ts.map +1 -0
- package/dist/workflow/hostEntry.js +24 -0
- package/dist/workflow/hostEnv.d.ts +143 -0
- package/dist/workflow/hostEnv.d.ts.map +1 -0
- package/dist/workflow/hostEnv.js +170 -0
- package/dist/workflow/loopback.d.ts +52 -0
- package/dist/workflow/loopback.d.ts.map +1 -0
- package/dist/workflow/loopback.js +76 -0
- package/dist/workflow/naming.d.ts +79 -0
- package/dist/workflow/naming.d.ts.map +1 -0
- package/dist/workflow/naming.js +138 -0
- package/dist/workflow/register.d.ts +19 -0
- package/dist/workflow/register.d.ts.map +1 -0
- package/dist/workflow/register.js +40 -0
- package/dist/workflow/schemas.d.ts +44 -0
- package/dist/workflow/schemas.d.ts.map +1 -0
- package/dist/workflow/schemas.js +43 -0
- package/dist/workflow/spec.d.ts +81 -0
- package/dist/workflow/spec.d.ts.map +1 -0
- package/dist/workflow/spec.js +0 -0
- package/dist/workflow/stepMessage.d.ts +122 -0
- package/dist/workflow/stepMessage.d.ts.map +1 -0
- package/dist/workflow/stepMessage.js +152 -0
- package/package.json +20 -9
- package/src/capability/manifest.ts +21 -0
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const EntitlementKey = z.string().min(1).max(64).regex(/^[a-z][a-z0-9_]*$/, "An entitlement key is lowercase letters, digits, and underscores, starting with a letter.").describe("A logical entitlement key (`pro`, `ads_removed`) — what gating code names. Never a store SKU: many products across many rails grant one key.");
|
|
3
|
+
/**
|
|
4
|
+
* One entitlement the caller holds, as the seam exposes it. This is the resolved read model, not a table:
|
|
5
|
+
* `@pithy-sh/payments` materializes a row per `(holder, entitlement)` and decodes it into this shape.
|
|
6
|
+
*
|
|
7
|
+
* **Who the holder is stays out of this shape, deliberately.** A payments project bills either a person or
|
|
8
|
+
* an organization, and its rows are keyed on that pair — but the seam resolves for *one* caller acting for
|
|
9
|
+
* *one* holder, so the pair is constant across a single `list()` and every element of the result would
|
|
10
|
+
* repeat it. Carrying it here would put a comparison within reach of {@link requireEntitlement} that the
|
|
11
|
+
* gate has no correct way to make (see {@link EntitlementResolver}), and would hand every non-payments
|
|
12
|
+
* provider two fields it cannot fill.
|
|
13
|
+
*
|
|
14
|
+
* `active` and `expiresAt` are both present, and both matter. The flag is an optimization written by
|
|
15
|
+
* the projection; the timestamp is the truth. {@link entitlementGrantsAccess} applies both, so a
|
|
16
|
+
* subscription that lapsed with no notification arriving stops granting access on the read path
|
|
17
|
+
* without waiting for a write.
|
|
18
|
+
*/
|
|
19
|
+
const Entitlement = z.object({
|
|
20
|
+
key: EntitlementKey.describe("The entitlement key this grant is for."),
|
|
21
|
+
active: z.boolean().describe("Whether the granting purchase was in an access-granting state when the projection last wrote this row. An optimization, not the truth — `expiresAt` is rechecked on every read."),
|
|
22
|
+
expiresAt: z.date().nullable().describe("When the grant lapses, or null for a grant that never does (a non-consumable purchase). Evaluated at read time, so a silent lapse revokes access without a write."),
|
|
23
|
+
source: z.string().nullable().describe("Provenance — an opaque reference to whatever currently grants this entitlement (a purchase id, or a support grant). Opaque to core; the provider decides what it means.")
|
|
24
|
+
}).describe("One entitlement the current caller holds, as the core seam exposes it — the resolved read model.");
|
|
25
|
+
/**
|
|
26
|
+
* The read-time rule, in one place: an entitlement grants access when the projection marked it active
|
|
27
|
+
* **and** it has not expired. Applied by {@link requireEntitlement} rather than left to each provider,
|
|
28
|
+
* so the recheck is a property of the seam instead of something a provider must remember.
|
|
29
|
+
*
|
|
30
|
+
* A read never writes. Repairing a stale row is the reconciliation Workflow's job, which keeps the hot
|
|
31
|
+
* path a pure read.
|
|
32
|
+
*/
|
|
33
|
+
function entitlementGrantsAccess(entitlement, now) {
|
|
34
|
+
if (!entitlement.active) return false;
|
|
35
|
+
return entitlement.expiresAt === null || now.getTime() < entitlement.expiresAt.getTime();
|
|
36
|
+
}
|
|
37
|
+
/** Every key in `entitlements` that grants access at `now`, deduped. The set a gate is checked against. */
|
|
38
|
+
function grantedEntitlementKeys(entitlements, now) {
|
|
39
|
+
const granted = /* @__PURE__ */ new Set();
|
|
40
|
+
for (const entitlement of entitlements) if (entitlementGrantsAccess(entitlement, now)) granted.add(entitlement.key);
|
|
41
|
+
return granted;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The default resolver when no provider is composed: hold nothing. Every gate denies, which is the
|
|
45
|
+
* fail-closed default the seam exists to guarantee. `createBackend` seeds it on every request, and a
|
|
46
|
+
* provider's middleware replaces it.
|
|
47
|
+
*/
|
|
48
|
+
const noEntitlementProvider = {
|
|
49
|
+
provider: null,
|
|
50
|
+
list: async () => []
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The audit action a denied entitlement gate records, through the core `emit()` seam. Part of the
|
|
54
|
+
* federated `domain/reason` taxonomy — core owns this one because core owns the gate. A blocked
|
|
55
|
+
* attempt is a first-class audit record (`outcome: "denied"`), not only a 403.
|
|
56
|
+
*/
|
|
57
|
+
const ENTITLEMENT_DENIED_ACTION = "entitlement/denied";
|
|
58
|
+
//#endregion
|
|
59
|
+
export { ENTITLEMENT_DENIED_ACTION, Entitlement, EntitlementKey, entitlementGrantsAccess, grantedEntitlementKeys, noEntitlementProvider };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The source with its comments blanked, so a `// TODO: requireEntitlement("pro")` is not read as a gate.
|
|
3
|
+
*
|
|
4
|
+
* **String-aware, and it has to be.** Doing this with two regexes over raw source looks equivalent and is
|
|
5
|
+
* not: a `/*` inside a string literal opens a comment the scanner never sees closed, and everything up to
|
|
6
|
+
* the next `*/` in the file vanishes. A Worker with `app.get("/assets/*", …)` followed by any doc comment
|
|
7
|
+
* loses whatever sat between them — so a real gate goes unseen and the check reports clean. The same trap
|
|
8
|
+
* in reverse is `https://`, whose `//` is not a comment.
|
|
9
|
+
*
|
|
10
|
+
* So this walks the source once, tracking which of code / string / template / line comment / block comment
|
|
11
|
+
* it is in, and replaces comment characters with spaces rather than removing them — offsets stay put, which
|
|
12
|
+
* keeps anything reported against this text meaningful. A real parser would be the third option and is far
|
|
13
|
+
* more than a two-identifier search needs.
|
|
14
|
+
*/
|
|
15
|
+
export declare function withoutComments(source: string): string;
|
|
16
|
+
/** Whether this source applies an entitlement gate, comments and string literals discounted. */
|
|
17
|
+
export declare function callsGate(source: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The files, of those given, that apply an entitlement gate — sorted, so a finding reads as a set.
|
|
20
|
+
*
|
|
21
|
+
* Takes the sources rather than a directory, which is what keeps this half free of `node:fs`. The CLI
|
|
22
|
+
* hands it a walked `src/`; a test in a Workers-typed program hands it `import.meta.glob(…, "?raw")`,
|
|
23
|
+
* which is the only way that program can read files at all.
|
|
24
|
+
*/
|
|
25
|
+
export declare function gateCallSites(sources: Readonly<Record<string, string>>): string[];
|
|
26
|
+
//# sourceMappingURL=gateScan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateScan.d.ts","sourceRoot":"","sources":["../../src/entitlement/gateScan.ts"],"names":[],"mappings":"AAkCA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAuCtD;AAED,gGAAgG;AAChG,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,EAAE,CAKjF"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//#region src/entitlement/gateScan.ts
|
|
2
|
+
/**
|
|
3
|
+
* Finding an entitlement gate in source text — the pure half of the composition check.
|
|
4
|
+
*
|
|
5
|
+
* `pithy doctor` scans a Worker's own source for calls to the gates in `./require`, because the runtime
|
|
6
|
+
* cannot tell the two failing cases apart: a legitimately unentitled user and a Worker that forgot to
|
|
7
|
+
* compose a provider both produce a 403. Walking a directory needs the filesystem. Deciding whether one
|
|
8
|
+
* string of source gates does not, and the halves are split here so the second can be used on its own.
|
|
9
|
+
*
|
|
10
|
+
* On its own, because an adopter has the same question about their own source with the answer inverted:
|
|
11
|
+
* not *where a gate must exist* but *where one may not*. No control-plane route in the Pithy dashboard may
|
|
12
|
+
* gate on a subscription — a customer's data must not become unreachable over a billing problem of ours —
|
|
13
|
+
* and a gate that is never reached at runtime is precisely the one a runtime test misses. That assertion
|
|
14
|
+
* is a few lines over `import.meta.glob(…, "?raw")`, provided it can import these rules rather than restate
|
|
15
|
+
* them. A restated copy drifts, and drifts quietly: both copies keep passing while they disagree.
|
|
16
|
+
*
|
|
17
|
+
* It lives beside the gates it names rather than in the CLI. A rename in `./require` is then one file's
|
|
18
|
+
* work, core is the package every adopter already depends on, and core's no-`node:` rule is asserted over
|
|
19
|
+
* every module here (`../worker-safety.test.ts`) — which is what makes this importable from a program
|
|
20
|
+
* typed for the Workers runtime at all. The CLI composes it; see `entitlementGap.ts` there.
|
|
21
|
+
*
|
|
22
|
+
* The scan is textual, deliberately. Loading a Worker's routes to inspect them would execute the code
|
|
23
|
+
* under audit, and the question is not subtle enough to need a parser: a call to one of two named helpers.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* A call to either gate. `\b` before the name keeps `myRequireEntitlement(` out, and requiring the open
|
|
27
|
+
* paren keeps a bare import or a re-export out — importing a gate is not applying one, and reporting an
|
|
28
|
+
* unused import would make the check noise rather than signal.
|
|
29
|
+
*/
|
|
30
|
+
const GATE_CALL = /\brequire(?:Any)?Entitlement\s*\(/;
|
|
31
|
+
/**
|
|
32
|
+
* The source with its comments blanked, so a `// TODO: requireEntitlement("pro")` is not read as a gate.
|
|
33
|
+
*
|
|
34
|
+
* **String-aware, and it has to be.** Doing this with two regexes over raw source looks equivalent and is
|
|
35
|
+
* not: a `/*` inside a string literal opens a comment the scanner never sees closed, and everything up to
|
|
36
|
+
* the next `*/` in the file vanishes. A Worker with `app.get("/assets/*", …)` followed by any doc comment
|
|
37
|
+
* loses whatever sat between them — so a real gate goes unseen and the check reports clean. The same trap
|
|
38
|
+
* in reverse is `https://`, whose `//` is not a comment.
|
|
39
|
+
*
|
|
40
|
+
* So this walks the source once, tracking which of code / string / template / line comment / block comment
|
|
41
|
+
* it is in, and replaces comment characters with spaces rather than removing them — offsets stay put, which
|
|
42
|
+
* keeps anything reported against this text meaningful. A real parser would be the third option and is far
|
|
43
|
+
* more than a two-identifier search needs.
|
|
44
|
+
*/
|
|
45
|
+
function withoutComments(source) {
|
|
46
|
+
const out = [];
|
|
47
|
+
let index = 0;
|
|
48
|
+
while (index < source.length) {
|
|
49
|
+
const char = source[index];
|
|
50
|
+
const next = source[index + 1];
|
|
51
|
+
if (char === "/" && next === "*") {
|
|
52
|
+
const close = source.indexOf("*/", index + 2);
|
|
53
|
+
const end = close === -1 ? source.length : close + 2;
|
|
54
|
+
out.push(source.slice(index, end).replace(/[^\n]/g, " "));
|
|
55
|
+
index = end;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (char === "/" && next === "/") {
|
|
59
|
+
const newline = source.indexOf("\n", index);
|
|
60
|
+
const end = newline === -1 ? source.length : newline;
|
|
61
|
+
out.push(" ".repeat(end - index));
|
|
62
|
+
index = end;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
66
|
+
let scan = index + 1;
|
|
67
|
+
while (scan < source.length && source[scan] !== char) {
|
|
68
|
+
if (source[scan] === "\n" && char !== "`") break;
|
|
69
|
+
scan += source[scan] === "\\" ? 2 : 1;
|
|
70
|
+
}
|
|
71
|
+
const end = Math.min(scan + 1, source.length);
|
|
72
|
+
out.push(source.slice(index, end));
|
|
73
|
+
index = end;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
out.push(char);
|
|
77
|
+
index += 1;
|
|
78
|
+
}
|
|
79
|
+
return out.join("");
|
|
80
|
+
}
|
|
81
|
+
/** Whether this source applies an entitlement gate, comments and string literals discounted. */
|
|
82
|
+
function callsGate(source) {
|
|
83
|
+
return GATE_CALL.test(withoutComments(source));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The files, of those given, that apply an entitlement gate — sorted, so a finding reads as a set.
|
|
87
|
+
*
|
|
88
|
+
* Takes the sources rather than a directory, which is what keeps this half free of `node:fs`. The CLI
|
|
89
|
+
* hands it a walked `src/`; a test in a Workers-typed program hands it `import.meta.glob(…, "?raw")`,
|
|
90
|
+
* which is the only way that program can read files at all.
|
|
91
|
+
*/
|
|
92
|
+
function gateCallSites(sources) {
|
|
93
|
+
return Object.entries(sources).filter(([, source]) => callsGate(source)).map(([file]) => file).sort((a, b) => a.localeCompare(b));
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { callsGate, gateCallSites, withoutComments };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from "hono";
|
|
2
|
+
import type { PithyHonoEnv } from "../capability/capability";
|
|
3
|
+
/**
|
|
4
|
+
* Require any one of `keys`. Every key is parsed at construction, so a store SKU where an entitlement
|
|
5
|
+
* key belongs (`com.acme.pro.monthly`) fails on deploy rather than gating on something no projection
|
|
6
|
+
* ever writes. An empty list is refused for the same reason: it reads as "nothing required" but would
|
|
7
|
+
* deny every caller forever.
|
|
8
|
+
*/
|
|
9
|
+
export declare function requireAnyEntitlement(keys: readonly string[]): MiddlewareHandler<PithyHonoEnv>;
|
|
10
|
+
/** Require one entitlement. The common case: one key gates one feature. */
|
|
11
|
+
export declare function requireEntitlement(key: string): MiddlewareHandler<PithyHonoEnv>;
|
|
12
|
+
//# sourceMappingURL=require.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require.d.ts","sourceRoot":"","sources":["../../src/entitlement/require.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAoG7D;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAmF9F;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAE/E"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { InternalError, PithyError, UnauthorizedError, messageOf } from "../error/pithyError.js";
|
|
2
|
+
import { ENTITLEMENT_DENIED_ACTION, EntitlementKey, grantedEntitlementKeys } from "./entitlement.js";
|
|
3
|
+
//#region src/entitlement/require.ts
|
|
4
|
+
/**
|
|
5
|
+
* The entitlement gate — the middleware half of the seam. `requireEntitlement("pro")` is the
|
|
6
|
+
* `verification`-strategy sibling every paid route carries: it says what the caller must *hold*, the
|
|
7
|
+
* way `requireAuth()` says who the caller must *be*, and it belongs on the route line beside it.
|
|
8
|
+
*
|
|
9
|
+
* app.get("/reports", requireAuth(), requireEntitlement("pro"), zValidator(…), handler)
|
|
10
|
+
*
|
|
11
|
+
* It lives in `@pithy-sh/core` rather than in `@pithy-sh/payments` for the same reason `requireAuth()`
|
|
12
|
+
* is copied into each capability instead of imported from `@pithy-sh/auth`: a gate that arrives with a
|
|
13
|
+
* package fails **open** when that package is absent. Depending on the core seam means an uncomposed
|
|
14
|
+
* provider leaves `noEntitlementProvider` on the request, and every gate denies.
|
|
15
|
+
*
|
|
16
|
+
* Two names, not one variadic, because "any of these" and "all of these" are both plausible readings
|
|
17
|
+
* of `requireEntitlement("pro", "team")` and the wrong reading is a security bug rather than a typo.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Why a gate denied — recorded in `detail` and in the audit event, never sent to the client.
|
|
21
|
+
*
|
|
22
|
+
* **Three readings, and keeping them apart is the whole job.** Nothing composed is one. A holder that has
|
|
23
|
+
* bought nothing is the second. A caller acting for **no** holder at all is the third — the ordinary state
|
|
24
|
+
* of somebody signed in with no organization selected, and before #412 it was spelled `payments resolved
|
|
25
|
+
* []`, identically to the second. They want opposite fixes: one is a sale, the other is a subject resolver
|
|
26
|
+
* returning nothing, and an operator reading one sentence had no way to tell which they were looking at.
|
|
27
|
+
*
|
|
28
|
+
* The holder is named only when the resolver reports one, and it is named as {@link EntitlementHolder}'s
|
|
29
|
+
* `label` — display text, never a value this gate compares. A resolver that cannot say omits it, and the
|
|
30
|
+
* sentence is exactly what it was.
|
|
31
|
+
*/
|
|
32
|
+
function denialDetail(keys, provider, held, reading) {
|
|
33
|
+
const required = keys.join(" | ");
|
|
34
|
+
if (provider === null) return `no entitlement provider is composed, so \`${required}\` cannot be held. Add a payments capability to this Worker's pithy.config.ts.`;
|
|
35
|
+
switch (reading.kind) {
|
|
36
|
+
case "nobody": return `${provider} resolved no holder for this caller, so nothing can be held; the route requires \`${required}\`. Under organization billing this is a caller acting for no organization, not an unentitled one.`;
|
|
37
|
+
case "failed": return `${provider} could not resolve a holder for this caller — the read raised, see the preceding log line; the route requires \`${required}\`.`;
|
|
38
|
+
case "known": return `${provider} resolved [${[...held].sort().join(", ")}] for ${reading.holder.label}; the route requires \`${required}\`.`;
|
|
39
|
+
default: return `${provider} resolved [${[...held].sort().join(", ")}]; the route requires \`${required}\`.`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** The gate's denial: one code, one status, whatever the reason. The reason rides in `detail`. */
|
|
43
|
+
function denied(keys, provider, held, reading) {
|
|
44
|
+
return new PithyError({
|
|
45
|
+
code: "payments/entitlement_required",
|
|
46
|
+
status: 403,
|
|
47
|
+
message: "This feature requires an active subscription or purchase.",
|
|
48
|
+
action: "Purchase or restore the product that grants access, then retry.",
|
|
49
|
+
detail: denialDetail(keys, provider, held, reading)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Require any one of `keys`. Every key is parsed at construction, so a store SKU where an entitlement
|
|
54
|
+
* key belongs (`com.acme.pro.monthly`) fails on deploy rather than gating on something no projection
|
|
55
|
+
* ever writes. An empty list is refused for the same reason: it reads as "nothing required" but would
|
|
56
|
+
* deny every caller forever.
|
|
57
|
+
*/
|
|
58
|
+
function requireAnyEntitlement(keys) {
|
|
59
|
+
if (keys.length === 0) throw new InternalError({
|
|
60
|
+
message: "requireAnyEntitlement() needs at least one entitlement key.",
|
|
61
|
+
action: "Name the entitlements that grant access, or drop the gate if the route is not paid."
|
|
62
|
+
});
|
|
63
|
+
const required = keys.map((key) => EntitlementKey.parse(key));
|
|
64
|
+
return async (c, next) => {
|
|
65
|
+
const auth = c.var.auth;
|
|
66
|
+
if (!auth) throw new UnauthorizedError({
|
|
67
|
+
message: "Authentication required.",
|
|
68
|
+
action: "Sign in and retry with a valid session or bearer token."
|
|
69
|
+
});
|
|
70
|
+
const resolver = c.var.entitlements;
|
|
71
|
+
let held;
|
|
72
|
+
try {
|
|
73
|
+
held = grantedEntitlementKeys(await resolver.list(), /* @__PURE__ */ new Date());
|
|
74
|
+
} catch (error) {
|
|
75
|
+
c.var.log.error("entitlement resolution failed", {
|
|
76
|
+
provider: resolver.provider,
|
|
77
|
+
cause: messageOf(error)
|
|
78
|
+
});
|
|
79
|
+
held = /* @__PURE__ */ new Set();
|
|
80
|
+
}
|
|
81
|
+
if (required.some((key) => held.has(key))) {
|
|
82
|
+
await next();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let reading = { kind: "unavailable" };
|
|
86
|
+
if (resolver.holder) try {
|
|
87
|
+
const answered = await resolver.holder();
|
|
88
|
+
reading = answered === void 0 ? { kind: "nobody" } : {
|
|
89
|
+
kind: "known",
|
|
90
|
+
holder: answered
|
|
91
|
+
};
|
|
92
|
+
} catch (error) {
|
|
93
|
+
reading = { kind: "failed" };
|
|
94
|
+
c.var.log.error("entitlement holder resolution failed", {
|
|
95
|
+
provider: resolver.provider,
|
|
96
|
+
cause: messageOf(error)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
await c.var.emit({
|
|
100
|
+
action: ENTITLEMENT_DENIED_ACTION,
|
|
101
|
+
outcome: "denied",
|
|
102
|
+
severity: "info",
|
|
103
|
+
actorType: "user",
|
|
104
|
+
actorId: auth.userId,
|
|
105
|
+
sessionId: auth.sessionId,
|
|
106
|
+
resourceType: "entitlement",
|
|
107
|
+
resourceId: required.join("|"),
|
|
108
|
+
tenant: reading.kind === "known" ? reading.holder.tenant : null,
|
|
109
|
+
metadata: {
|
|
110
|
+
provider: resolver.provider,
|
|
111
|
+
required,
|
|
112
|
+
held: [...held].sort()
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
throw denied(required, resolver.provider, held, reading);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Require one entitlement. The common case: one key gates one feature. */
|
|
119
|
+
function requireEntitlement(key) {
|
|
120
|
+
return requireAnyEntitlement([key]);
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { requireAnyEntitlement, requireEntitlement };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the *ambient* environment says about the composition being assembled — the two questions a
|
|
3
|
+
* capability may ask before it registers anything, and the one reader for both.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a capability can ask this at all
|
|
6
|
+
*
|
|
7
|
+
* A Worker is handed its bindings per request, so `c.env` does not exist when `createBackend` calls a
|
|
8
|
+
* capability's `routes()`. `process.env` does: with `nodejs_compat` and a compatibility date past
|
|
9
|
+
* 2025-04-01 — every Worker `pithy init` scaffolds — workerd populates `process.env` from the script's
|
|
10
|
+
* own `vars` and secrets, and it is populated at module scope, before the first request. Verified
|
|
11
|
+
* against a real `wrangler dev`: at module scope `Object.keys(process.env)` is exactly the declared
|
|
12
|
+
* vars.
|
|
13
|
+
*
|
|
14
|
+
* That is the whole reach of this module, and the limit is the interesting half:
|
|
15
|
+
*
|
|
16
|
+
* - **`ENVIRONMENT` is here**, because `pithy init` stamps it into every environment stanza of every
|
|
17
|
+
* Worker's `wrangler.jsonc` ({@link ENVIRONMENT_VAR}). A composition can therefore know it is `dev`
|
|
18
|
+
* without being told.
|
|
19
|
+
* - **The host's environment is not here.** `CI=true` in the shell that ran `wrangler dev` does *not*
|
|
20
|
+
* appear inside the Worker — workerd replaces `process.env` with the bindings rather than merging.
|
|
21
|
+
* {@link ./ci.ts} carries that consequence and what is done about it.
|
|
22
|
+
*
|
|
23
|
+
* Read at call time, never captured at import: a module-scope snapshot is unstubbable in a test and
|
|
24
|
+
* would freeze the answer for the life of an isolate that outlives a `.dev.vars` edit.
|
|
25
|
+
*/
|
|
26
|
+
/** A process environment as this module reads one — the shape of `process.env`, and nothing assumed of it. */
|
|
27
|
+
export type AmbientEnv = Readonly<Record<string, string | undefined>>;
|
|
28
|
+
/**
|
|
29
|
+
* The ambient environment, or an empty one where there is no `process` at all.
|
|
30
|
+
*
|
|
31
|
+
* Guarded rather than assumed: this module is bundled into Workers, and a runtime without
|
|
32
|
+
* `nodejs_compat` has no `process`. An empty environment answers "nothing was stamped", which is the
|
|
33
|
+
* refusing answer everywhere it is used — a missing `process` must never read as `dev`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function ambientEnv(): AmbientEnv;
|
|
36
|
+
/**
|
|
37
|
+
* Whether an environment variable counts as **set**: any non-blank value, blank is no override.
|
|
38
|
+
*
|
|
39
|
+
* One rule, stated once. It is `PITHY_OFFLINE`'s (#218), and the reason it is not "`=== "true"`" is
|
|
40
|
+
* that runners disagree — `true`, `1`, and an operator's own word all mean the same thing, and a
|
|
41
|
+
* variable someone deliberately blanked (`CI=`) is the one case that means "no".
|
|
42
|
+
*/
|
|
43
|
+
export declare function ambientFlag(env: AmbientEnv, name: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The environment this composition was stamped for — `dev`, `staging`, `prod`, or an adopter's own —
|
|
46
|
+
* and `undefined` where nothing stamped one.
|
|
47
|
+
*
|
|
48
|
+
* **`undefined`, never a defaulted `dev`.** The callers are gates, and a gate that assumes `dev` when
|
|
49
|
+
* it was told nothing opens itself in exactly the deployment whose `wrangler.jsonc` lost the var.
|
|
50
|
+
*/
|
|
51
|
+
export declare function compositionEnvironment(env?: AmbientEnv): string | undefined;
|
|
52
|
+
//# sourceMappingURL=ambient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ambient.d.ts","sourceRoot":"","sources":["../../src/env/ambient.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,8GAA8G;AAC9G,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAEtE;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI,UAAU,CAEvC;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAElE;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,GAAE,UAAyB,GAAG,MAAM,GAAG,SAAS,CAGzF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import "../worker/identity.js";
|
|
2
|
+
//#region src/env/ambient.ts
|
|
3
|
+
/**
|
|
4
|
+
* The ambient environment, or an empty one where there is no `process` at all.
|
|
5
|
+
*
|
|
6
|
+
* Guarded rather than assumed: this module is bundled into Workers, and a runtime without
|
|
7
|
+
* `nodejs_compat` has no `process`. An empty environment answers "nothing was stamped", which is the
|
|
8
|
+
* refusing answer everywhere it is used — a missing `process` must never read as `dev`.
|
|
9
|
+
*/
|
|
10
|
+
function ambientEnv() {
|
|
11
|
+
return globalThis.process?.env ?? {};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Whether an environment variable counts as **set**: any non-blank value, blank is no override.
|
|
15
|
+
*
|
|
16
|
+
* One rule, stated once. It is `PITHY_OFFLINE`'s (#218), and the reason it is not "`=== "true"`" is
|
|
17
|
+
* that runners disagree — `true`, `1`, and an operator's own word all mean the same thing, and a
|
|
18
|
+
* variable someone deliberately blanked (`CI=`) is the one case that means "no".
|
|
19
|
+
*/
|
|
20
|
+
function ambientFlag(env, name) {
|
|
21
|
+
return (env[name] ?? "").trim().length > 0;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The environment this composition was stamped for — `dev`, `staging`, `prod`, or an adopter's own —
|
|
25
|
+
* and `undefined` where nothing stamped one.
|
|
26
|
+
*
|
|
27
|
+
* **`undefined`, never a defaulted `dev`.** The callers are gates, and a gate that assumes `dev` when
|
|
28
|
+
* it was told nothing opens itself in exactly the deployment whose `wrangler.jsonc` lost the var.
|
|
29
|
+
*/
|
|
30
|
+
function compositionEnvironment(env = ambientEnv()) {
|
|
31
|
+
const value = (env["ENVIRONMENT"] ?? "").trim();
|
|
32
|
+
return value.length > 0 ? value : void 0;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { ambientEnv, ambientFlag, compositionEnvironment };
|
package/dist/env/ci.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type AmbientEnv } from "./ambient";
|
|
2
|
+
/**
|
|
3
|
+
* Is this continuous integration? One read, one rule, one place — the convention starts here.
|
|
4
|
+
*
|
|
5
|
+
* Nothing in this repository read `CI` before #243, so this is the declaration rather than a fourth
|
|
6
|
+
* copy of one. It exists because a gate that inlines `!!process.env.CI` at the point it guards is a
|
|
7
|
+
* gate whose meaning is re-decided by whoever writes the next one.
|
|
8
|
+
*
|
|
9
|
+
* ## What counts as set
|
|
10
|
+
*
|
|
11
|
+
* **Any non-blank value.** `CI=true`, `CI=1`, and `CI=woodpecker` are all CI; `CI=` (set but empty,
|
|
12
|
+
* which some runners do) and an absent `CI` are not. That is `PITHY_OFFLINE`'s rule (#218) applied
|
|
13
|
+
* unchanged, rather than a second rule for a second variable — see {@link ambientFlag}.
|
|
14
|
+
*
|
|
15
|
+
* ## Why anything reads it
|
|
16
|
+
*
|
|
17
|
+
* CI is where an unsupervised capability is most reachable and least watched. A `dev` composition is
|
|
18
|
+
* exactly what CI boots — integration suites, packaging checks, `pithy dev` itself — so "not `dev`"
|
|
19
|
+
* does not cover CI, and CI is not production, so neither signal implies the other. Anything gated on
|
|
20
|
+
* both asks twice, in two statements: an `||` folded into one expression is one edit from an `&&`, and
|
|
21
|
+
* the failure mode is a session-minting endpoint that answers.
|
|
22
|
+
*
|
|
23
|
+
* ## Where it can be read, and where it cannot
|
|
24
|
+
*
|
|
25
|
+
* In the CLI, and in anything else running under Node, this is the host's own `CI`. **Inside a Worker
|
|
26
|
+
* it is not**: workerd populates `process.env` from the script's bindings and nothing else, so the
|
|
27
|
+
* shell variable does not cross that boundary (see {@link ./ambient.ts}). A Worker-side gate is
|
|
28
|
+
* therefore only as truthful as what started the Worker — which is why `pithy dev` forwards `CI` into
|
|
29
|
+
* every Worker it launches as a var, and why a gate that depends on this must also carry a second,
|
|
30
|
+
* independent refusal that needs no forwarding.
|
|
31
|
+
*/
|
|
32
|
+
/** The variable every CI runner sets without configuration. The name is not ours to choose. */
|
|
33
|
+
export declare const CI_ENV = "CI";
|
|
34
|
+
/** Whether this process is running under continuous integration. Any non-blank `CI`; blank is not CI. */
|
|
35
|
+
export declare function isContinuousIntegration(env?: AmbientEnv): boolean;
|
|
36
|
+
//# sourceMappingURL=ci.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ci.d.ts","sourceRoot":"","sources":["../../src/env/ci.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAA2B,MAAM,WAAW,CAAC;AAErE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,+FAA+F;AAC/F,eAAO,MAAM,MAAM,OAAO,CAAC;AAE3B,yGAAyG;AACzG,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,UAAyB,GAAG,OAAO,CAE/E"}
|
package/dist/env/ci.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ambientEnv, ambientFlag } from "./ambient.js";
|
|
2
|
+
//#region src/env/ci.ts
|
|
3
|
+
/**
|
|
4
|
+
* Is this continuous integration? One read, one rule, one place — the convention starts here.
|
|
5
|
+
*
|
|
6
|
+
* Nothing in this repository read `CI` before #243, so this is the declaration rather than a fourth
|
|
7
|
+
* copy of one. It exists because a gate that inlines `!!process.env.CI` at the point it guards is a
|
|
8
|
+
* gate whose meaning is re-decided by whoever writes the next one.
|
|
9
|
+
*
|
|
10
|
+
* ## What counts as set
|
|
11
|
+
*
|
|
12
|
+
* **Any non-blank value.** `CI=true`, `CI=1`, and `CI=woodpecker` are all CI; `CI=` (set but empty,
|
|
13
|
+
* which some runners do) and an absent `CI` are not. That is `PITHY_OFFLINE`'s rule (#218) applied
|
|
14
|
+
* unchanged, rather than a second rule for a second variable — see {@link ambientFlag}.
|
|
15
|
+
*
|
|
16
|
+
* ## Why anything reads it
|
|
17
|
+
*
|
|
18
|
+
* CI is where an unsupervised capability is most reachable and least watched. A `dev` composition is
|
|
19
|
+
* exactly what CI boots — integration suites, packaging checks, `pithy dev` itself — so "not `dev`"
|
|
20
|
+
* does not cover CI, and CI is not production, so neither signal implies the other. Anything gated on
|
|
21
|
+
* both asks twice, in two statements: an `||` folded into one expression is one edit from an `&&`, and
|
|
22
|
+
* the failure mode is a session-minting endpoint that answers.
|
|
23
|
+
*
|
|
24
|
+
* ## Where it can be read, and where it cannot
|
|
25
|
+
*
|
|
26
|
+
* In the CLI, and in anything else running under Node, this is the host's own `CI`. **Inside a Worker
|
|
27
|
+
* it is not**: workerd populates `process.env` from the script's bindings and nothing else, so the
|
|
28
|
+
* shell variable does not cross that boundary (see {@link ./ambient.ts}). A Worker-side gate is
|
|
29
|
+
* therefore only as truthful as what started the Worker — which is why `pithy dev` forwards `CI` into
|
|
30
|
+
* every Worker it launches as a var, and why a gate that depends on this must also carry a second,
|
|
31
|
+
* independent refusal that needs no forwarding.
|
|
32
|
+
*/
|
|
33
|
+
/** The variable every CI runner sets without configuration. The name is not ours to choose. */
|
|
34
|
+
const CI_ENV = "CI";
|
|
35
|
+
/** Whether this process is running under continuous integration. Any non-blank `CI`; blank is not CI. */
|
|
36
|
+
function isContinuousIntegration(env = ambientEnv()) {
|
|
37
|
+
return ambientFlag(env, "CI");
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { CI_ENV, isContinuousIntegration };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a member of the dev set publishes its address, and how a sibling looks it up.
|
|
3
|
+
*
|
|
4
|
+
* `pithy dev` pins one port per worker at feature-create and exports `<STEM>_PORT` / `<STEM>_ORIGIN`
|
|
5
|
+
* for every one of them, so a worker reaches its peers at a known address rather than through
|
|
6
|
+
* wrangler's cross-`wrangler dev` service registry (CLAUDE.md §CLI). The stem is the join: the CLI
|
|
7
|
+
* writes the var, and code running *inside* the Worker reads it.
|
|
8
|
+
*
|
|
9
|
+
* **It lives in core because both ends are now real.** It was the CLI's private helper while only the
|
|
10
|
+
* CLI used it; the loopback Workflow dispatcher ({@link ../workflow/loopback.ts}) made the runtime the
|
|
11
|
+
* other end of the same wire, and two copies of a name-derivation rule are two answers the first day
|
|
12
|
+
* one of them learns about a character the other does not.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The env-var stem for a worker or capability: uppercased, every non-alphanumeric run collapsed to a
|
|
16
|
+
* single `_`, leading and trailing separators dropped. `media-cli` → `MEDIA_CLI`.
|
|
17
|
+
*
|
|
18
|
+
* Deterministic, so the name a worker publishes is the name every sibling looks up.
|
|
19
|
+
*/
|
|
20
|
+
export declare function envStem(name: string): string;
|
|
21
|
+
/** The var carrying one member's loopback address — `email` → `EMAIL_ORIGIN`. */
|
|
22
|
+
export declare function originVarName(name: string): string;
|
|
23
|
+
//# sourceMappingURL=stem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stem.d.ts","sourceRoot":"","sources":["../../src/env/stem.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AAEH;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK5C;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD"}
|
package/dist/env/stem.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/env/stem.ts
|
|
2
|
+
/**
|
|
3
|
+
* How a member of the dev set publishes its address, and how a sibling looks it up.
|
|
4
|
+
*
|
|
5
|
+
* `pithy dev` pins one port per worker at feature-create and exports `<STEM>_PORT` / `<STEM>_ORIGIN`
|
|
6
|
+
* for every one of them, so a worker reaches its peers at a known address rather than through
|
|
7
|
+
* wrangler's cross-`wrangler dev` service registry (CLAUDE.md §CLI). The stem is the join: the CLI
|
|
8
|
+
* writes the var, and code running *inside* the Worker reads it.
|
|
9
|
+
*
|
|
10
|
+
* **It lives in core because both ends are now real.** It was the CLI's private helper while only the
|
|
11
|
+
* CLI used it; the loopback Workflow dispatcher ({@link ../workflow/loopback.ts}) made the runtime the
|
|
12
|
+
* other end of the same wire, and two copies of a name-derivation rule are two answers the first day
|
|
13
|
+
* one of them learns about a character the other does not.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The env-var stem for a worker or capability: uppercased, every non-alphanumeric run collapsed to a
|
|
17
|
+
* single `_`, leading and trailing separators dropped. `media-cli` → `MEDIA_CLI`.
|
|
18
|
+
*
|
|
19
|
+
* Deterministic, so the name a worker publishes is the name every sibling looks up.
|
|
20
|
+
*/
|
|
21
|
+
function envStem(name) {
|
|
22
|
+
return name.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
23
|
+
}
|
|
24
|
+
/** The var carrying one member's loopback address — `email` → `EMAIL_ORIGIN`. */
|
|
25
|
+
function originVarName(name) {
|
|
26
|
+
return `${envStem(name)}_ORIGIN`;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { envStem, originVarName };
|