@pithy-sh/core 0.9.0 → 0.11.0
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/access/access.d.ts +172 -0
- package/dist/access/access.d.ts.map +1 -0
- package/dist/access/access.js +147 -0
- package/dist/access/acting.d.ts +49 -0
- package/dist/access/acting.d.ts.map +1 -0
- package/dist/access/acting.js +21 -0
- package/dist/access/policyCheck.d.ts +33 -0
- package/dist/access/policyCheck.d.ts.map +1 -0
- package/dist/access/policyCheck.js +48 -0
- package/dist/access/usage.d.ts +83 -0
- package/dist/access/usage.d.ts.map +1 -0
- package/dist/access/usage.js +104 -0
- package/dist/access/visibility.d.ts +66 -0
- package/dist/access/visibility.d.ts.map +1 -0
- package/dist/access/visibility.js +86 -0
- package/dist/asyncapi/document.d.ts +46 -0
- package/dist/asyncapi/document.d.ts.map +1 -0
- package/dist/asyncapi/document.js +115 -0
- package/dist/audit/auditEvent.d.ts +5 -0
- package/dist/audit/auditEvent.d.ts.map +1 -1
- package/dist/audit/auditEvent.js +2 -0
- package/dist/capability/bindings.d.ts +47 -2
- package/dist/capability/bindings.d.ts.map +1 -1
- package/dist/capability/bindings.js +117 -7
- package/dist/capability/capability.d.ts +105 -28
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/composition.d.ts.map +1 -1
- package/dist/capability/composition.js +5 -1
- package/dist/capability/manifest.d.ts +2 -1
- package/dist/capability/manifest.d.ts.map +1 -1
- package/dist/capability/manifest.js +1 -1
- package/dist/capability/settings.d.ts +96 -4
- package/dist/capability/settings.d.ts.map +1 -1
- package/dist/capability/settings.js +85 -4
- package/dist/cli/command.d.ts +364 -0
- package/dist/cli/command.d.ts.map +1 -0
- package/dist/cli/command.js +418 -0
- package/dist/cli/commandCatalog.generated.d.ts +2437 -0
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -0
- package/dist/cli/commandCatalog.generated.js +2564 -0
- package/dist/cli/commandFacts.d.ts +164 -0
- package/dist/cli/commandFacts.d.ts.map +1 -0
- package/dist/cli/commandFacts.js +216 -0
- package/dist/controlPlane/capability.d.ts.map +1 -1
- package/dist/controlPlane/capability.js +12 -2
- package/dist/controlPlane/context.d.ts +5 -0
- package/dist/controlPlane/context.d.ts.map +1 -1
- package/dist/controlPlane/context.js +3 -1
- package/dist/controlPlane/data/connection.d.ts +47 -0
- package/dist/controlPlane/data/connection.d.ts.map +1 -1
- package/dist/controlPlane/data/connection.js +55 -1
- package/dist/controlPlane/error/errors.d.ts.map +1 -1
- package/dist/controlPlane/error/errors.js +4 -3
- package/dist/controlPlane/http/cors.d.ts +4 -4
- package/dist/controlPlane/http/cors.js +4 -4
- package/dist/controlPlane/http/guard.d.ts +11 -0
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +28 -2
- package/dist/controlPlane/http/handlers.d.ts +60 -0
- package/dist/controlPlane/http/handlers.d.ts.map +1 -1
- package/dist/controlPlane/http/handlers.js +31 -2
- package/dist/controlPlane/http/readiness.d.ts +33 -0
- package/dist/controlPlane/http/readiness.d.ts.map +1 -0
- package/dist/controlPlane/http/readiness.js +39 -0
- package/dist/controlPlane/http/responses.d.ts +147 -0
- package/dist/controlPlane/http/responses.d.ts.map +1 -1
- package/dist/controlPlane/http/responses.js +61 -4
- package/dist/controlPlane/http/routes.d.ts +2 -1
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +50 -9
- package/dist/controlPlane/http/verify.d.ts.map +1 -1
- package/dist/controlPlane/http/verify.js +2 -1
- package/dist/controlPlane/migrationState/migrationState.d.ts +102 -0
- package/dist/controlPlane/migrationState/migrationState.d.ts.map +1 -0
- package/dist/controlPlane/migrationState/migrationState.js +142 -0
- package/dist/controlPlane/migrations/0003_connection_principal.d.ts +21 -0
- package/dist/controlPlane/migrations/0003_connection_principal.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0003_connection_principal.js +33 -0
- package/dist/controlPlane/scope/scope.d.ts +13 -2
- package/dist/controlPlane/scope/scope.d.ts.map +1 -1
- package/dist/controlPlane/scope/scope.js +14 -3
- package/dist/createBackend.d.ts +7 -0
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +41 -6
- package/dist/createEntrypoint.d.ts.map +1 -1
- package/dist/createEntrypoint.js +5 -3
- package/dist/entitlement/allowance.d.ts +25 -0
- package/dist/entitlement/allowance.d.ts.map +1 -0
- package/dist/entitlement/allowance.js +80 -0
- package/dist/entitlement/entitlement.d.ts +41 -0
- package/dist/entitlement/entitlement.d.ts.map +1 -1
- package/dist/entitlement/meter.d.ts +57 -0
- package/dist/entitlement/meter.d.ts.map +1 -0
- package/dist/entitlement/meter.js +13 -0
- package/dist/entitlement/require.js +1 -1
- package/dist/error/cause.d.ts +16 -116
- package/dist/error/cause.d.ts.map +1 -1
- package/dist/error/cause.js +19 -153
- package/dist/error/http.d.ts +184 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +368 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +126 -4
- package/dist/error/pithyError.d.ts +44 -0
- package/dist/error/pithyError.d.ts.map +1 -1
- package/dist/error/pithyError.js +56 -1
- package/dist/error/unresolved.d.ts +265 -0
- package/dist/error/unresolved.d.ts.map +1 -0
- package/dist/error/unresolved.js +599 -0
- package/dist/error/wire.d.ts +20 -0
- package/dist/error/wire.d.ts.map +1 -0
- package/dist/error/wire.js +19 -0
- package/dist/http/authContext.d.ts +7 -0
- package/dist/http/authContext.d.ts.map +1 -1
- package/dist/http/authContext.js +8 -1
- package/dist/http/oidcWebhook.d.ts +3 -3
- package/dist/http/oidcWebhook.js +3 -3
- package/dist/http/routeContract.js +1 -1
- package/dist/http/routeSpec.d.ts +66 -0
- package/dist/http/routeSpec.d.ts.map +1 -0
- package/dist/http/routeSpec.js +40 -0
- package/dist/http/sameOrigin.d.ts +1 -1
- package/dist/http/sameOrigin.js +1 -1
- package/dist/http/scopeGates.d.ts +20 -0
- package/dist/http/scopeGates.d.ts.map +1 -0
- package/dist/http/scopeGates.js +36 -0
- package/dist/http/signedWebhook.d.ts +3 -3
- package/dist/http/signedWebhook.js +3 -3
- package/dist/http/socket.d.ts +52 -0
- package/dist/http/socket.d.ts.map +1 -0
- package/dist/http/socket.js +2 -0
- package/dist/http/subprotocol.d.ts +29 -0
- package/dist/http/subprotocol.d.ts.map +1 -0
- package/dist/http/subprotocol.js +37 -0
- package/dist/http/validation.d.ts +40 -8
- package/dist/http/validation.d.ts.map +1 -1
- package/dist/http/validation.js +25 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/logger/record.d.ts +107 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/migrations/bookkeeping.d.ts +21 -0
- package/dist/migrations/bookkeeping.d.ts.map +1 -1
- package/dist/migrations/bookkeeping.js +32 -3
- package/dist/migrations/capabilitySets.d.ts +23 -0
- package/dist/migrations/capabilitySets.d.ts.map +1 -0
- package/dist/migrations/capabilitySets.js +74 -0
- package/dist/migrations/groups.d.ts +140 -0
- package/dist/migrations/groups.d.ts.map +1 -0
- package/dist/migrations/groups.js +191 -0
- package/dist/migrations/migrateStep.d.ts +13 -0
- package/dist/migrations/migrateStep.d.ts.map +1 -0
- package/dist/migrations/migrateStep.js +20 -0
- package/dist/migrations/owner.d.ts +7 -0
- package/dist/migrations/owner.d.ts.map +1 -1
- package/dist/migrations/owner.js +11 -7
- package/dist/migrations/registry.d.ts +26 -0
- package/dist/migrations/registry.d.ts.map +1 -1
- package/dist/migrations/registry.js +38 -7
- package/dist/migrations/retained.js +2 -2
- package/dist/migrations/runner.d.ts +76 -5
- package/dist/migrations/runner.d.ts.map +1 -1
- package/dist/migrations/runner.js +130 -19
- package/dist/migrations/state.d.ts +60 -0
- package/dist/migrations/state.d.ts.map +1 -0
- package/dist/migrations/state.js +46 -0
- package/dist/naming/domains.d.ts +17 -0
- package/dist/naming/domains.d.ts.map +1 -1
- package/dist/naming/domains.js +62 -4
- package/dist/naming/environment.d.ts.map +1 -1
- package/dist/naming/environment.js +2 -1
- package/dist/naming/feature.d.ts +27 -5
- package/dist/naming/feature.d.ts.map +1 -1
- package/dist/naming/feature.js +36 -6
- package/dist/naming/limits.d.ts +47 -3
- package/dist/naming/limits.d.ts.map +1 -1
- package/dist/naming/limits.js +47 -3
- package/dist/naming/packageName.d.ts +17 -0
- package/dist/naming/packageName.d.ts.map +1 -0
- package/dist/naming/packageName.js +22 -0
- package/dist/naming/provisionScope.d.ts +6 -0
- package/dist/naming/provisionScope.d.ts.map +1 -1
- package/dist/naming/provisionScope.js +10 -4
- package/dist/naming/resourceNames.d.ts +21 -2
- package/dist/naming/resourceNames.d.ts.map +1 -1
- package/dist/naming/resourceNames.js +21 -3
- package/dist/openapi/document.d.ts +106 -0
- package/dist/openapi/document.d.ts.map +1 -0
- package/dist/openapi/document.js +314 -0
- package/dist/openapi/routeGroups.d.ts +8 -0
- package/dist/openapi/routeGroups.d.ts.map +1 -0
- package/dist/openapi/routeGroups.js +23 -0
- package/dist/seed/writeD1.d.ts +7 -1
- package/dist/seed/writeD1.d.ts.map +1 -1
- package/dist/seed/writeD1.js +9 -3
- package/dist/seed/writeKv.d.ts +2 -0
- package/dist/seed/writeKv.d.ts.map +1 -1
- package/dist/seed/writeKv.js +6 -2
- package/dist/text/comments.js +66 -4
- package/dist/text/literals.d.ts +43 -0
- package/dist/text/literals.d.ts.map +1 -0
- package/dist/text/literals.js +293 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.d.ts.map +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/worker/serviceLoopback.d.ts +36 -0
- package/dist/worker/serviceLoopback.d.ts.map +1 -0
- package/dist/worker/serviceLoopback.js +74 -0
- package/dist/workflow/dispatchRoute.d.ts.map +1 -1
- package/dist/workflow/dispatchRoute.js +12 -6
- package/dist/workflow/faults.d.ts +12 -1
- package/dist/workflow/faults.d.ts.map +1 -1
- package/dist/workflow/faults.js +24 -18
- package/dist/workflow/hostEnv.d.ts +1 -0
- package/dist/workflow/hostEnv.d.ts.map +1 -1
- package/dist/workflow/hostEnv.js +4 -2
- package/dist/workflow/loopback.d.ts.map +1 -1
- package/dist/workflow/loopback.js +2 -1
- package/package.json +4 -4
- package/src/access/access.ts +298 -0
- package/src/access/acting.ts +59 -0
- package/src/access/policyCheck.ts +77 -0
- package/src/access/usage.ts +141 -0
- package/src/access/visibility.ts +109 -0
- package/src/asyncapi/document.ts +164 -0
- package/src/audit/auditEvent.ts +4 -0
- package/src/capability/bindings.ts +159 -13
- package/src/capability/capability.ts +106 -29
- package/src/capability/composition.ts +2 -1
- package/src/capability/settings.ts +165 -4
- package/src/cli/command.ts +865 -0
- package/src/cli/commandCatalog.generated.ts +2723 -0
- package/src/cli/commandFacts.ts +220 -0
- package/src/controlPlane/capability.ts +25 -1
- package/src/controlPlane/context.ts +4 -0
- package/src/controlPlane/data/connection.ts +90 -0
- package/src/controlPlane/error/errors.ts +10 -3
- package/src/controlPlane/http/cors.ts +4 -4
- package/src/controlPlane/http/guard.ts +35 -2
- package/src/controlPlane/http/handlers.ts +41 -0
- package/src/controlPlane/http/readiness.ts +50 -0
- package/src/controlPlane/http/responses.ts +140 -2
- package/src/controlPlane/http/routes.ts +82 -10
- package/src/controlPlane/http/verify.ts +2 -0
- package/src/controlPlane/migrationState/migrationState.ts +200 -0
- package/src/controlPlane/migrations/0003_connection_principal.ts +37 -0
- package/src/controlPlane/scope/scope.ts +14 -2
- package/src/createBackend.ts +59 -8
- package/src/createEntrypoint.ts +9 -3
- package/src/entitlement/allowance.ts +85 -0
- package/src/entitlement/entitlement.ts +38 -0
- package/src/entitlement/meter.ts +69 -0
- package/src/entitlement/require.ts +1 -1
- package/src/error/cause.ts +18 -181
- package/src/error/payload.ts +270 -4
- package/src/error/pithyError.ts +76 -0
- package/src/error/unresolved.ts +853 -0
- package/src/error/wire.ts +40 -0
- package/src/http/authContext.ts +13 -1
- package/src/http/oidcWebhook.ts +3 -3
- package/src/http/routeContract.ts +1 -1
- package/src/http/routeSpec.ts +81 -0
- package/src/http/sameOrigin.ts +1 -1
- package/src/http/scopeGates.ts +34 -0
- package/src/http/signedWebhook.ts +3 -3
- package/src/http/socket.ts +55 -0
- package/src/http/subprotocol.ts +47 -0
- package/src/http/validation.ts +50 -8
- package/src/index.ts +1 -0
- package/src/migrations/bookkeeping.ts +39 -2
- package/src/migrations/capabilitySets.ts +87 -0
- package/src/migrations/groups.ts +295 -0
- package/src/migrations/migrateStep.ts +19 -0
- package/src/migrations/owner.ts +10 -6
- package/src/migrations/registry.ts +46 -2
- package/src/migrations/retained.ts +2 -2
- package/src/migrations/runner.ts +214 -22
- package/src/migrations/state.ts +94 -0
- package/src/naming/domains.ts +75 -16
- package/src/naming/environment.ts +2 -1
- package/src/naming/feature.ts +46 -7
- package/src/naming/limits.ts +35 -3
- package/src/naming/packageName.ts +20 -0
- package/src/naming/provisionScope.ts +21 -2
- package/src/naming/resourceNames.ts +52 -5
- package/src/openapi/document.ts +412 -0
- package/src/openapi/routeGroups.ts +25 -0
- package/src/seed/writeD1.ts +13 -4
- package/src/seed/writeKv.ts +6 -2
- package/src/text/comments.ts +56 -4
- package/src/text/literals.ts +294 -0
- package/src/version.generated.ts +1 -1
- package/src/worker/serviceLoopback.ts +105 -0
- package/src/workflow/dispatchRoute.ts +13 -6
- package/src/workflow/faults.ts +56 -27
- package/src/workflow/hostEnv.ts +9 -1
- package/src/workflow/loopback.ts +2 -1
package/dist/createBackend.js
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { noActingOrganization } from "./access/acting.js";
|
|
3
4
|
import { noopEmit } from "./audit/recorder.js";
|
|
4
5
|
import { ValidationError } from "./error/pithyError.js";
|
|
6
|
+
import { pithyCommandOf } from "./cli/command.js";
|
|
5
7
|
import { BindingSpec } from "./capability/bindings.js";
|
|
6
8
|
import { recordComposition } from "./capability/composition.js";
|
|
7
9
|
import { validateBindings } from "./capability/validateBindings.js";
|
|
8
10
|
import { workerVersion } from "./worker/identity.js";
|
|
11
|
+
import { describeRoute } from "./http/routeSpec.js";
|
|
12
|
+
import { buildDbRegistry, composeDatabases } from "./data/databases.js";
|
|
9
13
|
import { isControlPlaneCapability } from "./controlPlane/capability.js";
|
|
10
14
|
import { allowedOriginSet, corsSurface, registerControlPlaneCors } from "./controlPlane/http/cors.js";
|
|
11
|
-
import {
|
|
15
|
+
import { registerReadinessGuards } from "./controlPlane/http/readiness.js";
|
|
12
16
|
import { noEntitlementProvider } from "./entitlement/entitlement.js";
|
|
13
17
|
import { bakedTranslator } from "./i18n/translator.js";
|
|
14
18
|
import { catalogFor, composeMessages } from "./i18n/registry.js";
|
|
15
19
|
import { buildKvRegistry, composeKv } from "./kv/namespaces.js";
|
|
16
20
|
import { bindRequestContext, createWorkerLogger } from "./logger/worker.js";
|
|
17
21
|
import { loggingErrorHandler } from "./logger/errorHandler.js";
|
|
22
|
+
import { recordRouteGroups } from "./openapi/routeGroups.js";
|
|
18
23
|
import { HEALTH_PATH } from "./worker/health.js";
|
|
19
24
|
import { registeredWorkflowBinding } from "./workflow/bindings.js";
|
|
20
25
|
import { buildWorkflowDispatcher } from "./workflow/dispatch.js";
|
|
21
26
|
import { composeWorkflows } from "./workflow/register.js";
|
|
22
27
|
import { Hono } from "hono";
|
|
28
|
+
import { z } from "zod";
|
|
23
29
|
//#region src/createBackend.ts
|
|
24
30
|
/**
|
|
25
31
|
* Collapse duplicate bindings (same `type:name`) — capability and derived specs overlap. A binding
|
|
@@ -55,6 +61,11 @@ function versionOf(env) {
|
|
|
55
61
|
function envNameOf(env, fallback) {
|
|
56
62
|
return typeof env.ENVIRONMENT === "string" && env.ENVIRONMENT.length > 0 ? env.ENVIRONMENT : fallback ?? "unknown";
|
|
57
63
|
}
|
|
64
|
+
/** What `GET /health` answers. */
|
|
65
|
+
const HealthBody = z.object({
|
|
66
|
+
status: z.literal("ok").describe("Always `ok`: an answer at all is the liveness signal."),
|
|
67
|
+
version: z.string().nullable().describe("The Cloudflare version id of the build answering, or null where the Worker cannot say.")
|
|
68
|
+
}).describe("Liveness, and which build is answering.");
|
|
58
69
|
/**
|
|
59
70
|
* Assemble capabilities into a deployable Hono app — a valid Worker `fetch` handler.
|
|
60
71
|
*
|
|
@@ -77,7 +88,10 @@ function createBackend(options) {
|
|
|
77
88
|
const present = new Set(all.map((cap) => cap.name));
|
|
78
89
|
for (const cap of all) for (const dep of cap.dependsOn ?? []) if (!present.has(dep)) throw new ValidationError({
|
|
79
90
|
message: `Capability "${cap.name}" requires the "${dep}" capability, which is not composed.`,
|
|
80
|
-
action: `Add ${dep}() to createBackend's capabilities (run
|
|
91
|
+
action: `Add ${dep}() to createBackend's capabilities (run \`${pithyCommandOf("add", {
|
|
92
|
+
args: [dep],
|
|
93
|
+
flags: { "--with-prerequisites": true }
|
|
94
|
+
}, { runtime: true })}\`).`
|
|
81
95
|
});
|
|
82
96
|
for (const cap of all) cap.compose?.({ capabilities: all });
|
|
83
97
|
recordComposition(all);
|
|
@@ -101,10 +115,11 @@ function createBackend(options) {
|
|
|
101
115
|
const app = new Hono();
|
|
102
116
|
app.onError(loggingErrorHandler);
|
|
103
117
|
const baseLogger = options.logger ?? createWorkerLogger();
|
|
118
|
+
const adminSurface = corsSurface(all);
|
|
104
119
|
const controlPlane = all.find(isControlPlaneCapability);
|
|
105
120
|
if (controlPlane) {
|
|
106
121
|
const cors = controlPlane.controlPlaneConfig;
|
|
107
|
-
registerControlPlaneCors(app,
|
|
122
|
+
registerControlPlaneCors(app, adminSurface, allowedOriginSet(cors), cors.corsMaxAgeSeconds);
|
|
108
123
|
}
|
|
109
124
|
let validated = false;
|
|
110
125
|
app.use("*", async (c, next) => {
|
|
@@ -114,6 +129,7 @@ function createBackend(options) {
|
|
|
114
129
|
validated = true;
|
|
115
130
|
}
|
|
116
131
|
if (c.get("auth") === void 0) c.set("auth", null);
|
|
132
|
+
if (c.get("actingOrganization") === void 0) c.set("actingOrganization", noActingOrganization);
|
|
117
133
|
if (c.get("locale") === void 0) c.set("locale", null);
|
|
118
134
|
if (c.get("t") === void 0) c.set("t", defaultTranslator);
|
|
119
135
|
if (c.get("controlPlane") === void 0) c.set("controlPlane", null);
|
|
@@ -161,13 +177,32 @@ function createBackend(options) {
|
|
|
161
177
|
* before `version_metadata` was declared reports "I cannot tell you", and `deploy` reports the check
|
|
162
178
|
* as inconclusive instead of failing it.
|
|
163
179
|
*/
|
|
164
|
-
|
|
180
|
+
const groups = [];
|
|
181
|
+
const healthStart = app.routes.length;
|
|
182
|
+
app.get(HEALTH_PATH, describeRoute({
|
|
183
|
+
summary: "Report liveness, and which build is answering.",
|
|
184
|
+
verification: "public",
|
|
185
|
+
responses: { 200: HealthBody }
|
|
186
|
+
}), (c) => c.json({
|
|
165
187
|
status: "ok",
|
|
166
188
|
version: workerVersion(c.env)
|
|
167
189
|
}));
|
|
190
|
+
groups.push({
|
|
191
|
+
tag: "core",
|
|
192
|
+
routes: app.routes.slice(healthStart)
|
|
193
|
+
});
|
|
168
194
|
for (const cap of all) for (const middleware of cap.middleware ?? []) middleware(app);
|
|
169
|
-
|
|
195
|
+
registerReadinessGuards(app, adminSurface, all.flatMap((cap) => cap.readiness ?? []));
|
|
196
|
+
for (const cap of all) {
|
|
197
|
+
const start = app.routes.length;
|
|
198
|
+
cap.routes?.(app);
|
|
199
|
+
groups.push({
|
|
200
|
+
tag: cap.name,
|
|
201
|
+
routes: app.routes.slice(start)
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
recordRouteGroups(app, groups);
|
|
170
205
|
return app;
|
|
171
206
|
}
|
|
172
207
|
//#endregion
|
|
173
|
-
export { createBackend };
|
|
208
|
+
export { HealthBody, createBackend };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createEntrypoint.d.ts","sourceRoot":"","sources":["../src/createEntrypoint.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAC3F,OAAO,KAAK,EAAE,UAAU,EAA0B,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAAE,KAAK,oBAAoB,EAAiB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"createEntrypoint.d.ts","sourceRoot":"","sources":["../src/createEntrypoint.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAC3F,OAAO,KAAK,EAAE,UAAU,EAA0B,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAAE,KAAK,oBAAoB,EAAiB,MAAM,iBAAiB,CAAC;AAK3E;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/G;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjH;;;;;;;;;;;;;;;;;;;OAmBG;IACH,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzG;AAkBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,EACxC,KAAK,CAAC,GAAG,SAAS,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,EACrF,OAAO,EAAE,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,eAAe,CAyD3D"}
|
package/dist/createEntrypoint.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { triggerWorkflow } from "./workflow/dispatch.js";
|
|
4
4
|
import { composeWorkflows, scheduledWorkflows } from "./workflow/register.js";
|
|
5
5
|
import { createBackend } from "./createBackend.js";
|
|
6
|
+
import { withLocalServices } from "./worker/serviceLoopback.js";
|
|
6
7
|
//#region src/createEntrypoint.ts
|
|
7
8
|
/**
|
|
8
9
|
* The raw MIME stream of a `ForwardableEmailMessage` is **single-use** — once one handler reads it,
|
|
@@ -31,17 +32,18 @@ function createEntrypoint(options) {
|
|
|
31
32
|
for (const cap of all) cap.boot?.({ capabilities: all });
|
|
32
33
|
const registry = composeWorkflows(all);
|
|
33
34
|
const scheduled = scheduledWorkflows(registry);
|
|
34
|
-
const entrypoint = { fetch: (request, env, ctx) => app.fetch(request, env, ctx) };
|
|
35
|
+
const entrypoint = { fetch: (request, env, ctx) => app.fetch(request, withLocalServices(env), ctx) };
|
|
35
36
|
if (scheduled.length > 0) entrypoint.scheduled = async (_controller, env) => {
|
|
36
37
|
const failures = [];
|
|
37
38
|
for (const entry of scheduled) try {
|
|
38
|
-
await triggerWorkflow(env, registry, entry.key, {});
|
|
39
|
+
await triggerWorkflow(withLocalServices(env), registry, entry.key, {});
|
|
39
40
|
} catch (error) {
|
|
40
41
|
failures.push(error);
|
|
41
42
|
}
|
|
42
43
|
if (failures[0] !== void 0) throw failures[0];
|
|
43
44
|
};
|
|
44
|
-
if (handlers.length > 0) entrypoint.email = async (message,
|
|
45
|
+
if (handlers.length > 0) entrypoint.email = async (message, given, ctx) => {
|
|
46
|
+
const env = withLocalServices(given);
|
|
45
47
|
if (handlers.length === 1) {
|
|
46
48
|
await handlers[0]?.(message, env, ctx);
|
|
47
49
|
return;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { Context } from "hono";
|
|
4
|
+
import type { PithyHonoEnv } from "../capability/capability";
|
|
5
|
+
import type { MeterLimit } from "./entitlement";
|
|
6
|
+
/** How many of `name` the caller's holder may have: a whole number, or `null` for unlimited. */
|
|
7
|
+
export declare function quota(c: Context<PithyHonoEnv>, name: string): Promise<number | null>;
|
|
8
|
+
/** Whether `name` is on for the caller's holder. Always a boolean. */
|
|
9
|
+
export declare function feature(c: Context<PithyHonoEnv>, name: string): Promise<boolean>;
|
|
10
|
+
/**
|
|
11
|
+
* The ceiling a capability enforces on a meter it counts itself, for the current caller.
|
|
12
|
+
*
|
|
13
|
+
* **No provider, or one that binds nothing to this meter, is `{ enforced: false }`** — and the capability
|
|
14
|
+
* keeps its own configured ceiling. Enforcement is opt-in per meter because a cap that arrives by surprise
|
|
15
|
+
* breaks an adopter who was already over it.
|
|
16
|
+
*
|
|
17
|
+
* **A provider that throws refuses, and logs.** Storage already says which way round to err: over-counting
|
|
18
|
+
* briefly and reclaiming is the right way round, and under-counting has no recovery. Treating a failed read
|
|
19
|
+
* as unenforced would hand a customer an unbounded allowance for as long as the read kept failing. The cost
|
|
20
|
+
* is real and is stated rather than discovered: our bug becomes a refused write for a paying customer.
|
|
21
|
+
* Enforcement being opt-in is what bounds it — an adopter who would rather over-serve leaves the meter
|
|
22
|
+
* advisory.
|
|
23
|
+
*/
|
|
24
|
+
export declare function meterLimit(c: Context<PithyHonoEnv>, meter: string): Promise<MeterLimit>;
|
|
25
|
+
//# sourceMappingURL=allowance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowance.d.ts","sourceRoot":"","sources":["../../src/entitlement/allowance.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAqChD,gGAAgG;AAChG,wBAAsB,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAI1F;AAED,sEAAsE;AACtE,wBAAsB,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAItF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAa7F"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { InternalError, messageOf } from "../error/pithyError.js";
|
|
4
|
+
//#region src/entitlement/allowance.ts
|
|
5
|
+
/**
|
|
6
|
+
* The allowance readers — how much the current caller may have, and whether a switch is on.
|
|
7
|
+
*
|
|
8
|
+
* const seats = await quota(c, "seats"); // number, or null for unlimited
|
|
9
|
+
* if (seats !== null && (await countMembers(orgId)) >= seats) throw new SeatLimitReached(…);
|
|
10
|
+
* if (await feature(c, "auditExport")) { … } // boolean, never null
|
|
11
|
+
*
|
|
12
|
+
* **Neither takes an entitlement key, and that is the point.** A route does not know which tier the
|
|
13
|
+
* caller's holder is on, and during an upgrade overlap it holds two. The provider resolves across every
|
|
14
|
+
* key the holder has and answers the higher, so an adopter compares numbers and never ranks tiers.
|
|
15
|
+
*
|
|
16
|
+
* **Read, never enforced.** A quota denies nothing on its own and a feature set to `false` denies
|
|
17
|
+
* nothing either — `requireEntitlement` still gates on the key alone. What a number means for a request
|
|
18
|
+
* is the adopter's code to decide, because only the adopter can count what it owns.
|
|
19
|
+
*
|
|
20
|
+
* **They live in core for the reason `requireEntitlement` does.** A reader that arrived with
|
|
21
|
+
* `@pithy-sh/payments` would be absent when payments is, and a route reading an allowance must then fail
|
|
22
|
+
* rather than read something. So with no provider composed — or one that declares no allowances — both
|
|
23
|
+
* throw `core/internal` naming the gap. Never `0`, which is a paywall shut for a reason nobody can see;
|
|
24
|
+
* never unlimited, which is a paywall that does not exist.
|
|
25
|
+
*/
|
|
26
|
+
/** The refusal when nothing on the request can answer an allowance. The adopter's wiring, so `core/internal`. */
|
|
27
|
+
function noProvider(c, kind, name) {
|
|
28
|
+
const provider = c.var.entitlements?.provider ?? null;
|
|
29
|
+
return new InternalError({
|
|
30
|
+
message: `No ${kind} "${name}" can be resolved here.`,
|
|
31
|
+
action: "Compose a payments capability that declares its entitlements in this Worker's pithy.config.ts.",
|
|
32
|
+
detail: provider === null ? `no entitlement provider is composed, so ${kind}("${name}") has nothing to read.` : `${provider} resolves entitlements and declares no allowances, so ${kind}("${name}") has nothing to read.`
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** How many of `name` the caller's holder may have: a whole number, or `null` for unlimited. */
|
|
36
|
+
async function quota(c, name) {
|
|
37
|
+
const resolver = c.var.entitlements;
|
|
38
|
+
if (!resolver?.quota) throw noProvider(c, "quota", name);
|
|
39
|
+
return resolver.quota(name);
|
|
40
|
+
}
|
|
41
|
+
/** Whether `name` is on for the caller's holder. Always a boolean. */
|
|
42
|
+
async function feature(c, name) {
|
|
43
|
+
const resolver = c.var.entitlements;
|
|
44
|
+
if (!resolver?.feature) throw noProvider(c, "feature", name);
|
|
45
|
+
return resolver.feature(name);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The ceiling a capability enforces on a meter it counts itself, for the current caller.
|
|
49
|
+
*
|
|
50
|
+
* **No provider, or one that binds nothing to this meter, is `{ enforced: false }`** — and the capability
|
|
51
|
+
* keeps its own configured ceiling. Enforcement is opt-in per meter because a cap that arrives by surprise
|
|
52
|
+
* breaks an adopter who was already over it.
|
|
53
|
+
*
|
|
54
|
+
* **A provider that throws refuses, and logs.** Storage already says which way round to err: over-counting
|
|
55
|
+
* briefly and reclaiming is the right way round, and under-counting has no recovery. Treating a failed read
|
|
56
|
+
* as unenforced would hand a customer an unbounded allowance for as long as the read kept failing. The cost
|
|
57
|
+
* is real and is stated rather than discovered: our bug becomes a refused write for a paying customer.
|
|
58
|
+
* Enforcement being opt-in is what bounds it — an adopter who would rather over-serve leaves the meter
|
|
59
|
+
* advisory.
|
|
60
|
+
*/
|
|
61
|
+
async function meterLimit(c, meter) {
|
|
62
|
+
const resolver = c.var.entitlements;
|
|
63
|
+
if (!resolver?.meterLimit) return { enforced: false };
|
|
64
|
+
try {
|
|
65
|
+
return await resolver.meterLimit(meter);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
c.var.log?.error("meter limit resolution failed", {
|
|
68
|
+
provider: resolver.provider,
|
|
69
|
+
meter,
|
|
70
|
+
cause: messageOf(error)
|
|
71
|
+
});
|
|
72
|
+
throw new InternalError({
|
|
73
|
+
message: "Usage could not be checked, so nothing was written.",
|
|
74
|
+
action: "Read the preceding log line for why the allowance read failed.",
|
|
75
|
+
detail: `${resolver.provider} threw resolving the ${meter} meter's ceiling; refusing rather than serving unbounded.`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { feature, meterLimit, quota };
|
|
@@ -66,7 +66,48 @@ export interface EntitlementResolver {
|
|
|
66
66
|
* See {@link EntitlementHolder} for why this is a label and a tenant rather than the holder itself.
|
|
67
67
|
*/
|
|
68
68
|
holder?(): Promise<EntitlementHolder | undefined>;
|
|
69
|
+
/**
|
|
70
|
+
* How many of `name` the current caller's holder may have — a whole number, or `null` for unlimited.
|
|
71
|
+
* Optional: a provider with no allowances omits it, and {@link import("./allowance").quota} refuses
|
|
72
|
+
* rather than inventing a number. A provider throws for a name it does not declare, never answers 0.
|
|
73
|
+
*/
|
|
74
|
+
quota?(name: string): Promise<number | null>;
|
|
75
|
+
/** Whether `name` is on for the current caller's holder. Never null. Optional, as {@link quota} is. */
|
|
76
|
+
feature?(name: string): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* The ceiling a capability that counts its own rows enforces for `meter` — storage's bytes, media's
|
|
79
|
+
* items — or `{ enforced: false }` where the provider binds no ceiling to it. Optional, and absent is
|
|
80
|
+
* `{ enforced: false }`: a capability composed with no provider keeps its own configured behavior.
|
|
81
|
+
*/
|
|
82
|
+
meterLimit?(meter: string): Promise<MeterLimit>;
|
|
69
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* What a provider says about one meter for the current caller: whether it binds a ceiling at all, and if
|
|
86
|
+
* it does, what the ceiling is and whose rows it covers. `limit: null` is unlimited — an enforced meter on
|
|
87
|
+
* a subject whose plan or override says so.
|
|
88
|
+
*
|
|
89
|
+
* Two states rather than a nullable number, because "this meter is advisory here" and "this subject is
|
|
90
|
+
* unlimited" are different facts and a capability must fall back to its own configured ceiling on the
|
|
91
|
+
* first and never on the second.
|
|
92
|
+
*
|
|
93
|
+
* **An enforced ceiling says which rows it is the whole of**, because a ceiling counted against the wrong
|
|
94
|
+
* rows is a different allowance. Under organization billing the plan's 10 GB is the company's: counted
|
|
95
|
+
* against one member's files it becomes 10 GB per member, and a paid quota is bypassed by an invitation.
|
|
96
|
+
* So `tenant` is the organization the ceiling was resolved for, exactly as {@link EntitlementHolder.tenant}
|
|
97
|
+
* is, and a capability sums every row it holds for that tenant, whoever wrote it. `null` means the holder
|
|
98
|
+
* is not a tenant: the ceiling is the caller's own, and the caller's rows are what count.
|
|
99
|
+
*
|
|
100
|
+
* It is a counting key, never a gate. A capability that would write a row for a different tenant than the
|
|
101
|
+
* one the ceiling covers refuses the write — that row would sit outside the sum, and under-counting has no
|
|
102
|
+
* recovery.
|
|
103
|
+
*/
|
|
104
|
+
export type MeterLimit = {
|
|
105
|
+
readonly enforced: false;
|
|
106
|
+
} | {
|
|
107
|
+
readonly enforced: true;
|
|
108
|
+
readonly limit: number | null;
|
|
109
|
+
readonly tenant: string | null;
|
|
110
|
+
};
|
|
70
111
|
/**
|
|
71
112
|
* How a resolver's holder appears in a denial's `detail` and on the denial's audit row — and **nothing a
|
|
72
113
|
* gate may compare**.
|
|
@@ -1 +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;
|
|
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;IAClD;;;;OAIG;IACH,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,uGAAuG;IACvG,OAAO,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC;;;;OAIG;IACH,UAAU,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACjD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { Capability } from "../capability/capability";
|
|
4
|
+
import type { Logger } from "../logger/logger";
|
|
5
|
+
/**
|
|
6
|
+
* The usage-meter seam — how a capability that writes the rows a meter counts asks whether one more is
|
|
7
|
+
* allowed, when there is no request to ask through.
|
|
8
|
+
*
|
|
9
|
+
* `@pithy-sh/email` enqueues from a route, from a Workflow step and from a cron, and only the first has a
|
|
10
|
+
* `c.var.entitlements`. So the meter it charges is found the way the ledger is found — among the composed
|
|
11
|
+
* capabilities, by a structural predicate — and it is handed the holder explicitly. Nothing here imports the
|
|
12
|
+
* provider: an email capability composed with no payments finds no meter and enqueues as it always did.
|
|
13
|
+
*
|
|
14
|
+
* **A meter refuses; it never bills.** `charge` either records the use or throws the provider's own refusal
|
|
15
|
+
* (`payments/quota_exceeded`, 402). What it counted is never reported to a store — the kit does not sell
|
|
16
|
+
* overage — so this seam has no way to read a count back out.
|
|
17
|
+
*/
|
|
18
|
+
/** Who a charge is against: the holder pair, exactly as the provider keys it. */
|
|
19
|
+
export interface MeterSubject {
|
|
20
|
+
/** The kind of holder — `user` or `organization` under `@pithy-sh/payments`. */
|
|
21
|
+
readonly subjectType: string;
|
|
22
|
+
/** The holder's id. */
|
|
23
|
+
readonly subjectId: string;
|
|
24
|
+
}
|
|
25
|
+
/** One charge against one meter. */
|
|
26
|
+
export interface MeterCharge {
|
|
27
|
+
/** Who uses it. */
|
|
28
|
+
readonly subject: MeterSubject;
|
|
29
|
+
/** Which meter — `emails`. */
|
|
30
|
+
readonly meter: string;
|
|
31
|
+
/** How many. A whole number from 1. */
|
|
32
|
+
readonly amount: number;
|
|
33
|
+
/** The clock. Defaults to now. */
|
|
34
|
+
readonly now?: Date;
|
|
35
|
+
/** Where a refused or failed charge is logged. Optional; the provider logs through its own when absent. */
|
|
36
|
+
readonly log?: Logger;
|
|
37
|
+
}
|
|
38
|
+
/** The meter a provider exposes. */
|
|
39
|
+
export interface UsageMeter {
|
|
40
|
+
/** The capability answering — `payments`. */
|
|
41
|
+
readonly provider: string;
|
|
42
|
+
/**
|
|
43
|
+
* Record `amount` of `meter` against `subject`, or refuse. An advisory meter records nothing and refuses
|
|
44
|
+
* nothing. **A charge that cannot be checked refuses**, because under-counting has no recovery.
|
|
45
|
+
*/
|
|
46
|
+
charge(env: Record<string, unknown>, charge: MeterCharge): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** A composed capability that provides a meter. */
|
|
49
|
+
export interface UsageMeterProvider extends Capability {
|
|
50
|
+
/** The meter. */
|
|
51
|
+
readonly usageMeter: UsageMeter;
|
|
52
|
+
}
|
|
53
|
+
/** Whether a composed capability provides a usage meter. Structural, so no provider package is imported. */
|
|
54
|
+
export declare function isUsageMeterProvider(capability: Capability): capability is UsageMeterProvider;
|
|
55
|
+
/** The composed usage meter, or undefined when nothing composed provides one. */
|
|
56
|
+
export declare function composedUsageMeter(capabilities: readonly Capability[]): UsageMeter | undefined;
|
|
57
|
+
//# sourceMappingURL=meter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meter.d.ts","sourceRoot":"","sources":["../../src/entitlement/meter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;;;;;;;;;;GAYG;AAEH,iFAAiF;AACjF,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,uBAAuB;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,oCAAoC;AACpC,MAAM,WAAW,WAAW;IAC1B,mBAAmB;IACnB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,8BAA8B;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kCAAkC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC;IACpB,2GAA2G;IAC3G,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,oCAAoC;AACpC,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E;AAED,mDAAmD;AACnD,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,iBAAiB;IACjB,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;CACjC;AAED,4GAA4G;AAC5G,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,IAAI,kBAAkB,CAG7F;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,GAAG,SAAS,CAE9F"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
//#region src/entitlement/meter.ts
|
|
4
|
+
/** Whether a composed capability provides a usage meter. Structural, so no provider package is imported. */
|
|
5
|
+
function isUsageMeterProvider(capability) {
|
|
6
|
+
return typeof capability.usageMeter?.charge === "function";
|
|
7
|
+
}
|
|
8
|
+
/** The composed usage meter, or undefined when nothing composed provides one. */
|
|
9
|
+
function composedUsageMeter(capabilities) {
|
|
10
|
+
return capabilities.find(isUsageMeterProvider)?.usageMeter;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { composedUsageMeter, isUsageMeterProvider };
|
|
@@ -8,7 +8,7 @@ import { ENTITLEMENT_DENIED_ACTION, EntitlementKey, grantedEntitlementKeys } fro
|
|
|
8
8
|
* `verification`-strategy sibling every paid route carries: it says what the caller must *hold*, the
|
|
9
9
|
* way `requireAuth()` says who the caller must *be*, and it belongs on the route line beside it.
|
|
10
10
|
*
|
|
11
|
-
* app.get("/reports", requireAuth(), requireEntitlement("pro"),
|
|
11
|
+
* app.get("/reports", requireAuth(), requireEntitlement("pro"), validate(…), handler)
|
|
12
12
|
*
|
|
13
13
|
* It lives in `@pithy-sh/core` rather than in `@pithy-sh/payments` for the same reason `requireAuth()`
|
|
14
14
|
* is copied into each capability instead of imported from `@pithy-sh/auth`: a gate that arrives with a
|
package/dist/error/cause.d.ts
CHANGED
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
* This module holds only what does not — the facts about the shapes a runtime hands over, and the filter
|
|
11
11
|
* that decides whether one of its strings is safe to put in front of an adopter.
|
|
12
12
|
*
|
|
13
|
+
* The one exception is an import that would not resolve, and it has its own module: `unresolved.ts`
|
|
14
|
+
* decides the kind and the remedy for all three surfaces, because three surfaces deciding it kept
|
|
15
|
+
* disagreeing (#709).
|
|
16
|
+
*
|
|
13
17
|
* **The filter is here for the same reason the facts are, and the argument took two issues to finish.**
|
|
14
|
-
* #223 moved `rootCause
|
|
18
|
+
* #223 moved `rootCause` and `prop` in and deliberately left `safeReason` beside
|
|
15
19
|
* the three refusals, on the reading that what a surface may say is that surface's business. That is true
|
|
16
20
|
* of the sentences and false of the filter: whether a string carries a path, a stack frame or half a
|
|
17
21
|
* parser's ANSI box is a property of the string, and the answer cannot differ between the CLI, a
|
|
@@ -24,11 +28,12 @@
|
|
|
24
28
|
/**
|
|
25
29
|
* Read a property off an unknown throwable without widening anything to `any`.
|
|
26
30
|
*
|
|
27
|
-
* Deliberately duck-typed.
|
|
28
|
-
*
|
|
29
|
-
* `instanceof Error
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* Deliberately duck-typed. A cause is `unknown`, and an `instanceof Error` gate is a claim about a
|
|
32
|
+
* runtime's classes that the runtime can break. Bun did: on 1.3 its `ResolveMessage` and `BuildMessage`
|
|
33
|
+
* were **not** `instanceof Error`, so a gate passed a whole suite under vitest, which runs on Node, and
|
|
34
|
+
* silently dropped the parser's own sentence on the runtime that ships. That is #207. Bun 1.4 made both
|
|
35
|
+
* real `Error`s (measured on 1.4.2), and nothing here had to change, which is the point: every reader of
|
|
36
|
+
* a cause in this kit goes through this function and reads fields, never a class.
|
|
32
37
|
*
|
|
33
38
|
* Non-enumerable properties are read too — `AggregateError.errors` is one, and {@link rootCause}
|
|
34
39
|
* depends on it.
|
|
@@ -43,7 +48,7 @@ export declare function prop(cause: unknown, key: string): unknown;
|
|
|
43
48
|
* and only by running on Bun with a failure of the *realistic* shape.
|
|
44
49
|
*
|
|
45
50
|
* **And the realistic shape is the wrapped one.** A stray brace cascades — one missing `}` in a
|
|
46
|
-
* `pithy.config.ts`
|
|
51
|
+
* `pithy.config.ts` produces four diagnostics on Bun 1.4.2 — so a syntax error arrives wrapped far more
|
|
47
52
|
* often than bare. Classified as-is, the wrapper matches nothing: not a resolution failure, not a parse
|
|
48
53
|
* error, just something with a message. The refusal then says *the config threw while loading, run the
|
|
49
54
|
* file directly* while holding the line and column in its hand. #207 fixed the bare case and left this
|
|
@@ -58,26 +63,6 @@ export declare function prop(cause: unknown, key: string): unknown;
|
|
|
58
63
|
* a runtime that stops wrapping needs no change here.
|
|
59
64
|
*/
|
|
60
65
|
export declare function rootCause(cause: unknown): unknown;
|
|
61
|
-
/**
|
|
62
|
-
* The wrapper **after the diagnostics are gone** — a build that failed, and that is all that is left.
|
|
63
|
-
*
|
|
64
|
-
* {@link rootCause} handles the first `import()`. It cannot handle the second: Bun caches a failed module
|
|
65
|
-
* and re-throws an `AggregateError` with **no `errors` at all**, just the count and the path. There is
|
|
66
|
-
* nothing to unwrap, so a classifier that only unwraps reads a syntax error as "it threw" from the second
|
|
67
|
-
* caller onward — which is what `pithy doctor` shows an adopter, because resolving the project's account
|
|
68
|
-
* loads the root config before the report does (#206, #223).
|
|
69
|
-
*
|
|
70
|
-
* Nothing else Bun throws degrades this way: a `ResolveMessage`, a bare `BuildMessage` and a module's own
|
|
71
|
-
* `Error` are byte-identical on every import. Only the wrapper forgets.
|
|
72
|
-
*
|
|
73
|
-
* So the wrapper is recognized by its own message. **What it proves is narrow and worth stating:** the
|
|
74
|
-
* module reached the builder and the builder produced diagnostics. It did not fail to resolve — Bun
|
|
75
|
-
* throws the first `ResolveMessage` bare, wrapping nothing, even for a file with several bad imports —
|
|
76
|
-
* and it did not run, so it cannot have thrown. It failed to parse or to build. A caller may say that
|
|
77
|
-
* much and no more: the message itself carries an absolute path and a count, neither of which is the
|
|
78
|
-
* adopter's business, so it is a *predicate* here rather than a string anyone is tempted to print.
|
|
79
|
-
*/
|
|
80
|
-
export declare function isBuildFailureWrapper(cause: unknown): boolean;
|
|
81
66
|
/**
|
|
82
67
|
* The thrown value's message, **de-colored** — `undefined` when it has none.
|
|
83
68
|
*
|
|
@@ -100,15 +85,6 @@ export declare function causeMessage(cause: unknown): string | undefined;
|
|
|
100
85
|
* developer one frame of digging, and a mistake puts a path, a source line or a stack frame in a field
|
|
101
86
|
* that is rendered.
|
|
102
87
|
*
|
|
103
|
-
* **Provenance first, then content.** Bun's build-failure wrapper is refused outright, before a single
|
|
104
|
-
* content test runs. The content tests exist for a diagnostic that *might* be safe; the wrapper never is,
|
|
105
|
-
* because its whole message is a count nobody asked for and a path that must not travel. Leaving it to
|
|
106
|
-
* the content tests is what leaked: `2 errors building "app/config:12:5.ts"` has no leading slash, sailed
|
|
107
|
-
* through the absolute-path check, and produced *The config does not parse: 2 errors building
|
|
108
|
-
* "app/config:12:5.ts". Line 12, column 5.* — a path the adopter never wrote, and a position fabricated
|
|
109
|
-
* out of a file name (#223). That suppression was written three times because the filter was. It is
|
|
110
|
-
* written once here, and {@link failurePosition} refuses the same shape for the same reason.
|
|
111
|
-
*
|
|
112
88
|
* A trailing period comes off: every caller puts this inside a sentence of its own.
|
|
113
89
|
*/
|
|
114
90
|
export declare function safeReason(cause: unknown): string | undefined;
|
|
@@ -127,85 +103,9 @@ export interface FailurePosition {
|
|
|
127
103
|
* path they are appended to. Whether a surface prints them is that surface's policy: two of the three
|
|
128
104
|
* classifiers do, and the capability loader deliberately does not.
|
|
129
105
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*/
|
|
134
|
-
export declare function failurePosition(cause: unknown): FailurePosition | undefined;
|
|
135
|
-
/** What kind of thing failed to resolve. Three causes that wore one sentence until #489. */
|
|
136
|
-
export type UnresolvedKind =
|
|
137
|
-
/** A bare specifier for a package that is not installed. Installing dependencies is the remedy. */
|
|
138
|
-
"package"
|
|
139
|
-
/** An installed package asked for a subpath it does not provide. Installing again changes nothing. */
|
|
140
|
-
| "package-subpath"
|
|
141
|
-
/** A relative import of a file in the project's own tree. Installing again changes nothing. */
|
|
142
|
-
| "local-file";
|
|
143
|
-
/**
|
|
144
|
-
* Which of the three unresolved-import causes this was, or `undefined` when it cannot be told.
|
|
145
|
-
*
|
|
146
|
-
* **The discriminator was already in the message and was being thrown away.** `unresolvedSpecifier`
|
|
147
|
-
* matches `Cannot find (?:package|module)` — and `package` versus `module` is exactly the fact that
|
|
148
|
-
* separates a missing dependency from the other two. It sat in a non-capturing group, so every cause
|
|
149
|
-
* arrived wearing one sentence, and for two of the three that sentence advised an install which is
|
|
150
|
-
* *guaranteed* to change nothing. That is #489's class exactly: guidance a caller cannot follow, worst
|
|
151
|
-
* for the caller it is written for, which has no human to shrug and try something else.
|
|
152
|
-
*
|
|
153
|
-
* Measured across Node 24.13.0 and Bun 1.3.14, because the two runtimes hand back different halves and
|
|
154
|
-
* each hands back exactly one usable one:
|
|
155
|
-
*
|
|
156
|
-
* | cause | Node says | Bun's `specifier` |
|
|
157
|
-
* |---|---|---|
|
|
158
|
-
* | package absent | `Cannot find package 'pkg'`, no `url` | `pkg` |
|
|
159
|
-
* | subpath missing | `Cannot find module '/abs/node_modules/pkg/src/nope.js'`, `url` under node_modules | `pkg/src/nope` |
|
|
160
|
-
* | adopter's file | `Cannot find module '/abs/apps/board/src/x'`, `url` outside node_modules | `./src/x` |
|
|
161
|
-
*
|
|
162
|
-
* Bun gives the **specifier as written**, so a leading `.` settles it. Node gives the **already-resolved
|
|
163
|
-
* path**, where being under `node_modules` settles it. Neither read works on both runtimes; the pair
|
|
164
|
-
* does, and it is the same predicate — *is this inside node_modules* — applied to whichever fact the
|
|
165
|
-
* runtime supplied.
|
|
166
|
-
*/
|
|
167
|
-
export declare function unresolvedKind(cause: unknown): UnresolvedKind | undefined;
|
|
168
|
-
/**
|
|
169
|
-
* The specifier that did not resolve.
|
|
170
|
-
*
|
|
171
|
-
* A recorded fact about two runtimes: Bun's `ResolveMessage` carries it as a **field**, and Node states it
|
|
172
|
-
* in prose. The field is preferred wherever there is one, because the prose around it names the referrer's
|
|
173
|
-
* absolute path — the specifier is the adopter's own import, the referrer is our frame.
|
|
174
|
-
*
|
|
175
|
-
* Whether a surface names it, and what it advises when it does, is that surface's policy and stays there.
|
|
176
|
-
*/
|
|
177
|
-
export declare function unresolvedSpecifier(cause: unknown): string | undefined;
|
|
178
|
-
/**
|
|
179
|
-
* The package whose own source contained the import that did not resolve — **never a path**.
|
|
180
|
-
*
|
|
181
|
-
* {@link unresolvedSpecifier} answers *what* did not resolve. This answers *where the import lives*, and
|
|
182
|
-
* the two are different facts whenever the failing import is not the adopter's own. #480 is that case
|
|
183
|
-
* verbatim: `pithy add email` on a fresh project refused with
|
|
184
|
-
*
|
|
185
|
-
* > Nothing resolves "@pithy-sh/core/src/capability/capability".
|
|
186
|
-
*
|
|
187
|
-
* while `@pithy-sh/core` resolved from the Worker at every step of the run, before and after. The
|
|
188
|
-
* specifier was honest — the runtime really could not resolve it — but the import belonged to
|
|
189
|
-
* `@pithy-sh/email`, which had just been installed and was the package genuinely not linked. So the
|
|
190
|
-
* refusal named the one dependency that was definitely fine, and an adopter following it ran `bun
|
|
191
|
-
* install` against a tree that already had core in it. The importer is the half that would have pointed
|
|
192
|
-
* at `email`, and both runtimes hand it over: **Bun's `ResolveMessage` carries `referrer` as a field**
|
|
193
|
-
* (measured on 1.3.14) and Node states it in prose as `imported from <path>`.
|
|
194
|
-
*
|
|
195
|
-
* **A package name, or nothing.** The referrer is an absolute path — our frame, and the reason
|
|
196
|
-
* {@link unresolvedSpecifier} drops the prose around the specifier — so what travels is the one part of
|
|
197
|
-
* it that is not a path: the package directory it sits under. A referrer outside `node_modules` is the
|
|
198
|
-
* adopter's own file, and there is nothing to say about it that the refusal does not already say by
|
|
199
|
-
* naming the config, so it answers `undefined` rather than a path. The path itself is never returned
|
|
200
|
-
* from this module, which is what makes leaking it impossible rather than merely discouraged.
|
|
201
|
-
*
|
|
202
|
-
* The **last** `node_modules` in the path, because every store layout nests: pnpm's
|
|
203
|
-
* `node_modules/.pnpm/<id>/node_modules/<pkg>` and Bun's isolated linker's
|
|
204
|
-
* `node_modules/.bun/<pkg>@<version>/node_modules/<pkg>` (both measured) put the real owner last, and so
|
|
205
|
-
* does an ordinary nested install. The name is then checked against what npm allows, so a store's own
|
|
206
|
-
* bookkeeping directory (`.bun`, `.pnpm`, `hono@4.6.3`) can only ever answer nothing.
|
|
207
|
-
*
|
|
208
|
-
* Whether a surface names it, and what it advises when it does, is that surface's policy and stays there.
|
|
106
|
+
* `isOwn`, when given, is asked about the file a structured position names, and a position in a file it
|
|
107
|
+
* disowns is no position at all: Bun's line and column in a file the config imports, quoted as the config's,
|
|
108
|
+
* send the adopter to a place in their config that is fine (#709).
|
|
209
109
|
*/
|
|
210
|
-
export declare function
|
|
110
|
+
export declare function failurePosition(cause: unknown, isOwn?: (file: string) => boolean): FailurePosition | undefined;
|
|
211
111
|
//# sourceMappingURL=cause.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cause.d.ts","sourceRoot":"","sources":["../../src/error/cause.ts"],"names":[],"mappings":"AAGA
|
|
1
|
+
{"version":3,"file":"cause.d.ts","sourceRoot":"","sources":["../../src/error/cause.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQjD;AAeD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAG/D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAO7D;AAED,mGAAmG;AACnG,MAAM,WAAW,eAAe;IAC9B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,eAAe,GAAG,SAAS,CAU9G"}
|