@pithy-sh/core 0.13.0 → 0.15.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/README.md +54 -0
- package/dist/audit/auditEvent.js +1 -1
- package/dist/audit/chain.d.ts +289 -0
- package/dist/audit/chain.d.ts.map +1 -0
- package/dist/audit/chain.js +400 -0
- package/dist/audit/citations.d.ts +52 -0
- package/dist/audit/citations.d.ts.map +1 -0
- package/dist/audit/citations.js +13 -0
- package/dist/capability/bindings.d.ts +22 -0
- package/dist/capability/bindings.d.ts.map +1 -1
- package/dist/capability/bindings.js +32 -2
- package/dist/capability/capability.d.ts +123 -16
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/capability.js +17 -2
- package/dist/capability/erasure.d.ts +75 -0
- package/dist/capability/erasure.d.ts.map +1 -0
- package/dist/capability/erasure.js +2 -0
- package/dist/capability/manifest.d.ts +5 -0
- package/dist/capability/manifest.d.ts.map +1 -1
- package/dist/capability/personColumns.d.ts +28 -0
- package/dist/capability/personColumns.d.ts.map +1 -0
- package/dist/capability/personColumns.js +66 -0
- package/dist/capability/personRows.d.ts +62 -0
- package/dist/capability/personRows.d.ts.map +1 -0
- package/dist/capability/personRows.js +59 -0
- package/dist/capability/purge.d.ts +86 -19
- package/dist/capability/purge.d.ts.map +1 -1
- package/dist/capability/purge.js +83 -5
- package/dist/capability/settings.d.ts +10 -0
- package/dist/capability/settings.d.ts.map +1 -1
- package/dist/capability/vectorIndex.d.ts +36 -0
- package/dist/capability/vectorIndex.d.ts.map +1 -0
- package/dist/capability/vectorIndex.js +25 -0
- package/dist/cli/commandCatalog.generated.d.ts +438 -3
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
- package/dist/cli/commandCatalog.generated.js +454 -2
- package/dist/cli/commandFacts.d.ts +8 -5
- package/dist/cli/commandFacts.d.ts.map +1 -1
- package/dist/cli/commandFacts.js +12 -3
- package/dist/consent/purpose.d.ts +70 -0
- package/dist/consent/purpose.d.ts.map +1 -0
- package/dist/consent/purpose.js +97 -0
- package/dist/consent/reader.d.ts +99 -0
- package/dist/consent/reader.d.ts.map +1 -0
- package/dist/consent/reader.js +33 -0
- package/dist/controlPlane/audit/actions.d.ts +15 -4
- package/dist/controlPlane/audit/actions.d.ts.map +1 -1
- package/dist/controlPlane/audit/actions.js +16 -5
- package/dist/controlPlane/capability.d.ts +10 -0
- package/dist/controlPlane/capability.d.ts.map +1 -1
- package/dist/controlPlane/capability.js +56 -8
- package/dist/controlPlane/context.d.ts +4 -0
- package/dist/controlPlane/context.d.ts.map +1 -1
- package/dist/controlPlane/context.js +3 -2
- package/dist/controlPlane/data/connection.d.ts +21 -2
- package/dist/controlPlane/data/connection.d.ts.map +1 -1
- package/dist/controlPlane/data/connection.js +13 -4
- package/dist/controlPlane/data/connections.d.ts +94 -0
- package/dist/controlPlane/data/connections.d.ts.map +1 -0
- package/dist/controlPlane/data/connections.js +151 -0
- package/dist/controlPlane/data/hold.d.ts +46 -0
- package/dist/controlPlane/data/hold.d.ts.map +1 -0
- package/dist/controlPlane/data/hold.js +36 -0
- package/dist/controlPlane/data/oidcTrust.d.ts +122 -0
- package/dist/controlPlane/data/oidcTrust.d.ts.map +1 -0
- package/dist/controlPlane/data/oidcTrust.js +173 -0
- package/dist/controlPlane/data/tables.d.ts +4 -0
- package/dist/controlPlane/data/tables.d.ts.map +1 -1
- package/dist/controlPlane/data/tables.js +9 -4
- package/dist/controlPlane/data/trustFirstCall.d.ts +16 -0
- package/dist/controlPlane/data/trustFirstCall.d.ts.map +1 -0
- package/dist/controlPlane/data/trustFirstCall.js +25 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts +165 -2
- package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -1
- package/dist/controlPlane/discovery/adminRoute.js +32 -8
- package/dist/controlPlane/discovery/routeInput.d.ts +23 -0
- package/dist/controlPlane/discovery/routeInput.d.ts.map +1 -0
- package/dist/controlPlane/discovery/routeInput.js +80 -0
- package/dist/controlPlane/discovery/toolName.d.ts +21 -0
- package/dist/controlPlane/discovery/toolName.d.ts.map +1 -0
- package/dist/controlPlane/discovery/toolName.js +25 -0
- package/dist/controlPlane/erasure/attest.d.ts +78 -0
- package/dist/controlPlane/erasure/attest.d.ts.map +1 -0
- package/dist/controlPlane/erasure/attest.js +164 -0
- package/dist/controlPlane/erasure/read.d.ts +39 -0
- package/dist/controlPlane/erasure/read.d.ts.map +1 -0
- package/dist/controlPlane/erasure/read.js +140 -0
- package/dist/controlPlane/error/errors.d.ts +61 -2
- package/dist/controlPlane/error/errors.d.ts.map +1 -1
- package/dist/controlPlane/error/errors.js +13 -0
- package/dist/controlPlane/evidence/derive.d.ts +124 -0
- package/dist/controlPlane/evidence/derive.d.ts.map +1 -0
- package/dist/controlPlane/evidence/derive.js +343 -0
- package/dist/controlPlane/evidence/mapping.d.ts +85 -0
- package/dist/controlPlane/evidence/mapping.d.ts.map +1 -0
- package/dist/controlPlane/evidence/mapping.js +114 -0
- package/dist/controlPlane/evidence/pack.d.ts +46 -0
- package/dist/controlPlane/evidence/pack.d.ts.map +1 -0
- package/dist/controlPlane/evidence/pack.js +124 -0
- package/dist/controlPlane/http/connections.d.ts +96 -0
- package/dist/controlPlane/http/connections.d.ts.map +1 -0
- package/dist/controlPlane/http/connections.js +109 -0
- package/dist/controlPlane/http/guard.d.ts +23 -2
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +69 -12
- package/dist/controlPlane/http/handlers.d.ts +212 -10
- package/dist/controlPlane/http/handlers.d.ts.map +1 -1
- package/dist/controlPlane/http/handlers.js +117 -26
- package/dist/controlPlane/http/holds.d.ts +57 -0
- package/dist/controlPlane/http/holds.d.ts.map +1 -0
- package/dist/controlPlane/http/holds.js +98 -0
- package/dist/controlPlane/http/presence.d.ts +27 -0
- package/dist/controlPlane/http/presence.d.ts.map +1 -0
- package/dist/controlPlane/http/presence.js +67 -0
- package/dist/controlPlane/http/processing.d.ts +73 -0
- package/dist/controlPlane/http/processing.d.ts.map +1 -0
- package/dist/controlPlane/http/processing.js +17 -0
- package/dist/controlPlane/http/responses.d.ts +927 -3
- package/dist/controlPlane/http/responses.d.ts.map +1 -1
- package/dist/controlPlane/http/responses.js +283 -7
- package/dist/controlPlane/http/routes.d.ts +12 -2
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +182 -5
- package/dist/controlPlane/http/schemas.d.ts +63 -0
- package/dist/controlPlane/http/schemas.d.ts.map +1 -1
- package/dist/controlPlane/http/schemas.js +39 -1
- package/dist/controlPlane/http/verify.d.ts.map +1 -1
- package/dist/controlPlane/http/verify.js +13 -1
- package/dist/controlPlane/http/verifyTrust.d.ts +76 -0
- package/dist/controlPlane/http/verifyTrust.d.ts.map +1 -0
- package/dist/controlPlane/http/verifyTrust.js +75 -0
- package/dist/controlPlane/migrations/0004_connection_trusts.d.ts +15 -0
- package/dist/controlPlane/migrations/0004_connection_trusts.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0004_connection_trusts.js +23 -0
- package/dist/controlPlane/migrations/0005_holds.d.ts +12 -0
- package/dist/controlPlane/migrations/0005_holds.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0005_holds.js +26 -0
- package/dist/controlPlane/processing/derive.d.ts +77 -0
- package/dist/controlPlane/processing/derive.d.ts.map +1 -0
- package/dist/controlPlane/processing/derive.js +206 -0
- package/dist/controlPlane/processing/read.d.ts +10 -0
- package/dist/controlPlane/processing/read.d.ts.map +1 -0
- package/dist/controlPlane/processing/read.js +89 -0
- package/dist/controlPlane/scope/kind.d.ts +46 -0
- package/dist/controlPlane/scope/kind.d.ts.map +1 -0
- package/dist/controlPlane/scope/kind.js +52 -0
- package/dist/controlPlane/scope/scope.d.ts +58 -3
- package/dist/controlPlane/scope/scope.d.ts.map +1 -1
- package/dist/controlPlane/scope/scope.js +59 -4
- package/dist/controlPlane/token/jws.js +1 -1
- package/dist/controlPlane/token/mint.js +1 -1
- package/dist/controlPlane/wire.d.ts +23 -0
- package/dist/controlPlane/wire.d.ts.map +1 -1
- package/dist/controlPlane/wire.js +24 -1
- package/dist/createBackend.d.ts +13 -0
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +11 -3
- package/dist/createEntrypoint.d.ts +7 -0
- package/dist/createEntrypoint.d.ts.map +1 -1
- package/dist/createEntrypoint.js +4 -0
- package/dist/error/http.d.ts +168 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +336 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +112 -0
- package/dist/error/pithyError.d.ts +22 -0
- package/dist/error/pithyError.d.ts.map +1 -1
- package/dist/error/pithyError.js +31 -1
- package/dist/http/routeSpec.d.ts +46 -1
- package/dist/http/routeSpec.d.ts.map +1 -1
- package/dist/http/routeSpec.js +39 -5
- package/dist/http/verification.d.ts.map +1 -1
- package/dist/http/verification.js +1 -1
- package/dist/i18n/translator.d.ts +2 -1
- package/dist/i18n/translator.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/logger/record.d.ts +98 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/mcp/annotations.d.ts +20 -0
- package/dist/mcp/annotations.d.ts.map +1 -0
- package/dist/mcp/annotations.js +23 -0
- package/dist/mcp/tools.d.ts +75 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +118 -0
- package/dist/openapi/document.d.ts.map +1 -1
- package/dist/openapi/document.js +8 -26
- package/dist/openapi/operations.d.ts +35 -0
- package/dist/openapi/operations.d.ts.map +1 -0
- package/dist/openapi/operations.js +37 -0
- package/dist/presence/channel.d.ts +11 -0
- package/dist/presence/channel.d.ts.map +1 -0
- package/dist/presence/channel.js +65 -0
- package/dist/presence/declaration.d.ts +154 -0
- package/dist/presence/declaration.d.ts.map +1 -0
- package/dist/presence/declaration.js +164 -0
- package/dist/presence/durableObject.d.ts +61 -0
- package/dist/presence/durableObject.d.ts.map +1 -0
- package/dist/presence/durableObject.js +257 -0
- package/dist/presence/frames.d.ts +166 -0
- package/dist/presence/frames.d.ts.map +1 -0
- package/dist/presence/frames.js +90 -0
- package/dist/presence/http/guard.d.ts +11 -0
- package/dist/presence/http/guard.d.ts.map +1 -0
- package/dist/presence/http/guard.js +20 -0
- package/dist/presence/http/responses.d.ts +31 -0
- package/dist/presence/http/responses.d.ts.map +1 -0
- package/dist/presence/http/responses.js +34 -0
- package/dist/presence/http/routes.d.ts +24 -0
- package/dist/presence/http/routes.d.ts.map +1 -0
- package/dist/presence/http/routes.js +127 -0
- package/dist/presence/http/schemas.d.ts +29 -0
- package/dist/presence/http/schemas.d.ts.map +1 -0
- package/dist/presence/http/schemas.js +28 -0
- package/dist/presence/names.d.ts +23 -0
- package/dist/presence/names.d.ts.map +1 -0
- package/dist/presence/names.js +22 -0
- package/dist/presence/room.d.ts +95 -0
- package/dist/presence/room.d.ts.map +1 -0
- package/dist/presence/room.js +105 -0
- package/dist/presence/ticket.d.ts +55 -0
- package/dist/presence/ticket.d.ts.map +1 -0
- package/dist/presence/ticket.js +109 -0
- package/dist/privacy/classify.js +1 -1
- package/dist/privacy/encrypted.js +1 -1
- package/dist/processing/activity.d.ts +134 -0
- package/dist/processing/activity.d.ts.map +1 -0
- package/dist/processing/activity.js +161 -0
- package/dist/processing/composed.d.ts +18 -0
- package/dist/processing/composed.d.ts.map +1 -0
- package/dist/processing/composed.js +28 -0
- package/dist/processing/vocabulary.d.ts +26 -0
- package/dist/processing/vocabulary.d.ts.map +1 -0
- package/dist/processing/vocabulary.js +35 -0
- package/dist/queue/consumer.d.ts +59 -0
- package/dist/queue/consumer.d.ts.map +1 -0
- package/dist/queue/consumer.js +46 -0
- package/dist/retention/composed.d.ts +37 -0
- package/dist/retention/composed.d.ts.map +1 -0
- package/dist/retention/composed.js +66 -0
- package/dist/retention/holds.d.ts +61 -0
- package/dist/retention/holds.d.ts.map +1 -0
- package/dist/retention/holds.js +107 -0
- package/dist/retention/holdsPurge.d.ts +22 -0
- package/dist/retention/holdsPurge.d.ts.map +1 -0
- package/dist/retention/holdsPurge.js +50 -0
- package/dist/retention/policy.d.ts +132 -0
- package/dist/retention/policy.d.ts.map +1 -0
- package/dist/retention/policy.js +197 -0
- package/dist/session/rotated.d.ts +34 -0
- package/dist/session/rotated.d.ts.map +1 -0
- package/dist/session/rotated.js +25 -0
- package/dist/test-utils/connectionFixtures.d.ts +13 -0
- package/dist/test-utils/connectionFixtures.d.ts.map +1 -0
- package/dist/test-utils/connectionFixtures.js +58 -0
- package/dist/test-utils/interleave.d.ts +12 -0
- package/dist/test-utils/interleave.d.ts.map +1 -0
- package/dist/test-utils/interleave.js +26 -0
- package/dist/testWorker.d.ts +8 -0
- package/dist/testWorker.d.ts.map +1 -0
- package/dist/testWorker.js +9 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workflow/dispatchRoute.js +1 -1
- package/package.json +1 -1
- package/src/audit/auditEvent.ts +1 -1
- package/src/audit/chain.ts +551 -0
- package/src/audit/citations.ts +63 -0
- package/src/capability/bindings.ts +61 -0
- package/src/capability/capability.ts +156 -19
- package/src/capability/erasure.ts +82 -0
- package/src/capability/personColumns.ts +83 -0
- package/src/capability/personRows.ts +125 -0
- package/src/capability/purge.ts +158 -21
- package/src/capability/settings.ts +7 -0
- package/src/capability/vectorIndex.ts +60 -0
- package/src/cli/commandCatalog.generated.ts +487 -2
- package/src/cli/commandFacts.ts +12 -3
- package/src/cloudflare-test.d.ts +3 -1
- package/src/consent/purpose.ts +160 -0
- package/src/consent/reader.ts +134 -0
- package/src/controlPlane/audit/actions.ts +15 -4
- package/src/controlPlane/capability.ts +99 -20
- package/src/controlPlane/context.ts +10 -2
- package/src/controlPlane/data/connection.ts +16 -2
- package/src/controlPlane/data/connections.ts +231 -0
- package/src/controlPlane/data/hold.ts +52 -0
- package/src/controlPlane/data/oidcTrust.ts +249 -0
- package/src/controlPlane/data/tables.ts +7 -0
- package/src/controlPlane/data/trustFirstCall.ts +37 -0
- package/src/controlPlane/discovery/adminRoute.ts +71 -9
- package/src/controlPlane/discovery/routeInput.ts +86 -0
- package/src/controlPlane/discovery/toolName.ts +23 -0
- package/src/controlPlane/erasure/attest.ts +205 -0
- package/src/controlPlane/erasure/read.ts +169 -0
- package/src/controlPlane/error/errors.ts +69 -2
- package/src/controlPlane/evidence/derive.ts +426 -0
- package/src/controlPlane/evidence/mapping.ts +118 -0
- package/src/controlPlane/evidence/pack.ts +190 -0
- package/src/controlPlane/http/connections.ts +129 -0
- package/src/controlPlane/http/guard.ts +118 -9
- package/src/controlPlane/http/handlers.ts +172 -40
- package/src/controlPlane/http/holds.ts +108 -0
- package/src/controlPlane/http/presence.ts +71 -0
- package/src/controlPlane/http/processing.ts +19 -0
- package/src/controlPlane/http/responses.ts +590 -7
- package/src/controlPlane/http/routes.ts +302 -3
- package/src/controlPlane/http/schemas.ts +126 -0
- package/src/controlPlane/http/verify.ts +19 -2
- package/src/controlPlane/http/verifyTrust.ts +176 -0
- package/src/controlPlane/migrations/0004_connection_trusts.ts +30 -0
- package/src/controlPlane/migrations/0005_holds.ts +40 -0
- package/src/controlPlane/processing/derive.ts +359 -0
- package/src/controlPlane/processing/read.ts +102 -0
- package/src/controlPlane/scope/kind.ts +63 -0
- package/src/controlPlane/scope/scope.ts +65 -3
- package/src/controlPlane/wire.ts +26 -0
- package/src/createBackend.ts +31 -6
- package/src/createEntrypoint.ts +17 -0
- package/src/error/payload.ts +234 -0
- package/src/error/pithyError.ts +42 -0
- package/src/http/routeSpec.ts +75 -5
- package/src/http/verification.ts +3 -1
- package/src/i18n/translator.ts +2 -1
- package/src/index.ts +2 -0
- package/src/mcp/annotations.ts +38 -0
- package/src/mcp/tools.ts +220 -0
- package/src/openapi/document.ts +8 -28
- package/src/openapi/operations.ts +62 -0
- package/src/presence/channel.ts +46 -0
- package/src/presence/declaration.ts +283 -0
- package/src/presence/durableObject.ts +287 -0
- package/src/presence/frames.ts +153 -0
- package/src/presence/http/guard.ts +23 -0
- package/src/presence/http/responses.ts +63 -0
- package/src/presence/http/routes.ts +165 -0
- package/src/presence/http/schemas.ts +39 -0
- package/src/presence/names.ts +45 -0
- package/src/presence/room.ts +145 -0
- package/src/presence/ticket.ts +145 -0
- package/src/processing/activity.ts +289 -0
- package/src/processing/composed.ts +33 -0
- package/src/processing/vocabulary.ts +43 -0
- package/src/queue/consumer.ts +104 -0
- package/src/retention/composed.ts +75 -0
- package/src/retention/holds.ts +175 -0
- package/src/retention/holdsPurge.ts +50 -0
- package/src/retention/policy.ts +305 -0
- package/src/session/rotated.ts +53 -0
- package/src/test-utils/connectionFixtures.ts +58 -0
- package/src/test-utils/interleave.ts +31 -0
- package/src/testWorker.ts +13 -0
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { Context } from "hono";
|
|
5
|
+
import type { PithyHonoEnv } from "../../capability/capability";
|
|
6
|
+
import type { ConsentPurpose } from "../../consent/purpose";
|
|
7
|
+
import { type ConsentReader, composedConsent, composedConsentPurposes } from "../../consent/reader";
|
|
8
|
+
import { causeMessage } from "../../error/cause";
|
|
9
|
+
import type { PrivacyInventory } from "../../privacy/report";
|
|
10
|
+
import { composedProcessing, effectiveProcessing } from "../../processing/composed";
|
|
11
|
+
import { workerVersionMetadata } from "../../worker/identity";
|
|
12
|
+
import type { ControlPlaneContext } from "../context";
|
|
13
|
+
import type { EvidenceControlId, EvidenceState } from "../evidence/mapping";
|
|
14
|
+
import { readEvidencePack } from "../evidence/pack";
|
|
15
|
+
import type { ControlPlaneHandlerDeps } from "../http/handlers";
|
|
16
|
+
import type { ControlPlaneProcessingResponse } from "../http/responses";
|
|
17
|
+
import { EVIDENCE_READ_SCOPE, PRIVACY_READ_SCOPE } from "../scope/scope";
|
|
18
|
+
import { type ConsentInput, deriveProcessingRecord, workersAiRecipients } from "./derive";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The reads behind the processing record: what the Worker composes, declares and records, gathered once per call.
|
|
22
|
+
* Each read that fails costs the record what it would have said, named in `notDerivable`, and the record still
|
|
23
|
+
* answers. A cause goes to the Worker's log and never into the record.
|
|
24
|
+
*
|
|
25
|
+
* **It never widens a scope.** The measures carry the evidence pack's states only when the connection also holds
|
|
26
|
+
* `evidence:read`; without it they are cited by control and their state is null. A gap names a stored table or field
|
|
27
|
+
* only when the connection also holds `privacy:read`; without it, the capability and the class.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The privacy inventory, or null where the walk threw. */
|
|
31
|
+
function inventoryOf(c: Context<PithyHonoEnv>, deps: ControlPlaneHandlerDeps): PrivacyInventory | null {
|
|
32
|
+
try {
|
|
33
|
+
return deps.privacyInventory();
|
|
34
|
+
} catch (cause) {
|
|
35
|
+
c.var.log.warn("processing: privacy inventory unreadable", { reason: causeMessage(cause) ?? String(cause) });
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What the composed consent capability says, or that none is composed. */
|
|
41
|
+
async function consentOf(c: Context<PithyHonoEnv>, deps: ControlPlaneHandlerDeps): Promise<ConsentInput> {
|
|
42
|
+
const composition = deps.composition();
|
|
43
|
+
const declared = composedConsentPurposes(composition);
|
|
44
|
+
const reader = composedConsent(composition, c.env as Record<string, unknown>);
|
|
45
|
+
if (reader === null) return { state: "not-composed", declared };
|
|
46
|
+
if (reader.standings !== undefined) {
|
|
47
|
+
try {
|
|
48
|
+
return { state: "read", standings: await reader.standings() };
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
c.var.log.warn("processing: consent record unreadable", { reason: causeMessage(cause) ?? String(cause) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { state: "unreadable", catalog: catalogOf(c, reader) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The reader's whole catalog from memory, or null where it cannot list one. */
|
|
57
|
+
function catalogOf(c: Context<PithyHonoEnv>, reader: ConsentReader): readonly ConsentPurpose[] | null {
|
|
58
|
+
if (reader.purposes === undefined) return null;
|
|
59
|
+
try {
|
|
60
|
+
return reader.purposes();
|
|
61
|
+
} catch (cause) {
|
|
62
|
+
c.var.log.warn("processing: consent catalog unreadable", { reason: causeMessage(cause) ?? String(cause) });
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Each control's state, read through the evidence pack, or null without `evidence:read`. */
|
|
68
|
+
async function measuresOf(
|
|
69
|
+
c: Context<PithyHonoEnv>,
|
|
70
|
+
deps: ControlPlaneHandlerDeps,
|
|
71
|
+
context: ControlPlaneContext,
|
|
72
|
+
): Promise<Partial<Record<EvidenceControlId, EvidenceState>> | null> {
|
|
73
|
+
if (!context.grantedScopes.includes(EVIDENCE_READ_SCOPE)) return null;
|
|
74
|
+
const pack = await readEvidencePack(c, deps, context);
|
|
75
|
+
return Object.fromEntries(pack.controls.map((entry) => [entry.control, entry.state]));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Read everything the record derives from, and derive it. */
|
|
79
|
+
export async function readProcessingRecord(
|
|
80
|
+
c: Context<PithyHonoEnv>,
|
|
81
|
+
deps: ControlPlaneHandlerDeps,
|
|
82
|
+
context: ControlPlaneContext,
|
|
83
|
+
): Promise<ControlPlaneProcessingResponse> {
|
|
84
|
+
const env = c.env as Record<string, unknown>;
|
|
85
|
+
const composition = deps.composition();
|
|
86
|
+
const [consent, measures] = await Promise.all([consentOf(c, deps), measuresOf(c, deps, context)]);
|
|
87
|
+
const record = deriveProcessingRecord({
|
|
88
|
+
activities: composedProcessing() ?? effectiveProcessing(undefined, composition),
|
|
89
|
+
inventory: inventoryOf(c, deps),
|
|
90
|
+
retention: deps.retention?.() ?? null,
|
|
91
|
+
consent,
|
|
92
|
+
bindingRecipients: workersAiRecipients(composition, env),
|
|
93
|
+
measures,
|
|
94
|
+
fieldNames: context.grantedScopes.includes(PRIVACY_READ_SCOPE),
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
environment: context.environment,
|
|
98
|
+
generatedAt: deps.now().toISOString(),
|
|
99
|
+
version: workerVersionMetadata(c.env).id,
|
|
100
|
+
...record,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What kind of operation a control-plane scope grants, declared by the capability that owns the scope.
|
|
8
|
+
*
|
|
9
|
+
* Scopes are federated and open, so a management client cannot tell from the string whether `auth:users:suspend`
|
|
10
|
+
* reads, writes or does something worse. The kind is the closed vocabulary it maps its own roles onto instead: the
|
|
11
|
+
* app tools' effects, with `reads` beneath them and `credentials` (mints, rotates or revokes a credential) above.
|
|
12
|
+
* Ordered from least to most restrictive. Written out rather than spread from `TOOL_EFFECTS`, so a scope declaration
|
|
13
|
+
* stays importable from a browser program without the route machinery; `kind.test.ts` holds the two together.
|
|
14
|
+
*/
|
|
15
|
+
export const SCOPE_KINDS = ["reads", "writes", "sends", "charges", "deletes", "credentials"] as const;
|
|
16
|
+
|
|
17
|
+
export const ScopeKind = z
|
|
18
|
+
.enum(SCOPE_KINDS)
|
|
19
|
+
.describe(
|
|
20
|
+
"What kind of operation a scope grants: `reads`, `writes`, `sends` (mail or a message leaves the system), `charges` (money or an entitlement moves), `deletes`, or `credentials` (a credential is minted, rotated or revoked).",
|
|
21
|
+
);
|
|
22
|
+
export type ScopeKind = z.infer<typeof ScopeKind>;
|
|
23
|
+
|
|
24
|
+
/** The kind a client assumes for a scoped route that states none: a Worker too old to say. */
|
|
25
|
+
export const MOST_RESTRICTIVE_SCOPE_KIND: ScopeKind = "credentials";
|
|
26
|
+
|
|
27
|
+
/** The part of an admin route a kind is read from. */
|
|
28
|
+
export interface ScopedRoute {
|
|
29
|
+
readonly scope: string | null;
|
|
30
|
+
readonly kind?: ScopeKind | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The kind of a route as a client reads it: its declared kind, {@link MOST_RESTRICTIVE_SCOPE_KIND} for a scoped
|
|
35
|
+
* route that declares none, and null for a route with no scope, which grants nothing to classify.
|
|
36
|
+
*/
|
|
37
|
+
export function scopeKindOf(route: ScopedRoute): ScopeKind | null {
|
|
38
|
+
if (route.scope === null) return null;
|
|
39
|
+
return route.kind ?? MOST_RESTRICTIVE_SCOPE_KIND;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One scope whose routes declare more than one kind. */
|
|
43
|
+
export interface ScopeKindConflict {
|
|
44
|
+
readonly scope: string;
|
|
45
|
+
/** Every kind declared for it, in the order first seen. */
|
|
46
|
+
readonly kinds: readonly ScopeKind[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Every scope whose routes disagree about its kind. Holding a scope confers every route that requires it, so a
|
|
51
|
+
* scope has exactly one kind, and two routes declaring different ones is a declaration to fix.
|
|
52
|
+
*/
|
|
53
|
+
export function scopeKindConflicts(routes: Iterable<ScopedRoute>): ScopeKindConflict[] {
|
|
54
|
+
const kinds = new Map<string, ScopeKind[]>();
|
|
55
|
+
for (const route of routes) {
|
|
56
|
+
const kind = scopeKindOf(route);
|
|
57
|
+
if (route.scope === null || kind === null) continue;
|
|
58
|
+
const seen = kinds.get(route.scope) ?? [];
|
|
59
|
+
if (!seen.includes(kind)) seen.push(kind);
|
|
60
|
+
kinds.set(route.scope, seen);
|
|
61
|
+
}
|
|
62
|
+
return [...kinds].filter(([, seen]) => seen.length > 1).map(([scope, seen]) => ({ scope, kinds: seen }));
|
|
63
|
+
}
|
|
@@ -79,7 +79,7 @@ export const KEYS_ROTATE_SCOPE: ControlPlaneScope = "keys:rotate";
|
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* Read where each database this Worker binds stands: what is applied, what is pending, what is undeclared,
|
|
82
|
-
* and which run put it there
|
|
82
|
+
* and which run put it there. Read-only by construction — every route behind it is a `GET`, and
|
|
83
83
|
* nothing over the seam runs, reverses or resets a migration. The two migration health keys on the seam's
|
|
84
84
|
* manifest entry sit behind it too, so a connection without it gets no number rather than a zero.
|
|
85
85
|
*/
|
|
@@ -87,7 +87,7 @@ export const MIGRATIONS_READ_SCOPE: ControlPlaneScope = "migrations:read";
|
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* Read this Worker's full privacy inventory: every storage schema holding a marked field, each marked field's
|
|
90
|
-
* path, its class, and where it is stored
|
|
90
|
+
* path, its class, and where it is stored. Read-only — the one route behind it is a `GET`.
|
|
91
91
|
*
|
|
92
92
|
* **Its own scope rather than `manifest:read`**, because field names are the kind of thing an adopter may
|
|
93
93
|
* reasonably withhold from a client that has no business seeing them. The manifest's privacy block carries
|
|
@@ -95,11 +95,73 @@ export const MIGRATIONS_READ_SCOPE: ControlPlaneScope = "migrations:read";
|
|
|
95
95
|
*/
|
|
96
96
|
export const PRIVACY_READ_SCOPE: ControlPlaneScope = "privacy:read";
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Read this Worker's evidence pack: the SOC 2 and ISO 27001 evidence it derives from its own declarations.
|
|
100
|
+
* Read-only — the one route behind it is a `GET`, and it performs nothing it reports on.
|
|
101
|
+
*
|
|
102
|
+
* **Its own scope**, because the pack reads across the seam: every connection's principal, kind, purpose and
|
|
103
|
+
* grant, and every route's verification. It never widens another scope. A fact a capability keeps behind its own
|
|
104
|
+
* read — migration counts behind {@link MIGRATIONS_READ_SCOPE}, a capability's health behind that capability's
|
|
105
|
+
* scope — stays behind it, and a connection without that scope gets `not-derivable` naming it.
|
|
106
|
+
*/
|
|
107
|
+
export const EVIDENCE_READ_SCOPE: ControlPlaneScope = "evidence:read";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Read the erasure attestation: one subject's purge, or every purge in a period, with the audit events that
|
|
111
|
+
* record each step. Read-only — both routes behind it are `GET`s — and kind `reads`.
|
|
112
|
+
*
|
|
113
|
+
* **Its own scope**, because it answers across subjects: which accounts and organizations were deleted, by which
|
|
114
|
+
* kind of actor and why. It carries nothing about a person beyond the id the lifecycle trail already keeps, and it
|
|
115
|
+
* never widens `audit:events:read`: it cites an event by id and chain position, never its contents.
|
|
116
|
+
*/
|
|
117
|
+
export const ERASURE_READ_SCOPE: ControlPlaneScope = "erasure:read";
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Read the record of processing activities, GDPR Article 30: each activity's purpose, data classes, legal basis,
|
|
121
|
+
* recipients, retention and measures, and every gap. Read-only — the one route behind it is a `GET` — and kind
|
|
122
|
+
* `reads`.
|
|
123
|
+
*
|
|
124
|
+
* **Its own scope**, because it reads across the Worker: every activity, the consent counts behind each consent
|
|
125
|
+
* basis, and where each stored class sits. It never widens another: the measures carry the evidence pack's states
|
|
126
|
+
* only for a connection that also holds {@link EVIDENCE_READ_SCOPE}, and a gap names a stored table or field only for
|
|
127
|
+
* one that also holds {@link PRIVACY_READ_SCOPE}. Without it, a gap names the capability and the class.
|
|
128
|
+
*/
|
|
129
|
+
export const PROCESSING_READ_SCOPE: ControlPlaneScope = "processing:read";
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Place and release a legal hold on a user, an organization or a tenant's audit trail. A write — both routes
|
|
133
|
+
* behind it are `POST`s — and kind `writes`, so `pithy dashboard connect` offers it and never grants it by default.
|
|
134
|
+
*
|
|
135
|
+
* **It can only preserve.** A hold suspends purges, prunes and inactivity deletions; releasing one returns the subject
|
|
136
|
+
* to the policy the Worker deploys with. No route behind it, or anywhere, shortens a retention.
|
|
137
|
+
*/
|
|
138
|
+
export const RETENTION_HOLD_SCOPE: ControlPlaneScope = "retention:hold";
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Read the legal holds: every hold, standing or released, with who placed it and why. Read-only — the one
|
|
142
|
+
* route behind it is a `GET` — and kind `reads`. The manifest's count of standing holds sits behind it too:
|
|
143
|
+
* `activeHolds`, in the health of the `controlplane` entry.
|
|
144
|
+
*/
|
|
145
|
+
export const RETENTION_READ_SCOPE: ControlPlaneScope = "retention:read";
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* List every connection bound to this Worker's environment: who authorized it, what kind of client it is for, its purpose, its
|
|
149
|
+
* credential kinds, its grant and when it was registered or revoked. Kind `reads`. Never key material.
|
|
150
|
+
*/
|
|
151
|
+
export const CONNECTIONS_READ_SCOPE: ControlPlaneScope = "connections:read";
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Revoke every key and trust another connection in this Worker's environment holds, with a reason. Kind
|
|
155
|
+
* `credentials`. Refused on the caller's own connection, which ends through `disconnect`.
|
|
156
|
+
*/
|
|
157
|
+
export const CONNECTIONS_REVOKE_SCOPE: ControlPlaneScope = "connections:revoke";
|
|
158
|
+
|
|
98
159
|
/**
|
|
99
160
|
* The seam's base grant — what `pithy dashboard connect` grants whatever the Worker composes. Ping is absent
|
|
100
161
|
* because it is not grantable; see {@link ANY_VERIFIED_CALLER}.
|
|
101
162
|
*
|
|
102
|
-
* **
|
|
163
|
+
* **The seam's reads are absent on purpose** — {@link MIGRATIONS_READ_SCOPE}, {@link PRIVACY_READ_SCOPE},
|
|
164
|
+
* {@link EVIDENCE_READ_SCOPE}, {@link ERASURE_READ_SCOPE}, {@link PROCESSING_READ_SCOPE}. Each is a read —
|
|
103
165
|
* every route requiring it is a `GET` — so `connect` derives it from the seam's `adminRoutes` the way it
|
|
104
166
|
* derives every capability's reads. The base exists for `keys:rotate`, which is not a read and would
|
|
105
167
|
* otherwise drop out.
|
package/src/controlPlane/wire.ts
CHANGED
|
@@ -33,6 +33,32 @@
|
|
|
33
33
|
*/
|
|
34
34
|
export const CONTROL_PLANE_HEADER = "pithy-control-plane";
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The header a workload presents its OIDC token on, when it calls under a trust rather than a key (#722,
|
|
38
|
+
* `docs/AGENT-ACCESS.md` §6.2).
|
|
39
|
+
*
|
|
40
|
+
* Not `Authorization`, for the same reason as {@link CONTROL_PLANE_HEADER}, and not that header either: a call
|
|
41
|
+
* presents exactly one credential, and two names make "which one is this" a question the verifier never has
|
|
42
|
+
* to guess at. The token is the provider's own, verified per call; nothing is exchanged for it.
|
|
43
|
+
*/
|
|
44
|
+
export const CONTROL_PLANE_OIDC_HEADER = "pithy-control-plane-oidc";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The header naming which of this environment's trusts an OIDC call claims, by its id — the way a token's
|
|
48
|
+
* `kid` names a key. Read before the token is verified, so it selects and grants nothing: the trust's issuer,
|
|
49
|
+
* audience and claims are what the token is then held to.
|
|
50
|
+
*/
|
|
51
|
+
export const CONTROL_PLANE_TRUST_HEADER = "pithy-control-plane-trust";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The header naming the one scope an OIDC call exercises.
|
|
55
|
+
*
|
|
56
|
+
* A signed control-plane token names its scope inside the signature. A provider's identity token cannot — it
|
|
57
|
+
* proves **who**, never **what** — so the call names it out loud, and the seam checks it against the
|
|
58
|
+
* connection's grant exactly as it checks a token's. A call that names none is refused.
|
|
59
|
+
*/
|
|
60
|
+
export const CONTROL_PLANE_SCOPE_HEADER = "pithy-control-plane-scope";
|
|
61
|
+
|
|
36
62
|
/**
|
|
37
63
|
* The response header carrying the build that answered a control-plane call.
|
|
38
64
|
*
|
package/src/createBackend.ts
CHANGED
|
@@ -30,9 +30,13 @@ import { buildKvRegistry, composeKv, type KvRegistry } from "./kv/namespaces";
|
|
|
30
30
|
import { loggingErrorHandler } from "./logger/errorHandler";
|
|
31
31
|
import type { Logger } from "./logger/logger";
|
|
32
32
|
import { bindRequestContext, createWorkerLogger } from "./logger/worker";
|
|
33
|
+
import { assertToolDeclarations } from "./mcp/tools";
|
|
33
34
|
import type { RouteGroup } from "./openapi/document";
|
|
34
35
|
import { recordRouteGroups } from "./openapi/routeGroups";
|
|
36
|
+
import { registerPresenceRoutes } from "./presence/http/routes";
|
|
35
37
|
import { refuseEncryptedKvStores } from "./privacy/encryptedColumns";
|
|
38
|
+
import { effectiveProcessing, recordProcessing } from "./processing/composed";
|
|
39
|
+
import { effectiveRetention, recordRetention } from "./retention/composed";
|
|
36
40
|
import { HEALTH_PATH } from "./worker/health";
|
|
37
41
|
import { workerVersion } from "./worker/identity";
|
|
38
42
|
import { registeredWorkflowBinding } from "./workflow/bindings";
|
|
@@ -62,6 +66,19 @@ export interface CreateBackendOptions<Caps extends readonly Capability[], App ex
|
|
|
62
66
|
* absent — e.g. a non-provisioned local run. Falls through to `unknown` when neither is present.
|
|
63
67
|
*/
|
|
64
68
|
env?: string;
|
|
69
|
+
/**
|
|
70
|
+
* How long this Worker keeps deleted accounts, the audit trail and each class of personal data — the
|
|
71
|
+
* `retention` block of the Worker's `pithy.config.ts`, which `createEntrypoint(config)` hands through. Parsed and
|
|
72
|
+
* resolved here, at assembly, so a block that does not parse, or keeps a floored audit action under the floor,
|
|
73
|
+
* refuses to deploy. See `retention/policy.ts`.
|
|
74
|
+
*/
|
|
75
|
+
retention?: unknown;
|
|
76
|
+
/**
|
|
77
|
+
* The adopter's own processing activities, and the legal bases they state for a capability's — the `processing`
|
|
78
|
+
* block of the Worker's `pithy.config.ts`. Resolved here over what this Worker composes, so a block that does not
|
|
79
|
+
* parse, or overrides an activity nothing declares, refuses to deploy. See `processing/activity.ts`.
|
|
80
|
+
*/
|
|
81
|
+
processing?: unknown;
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
/** The Hono env `createBackend` returns — `db`/`kv`/`workflows` typed precisely from the merged capabilities. */
|
|
@@ -168,8 +185,12 @@ export function createBackend<
|
|
|
168
185
|
|
|
169
186
|
// Recorded after the hooks, so a capability found later is one whose own wiring is complete. This is
|
|
170
187
|
// the only way back to a composed seam from a Workflow step, which the runtime constructs with `env`
|
|
171
|
-
// and nothing else — see `capability/composition.ts
|
|
188
|
+
// and nothing else — see `capability/composition.ts`.
|
|
172
189
|
recordComposition(all);
|
|
190
|
+
// The retention block, resolved over what this Worker composes and recorded beside it, so a purge, a prune and
|
|
191
|
+
// the inactivity pass read the policy the routes report. A block under the audit floor throws here.
|
|
192
|
+
recordRetention(effectiveRetention(options.retention, all));
|
|
193
|
+
recordProcessing(effectiveProcessing(options.processing, all));
|
|
173
194
|
|
|
174
195
|
const databases = composeDatabases(all);
|
|
175
196
|
// Every composed capability's English, merged under the domain rule. Built once at assembly rather
|
|
@@ -179,7 +200,7 @@ export function createBackend<
|
|
|
179
200
|
// `@pithy-sh/i18n`'s middleware replaces it per request; nothing else ever does.
|
|
180
201
|
const defaultTranslator = bakedTranslator(catalogFor(messages, DEFAULT_LOCALE));
|
|
181
202
|
const namespaces = composeKv(all);
|
|
182
|
-
// An encrypted field in a KV store is refused here, at boot
|
|
203
|
+
// An encrypted field in a KV store is refused here, at boot: a KV envelope cannot be found by key version,
|
|
183
204
|
// so it could never be rotated. The refusal names the store, the field and D1.
|
|
184
205
|
refuseEncryptedKvStores(all);
|
|
185
206
|
const workflows = composeWorkflows(all);
|
|
@@ -203,8 +224,7 @@ export function createBackend<
|
|
|
203
224
|
// registries); the precise types ride on the return below.
|
|
204
225
|
const app = new Hono<PithyHonoEnv>();
|
|
205
226
|
// The logging handler, not the bare encoder: a fault's `action` names the var or command an operator
|
|
206
|
-
// must fix, `clientError` strips it on the way to the wire
|
|
207
|
-
// reached no surface at all in a deployed Worker — one access log line reading `status: 500` (#521).
|
|
227
|
+
// must fix, and `clientError` strips it on the way to the wire, so the log is the one surface it reaches.
|
|
208
228
|
app.onError(loggingErrorHandler);
|
|
209
229
|
|
|
210
230
|
// Set once at assembly (the per-request `env` constraint holds: correlation binds per request below,
|
|
@@ -296,7 +316,7 @@ export function createBackend<
|
|
|
296
316
|
* The path is `worker/health.ts`'s, not a literal here. Three other sites need the same string — the
|
|
297
317
|
* CLI's route allowlist, `pithy deploy`'s post-deploy probe, and the bare home screen a no-auth
|
|
298
318
|
* scaffold seeds — and that last one is in somebody else's repository, rendering nothing but this
|
|
299
|
-
* request's answer
|
|
319
|
+
* request's answer.
|
|
300
320
|
*
|
|
301
321
|
* The version is what turns `pithy deploy`'s post-deploy check from a liveness probe into an
|
|
302
322
|
* assertion. `status: "ok"` at the declared domain proves *a* Worker is there; it does not prove it is
|
|
@@ -332,7 +352,7 @@ export function createBackend<
|
|
|
332
352
|
// Readiness guards after every capability's middleware, so the verifier, `emit` and every rate limit
|
|
333
353
|
// are in place, and before any capability's routes, so a terminal handler cannot outrun them. Each is
|
|
334
354
|
// skipped on the admin surface: the control plane answers whatever else is wrong with the Worker,
|
|
335
|
-
// because it is how a management client finds out what that is
|
|
355
|
+
// because it is how a management client finds out what that is.
|
|
336
356
|
registerReadinessGuards(
|
|
337
357
|
app,
|
|
338
358
|
adminSurface,
|
|
@@ -343,9 +363,14 @@ export function createBackend<
|
|
|
343
363
|
for (const cap of all) {
|
|
344
364
|
const start = app.routes.length;
|
|
345
365
|
cap.routes?.(app);
|
|
366
|
+
// A capability's presence routes are its own, tagged with it, under the base path it declared.
|
|
367
|
+
if (cap.presence && cap.presence.rooms.length > 0) registerPresenceRoutes(app, cap.presence);
|
|
346
368
|
groups.push({ tag: cap.name, routes: app.routes.slice(start) });
|
|
347
369
|
}
|
|
348
370
|
recordRouteGroups(app, groups);
|
|
371
|
+
// A route that opts in as an MCP tool says so on `describeRoute`, which cannot see its method — so a non-GET
|
|
372
|
+
// claiming to be a read, or two routes sharing an operation, is refused here, at boot (`mcp/tools.ts`).
|
|
373
|
+
assertToolDeclarations(groups);
|
|
349
374
|
|
|
350
375
|
return app as unknown as Hono<BackendEnv<readonly [...Caps, App]>>;
|
|
351
376
|
}
|
package/src/createEntrypoint.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import type { ExecutionContext, ForwardableEmailMessage } from "@cloudflare/workers-types";
|
|
5
5
|
import type { Capability, CapabilityEmailHandler } from "./capability/capability";
|
|
6
6
|
import { type CreateBackendOptions, createBackend } from "./createBackend";
|
|
7
|
+
import { createWorkerLogger } from "./logger/worker";
|
|
8
|
+
import { type CapabilityQueueHandler, dispatchQueueBatch, type QueueBatch } from "./queue/consumer";
|
|
7
9
|
import { withLocalServices } from "./worker/serviceLoopback";
|
|
8
10
|
import { triggerWorkflow } from "./workflow/dispatch";
|
|
9
11
|
import { composeWorkflows, scheduledWorkflows } from "./workflow/register";
|
|
@@ -54,6 +56,12 @@ export interface PithyEntrypoint {
|
|
|
54
56
|
* tested in staging.
|
|
55
57
|
*/
|
|
56
58
|
scheduled?: (controller: unknown, env: Record<string, unknown>, ctx: ExecutionContext) => Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* The Worker `queue` handler — hands each composed capability the messages of a batch that name it (#824).
|
|
61
|
+
* **Present only when at least one capability consumes a queue**, for `email`'s reason: an inert handler would
|
|
62
|
+
* acknowledge every message delivered to it and drop it.
|
|
63
|
+
*/
|
|
64
|
+
queue?: (batch: QueueBatch, env: Record<string, unknown>, ctx: ExecutionContext) => Promise<void>;
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
/** The cron expression a `scheduled` event fired for, or `undefined` when the event names none. */
|
|
@@ -131,6 +139,15 @@ export function createEntrypoint<
|
|
|
131
139
|
};
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
// One `queue` export for every capability that consumes, routed by the capability each message body names.
|
|
143
|
+
const consumers = new Map<string, CapabilityQueueHandler>(
|
|
144
|
+
all.flatMap((cap) => (cap.queue ? [[cap.name, cap.queue] as const] : [])),
|
|
145
|
+
);
|
|
146
|
+
if (consumers.size > 0) {
|
|
147
|
+
entrypoint.queue = (batch, env, ctx) =>
|
|
148
|
+
dispatchQueueBatch(batch, consumers, withLocalServices(env), ctx, createWorkerLogger({ name: "queue" }));
|
|
149
|
+
}
|
|
150
|
+
|
|
134
151
|
// Only expose an `email` handler when something actually handles inbound mail — an inert one would
|
|
135
152
|
// silently drop any message Email Routing delivered to this Worker.
|
|
136
153
|
if (handlers.length > 0) {
|