@pithy-sh/core 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/address/address.d.ts +57 -0
- package/dist/address/address.d.ts.map +1 -0
- package/dist/address/address.js +71 -0
- package/dist/audit/auditEvent.d.ts +89 -0
- package/dist/audit/auditEvent.d.ts.map +1 -0
- package/dist/audit/auditEvent.js +69 -0
- package/dist/audit/recorder.d.ts +18 -0
- package/dist/audit/recorder.d.ts.map +1 -0
- package/dist/audit/recorder.js +8 -0
- package/dist/capability/bindings.d.ts +62 -0
- package/dist/capability/bindings.d.ts.map +1 -0
- package/dist/capability/bindings.js +97 -0
- package/dist/capability/capability.d.ts +501 -0
- package/dist/capability/capability.d.ts.map +1 -0
- package/dist/capability/capability.js +16 -0
- package/dist/capability/client.d.ts +70 -0
- package/dist/capability/client.d.ts.map +1 -0
- package/dist/capability/client.js +86 -0
- package/dist/capability/compose.d.ts +25 -0
- package/dist/capability/compose.d.ts.map +1 -0
- package/dist/capability/compose.js +54 -0
- package/dist/capability/composition.d.ts +26 -0
- package/dist/capability/composition.d.ts.map +1 -0
- package/dist/capability/composition.js +84 -0
- package/dist/capability/config.d.ts +7 -0
- package/dist/capability/config.d.ts.map +1 -0
- package/dist/capability/config.js +14 -0
- package/dist/capability/devSecret.d.ts +32 -0
- package/dist/capability/devSecret.d.ts.map +1 -0
- package/dist/capability/devSecret.js +28 -0
- package/dist/capability/manifest.d.ts +298 -0
- package/dist/capability/manifest.d.ts.map +1 -0
- package/dist/capability/manifest.js +356 -0
- package/dist/capability/secretOrigin.d.ts +121 -0
- package/dist/capability/secretOrigin.d.ts.map +1 -0
- package/dist/capability/secretOrigin.js +134 -0
- package/dist/capability/settings.d.ts +119 -0
- package/dist/capability/settings.d.ts.map +1 -0
- package/dist/capability/settings.js +66 -0
- package/dist/capability/validateBindings.d.ts +4 -0
- package/dist/capability/validateBindings.d.ts.map +1 -0
- package/dist/capability/validateBindings.js +34 -0
- package/dist/capability/vanishingKey.d.ts +31 -0
- package/dist/capability/vanishingKey.d.ts.map +1 -0
- package/dist/capability/vanishingKey.js +79 -0
- package/dist/controlPlane/audit/actions.d.ts +70 -0
- package/dist/controlPlane/audit/actions.d.ts.map +1 -0
- package/dist/controlPlane/audit/actions.js +76 -0
- package/dist/controlPlane/capability.d.ts +47 -0
- package/dist/controlPlane/capability.d.ts.map +1 -0
- package/dist/controlPlane/capability.js +138 -0
- package/dist/controlPlane/config/config.d.ts +19 -0
- package/dist/controlPlane/config/config.d.ts.map +1 -0
- package/dist/controlPlane/config/config.js +85 -0
- package/dist/controlPlane/context.d.ts +27 -0
- package/dist/controlPlane/context.d.ts.map +1 -0
- package/dist/controlPlane/context.js +29 -0
- package/dist/controlPlane/data/connection.d.ts +79 -0
- package/dist/controlPlane/data/connection.d.ts.map +1 -0
- package/dist/controlPlane/data/connection.js +55 -0
- package/dist/controlPlane/data/keyLifecycle.d.ts +62 -0
- package/dist/controlPlane/data/keyLifecycle.d.ts.map +1 -0
- package/dist/controlPlane/data/keyLifecycle.js +126 -0
- package/dist/controlPlane/data/replay.d.ts +18 -0
- package/dist/controlPlane/data/replay.d.ts.map +1 -0
- package/dist/controlPlane/data/replay.js +20 -0
- package/dist/controlPlane/data/tables.d.ts +34 -0
- package/dist/controlPlane/data/tables.d.ts.map +1 -0
- package/dist/controlPlane/data/tables.js +37 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts +313 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -0
- package/dist/controlPlane/discovery/adminRoute.js +136 -0
- package/dist/controlPlane/discovery/configuration.d.ts +108 -0
- package/dist/controlPlane/discovery/configuration.d.ts.map +1 -0
- package/dist/controlPlane/discovery/configuration.js +187 -0
- package/dist/controlPlane/discovery/drift.d.ts +64 -0
- package/dist/controlPlane/discovery/drift.d.ts.map +1 -0
- package/dist/controlPlane/discovery/drift.js +58 -0
- package/dist/controlPlane/discovery/health.d.ts +99 -0
- package/dist/controlPlane/discovery/health.d.ts.map +1 -0
- package/dist/controlPlane/discovery/health.js +151 -0
- package/dist/controlPlane/discovery/healthSummary.d.ts +236 -0
- package/dist/controlPlane/discovery/healthSummary.d.ts.map +1 -0
- package/dist/controlPlane/discovery/healthSummary.js +278 -0
- package/dist/controlPlane/error/errors.d.ts +67 -0
- package/dist/controlPlane/error/errors.d.ts.map +1 -0
- package/dist/controlPlane/error/errors.js +79 -0
- package/dist/controlPlane/http/cors.d.ts +112 -0
- package/dist/controlPlane/http/cors.d.ts.map +1 -0
- package/dist/controlPlane/http/cors.js +192 -0
- package/dist/controlPlane/http/guard.d.ts +77 -0
- package/dist/controlPlane/http/guard.d.ts.map +1 -0
- package/dist/controlPlane/http/guard.js +142 -0
- package/dist/controlPlane/http/handlers.d.ts +179 -0
- package/dist/controlPlane/http/handlers.d.ts.map +1 -0
- package/dist/controlPlane/http/handlers.js +245 -0
- package/dist/controlPlane/http/responses.d.ts +99 -0
- package/dist/controlPlane/http/responses.d.ts.map +1 -0
- package/dist/controlPlane/http/responses.js +63 -0
- package/dist/controlPlane/http/routes.d.ts +40 -0
- package/dist/controlPlane/http/routes.d.ts.map +1 -0
- package/dist/controlPlane/http/routes.js +83 -0
- package/dist/controlPlane/http/schemas.d.ts +39 -0
- package/dist/controlPlane/http/schemas.d.ts.map +1 -0
- package/dist/controlPlane/http/schemas.js +31 -0
- package/dist/controlPlane/http/verify.d.ts +84 -0
- package/dist/controlPlane/http/verify.d.ts.map +1 -0
- package/dist/controlPlane/http/verify.js +65 -0
- package/dist/controlPlane/migrations/0001_init.d.ts +51 -0
- package/dist/controlPlane/migrations/0001_init.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0001_init.js +65 -0
- package/dist/controlPlane/replay/d1Guard.d.ts +29 -0
- package/dist/controlPlane/replay/d1Guard.d.ts.map +1 -0
- package/dist/controlPlane/replay/d1Guard.js +39 -0
- package/dist/controlPlane/replay/guard.d.ts +52 -0
- package/dist/controlPlane/replay/guard.d.ts.map +1 -0
- package/dist/controlPlane/replay/guard.js +0 -0
- package/dist/controlPlane/replay/kvGuard.d.ts +99 -0
- package/dist/controlPlane/replay/kvGuard.d.ts.map +1 -0
- package/dist/controlPlane/replay/kvGuard.js +101 -0
- package/dist/controlPlane/scope/scope.d.ts +68 -0
- package/dist/controlPlane/scope/scope.d.ts.map +1 -0
- package/dist/controlPlane/scope/scope.js +63 -0
- package/dist/controlPlane/token/base64url.d.ts +10 -0
- package/dist/controlPlane/token/base64url.d.ts.map +1 -0
- package/dist/controlPlane/token/base64url.js +48 -0
- package/dist/controlPlane/token/claims.d.ts +83 -0
- package/dist/controlPlane/token/claims.d.ts.map +1 -0
- package/dist/controlPlane/token/claims.js +86 -0
- package/dist/controlPlane/token/digest.d.ts +44 -0
- package/dist/controlPlane/token/digest.d.ts.map +1 -0
- package/dist/controlPlane/token/digest.js +56 -0
- package/dist/controlPlane/token/jws.d.ts +52 -0
- package/dist/controlPlane/token/jws.d.ts.map +1 -0
- package/dist/controlPlane/token/jws.js +65 -0
- package/dist/controlPlane/token/mint.d.ts +56 -0
- package/dist/controlPlane/token/mint.d.ts.map +1 -0
- package/dist/controlPlane/token/mint.js +55 -0
- package/dist/controlPlane/wire.d.ts +122 -0
- package/dist/controlPlane/wire.d.ts.map +1 -0
- package/dist/controlPlane/wire.js +123 -0
- package/dist/createBackend.d.ts +58 -0
- package/dist/createBackend.d.ts.map +1 -0
- package/dist/createBackend.js +170 -0
- package/dist/createEntrypoint.d.ts +53 -0
- package/dist/createEntrypoint.d.ts.map +1 -0
- package/dist/createEntrypoint.js +52 -0
- package/dist/data/boundParameters.d.ts +83 -0
- package/dist/data/boundParameters.d.ts.map +1 -0
- package/dist/data/boundParameters.js +162 -0
- package/dist/data/codecs.d.ts +34 -0
- package/dist/data/codecs.d.ts.map +1 -0
- package/dist/data/codecs.js +135 -0
- package/dist/data/cursor.d.ts +81 -0
- package/dist/data/cursor.d.ts.map +1 -0
- package/dist/data/cursor.js +104 -0
- package/dist/data/databases.d.ts +55 -0
- package/dist/data/databases.d.ts.map +1 -0
- package/dist/data/databases.js +32 -0
- package/dist/data/db.d.ts +42 -0
- package/dist/data/db.d.ts.map +1 -0
- package/dist/data/db.js +24 -0
- package/dist/data/withD1Retry.d.ts +91 -0
- package/dist/data/withD1Retry.d.ts.map +1 -0
- package/dist/data/withD1Retry.js +152 -0
- package/dist/entitlement/entitlement.d.ts +118 -0
- package/dist/entitlement/entitlement.d.ts.map +1 -0
- package/dist/entitlement/entitlement.js +59 -0
- package/dist/entitlement/gateScan.d.ts +26 -0
- package/dist/entitlement/gateScan.d.ts.map +1 -0
- package/dist/entitlement/gateScan.js +96 -0
- package/dist/entitlement/require.d.ts +12 -0
- package/dist/entitlement/require.d.ts.map +1 -0
- package/dist/entitlement/require.js +123 -0
- package/dist/env/ambient.d.ts +52 -0
- package/dist/env/ambient.d.ts.map +1 -0
- package/dist/env/ambient.js +35 -0
- package/dist/env/ci.d.ts +36 -0
- package/dist/env/ci.d.ts.map +1 -0
- package/dist/env/ci.js +40 -0
- package/dist/env/stem.d.ts +23 -0
- package/dist/env/stem.d.ts.map +1 -0
- package/dist/env/stem.js +29 -0
- package/dist/error/cause.d.ts +143 -0
- package/dist/error/cause.d.ts.map +1 -0
- package/dist/error/cause.js +192 -0
- package/dist/error/client.d.ts +35 -0
- package/dist/error/client.d.ts.map +1 -0
- package/dist/error/client.js +38 -0
- package/dist/error/extend.d.ts +101 -0
- package/dist/error/extend.d.ts.map +1 -0
- package/dist/error/extend.js +76 -0
- package/dist/error/http.d.ts +1516 -0
- package/dist/error/http.d.ts.map +1 -0
- package/dist/error/http.js +72 -0
- package/dist/error/payload.d.ts +3034 -0
- package/dist/error/payload.d.ts.map +1 -0
- package/dist/error/payload.js +1110 -0
- package/dist/error/pithyError.d.ts +154 -0
- package/dist/error/pithyError.d.ts.map +1 -0
- package/dist/error/pithyError.js +197 -0
- package/dist/error/terminal.d.ts +28 -0
- package/dist/error/terminal.d.ts.map +1 -0
- package/dist/error/terminal.js +33 -0
- package/dist/http/authContext.d.ts +21 -0
- package/dist/http/authContext.d.ts.map +1 -0
- package/dist/http/authContext.js +23 -0
- package/dist/http/routeContract.d.ts +39 -0
- package/dist/http/routeContract.d.ts.map +1 -0
- package/dist/http/routeContract.js +82 -0
- package/dist/http/sameOrigin.d.ts +47 -0
- package/dist/http/sameOrigin.d.ts.map +1 -0
- package/dist/http/sameOrigin.js +24 -0
- package/dist/http/signedWebhook.d.ts +225 -0
- package/dist/http/signedWebhook.d.ts.map +1 -0
- package/dist/http/signedWebhook.js +247 -0
- package/dist/http/validation.d.ts +37 -0
- package/dist/http/validation.d.ts.map +1 -0
- package/dist/http/validation.js +14 -0
- package/dist/http/verification.d.ts +10 -0
- package/dist/http/verification.d.ts.map +1 -0
- package/dist/http/verification.js +17 -0
- package/dist/i18n/acceptLanguage.d.ts +22 -0
- package/dist/i18n/acceptLanguage.d.ts.map +1 -0
- package/dist/i18n/acceptLanguage.js +55 -0
- package/dist/i18n/catalog.d.ts +39 -0
- package/dist/i18n/catalog.d.ts.map +1 -0
- package/dist/i18n/catalog.js +57 -0
- package/dist/i18n/locale.d.ts +74 -0
- package/dist/i18n/locale.d.ts.map +1 -0
- package/dist/i18n/locale.js +109 -0
- package/dist/i18n/localeMarker.d.ts +29 -0
- package/dist/i18n/localeMarker.d.ts.map +1 -0
- package/dist/i18n/localeMarker.js +112 -0
- package/dist/i18n/match.d.ts +26 -0
- package/dist/i18n/match.d.ts.map +1 -0
- package/dist/i18n/match.js +88 -0
- package/dist/i18n/registry.d.ts +25 -0
- package/dist/i18n/registry.d.ts.map +1 -0
- package/dist/i18n/registry.js +51 -0
- package/dist/i18n/translator.d.ts +107 -0
- package/dist/i18n/translator.d.ts.map +1 -0
- package/dist/i18n/translator.js +80 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/kv/kv.d.ts +166 -0
- package/dist/kv/kv.d.ts.map +1 -0
- package/dist/kv/kv.js +261 -0
- package/dist/kv/namespaces.d.ts +58 -0
- package/dist/kv/namespaces.d.ts.map +1 -0
- package/dist/kv/namespaces.js +40 -0
- package/dist/logger/local.d.ts +41 -0
- package/dist/logger/local.d.ts.map +1 -0
- package/dist/logger/local.js +45 -0
- package/dist/logger/logger.d.ts +56 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +83 -0
- package/dist/logger/record.d.ts +919 -0
- package/dist/logger/record.d.ts.map +1 -0
- package/dist/logger/record.js +79 -0
- package/dist/logger/worker.d.ts +74 -0
- package/dist/logger/worker.d.ts.map +1 -0
- package/dist/logger/worker.js +54 -0
- package/dist/migrations/batch.d.ts +10 -0
- package/dist/migrations/batch.d.ts.map +1 -0
- package/dist/migrations/batch.js +193 -0
- package/dist/migrations/bookkeeping.d.ts +24 -0
- package/dist/migrations/bookkeeping.d.ts.map +1 -0
- package/dist/migrations/bookkeeping.js +57 -0
- package/dist/migrations/owner.d.ts +41 -0
- package/dist/migrations/owner.d.ts.map +1 -0
- package/dist/migrations/owner.js +105 -0
- package/dist/migrations/registry.d.ts +36 -0
- package/dist/migrations/registry.d.ts.map +1 -0
- package/dist/migrations/registry.js +70 -0
- package/dist/migrations/runner.d.ts +67 -0
- package/dist/migrations/runner.d.ts.map +1 -0
- package/dist/migrations/runner.js +218 -0
- package/dist/naming/domains.d.ts +108 -0
- package/dist/naming/domains.d.ts.map +1 -0
- package/dist/naming/domains.js +156 -0
- package/dist/naming/environment.d.ts +118 -0
- package/dist/naming/environment.d.ts.map +1 -0
- package/dist/naming/environment.js +190 -0
- package/dist/naming/feature.d.ts +68 -0
- package/dist/naming/feature.d.ts.map +1 -0
- package/dist/naming/feature.js +106 -0
- package/dist/naming/limits.d.ts +229 -0
- package/dist/naming/limits.d.ts.map +1 -0
- package/dist/naming/limits.js +187 -0
- package/dist/naming/provisionScope.d.ts +91 -0
- package/dist/naming/provisionScope.d.ts.map +1 -0
- package/dist/naming/provisionScope.js +65 -0
- package/dist/naming/resource.d.ts +169 -0
- package/dist/naming/resource.d.ts.map +1 -0
- package/dist/naming/resource.js +220 -0
- package/dist/naming/resourceNames.d.ts +83 -0
- package/dist/naming/resourceNames.d.ts.map +1 -0
- package/dist/naming/resourceNames.js +102 -0
- package/dist/naming/segment.d.ts +29 -0
- package/dist/naming/segment.d.ts.map +1 -0
- package/dist/naming/segment.js +31 -0
- package/dist/projection/asRead.d.ts +72 -0
- package/dist/projection/asRead.d.ts.map +1 -0
- package/dist/projection/asRead.js +169 -0
- package/dist/projection/published.d.ts +95 -0
- package/dist/projection/published.d.ts.map +1 -0
- package/dist/projection/published.js +114 -0
- package/dist/schema/describedness.d.ts +55 -0
- package/dist/schema/describedness.d.ts.map +1 -0
- package/dist/schema/describedness.js +180 -0
- package/dist/seed/compose.d.ts +41 -0
- package/dist/seed/compose.d.ts.map +1 -0
- package/dist/seed/compose.js +51 -0
- package/dist/seed/devLogin.d.ts +50 -0
- package/dist/seed/devLogin.d.ts.map +1 -0
- package/dist/seed/devLogin.js +52 -0
- package/dist/seed/exampleIdentities.d.ts +58 -0
- package/dist/seed/exampleIdentities.d.ts.map +1 -0
- package/dist/seed/exampleIdentities.js +26 -0
- package/dist/seed/metadata.d.ts +20 -0
- package/dist/seed/metadata.d.ts.map +1 -0
- package/dist/seed/metadata.js +27 -0
- package/dist/seed/seed.d.ts +264 -0
- package/dist/seed/seed.d.ts.map +1 -0
- package/dist/seed/seed.js +37 -0
- package/dist/seed/seededRows.d.ts +26 -0
- package/dist/seed/seededRows.d.ts.map +1 -0
- package/dist/seed/seededRows.js +20 -0
- package/dist/seed/writeD1.d.ts +43 -0
- package/dist/seed/writeD1.d.ts.map +1 -0
- package/dist/seed/writeD1.js +73 -0
- package/dist/seed/writeKv.d.ts +29 -0
- package/dist/seed/writeKv.d.ts.map +1 -0
- package/dist/seed/writeKv.js +63 -0
- package/dist/semver/semver.d.ts +62 -0
- package/dist/semver/semver.d.ts.map +1 -0
- package/dist/semver/semver.js +137 -0
- package/dist/text/comments.d.ts +62 -0
- package/dist/text/comments.d.ts.map +1 -0
- package/dist/text/comments.js +161 -0
- package/dist/version.generated.d.ts +5 -0
- package/dist/version.generated.d.ts.map +1 -0
- package/dist/version.generated.js +7 -0
- package/dist/worker/health.d.ts +39 -0
- package/dist/worker/health.d.ts.map +1 -0
- package/dist/worker/health.js +41 -0
- package/dist/worker/identity.d.ts +191 -0
- package/dist/worker/identity.d.ts.map +1 -0
- package/dist/worker/identity.js +162 -0
- package/dist/workflow/bindings.d.ts +41 -0
- package/dist/workflow/bindings.d.ts.map +1 -0
- package/dist/workflow/bindings.js +53 -0
- package/dist/workflow/dispatch.d.ts +98 -0
- package/dist/workflow/dispatch.d.ts.map +1 -0
- package/dist/workflow/dispatch.js +148 -0
- package/dist/workflow/dispatchRoute.d.ts +80 -0
- package/dist/workflow/dispatchRoute.d.ts.map +1 -0
- package/dist/workflow/dispatchRoute.js +134 -0
- package/dist/workflow/faults.d.ts +97 -0
- package/dist/workflow/faults.d.ts.map +1 -0
- package/dist/workflow/faults.js +111 -0
- package/dist/workflow/host.d.ts +176 -0
- package/dist/workflow/host.d.ts.map +1 -0
- package/dist/workflow/host.js +116 -0
- package/dist/workflow/hostEntry.d.ts +50 -0
- package/dist/workflow/hostEntry.d.ts.map +1 -0
- package/dist/workflow/hostEntry.js +24 -0
- package/dist/workflow/hostEnv.d.ts +143 -0
- package/dist/workflow/hostEnv.d.ts.map +1 -0
- package/dist/workflow/hostEnv.js +170 -0
- package/dist/workflow/loopback.d.ts +52 -0
- package/dist/workflow/loopback.d.ts.map +1 -0
- package/dist/workflow/loopback.js +76 -0
- package/dist/workflow/naming.d.ts +79 -0
- package/dist/workflow/naming.d.ts.map +1 -0
- package/dist/workflow/naming.js +138 -0
- package/dist/workflow/register.d.ts +19 -0
- package/dist/workflow/register.d.ts.map +1 -0
- package/dist/workflow/register.js +40 -0
- package/dist/workflow/schemas.d.ts +44 -0
- package/dist/workflow/schemas.d.ts.map +1 -0
- package/dist/workflow/schemas.js +43 -0
- package/dist/workflow/spec.d.ts +81 -0
- package/dist/workflow/spec.d.ts.map +1 -0
- package/dist/workflow/spec.js +0 -0
- package/dist/workflow/stepMessage.d.ts +122 -0
- package/dist/workflow/stepMessage.d.ts.map +1 -0
- package/dist/workflow/stepMessage.js +152 -0
- package/package.json +20 -9
- package/src/capability/manifest.ts +21 -0
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/data/cursor.ts
|
|
3
|
+
/**
|
|
4
|
+
* Keyset pagination, once, for every capability that lists an unbounded table.
|
|
5
|
+
*
|
|
6
|
+
* ## Why keyset and never offset
|
|
7
|
+
*
|
|
8
|
+
* `OFFSET` shifts under a client whenever a row is inserted, and every table these paginate — audit
|
|
9
|
+
* events, email jobs, users, ledger transactions — is written to constantly while somebody is reading
|
|
10
|
+
* it. With `OFFSET 25` a row inserted at the head pushes one row from page 1 onto page 2, so a client
|
|
11
|
+
* paging through sees it twice and misses nothing only by luck; a deletion drops a row entirely. A
|
|
12
|
+
* keyset cursor names the last row's sort position, so the next page starts exactly where the previous
|
|
13
|
+
* ended whatever happened in between.
|
|
14
|
+
*
|
|
15
|
+
* ## Why this is in core rather than copied
|
|
16
|
+
*
|
|
17
|
+
* `@pithy-sh/support` had the only implementation, and Part 3 of #89 needs the same thing in four more
|
|
18
|
+
* packages. Four copies of a security-adjacent decode — where the difference between "malformed cursor"
|
|
19
|
+
* and "500" is a caller's ability to probe — is four places for one of them to be wrong. Capabilities
|
|
20
|
+
* depend on core seams, which is exactly what this is.
|
|
21
|
+
*
|
|
22
|
+
* ## The shape of a page
|
|
23
|
+
*
|
|
24
|
+
* A caller asks for `limit`; the query fetches `limit + 1` and, when it gets them, drops the extra and
|
|
25
|
+
* returns a `nextCursor`. That is what makes "is there another page" answerable **without a count
|
|
26
|
+
* query**, which on an audit table is the difference between a page load and a table scan.
|
|
27
|
+
*/
|
|
28
|
+
/** A sensible page when a caller names none. Small enough to render, large enough to be one request. */
|
|
29
|
+
const DEFAULT_PAGE_SIZE = 25;
|
|
30
|
+
/** The most a caller may ask for in one page, whatever they send. An unbounded page is a table scan. */
|
|
31
|
+
const MAX_PAGE_SIZE = 100;
|
|
32
|
+
/**
|
|
33
|
+
* The `limit` a query should actually use, clamped into range.
|
|
34
|
+
*
|
|
35
|
+
* Clamped rather than rejected: a client asking for 1000 wants "as many as you'll give me", and failing
|
|
36
|
+
* the request teaches it nothing a capped page does not. A caller asking for 0 or a negative gets one
|
|
37
|
+
* row rather than an empty page that looks like the end of the list.
|
|
38
|
+
*/
|
|
39
|
+
function pageLimit(requested) {
|
|
40
|
+
return Math.min(Math.max(requested ?? 25, 1), 100);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* An opaque position in a descending `(sort, id)` ordering.
|
|
44
|
+
*
|
|
45
|
+
* `sort` is the primary column's value — a ms-epoch for a date, a number for a monotonic id. `id` is the
|
|
46
|
+
* tiebreak that makes the position exact: without it, two rows sharing a timestamp straddle a page
|
|
47
|
+
* boundary and one of them is skipped or repeated.
|
|
48
|
+
*/
|
|
49
|
+
const PageCursor = z.object({
|
|
50
|
+
sort: z.union([z.number(), z.string()]).describe("The sort column's value on the last row of the previous page — a ms-epoch for a date column, a number for a monotonic id, a string for an ISO-8601 date column. A string and a number compare differently in SQLite, so this carries whichever the column actually stores."),
|
|
51
|
+
id: z.union([z.number(), z.string()]).describe("That row's primary key, the tiebreak that makes the position exact. Two rows sharing a sort value would otherwise straddle a page boundary, and one of them would be skipped or returned twice.")
|
|
52
|
+
}).describe("An opaque position in a descending (sort, id) ordering — where the next page starts.");
|
|
53
|
+
/**
|
|
54
|
+
* Encode a cursor for the wire.
|
|
55
|
+
*
|
|
56
|
+
* Base64url over JSON, and **opaque by intent rather than by obscurity**: the point is that a client
|
|
57
|
+
* cannot construct one by hand and therefore cannot come to depend on its shape, so the ordering can
|
|
58
|
+
* change later without breaking every caller. It is not a secret — it holds a sort value and an id the
|
|
59
|
+
* caller was just given, both of which were in the page it came from.
|
|
60
|
+
*/
|
|
61
|
+
function encodeCursor(cursor) {
|
|
62
|
+
const bytes = new TextEncoder().encode(JSON.stringify(cursor));
|
|
63
|
+
let binary = "";
|
|
64
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
65
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Decode a cursor. Returns undefined for anything malformed.
|
|
69
|
+
*
|
|
70
|
+
* **A bad cursor is a first page, not a 500.** Cursors travel through URLs, get truncated by clients,
|
|
71
|
+
* and outlive deploys; treating a malformed one as an error turns an ordinary client bug into a page
|
|
72
|
+
* that never loads, and hands anyone probing the endpoint a way to tell a parse failure from a
|
|
73
|
+
* not-found. Every failure mode here — bad base64, bad JSON, wrong shape — collapses to the same
|
|
74
|
+
* undefined.
|
|
75
|
+
*/
|
|
76
|
+
function decodeCursor(value) {
|
|
77
|
+
if (!value) return void 0;
|
|
78
|
+
try {
|
|
79
|
+
const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
|
|
80
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
81
|
+
const parsed = PageCursor.safeParse(JSON.parse(new TextDecoder().decode(bytes)));
|
|
82
|
+
return parsed.success ? parsed.data : void 0;
|
|
83
|
+
} catch {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Split an over-fetched result into the page and the cursor that follows it.
|
|
89
|
+
*
|
|
90
|
+
* Pass the rows a query returned when asked for `limit + 1`. When there are more than `limit`, the extra
|
|
91
|
+
* is dropped and `nextCursor` names the last row of the page; otherwise `nextCursor` is null and the
|
|
92
|
+
* client knows it has reached the end without a second request.
|
|
93
|
+
*/
|
|
94
|
+
function toPage(rows, limit, position) {
|
|
95
|
+
const hasMore = rows.length > limit;
|
|
96
|
+
const items = hasMore ? rows.slice(0, limit) : [...rows];
|
|
97
|
+
const last = items[items.length - 1];
|
|
98
|
+
return {
|
|
99
|
+
items,
|
|
100
|
+
nextCursor: hasMore && last !== void 0 ? encodeCursor(position(last)) : null
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, PageCursor, decodeCursor, encodeCursor, pageLimit, toPage };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import type { Migration } from "kysely/migration";
|
|
3
|
+
import type { z } from "zod";
|
|
4
|
+
import type { Capability } from "../capability/capability";
|
|
5
|
+
import { type BindingGroup } from "../capability/compose";
|
|
6
|
+
import { type DatabaseSchema, type SchemaMap } from "./db";
|
|
7
|
+
/**
|
|
8
|
+
* One named D1 database a capability contributes tables to: the `binding` it lives in plus this
|
|
9
|
+
* capability's slice of its schema. A production Worker commonly binds several D1 databases (app
|
|
10
|
+
* data, analytics, …); each is a named database here, and `createBackend` serves one typed `Kysely`
|
|
11
|
+
* per name on `c.var.db.<name>`. Capabilities targeting the same database name merge their slices.
|
|
12
|
+
*/
|
|
13
|
+
export interface DatabaseSpec<Tables extends SchemaMap = SchemaMap> {
|
|
14
|
+
/** The D1 binding name in the Worker env this database lives in. */
|
|
15
|
+
binding: string;
|
|
16
|
+
/** This capability's slice of the database's schema (table name → Zod table schema). */
|
|
17
|
+
tables: Tables;
|
|
18
|
+
/**
|
|
19
|
+
* This capability's migrations for this database, by stable local key (e.g. "0001_init") —
|
|
20
|
+
* co-located with the tables they create, so the database association is declared once. The
|
|
21
|
+
* migration namespace is the capability's name; `pithy migrate` composes every capability's
|
|
22
|
+
* sets through `createMigrationRegistry` and runs them per database.
|
|
23
|
+
*/
|
|
24
|
+
migrations?: Record<string, Migration>;
|
|
25
|
+
/** Sort order within this database relative to other capabilities (core low, app high). Required with `migrations`. */
|
|
26
|
+
migrationOrder?: number;
|
|
27
|
+
}
|
|
28
|
+
/** A capability's databases: database name → {@link DatabaseSpec}. */
|
|
29
|
+
export type DatabaseSpecMap = Record<string, DatabaseSpec>;
|
|
30
|
+
/**
|
|
31
|
+
* The typed database registry exposed on `c.var.db`: each named database becomes its live `Kysely`
|
|
32
|
+
* over that database's merged schema — so `c.var.db.app.selectFrom("…")` and
|
|
33
|
+
* `c.var.db.analytics.selectFrom("…")` are each typed to their own tables.
|
|
34
|
+
*/
|
|
35
|
+
export type DbRegistry<Dbs extends DatabaseSpecMap> = {
|
|
36
|
+
[Name in keyof Dbs]: Dbs[Name] extends DatabaseSpec<infer Tables> ? Kysely<DatabaseSchema<Tables>> : never;
|
|
37
|
+
};
|
|
38
|
+
/** The merged databases: each database name → its binding and the union of every capability's tables. */
|
|
39
|
+
export type MergedDatabases = Record<string, BindingGroup<z.ZodType>>;
|
|
40
|
+
/**
|
|
41
|
+
* Merge every capability's `databases` into one map keyed by database name. Capabilities targeting
|
|
42
|
+
* the same name union their table slices (the project-wide schema, per database); a table claimed
|
|
43
|
+
* twice in one database, or a database name bound to two bindings, throws at assembly.
|
|
44
|
+
*/
|
|
45
|
+
export declare function composeDatabases(capabilities: Capability[]): MergedDatabases;
|
|
46
|
+
/** A registry of live Kysely instances keyed by database name; the per-request value of `c.var.db`. */
|
|
47
|
+
type LiveDbRegistry = Record<string, Kysely<DatabaseSchema<SchemaMap>>>;
|
|
48
|
+
/**
|
|
49
|
+
* Build the per-request database registry from the merged databases and the request's env. Each
|
|
50
|
+
* `Kysely` is constructed lazily on first access from its D1 binding — a request that never touches
|
|
51
|
+
* `c.var.db.analytics` never builds it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildDbRegistry(env: Record<string, unknown>, databases: MergedDatabases): LiveDbRegistry;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=databases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"databases.d.ts","sourceRoot":"","sources":["../../src/data/databases.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,KAAK,YAAY,EAA2C,MAAM,uBAAuB,CAAC;AACnG,OAAO,EAAkB,KAAK,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,MAAM,CAAC;AAE3E;;;;;GAKG;AACH,MAAM,WAAW,YAAY,CAAC,MAAM,SAAS,SAAS,GAAG,SAAS;IAChE,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,wFAAwF;IACxF,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACvC,uHAAuH;IACvH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,sEAAsE;AACtE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AAE3D;;;;GAIG;AACH,MAAM,MAAM,UAAU,CAAC,GAAG,SAAS,eAAe,IAAI;KACnD,IAAI,IAAI,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,YAAY,CAAC,MAAM,MAAM,CAAC,GAAG,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK;CAC3G,CAAC;AAEF,yGAAyG;AACzG,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;AAEtE;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,UAAU,EAAE,GAAG,eAAe,CAO5E;AAED,uGAAuG;AACvG,KAAK,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;AAExE;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,eAAe,GAAG,cAAc,CAaxG"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createDatabase } from "./db.js";
|
|
2
|
+
import { bindingGroupsFrom, composeBindingGroups } from "../capability/compose.js";
|
|
3
|
+
//#region src/data/databases.ts
|
|
4
|
+
/**
|
|
5
|
+
* Merge every capability's `databases` into one map keyed by database name. Capabilities targeting
|
|
6
|
+
* the same name union their table slices (the project-wide schema, per database); a table claimed
|
|
7
|
+
* twice in one database, or a database name bound to two bindings, throws at assembly.
|
|
8
|
+
*/
|
|
9
|
+
function composeDatabases(capabilities) {
|
|
10
|
+
return composeBindingGroups(capabilities, (cap) => bindingGroupsFrom(cap.databases, (db) => db.tables), "database", "table");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Build the per-request database registry from the merged databases and the request's env. Each
|
|
14
|
+
* `Kysely` is constructed lazily on first access from its D1 binding — a request that never touches
|
|
15
|
+
* `c.var.db.analytics` never builds it.
|
|
16
|
+
*/
|
|
17
|
+
function buildDbRegistry(env, databases) {
|
|
18
|
+
const registry = {};
|
|
19
|
+
for (const [name, group] of Object.entries(databases)) {
|
|
20
|
+
let instance;
|
|
21
|
+
Object.defineProperty(registry, name, {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
get() {
|
|
24
|
+
if (!instance) instance = createDatabase(env[group.binding], group.items);
|
|
25
|
+
return instance;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return registry;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { buildDbRegistry, composeDatabases };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { D1Database } from "@cloudflare/workers-types";
|
|
2
|
+
import { type Generated, Kysely } from "kysely";
|
|
3
|
+
import type { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* The Kysely D1 database builder.
|
|
6
|
+
*
|
|
7
|
+
* One master map of table name → Zod table schema is the whole database
|
|
8
|
+
* definition. The database interface Kysely sees is derived from each schema's
|
|
9
|
+
* `z.input` side — the SQLite row shape — never a hand-written row interface.
|
|
10
|
+
* `CamelCasePlugin` is mandatory: tables use snake_case columns, query code
|
|
11
|
+
* uses camelCase, and the plugin bridges the two in both directions.
|
|
12
|
+
*/
|
|
13
|
+
/** A master map of table name → that table's Zod schema. */
|
|
14
|
+
export type SchemaMap = Record<string, z.ZodType>;
|
|
15
|
+
/**
|
|
16
|
+
* Present a numeric `id` to Kysely as `Generated<number>`: optional on insert
|
|
17
|
+
* (D1 assigns it), a number on select. A table without a numeric `id` is left
|
|
18
|
+
* unchanged. Externally-exposed entities use a text id and so are untouched.
|
|
19
|
+
*/
|
|
20
|
+
export type WithGeneratedId<Row> = Row extends {
|
|
21
|
+
id: number;
|
|
22
|
+
} ? Omit<Row, "id"> & {
|
|
23
|
+
id: Generated<number>;
|
|
24
|
+
} : Row;
|
|
25
|
+
/** The Kysely database interface derived from a schema map's `z.input` (row) side. */
|
|
26
|
+
export type DatabaseSchema<M extends SchemaMap> = {
|
|
27
|
+
[K in keyof M]: WithGeneratedId<z.input<M[K]>>;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Build a typed Kysely instance over D1 for `map`. The `map` argument carries
|
|
31
|
+
* the table types — it drives inference of the database interface; Kysely needs
|
|
32
|
+
* no values from it at runtime. `CamelCasePlugin` is always installed.
|
|
33
|
+
*
|
|
34
|
+
* So is `guardBoundParameters`. This is the one seam every Kysely instance in
|
|
35
|
+
* the repository comes from, so it is the one place the bound-parameter ceiling
|
|
36
|
+
* can be stated for all of them — including for query sites that have never
|
|
37
|
+
* heard of it. Five capabilities bound past the cap while the arithmetic to
|
|
38
|
+
* avoid it sat unimported in `boundParameters.ts` (#246, #250); a rule that
|
|
39
|
+
* every call site has to remember is a rule in the wrong place.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createDatabase<M extends SchemaMap>(d1: D1Database, map: M): Kysely<DatabaseSchema<M>>;
|
|
42
|
+
//# sourceMappingURL=db.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/data/db.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEjE,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B;;;;;;;;GAQG;AAEH,4DAA4D;AAC5D,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;AAElD;;;;GAIG;AACH,MAAM,MAAM,eAAe,CAAC,GAAG,IAAI,GAAG,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG;IAAE,EAAE,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;CAAE,GAAG,GAAG,CAAC;AAElH,sFAAsF;AACtF,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,SAAS,IAAI;KAC/C,CAAC,IAAI,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC/C,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,SAAS,EAAE,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAMrG"}
|
package/dist/data/db.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { guardBoundParameters } from "./boundParameters.js";
|
|
2
|
+
import { CamelCasePlugin, Kysely } from "kysely";
|
|
3
|
+
import { D1Dialect } from "kysely-d1";
|
|
4
|
+
//#region src/data/db.ts
|
|
5
|
+
/**
|
|
6
|
+
* Build a typed Kysely instance over D1 for `map`. The `map` argument carries
|
|
7
|
+
* the table types — it drives inference of the database interface; Kysely needs
|
|
8
|
+
* no values from it at runtime. `CamelCasePlugin` is always installed.
|
|
9
|
+
*
|
|
10
|
+
* So is `guardBoundParameters`. This is the one seam every Kysely instance in
|
|
11
|
+
* the repository comes from, so it is the one place the bound-parameter ceiling
|
|
12
|
+
* can be stated for all of them — including for query sites that have never
|
|
13
|
+
* heard of it. Five capabilities bound past the cap while the arithmetic to
|
|
14
|
+
* avoid it sat unimported in `boundParameters.ts` (#246, #250); a rule that
|
|
15
|
+
* every call site has to remember is a rule in the wrong place.
|
|
16
|
+
*/
|
|
17
|
+
function createDatabase(d1, map) {
|
|
18
|
+
return new Kysely({
|
|
19
|
+
dialect: new D1Dialect({ database: guardBoundParameters(d1) }),
|
|
20
|
+
plugins: [new CamelCasePlugin()]
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { createDatabase };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry a D1 write through transient faults. Ported from the CMS `withD1Retry`, adapted to
|
|
3
|
+
* Pithy: no injected logger (core has none), an optional `onRetry` notifier instead, and a
|
|
4
|
+
* documented idempotency guard. The shared helper every capability's D1 writes can wrap; the
|
|
5
|
+
* audit recorder is its first consumer.
|
|
6
|
+
*
|
|
7
|
+
* D1 surfaces a whole family of transient failures by message — a statement that exceeded its
|
|
8
|
+
* timeout, a database briefly locked/busy under contention, a dropped network connection to the
|
|
9
|
+
* backing store, a storage object reset mid-flight, or an opaque internal error. All of these are
|
|
10
|
+
* worth retrying with exponential backoff; a deterministic failure (a constraint violation, a SQL
|
|
11
|
+
* error) is not. The matchers key off the error message because that is all D1 exposes; a
|
|
12
|
+
* non-matching error is re-thrown untouched (this is a passthrough wrapper, never an originator — it
|
|
13
|
+
* does not wrap foreign throws in a `PithyError`; the caller's own layer owns that). By default
|
|
14
|
+
* **every** transient class is retried, so a caller gets robust coverage without enumerating D1's
|
|
15
|
+
* error vocabulary.
|
|
16
|
+
*
|
|
17
|
+
* The idempotency guard is always active and independent of `retryOn`: a `unique-constraint`
|
|
18
|
+
* failure **on a retry** (attempt > 0) means the prior attempt already committed the row before its
|
|
19
|
+
* transport hiccup — the retry is the duplicate, not a real conflict — so the wrapper returns rather
|
|
20
|
+
* than throwing. A unique-constraint never *initiates* a retry (it is not a transient fault), so on
|
|
21
|
+
* the **first** attempt it is a genuine conflict and propagates. This is what makes a broad retry
|
|
22
|
+
* policy safe for a write: pair it with a unique idempotency key (the audit recorder's `eventId`) and
|
|
23
|
+
* a post-commit retry can never double-write.
|
|
24
|
+
*/
|
|
25
|
+
/** A retryable D1 fault class, matched against the error message. */
|
|
26
|
+
export declare const D1RetryableError: {
|
|
27
|
+
/** A statement exceeded its execution timeout (`exceeded timeout`, `storage timeout`, `Timed out`). */
|
|
28
|
+
readonly TIMEOUT: "timeout";
|
|
29
|
+
/** The database was locked or busy under contention (`database is locked/busy`, `SQLITE_BUSY`). */
|
|
30
|
+
readonly DATABASE_BUSY: "database-busy";
|
|
31
|
+
/** The connection to the backing store dropped mid-request (`Network connection lost`, `connection … reset`). */
|
|
32
|
+
readonly NETWORK: "network";
|
|
33
|
+
/** The storage object was reset mid-flight (`reset because its code was updated`, `caused object to be reset`). */
|
|
34
|
+
readonly STORAGE_RESET: "storage-reset";
|
|
35
|
+
/** An opaque D1/storage internal error (`internal error`, `Cannot resolve D1`). */
|
|
36
|
+
readonly INTERNAL: "internal";
|
|
37
|
+
/** A UNIQUE constraint failed — never a retry initiator; drives only the idempotency guard (see module doc). */
|
|
38
|
+
readonly UNIQUE_CONSTRAINT: "unique-constraint";
|
|
39
|
+
};
|
|
40
|
+
export type D1RetryableError = (typeof D1RetryableError)[keyof typeof D1RetryableError];
|
|
41
|
+
/** Every transient (retry-initiating) fault class — the default `retryOn`. Excludes the guard-only `unique-constraint`. */
|
|
42
|
+
export declare const D1_TRANSIENT_ERRORS: readonly D1RetryableError[];
|
|
43
|
+
/** One D1 retry attempt's context, handed to {@link D1RetryOptions.onRetry}. */
|
|
44
|
+
export interface D1RetryInfo {
|
|
45
|
+
/** The fault class that triggered the retry. */
|
|
46
|
+
error: D1RetryableError;
|
|
47
|
+
/** The attempt that just failed (0-based). */
|
|
48
|
+
attempt: number;
|
|
49
|
+
/** The configured maximum number of retries. */
|
|
50
|
+
maxRetries: number;
|
|
51
|
+
/** How long the wrapper will wait before the next attempt, in ms. */
|
|
52
|
+
delayMs: number;
|
|
53
|
+
}
|
|
54
|
+
/** Options for {@link withD1Retry}. All optional; the defaults retry timeouts three times. */
|
|
55
|
+
export interface D1RetryOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Which transient fault classes initiate a retry. Defaults to {@link D1_TRANSIENT_ERRORS} — every
|
|
58
|
+
* transient class. Narrow it only when a caller deliberately wants to ignore some. Listing
|
|
59
|
+
* `unique-constraint` here has no effect — it is never a retry initiator; it only ever drives the
|
|
60
|
+
* always-on idempotency guard (see module doc).
|
|
61
|
+
*/
|
|
62
|
+
retryOn?: readonly D1RetryableError[];
|
|
63
|
+
/** Maximum retries after the initial attempt. Defaults to `3`. */
|
|
64
|
+
maxRetries?: number;
|
|
65
|
+
/** Backoff base delay, in ms. Defaults to `200`. */
|
|
66
|
+
initialDelayMs?: number;
|
|
67
|
+
/** Backoff growth factor. Defaults to `2` (200ms, 400ms, 800ms…). */
|
|
68
|
+
backoffMultiplier?: number;
|
|
69
|
+
/** Notified before each backoff wait — for an optional log line. Never throws into the retry. */
|
|
70
|
+
onRetry?: (info: D1RetryInfo) => void;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The transient D1 fault class an error is, or `null` when D1 did not report one of them.
|
|
74
|
+
*
|
|
75
|
+
* The same matchers this wrapper retries on, exported because a second layer has to ask the same
|
|
76
|
+
* question: a Workflow step decides whether to re-drive a body that raised, and "is this D1 fault
|
|
77
|
+
* transient" must have exactly one answer in this kit (pithy-sh/pithy#338). A step that classified D1
|
|
78
|
+
* for itself would be a second vocabulary — and the one that drifted would be the one nobody read.
|
|
79
|
+
*
|
|
80
|
+
* `unique-constraint` is deliberately not reported: it is the idempotency guard's signal, never a
|
|
81
|
+
* transient fault, so a caller asking "should this be retried" is told no.
|
|
82
|
+
*/
|
|
83
|
+
export declare function d1TransientFault(error: unknown): D1RetryableError | null;
|
|
84
|
+
/**
|
|
85
|
+
* Run `fn`, retrying it through the configured transient D1 faults with exponential backoff.
|
|
86
|
+
* Returns `fn`'s result; on the idempotency-guard path (a unique-constraint on a retry) returns
|
|
87
|
+
* `undefined` cast to `T`, since the prior attempt already produced the effect. A non-retryable
|
|
88
|
+
* error, or exhausted retries, throws the last error untouched.
|
|
89
|
+
*/
|
|
90
|
+
export declare function withD1Retry<T>(fn: () => Promise<T>, options?: D1RetryOptions): Promise<T>;
|
|
91
|
+
//# sourceMappingURL=withD1Retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withD1Retry.d.ts","sourceRoot":"","sources":["../../src/data/withD1Retry.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,qEAAqE;AACrE,eAAO,MAAM,gBAAgB;IAC3B,uGAAuG;aACvG,OAAO,EAAE,SAAS;IAClB,mGAAmG;aACnG,aAAa,EAAE,eAAe;IAC9B,iHAAiH;aACjH,OAAO,EAAE,SAAS;IAClB,mHAAmH;aACnH,aAAa,EAAE,eAAe;IAC9B,mFAAmF;aACnF,QAAQ,EAAE,UAAU;IACpB,gHAAgH;aAChH,iBAAiB,EAAE,mBAAmB;CAC9B,CAAC;AACX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,OAAO,gBAAgB,CAAC,CAAC;AAExF,2HAA2H;AAC3H,eAAO,MAAM,mBAAmB,EAAE,SAAS,gBAAgB,EAM1D,CAAC;AAEF,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,gDAAgD;IAChD,KAAK,EAAE,gBAAgB,CAAC;IACxB,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,8FAA8F;AAC9F,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACtC,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iGAAiG;IACjG,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;CACvC;AA0CD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAExE;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC,CA6B/F"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
//#region src/data/withD1Retry.ts
|
|
2
|
+
/**
|
|
3
|
+
* Retry a D1 write through transient faults. Ported from the CMS `withD1Retry`, adapted to
|
|
4
|
+
* Pithy: no injected logger (core has none), an optional `onRetry` notifier instead, and a
|
|
5
|
+
* documented idempotency guard. The shared helper every capability's D1 writes can wrap; the
|
|
6
|
+
* audit recorder is its first consumer.
|
|
7
|
+
*
|
|
8
|
+
* D1 surfaces a whole family of transient failures by message — a statement that exceeded its
|
|
9
|
+
* timeout, a database briefly locked/busy under contention, a dropped network connection to the
|
|
10
|
+
* backing store, a storage object reset mid-flight, or an opaque internal error. All of these are
|
|
11
|
+
* worth retrying with exponential backoff; a deterministic failure (a constraint violation, a SQL
|
|
12
|
+
* error) is not. The matchers key off the error message because that is all D1 exposes; a
|
|
13
|
+
* non-matching error is re-thrown untouched (this is a passthrough wrapper, never an originator — it
|
|
14
|
+
* does not wrap foreign throws in a `PithyError`; the caller's own layer owns that). By default
|
|
15
|
+
* **every** transient class is retried, so a caller gets robust coverage without enumerating D1's
|
|
16
|
+
* error vocabulary.
|
|
17
|
+
*
|
|
18
|
+
* The idempotency guard is always active and independent of `retryOn`: a `unique-constraint`
|
|
19
|
+
* failure **on a retry** (attempt > 0) means the prior attempt already committed the row before its
|
|
20
|
+
* transport hiccup — the retry is the duplicate, not a real conflict — so the wrapper returns rather
|
|
21
|
+
* than throwing. A unique-constraint never *initiates* a retry (it is not a transient fault), so on
|
|
22
|
+
* the **first** attempt it is a genuine conflict and propagates. This is what makes a broad retry
|
|
23
|
+
* policy safe for a write: pair it with a unique idempotency key (the audit recorder's `eventId`) and
|
|
24
|
+
* a post-commit retry can never double-write.
|
|
25
|
+
*/
|
|
26
|
+
/** A retryable D1 fault class, matched against the error message. */
|
|
27
|
+
const D1RetryableError = {
|
|
28
|
+
/** A statement exceeded its execution timeout (`exceeded timeout`, `storage timeout`, `Timed out`). */
|
|
29
|
+
TIMEOUT: "timeout",
|
|
30
|
+
/** The database was locked or busy under contention (`database is locked/busy`, `SQLITE_BUSY`). */
|
|
31
|
+
DATABASE_BUSY: "database-busy",
|
|
32
|
+
/** The connection to the backing store dropped mid-request (`Network connection lost`, `connection … reset`). */
|
|
33
|
+
NETWORK: "network",
|
|
34
|
+
/** The storage object was reset mid-flight (`reset because its code was updated`, `caused object to be reset`). */
|
|
35
|
+
STORAGE_RESET: "storage-reset",
|
|
36
|
+
/** An opaque D1/storage internal error (`internal error`, `Cannot resolve D1`). */
|
|
37
|
+
INTERNAL: "internal",
|
|
38
|
+
/** A UNIQUE constraint failed — never a retry initiator; drives only the idempotency guard (see module doc). */
|
|
39
|
+
UNIQUE_CONSTRAINT: "unique-constraint"
|
|
40
|
+
};
|
|
41
|
+
/** Every transient (retry-initiating) fault class — the default `retryOn`. Excludes the guard-only `unique-constraint`. */
|
|
42
|
+
const D1_TRANSIENT_ERRORS = [
|
|
43
|
+
D1RetryableError.TIMEOUT,
|
|
44
|
+
D1RetryableError.DATABASE_BUSY,
|
|
45
|
+
D1RetryableError.NETWORK,
|
|
46
|
+
D1RetryableError.STORAGE_RESET,
|
|
47
|
+
D1RetryableError.INTERNAL
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Lowercased message substrings that identify each fault class. Matching is case-insensitive (the
|
|
51
|
+
* message is lowered first) and substring-based, because D1 wraps the same underlying fault in
|
|
52
|
+
* varying envelopes (`D1_ERROR: …`, a bare SQLite message, a Workers runtime message). Each list is
|
|
53
|
+
* the set of signatures Cloudflare is known to surface for that class.
|
|
54
|
+
*/
|
|
55
|
+
const errorMatchers = {
|
|
56
|
+
timeout: [
|
|
57
|
+
"exceeded timeout",
|
|
58
|
+
"storage timeout",
|
|
59
|
+
"timed out"
|
|
60
|
+
],
|
|
61
|
+
"database-busy": [
|
|
62
|
+
"database is locked",
|
|
63
|
+
"database is busy",
|
|
64
|
+
"sqlite_busy"
|
|
65
|
+
],
|
|
66
|
+
network: [
|
|
67
|
+
"network connection lost",
|
|
68
|
+
"connection was reset",
|
|
69
|
+
"connection reset",
|
|
70
|
+
"connection lost"
|
|
71
|
+
],
|
|
72
|
+
"storage-reset": [
|
|
73
|
+
"reset because its code was updated",
|
|
74
|
+
"caused object to be reset",
|
|
75
|
+
"durable object reset"
|
|
76
|
+
],
|
|
77
|
+
internal: [
|
|
78
|
+
"internal error",
|
|
79
|
+
"cannot resolve d1",
|
|
80
|
+
"an unknown error occurred"
|
|
81
|
+
],
|
|
82
|
+
"unique-constraint": ["unique constraint failed"]
|
|
83
|
+
};
|
|
84
|
+
/** Whether a lowercased message matches any signature of `fault`. */
|
|
85
|
+
function matchesFault(message, fault) {
|
|
86
|
+
return errorMatchers[fault].some((signature) => message.includes(signature));
|
|
87
|
+
}
|
|
88
|
+
/** Whether an error is a UNIQUE-constraint failure — the idempotency-guard signal. */
|
|
89
|
+
function isUniqueConstraint(error) {
|
|
90
|
+
return error instanceof Error && matchesFault(error.message.toLowerCase(), D1RetryableError.UNIQUE_CONSTRAINT);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The transient fault class an error matches within `retryOn`, or `null` if none. `unique-constraint`
|
|
94
|
+
* is excluded from initiation — it is handled only by the idempotency guard — so listing it in
|
|
95
|
+
* `retryOn` is a no-op.
|
|
96
|
+
*/
|
|
97
|
+
function matchTransient(error, retryOn) {
|
|
98
|
+
if (!(error instanceof Error)) return null;
|
|
99
|
+
const message = error.message.toLowerCase();
|
|
100
|
+
for (const fault of retryOn) {
|
|
101
|
+
if (fault === D1RetryableError.UNIQUE_CONSTRAINT) continue;
|
|
102
|
+
if (matchesFault(message, fault)) return fault;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The transient D1 fault class an error is, or `null` when D1 did not report one of them.
|
|
108
|
+
*
|
|
109
|
+
* The same matchers this wrapper retries on, exported because a second layer has to ask the same
|
|
110
|
+
* question: a Workflow step decides whether to re-drive a body that raised, and "is this D1 fault
|
|
111
|
+
* transient" must have exactly one answer in this kit (pithy-sh/pithy#338). A step that classified D1
|
|
112
|
+
* for itself would be a second vocabulary — and the one that drifted would be the one nobody read.
|
|
113
|
+
*
|
|
114
|
+
* `unique-constraint` is deliberately not reported: it is the idempotency guard's signal, never a
|
|
115
|
+
* transient fault, so a caller asking "should this be retried" is told no.
|
|
116
|
+
*/
|
|
117
|
+
function d1TransientFault(error) {
|
|
118
|
+
return matchTransient(error, D1_TRANSIENT_ERRORS);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Run `fn`, retrying it through the configured transient D1 faults with exponential backoff.
|
|
122
|
+
* Returns `fn`'s result; on the idempotency-guard path (a unique-constraint on a retry) returns
|
|
123
|
+
* `undefined` cast to `T`, since the prior attempt already produced the effect. A non-retryable
|
|
124
|
+
* error, or exhausted retries, throws the last error untouched.
|
|
125
|
+
*/
|
|
126
|
+
async function withD1Retry(fn, options) {
|
|
127
|
+
const retryOn = options?.retryOn ?? D1_TRANSIENT_ERRORS;
|
|
128
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
129
|
+
const initialDelayMs = options?.initialDelayMs ?? 200;
|
|
130
|
+
const backoffMultiplier = options?.backoffMultiplier ?? 2;
|
|
131
|
+
let lastError;
|
|
132
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
133
|
+
return await fn();
|
|
134
|
+
} catch (error) {
|
|
135
|
+
lastError = error;
|
|
136
|
+
if (attempt > 0 && isUniqueConstraint(error)) return void 0;
|
|
137
|
+
const fault = matchTransient(error, retryOn);
|
|
138
|
+
if (!fault) throw error;
|
|
139
|
+
if (attempt >= maxRetries) break;
|
|
140
|
+
const delayMs = initialDelayMs * backoffMultiplier ** attempt;
|
|
141
|
+
options?.onRetry?.({
|
|
142
|
+
error: fault,
|
|
143
|
+
attempt,
|
|
144
|
+
maxRetries,
|
|
145
|
+
delayMs
|
|
146
|
+
});
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
148
|
+
}
|
|
149
|
+
throw lastError;
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
export { D1RetryableError, D1_TRANSIENT_ERRORS, d1TransientFault, withD1Retry };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const EntitlementKey: z.ZodString;
|
|
3
|
+
export type EntitlementKey = z.output<typeof EntitlementKey>;
|
|
4
|
+
/**
|
|
5
|
+
* One entitlement the caller holds, as the seam exposes it. This is the resolved read model, not a table:
|
|
6
|
+
* `@pithy-sh/payments` materializes a row per `(holder, entitlement)` and decodes it into this shape.
|
|
7
|
+
*
|
|
8
|
+
* **Who the holder is stays out of this shape, deliberately.** A payments project bills either a person or
|
|
9
|
+
* an organization, and its rows are keyed on that pair — but the seam resolves for *one* caller acting for
|
|
10
|
+
* *one* holder, so the pair is constant across a single `list()` and every element of the result would
|
|
11
|
+
* repeat it. Carrying it here would put a comparison within reach of {@link requireEntitlement} that the
|
|
12
|
+
* gate has no correct way to make (see {@link EntitlementResolver}), and would hand every non-payments
|
|
13
|
+
* provider two fields it cannot fill.
|
|
14
|
+
*
|
|
15
|
+
* `active` and `expiresAt` are both present, and both matter. The flag is an optimization written by
|
|
16
|
+
* the projection; the timestamp is the truth. {@link entitlementGrantsAccess} applies both, so a
|
|
17
|
+
* subscription that lapsed with no notification arriving stops granting access on the read path
|
|
18
|
+
* without waiting for a write.
|
|
19
|
+
*/
|
|
20
|
+
export declare const Entitlement: z.ZodObject<{
|
|
21
|
+
key: z.ZodString;
|
|
22
|
+
active: z.ZodBoolean;
|
|
23
|
+
expiresAt: z.ZodNullable<z.ZodDate>;
|
|
24
|
+
source: z.ZodNullable<z.ZodString>;
|
|
25
|
+
}, z.core.$strip>;
|
|
26
|
+
export type Entitlement = z.infer<typeof Entitlement>;
|
|
27
|
+
/**
|
|
28
|
+
* The read-time rule, in one place: an entitlement grants access when the projection marked it active
|
|
29
|
+
* **and** it has not expired. Applied by {@link requireEntitlement} rather than left to each provider,
|
|
30
|
+
* so the recheck is a property of the seam instead of something a provider must remember.
|
|
31
|
+
*
|
|
32
|
+
* A read never writes. Repairing a stale row is the reconciliation Workflow's job, which keeps the hot
|
|
33
|
+
* path a pure read.
|
|
34
|
+
*/
|
|
35
|
+
export declare function entitlementGrantsAccess(entitlement: Entitlement, now: Date): boolean;
|
|
36
|
+
/** Every key in `entitlements` that grants access at `now`, deduped. The set a gate is checked against. */
|
|
37
|
+
export declare function grantedEntitlementKeys(entitlements: readonly Entitlement[], now: Date): Set<string>;
|
|
38
|
+
/**
|
|
39
|
+
* The resolver seam on the request context (`c.var.entitlements`). One method: every entitlement the
|
|
40
|
+
* **current caller** holds. The resolver is built per request by the provider's middleware, so it
|
|
41
|
+
* already knows who is asking — a gate never passes a holder, and so can never gate on the wrong one.
|
|
42
|
+
*
|
|
43
|
+
* **That property is why the seam still takes no argument, now that a provider's holder can be an
|
|
44
|
+
* organization rather than a person.** `@pithy-sh/payments` decides which subject the caller is acting
|
|
45
|
+
* for *before* it constructs the resolver: the resolution happens once, in its middleware, against the
|
|
46
|
+
* adopter's own membership model, and the resolver closes over the answer. So a gate cannot pass the
|
|
47
|
+
* wrong holder because it passes none, and the alternative — a `list(holder)` — would put that choice at
|
|
48
|
+
* every call site, where a route reaching for `c.var.auth.userId` under organization billing looks
|
|
49
|
+
* exactly like correct code and quietly checks a company's plan against one employee.
|
|
50
|
+
*
|
|
51
|
+
* `provider` names the capability answering, or is `null` when none is composed. It exists so a denial
|
|
52
|
+
* can say *which* of the two reasons it was — genuinely unentitled, or nothing wired — in `detail`,
|
|
53
|
+
* where an operator sees it and a client never does.
|
|
54
|
+
*/
|
|
55
|
+
export interface EntitlementResolver {
|
|
56
|
+
/** The capability resolving entitlements (`payments`), or null when none is composed. */
|
|
57
|
+
readonly provider: string | null;
|
|
58
|
+
/** Every entitlement the current caller holds, granting or not. The gate applies the read-time rule. */
|
|
59
|
+
list(): Promise<readonly Entitlement[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Who {@link list} answered for, **for the log and the audit trail only**. Optional: a resolver that
|
|
62
|
+
* cannot say omits it, and every gate behaves exactly as it did before this existed.
|
|
63
|
+
*
|
|
64
|
+
* See {@link EntitlementHolder} for why this is a label and a tenant rather than the holder itself.
|
|
65
|
+
*/
|
|
66
|
+
holder?(): Promise<EntitlementHolder | undefined>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* How a resolver's holder appears in a denial's `detail` and on the denial's audit row — and **nothing a
|
|
70
|
+
* gate may compare**.
|
|
71
|
+
*
|
|
72
|
+
* A provider's holder stopped being a person when `@pithy-sh/payments` learned to bill organizations, and
|
|
73
|
+
* that left a denial unable to say which of two things had happened: a company that has bought nothing,
|
|
74
|
+
* and a caller acting for **no** company at all — the ordinary state of somebody signed in with no
|
|
75
|
+
* organization selected. Both produced `payments resolved []`. They are different problems: one is a sale,
|
|
76
|
+
* the other is a subject resolver returning nothing. The same gap left the denial's audit row with no
|
|
77
|
+
* `tenant`, so a trail could not answer "which of our customers is hitting the paywall" — `actorId` cannot,
|
|
78
|
+
* because one person acts in two tenants.
|
|
79
|
+
*
|
|
80
|
+
* **So why not put the holder on the seam?** Because {@link EntitlementResolver} deliberately takes no
|
|
81
|
+
* holder and returns none: the provider decides who the caller acts for *once*, in its own middleware,
|
|
82
|
+
* before it builds the resolver, and a gate that never receives a holder can never check the wrong one.
|
|
83
|
+
* A field carrying the holder itself would put that choice back within reach — a route comparing a resolved
|
|
84
|
+
* subject against `c.var.auth.userId` looks like careful code and quietly checks a company's plan against
|
|
85
|
+
* one employee.
|
|
86
|
+
*
|
|
87
|
+
* The shape is what keeps that from happening. Neither field is the holder:
|
|
88
|
+
*
|
|
89
|
+
* - **`label`** is display text for one log line. It is not parsed, not matched, and has no format this
|
|
90
|
+
* package defines — a provider renders it however reads best (`@pithy-sh/payments` uses `user:ada` and
|
|
91
|
+
* `organization:acme`, its own encoding).
|
|
92
|
+
* - **`tenant`** is the audit dimension and nothing else, opaque exactly as `AuditEvent.tenant` is. `null`
|
|
93
|
+
* means *the holder is not a tenant* — which is the honest answer under per-person billing, where the
|
|
94
|
+
* holder is the actor and a tenant echoing `actorId` would be a dimension the app does not have.
|
|
95
|
+
*
|
|
96
|
+
* There is nothing here to compare a caller against, because there is no caller-shaped value: `label` is
|
|
97
|
+
* prose and `tenant` is a dimension. `require.test.ts` pins the property directly — the gate's decision is
|
|
98
|
+
* identical for every holder, including none.
|
|
99
|
+
*/
|
|
100
|
+
export interface EntitlementHolder {
|
|
101
|
+
/** Display text naming the holder, for a denial's `detail`. Never parsed, never matched. */
|
|
102
|
+
readonly label: string;
|
|
103
|
+
/** The tenant this was resolved for, or null when the holder is not a tenant. `AuditEvent.tenant`. */
|
|
104
|
+
readonly tenant: string | null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The default resolver when no provider is composed: hold nothing. Every gate denies, which is the
|
|
108
|
+
* fail-closed default the seam exists to guarantee. `createBackend` seeds it on every request, and a
|
|
109
|
+
* provider's middleware replaces it.
|
|
110
|
+
*/
|
|
111
|
+
export declare const noEntitlementProvider: EntitlementResolver;
|
|
112
|
+
/**
|
|
113
|
+
* The audit action a denied entitlement gate records, through the core `emit()` seam. Part of the
|
|
114
|
+
* federated `domain/reason` taxonomy — core owns this one because core owns the gate. A blocked
|
|
115
|
+
* attempt is a first-class audit record (`outcome: "denied"`), not only a 403.
|
|
116
|
+
*/
|
|
117
|
+
export declare const ENTITLEMENT_DENIED_ACTION = "entitlement/denied";
|
|
118
|
+
//# sourceMappingURL=entitlement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entitlement.d.ts","sourceRoot":"","sources":["../../src/entitlement/entitlement.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2BxB,eAAO,MAAM,cAAc,aAUxB,CAAC;AACJ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE7D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW;;;;;iBAqBuF,CAAC;AAChH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,WAAW,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAGpF;AAED,2GAA2G;AAC3G,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,EAAE,GAAG,EAAE,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAMnG;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,mBAAmB;IAClC,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,wGAAwG;IACxG,IAAI,IAAI,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAC;IACxC;;;;;OAKG;IACH,MAAM,CAAC,IAAI,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;CACnD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,WAAW,iBAAiB;IAChC,4FAA4F;IAC5F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,sGAAsG;IACtG,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,EAAE,mBAGnC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,uBAAuB,CAAC"}
|