@pithy-sh/core 0.13.0 → 0.14.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 +433 -3
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
- package/dist/cli/commandCatalog.generated.js +449 -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 +482 -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
|
@@ -2,24 +2,36 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
import type { Context } from "hono";
|
|
5
|
-
import type { PithyHonoEnv } from "../../capability/capability";
|
|
5
|
+
import type { Capability, PithyHonoEnv } from "../../capability/capability";
|
|
6
|
+
import { commandName } from "../../cli/command";
|
|
6
7
|
import { causeMessage } from "../../error/cause";
|
|
8
|
+
import type { RouteGroup } from "../../openapi/document";
|
|
9
|
+
import type { PresenceDeclaration } from "../../presence/declaration";
|
|
7
10
|
import { privacyAttestation, privacyPosture } from "../../privacy/posture";
|
|
8
11
|
import type { PrivacyInventory } from "../../privacy/report";
|
|
9
12
|
import type { RetirementVerdict } from "../../privacy/retirement";
|
|
13
|
+
import type { EffectiveRetention } from "../../retention/policy";
|
|
10
14
|
import { workerVersionMetadata } from "../../worker/identity";
|
|
11
15
|
import { ControlPlaneAuditActions, safeEmit } from "../audit/actions";
|
|
12
16
|
import type { ControlPlaneConfig } from "../config/config";
|
|
13
17
|
import type { ControlPlaneContext } from "../context";
|
|
14
|
-
import {
|
|
18
|
+
import type { RegisteredKey } from "../data/connection";
|
|
19
|
+
import { type ConnectionForWrite, readConnectionForWrite, writeKeys } from "../data/connections";
|
|
15
20
|
import { appendKey, expireKey, pruneKeys } from "../data/keyLifecycle";
|
|
16
|
-
import {
|
|
21
|
+
import type { OidcTrust } from "../data/oidcTrust";
|
|
22
|
+
import type { ControlPlaneDatabase } from "../data/tables";
|
|
17
23
|
import type { CapabilityDeclaration, ControlPlaneManifestWire } from "../discovery/adminRoute";
|
|
18
24
|
import { type CapabilityHealthSource, readCapabilityHealth } from "../discovery/health";
|
|
19
25
|
import { healthWire } from "../discovery/healthSummary";
|
|
26
|
+
import { routeInputs, withRouteInputs } from "../discovery/routeInput";
|
|
27
|
+
import { readErasurePeriod, readErasureSubject } from "../erasure/read";
|
|
20
28
|
import { ControlPlaneInvalidCredentialError, ControlPlaneKeyConflictError } from "../error/errors";
|
|
29
|
+
import { readEvidencePack } from "../evidence/pack";
|
|
21
30
|
import { type ComposedMigrationDatabase, readDatabaseMigrations } from "../migrationState/migrationState";
|
|
22
31
|
import type {
|
|
32
|
+
ControlPlaneErasurePeriodResponse,
|
|
33
|
+
ControlPlaneErasureSubjectResponse,
|
|
34
|
+
ControlPlaneEvidenceResponse,
|
|
23
35
|
ControlPlaneKeysResponse,
|
|
24
36
|
ControlPlaneMigrationsResponse,
|
|
25
37
|
ControlPlanePingResponse,
|
|
@@ -27,8 +39,15 @@ import type {
|
|
|
27
39
|
ExpireKeyResponse,
|
|
28
40
|
PublicKeyView,
|
|
29
41
|
RegisterKeyResponse,
|
|
42
|
+
TrustView,
|
|
30
43
|
} from "./responses";
|
|
31
|
-
import type {
|
|
44
|
+
import type {
|
|
45
|
+
ErasurePeriodQuery,
|
|
46
|
+
ErasureSubjectParams,
|
|
47
|
+
ExpireKeyParams,
|
|
48
|
+
ExpireKeyRequest,
|
|
49
|
+
RegisterKeyRequest,
|
|
50
|
+
} from "./schemas";
|
|
32
51
|
|
|
33
52
|
/**
|
|
34
53
|
* The seam's own route handlers.
|
|
@@ -77,25 +96,45 @@ export interface ControlPlaneHandlerDeps {
|
|
|
77
96
|
composedHealth: () => ReadonlyMap<string, CapabilityHealthSource>;
|
|
78
97
|
/**
|
|
79
98
|
* Every database this Worker binds, with the provider this build composes for it — captured by the same
|
|
80
|
-
* `compose` hook through `collectMigrationSets`, the function `pithy migrate` composes with
|
|
99
|
+
* `compose` hook through `collectMigrationSets`, the function `pithy migrate` composes with.
|
|
81
100
|
*/
|
|
82
101
|
composedMigrations: () => readonly ComposedMigrationDatabase[];
|
|
83
102
|
/**
|
|
84
|
-
* The privacy inventory of every storage schema this Worker composed
|
|
103
|
+
* The privacy inventory of every storage schema this Worker composed, walked on the first call and
|
|
85
104
|
* cached for the isolate. A thunk, so a Worker whose control plane is never called never walks.
|
|
86
105
|
*/
|
|
87
106
|
privacyInventory: () => PrivacyInventory;
|
|
88
107
|
/**
|
|
89
|
-
* The field key's retirement verdict
|
|
108
|
+
* The field key's retirement verdict: the guard over this Worker's encrypted columns, read through its own
|
|
90
109
|
* bindings. Never throws: whatever it cannot read is incomplete coverage.
|
|
91
110
|
*/
|
|
92
111
|
retirement: (env: Record<string, unknown>) => Promise<RetirementVerdict>;
|
|
112
|
+
/**
|
|
113
|
+
* Every presence room kind this Worker composed, by kind, with the declaration that serves it — captured
|
|
114
|
+
* by the same `compose` hook. A thunk for the same reason as {@link composedCapabilities}.
|
|
115
|
+
*/
|
|
116
|
+
composedPresence: () => ReadonlyMap<string, PresenceDeclaration>;
|
|
117
|
+
/**
|
|
118
|
+
* Every capability this Worker composed, as the objects themselves — the evidence pack reads which one records
|
|
119
|
+
* audit events and the bindings it writes through. A thunk for the same reason as the rest.
|
|
120
|
+
*/
|
|
121
|
+
composition: () => readonly Capability[];
|
|
122
|
+
/**
|
|
123
|
+
* The route table `createBackend` recorded for the app these routes mounted on, or `undefined` for an app it did
|
|
124
|
+
* not assemble. Read per call, after every capability's routes have mounted, so the evidence pack sees them all.
|
|
125
|
+
*/
|
|
126
|
+
routeGroups: () => readonly RouteGroup[] | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* The retention block as this Worker resolved it at assembly, or null when no backend recorded one. The
|
|
129
|
+
* manifest reports it.
|
|
130
|
+
*/
|
|
131
|
+
retention?: () => EffectiveRetention | null;
|
|
93
132
|
/** The clock, injected so a test can stand at any instant. */
|
|
94
133
|
now: () => Date;
|
|
95
134
|
}
|
|
96
135
|
|
|
97
136
|
/** The verified caller. Never null here — `requireControlPlane` ran first, or this code is unreachable. */
|
|
98
|
-
function caller(c: Context<PithyHonoEnv>): ControlPlaneContext {
|
|
137
|
+
export function caller(c: Context<PithyHonoEnv>): ControlPlaneContext {
|
|
99
138
|
const context = c.var.controlPlane;
|
|
100
139
|
if (!context) {
|
|
101
140
|
// Defense in depth against a route line assembled without the gate. It cannot happen through
|
|
@@ -106,37 +145,33 @@ function caller(c: Context<PithyHonoEnv>): ControlPlaneContext {
|
|
|
106
145
|
return context;
|
|
107
146
|
}
|
|
108
147
|
|
|
109
|
-
/** Load the connection this call authenticated against. It verified moments ago
|
|
110
|
-
async function loadConnection(
|
|
111
|
-
db
|
|
112
|
-
|
|
113
|
-
): Promise<{ connection: ControlPlaneConnection }> {
|
|
114
|
-
const row = await db
|
|
115
|
-
.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE)
|
|
116
|
-
.selectAll()
|
|
117
|
-
.where("id", "=", connectionId)
|
|
118
|
-
.executeTakeFirst();
|
|
119
|
-
if (!row) {
|
|
148
|
+
/** Load the connection this call authenticated against, as stored, for a key write. It verified moments ago. */
|
|
149
|
+
async function loadConnection(db: ControlPlaneDatabase, connectionId: string): Promise<ConnectionForWrite> {
|
|
150
|
+
const loaded = await readConnectionForWrite(db, connectionId);
|
|
151
|
+
if (loaded === null) {
|
|
120
152
|
// The row was deleted between verification and here — a revocation landing mid-request. Denying is
|
|
121
153
|
// the correct answer: revocation is meant to be immediate.
|
|
122
154
|
throw new ControlPlaneInvalidCredentialError({ detail: `connection ${connectionId} disappeared mid-request` });
|
|
123
155
|
}
|
|
124
|
-
return
|
|
156
|
+
return loaded;
|
|
125
157
|
}
|
|
126
158
|
|
|
127
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* Write a connection's key array back, stamping `updatedAt`. One UPDATE, so a rotation cannot half-apply, and only
|
|
161
|
+
* while the keys are still what `loaded` read: a revocation landing between the read and this write is never undone.
|
|
162
|
+
*/
|
|
128
163
|
async function saveKeys(
|
|
129
164
|
db: ControlPlaneDatabase,
|
|
130
|
-
|
|
165
|
+
loaded: ConnectionForWrite,
|
|
131
166
|
keys: readonly RegisteredKey[],
|
|
132
167
|
now: Date,
|
|
133
168
|
): Promise<void> {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
.
|
|
137
|
-
|
|
138
|
-
.
|
|
139
|
-
|
|
169
|
+
if (await writeKeys(db, loaded, keys, now)) return;
|
|
170
|
+
throw new ControlPlaneKeyConflictError({
|
|
171
|
+
message: "The connection's keys changed while this call ran.",
|
|
172
|
+
action: "Make the call again.",
|
|
173
|
+
detail: `connection ${loaded.connection.id} had its keys written between this call's read and its write`,
|
|
174
|
+
});
|
|
140
175
|
}
|
|
141
176
|
|
|
142
177
|
/**
|
|
@@ -156,6 +191,37 @@ function publicKeyView(key: RegisteredKey): PublicKeyView {
|
|
|
156
191
|
};
|
|
157
192
|
}
|
|
158
193
|
|
|
194
|
+
/** The public view of one OIDC trust: its issuer, audience and claims — the population it admits. */
|
|
195
|
+
function trustView(trust: OidcTrust): TrustView {
|
|
196
|
+
return {
|
|
197
|
+
trustId: trust.trustId,
|
|
198
|
+
issuer: trust.issuer,
|
|
199
|
+
audience: trust.audience,
|
|
200
|
+
claims: { ...trust.claims },
|
|
201
|
+
authorizedBy: trust.authorizedBy,
|
|
202
|
+
validFrom: trust.validFrom.toISOString(),
|
|
203
|
+
revokedAt: trust.revokedAt?.toISOString() ?? null,
|
|
204
|
+
firstCallAt: trust.firstCallAt?.toISOString() ?? null,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Refuse a key write to a caller that verified under an OIDC trust.
|
|
210
|
+
*
|
|
211
|
+
* A trust exists so that no key is held on the workload's host. Letting a trust's caller register a key would
|
|
212
|
+
* turn a short-lived identity into a standing credential at the first call — an exchange by another name, and
|
|
213
|
+
* the custody the trust was registered to avoid. Granting `keys:rotate` to a connection is a statement about
|
|
214
|
+
* its keys, never a license for a workload to mint one.
|
|
215
|
+
*/
|
|
216
|
+
function refuseTrustCaller(context: ControlPlaneContext, what: string): void {
|
|
217
|
+
if (context.credentialKind !== "trust") return;
|
|
218
|
+
throw new ControlPlaneKeyConflictError({
|
|
219
|
+
message: "A call under a workload trust cannot change this connection's keys.",
|
|
220
|
+
action: `Register or expire a key from a client that holds one, or with ${commandName("dashboard connect")}.`,
|
|
221
|
+
detail: `${what} refused: trust ${context.keyId} verified this call`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
159
225
|
/**
|
|
160
226
|
* `GET /control-plane/ping` — connectivity and key proof.
|
|
161
227
|
*
|
|
@@ -186,11 +252,11 @@ export function pingHandler(deps: ControlPlaneHandlerDeps) {
|
|
|
186
252
|
* Discovery over configuration. A client builds its navigation from this rather than from settings
|
|
187
253
|
* someone maintains, so a Worker with no payments capability has no purchases pane as a matter of fact.
|
|
188
254
|
*
|
|
189
|
-
* **And the numbers a client would otherwise pay a second round trip for
|
|
255
|
+
* **And the numbers a client would otherwise pay a second round trip for**. Each capability's
|
|
190
256
|
* bounded summary is resolved here, behind the scope that capability's own read is behind: a connection
|
|
191
257
|
* without it gets no number rather than a zero, and never costs the adopter's Worker the query.
|
|
192
258
|
*
|
|
193
|
-
* **One capability's failure costs one capability's number
|
|
259
|
+
* **One capability's failure costs one capability's number**. A producer that throws, or that
|
|
194
260
|
* reports something its declaration cannot name, lands on `unavailable` for that entry alone —
|
|
195
261
|
* `readCapabilityHealth` catches it, so nothing rejects, so `Promise.all` still resolves and every
|
|
196
262
|
* sibling still reports. This route is what the whole management surface is built from; a manifest that
|
|
@@ -198,13 +264,21 @@ export function pingHandler(deps: ControlPlaneHandlerDeps) {
|
|
|
198
264
|
* name.
|
|
199
265
|
*/
|
|
200
266
|
export function manifestHandler(deps: ControlPlaneHandlerDeps) {
|
|
267
|
+
// Each admin route's input, converted once per route table rather than once per read. Keyed on the
|
|
268
|
+
// table itself, so an app whose groups were recorded late is converted when it first has them.
|
|
269
|
+
let converted: { groups: readonly RouteGroup[]; inputs: ReturnType<typeof routeInputs> } | undefined;
|
|
270
|
+
const inputsFor = (groups: readonly RouteGroup[] | undefined) => {
|
|
271
|
+
if (groups === undefined) return undefined;
|
|
272
|
+
if (converted?.groups !== groups) converted = { groups, inputs: routeInputs(groups) };
|
|
273
|
+
return converted.inputs;
|
|
274
|
+
};
|
|
201
275
|
return async (c: Context<PithyHonoEnv>) => {
|
|
202
276
|
const context = caller(c);
|
|
203
277
|
const sources = deps.composedHealth();
|
|
204
278
|
// In parallel: each producer is bounded by its own declaration, and a Worker composing several has
|
|
205
279
|
// no reason to serialize numbers that do not depend on each other.
|
|
206
280
|
const capabilities = await Promise.all(
|
|
207
|
-
deps.composedCapabilities().map(async (declaration) => ({
|
|
281
|
+
withRouteInputs(deps.composedCapabilities(), inputsFor(deps.routeGroups())).map(async (declaration) => ({
|
|
208
282
|
...declaration,
|
|
209
283
|
// Spread, so the values and the failure flag are written together or not at all. A handler that
|
|
210
284
|
// set one and forgot the other is the shape of defect the four-state value exists to prevent.
|
|
@@ -220,24 +294,32 @@ export function manifestHandler(deps: ControlPlaneHandlerDeps) {
|
|
|
220
294
|
environment: context.environment,
|
|
221
295
|
connectionId: context.connectionId,
|
|
222
296
|
version: build.id,
|
|
223
|
-
// Beside the id, because the id alone cannot tell a deploy from a rollback
|
|
297
|
+
// Beside the id, because the id alone cannot tell a deploy from a rollback: a rollback
|
|
224
298
|
// reactivates a version already seen, and this is the one field that moves backwards when it does.
|
|
225
299
|
versionCreatedAt: build.createdAt,
|
|
226
300
|
// Classes and counts, never field names — those are `GET /privacy`'s, behind their own scope. A project
|
|
227
301
|
// that declared nothing walks nothing; otherwise the walk is the isolate's cached one.
|
|
228
|
-
// The retirement verdict beside it
|
|
302
|
+
// The retirement verdict beside it, so a dashboard reads the guard rather than inferring it.
|
|
229
303
|
privacy: {
|
|
230
304
|
...privacyPosture(privacyAttestation(c.env), deps.privacyInventory),
|
|
231
305
|
retirement: await deps.retirement(c.env as Record<string, unknown>),
|
|
232
306
|
},
|
|
233
307
|
capabilities,
|
|
234
308
|
grantedScopes: [...context.grantedScopes],
|
|
309
|
+
// How long this Worker keeps what it holds, as it takes effect: policy, not data, so on every manifest.
|
|
310
|
+
...retentionOf(deps),
|
|
235
311
|
} satisfies ControlPlaneManifestWire);
|
|
236
312
|
};
|
|
237
313
|
}
|
|
238
314
|
|
|
315
|
+
/** The resolved retention for the manifest, or nothing where this Worker recorded none. */
|
|
316
|
+
function retentionOf(deps: ControlPlaneHandlerDeps): { retention?: EffectiveRetention } {
|
|
317
|
+
const retention = deps.retention?.() ?? null;
|
|
318
|
+
return retention === null ? {} : { retention };
|
|
319
|
+
}
|
|
320
|
+
|
|
239
321
|
/**
|
|
240
|
-
* `GET /control-plane/migrations` — where each database this Worker binds stands
|
|
322
|
+
* `GET /control-plane/migrations` — where each database this Worker binds stands.
|
|
241
323
|
*
|
|
242
324
|
* Read-only: it reports, and nothing over the seam runs, reverses or resets a migration.
|
|
243
325
|
*
|
|
@@ -274,10 +356,11 @@ export function listKeysHandler(deps: ControlPlaneHandlerDeps) {
|
|
|
274
356
|
environment: connection.environment,
|
|
275
357
|
keys: connection.keys.map(publicKeyView),
|
|
276
358
|
// The review a credential listing exists for needs these, and it is the adopter's to perform —
|
|
277
|
-
// on their own side, from their own row
|
|
359
|
+
// on their own side, from their own row.
|
|
278
360
|
authorizedBy: connection.authorizedBy,
|
|
279
361
|
actorKind: connection.actorKind,
|
|
280
362
|
purpose: connection.purpose,
|
|
363
|
+
trusts: (connection.trusts ?? []).map(trustView),
|
|
281
364
|
} satisfies ControlPlaneKeysResponse);
|
|
282
365
|
};
|
|
283
366
|
}
|
|
@@ -295,9 +378,11 @@ export function listKeysHandler(deps: ControlPlaneHandlerDeps) {
|
|
|
295
378
|
export function registerKeyHandler(deps: ControlPlaneHandlerDeps) {
|
|
296
379
|
return async (c: Context<PithyHonoEnv>, body: RegisterKeyRequest) => {
|
|
297
380
|
const context = caller(c);
|
|
381
|
+
refuseTrustCaller(context, "key registration");
|
|
298
382
|
const db = deps.database(c);
|
|
299
383
|
const now = deps.now();
|
|
300
|
-
const
|
|
384
|
+
const loaded = await loadConnection(db, context.connectionId);
|
|
385
|
+
const { connection } = loaded;
|
|
301
386
|
|
|
302
387
|
const appended = appendKey(
|
|
303
388
|
connection.keys,
|
|
@@ -309,7 +394,7 @@ export function registerKeyHandler(deps: ControlPlaneHandlerDeps) {
|
|
|
309
394
|
retentionMs: deps.config.keyRetentionDays * MS_PER_DAY,
|
|
310
395
|
maxKeys: deps.config.maxKeys,
|
|
311
396
|
});
|
|
312
|
-
await saveKeys(db,
|
|
397
|
+
await saveKeys(db, loaded, pruned, now);
|
|
313
398
|
|
|
314
399
|
await safeEmit(
|
|
315
400
|
c.var.emit,
|
|
@@ -365,9 +450,11 @@ export function registerKeyHandler(deps: ControlPlaneHandlerDeps) {
|
|
|
365
450
|
export function expireKeyHandler(deps: ControlPlaneHandlerDeps) {
|
|
366
451
|
return async (c: Context<PithyHonoEnv>, params: ExpireKeyParams, body: ExpireKeyRequest) => {
|
|
367
452
|
const context = caller(c);
|
|
453
|
+
refuseTrustCaller(context, "key expiry");
|
|
368
454
|
const db = deps.database(c);
|
|
369
455
|
const now = deps.now();
|
|
370
|
-
const
|
|
456
|
+
const loaded = await loadConnection(db, context.connectionId);
|
|
457
|
+
const { connection } = loaded;
|
|
371
458
|
|
|
372
459
|
if (body.provenKeyId !== context.keyId) {
|
|
373
460
|
throw new ControlPlaneKeyConflictError({
|
|
@@ -378,7 +465,7 @@ export function expireKeyHandler(deps: ControlPlaneHandlerDeps) {
|
|
|
378
465
|
}
|
|
379
466
|
|
|
380
467
|
const expired = expireKey(connection.keys, params.keyId, { provenKeyId: body.provenKeyId, now });
|
|
381
|
-
await saveKeys(db,
|
|
468
|
+
await saveKeys(db, loaded, expired, now);
|
|
382
469
|
|
|
383
470
|
await safeEmit(
|
|
384
471
|
c.var.emit,
|
|
@@ -412,7 +499,7 @@ export function expireKeyHandler(deps: ControlPlaneHandlerDeps) {
|
|
|
412
499
|
}
|
|
413
500
|
|
|
414
501
|
/**
|
|
415
|
-
* `GET /control-plane/privacy` — the full privacy inventory
|
|
502
|
+
* `GET /control-plane/privacy` — the full privacy inventory.
|
|
416
503
|
*
|
|
417
504
|
* Every storage schema this Worker composed that holds a marked field, each field's path and class, and where it
|
|
418
505
|
* is stored — derived from the running code, so unmarking a field removes it on the next deploy with nothing in
|
|
@@ -438,3 +525,48 @@ export function privacyHandler(deps: ControlPlaneHandlerDeps) {
|
|
|
438
525
|
} satisfies ControlPlanePrivacyResponse);
|
|
439
526
|
};
|
|
440
527
|
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* `GET /control-plane/evidence` — the evidence pack.
|
|
531
|
+
*
|
|
532
|
+
* SOC 2 and ISO 27001 evidence derived from what this Worker composes, records and declares: each control's
|
|
533
|
+
* references, the facts read and their sources, and `evidenced`, `gap` or `not-derivable` with the reason. It
|
|
534
|
+
* proves controls present and performs nothing. Behind `evidence:read`, and it never widens another scope: what
|
|
535
|
+
* a capability keeps behind its own read stays there. Read-only.
|
|
536
|
+
*/
|
|
537
|
+
export function evidenceHandler(deps: ControlPlaneHandlerDeps) {
|
|
538
|
+
return async (c: Context<PithyHonoEnv>) => {
|
|
539
|
+
const context = caller(c);
|
|
540
|
+
return c.json((await readEvidencePack(c, deps, context)) satisfies ControlPlaneEvidenceResponse);
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* `GET /control-plane/erasure/:subjectType/:subjectId` — one subject's erasure, attested.
|
|
546
|
+
*
|
|
547
|
+
* Read from the purge's own lifecycle rows, through the capability that owns the subject, and cited in the audit
|
|
548
|
+
* chain through the composed audit. A subject with no deletion in force is a 404. Read-only.
|
|
549
|
+
*/
|
|
550
|
+
export function erasureSubjectHandler(deps: ControlPlaneHandlerDeps) {
|
|
551
|
+
return async (c: Context<PithyHonoEnv>, params: ErasureSubjectParams) => {
|
|
552
|
+
caller(c);
|
|
553
|
+
const env = (c.env ?? {}) as Record<string, unknown>;
|
|
554
|
+
return c.json(
|
|
555
|
+
(await readErasureSubject(
|
|
556
|
+
env,
|
|
557
|
+
deps,
|
|
558
|
+
params.subjectType,
|
|
559
|
+
params.subjectId,
|
|
560
|
+
)) satisfies ControlPlaneErasureSubjectResponse,
|
|
561
|
+
);
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** `GET /control-plane/erasure?since&until` — every purge in a period, counted. Read-only. */
|
|
566
|
+
export function erasurePeriodHandler(deps: ControlPlaneHandlerDeps) {
|
|
567
|
+
return async (c: Context<PithyHonoEnv>, query: ErasurePeriodQuery) => {
|
|
568
|
+
caller(c);
|
|
569
|
+
const env = (c.env ?? {}) as Record<string, unknown>;
|
|
570
|
+
return c.json((await readErasurePeriod(env, deps, query)) satisfies ControlPlaneErasurePeriodResponse);
|
|
571
|
+
};
|
|
572
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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 { countActiveHolds, listHolds, placeHold, releaseHold } from "../../retention/holds";
|
|
7
|
+
import { ControlPlaneAuditActions, safeEmit } from "../audit/actions";
|
|
8
|
+
import type { LegalHold } from "../data/hold";
|
|
9
|
+
import { type ControlPlaneHandlerDeps, caller } from "./handlers";
|
|
10
|
+
import type { ControlPlaneHoldResponse, ControlPlaneHoldsResponse, LegalHoldView } from "./responses";
|
|
11
|
+
import type { HoldListQuery, HoldParams, PlaceHoldRequest, ReleaseHoldRequest } from "./schemas";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The legal-hold routes: place, release, list.
|
|
15
|
+
*
|
|
16
|
+
* Placing and releasing are each audited under `controlplane/*`, which the retention floor keeps at least a year,
|
|
17
|
+
* with the subject, the hold and the reason. A hold suspends every purge, prune and inactivity deletion touching its
|
|
18
|
+
* subject; the capabilities that erase read it through `retention/holds.ts`. **Nothing here shortens a retention**:
|
|
19
|
+
* a hold can only preserve, and its release returns the subject to the policy the Worker was deployed with.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** The default page of `GET /control-plane/holds`. */
|
|
23
|
+
const DEFAULT_HOLD_LIMIT = 100;
|
|
24
|
+
|
|
25
|
+
/** A hold, as the routes answer it. */
|
|
26
|
+
export function holdView(hold: LegalHold): LegalHoldView {
|
|
27
|
+
return {
|
|
28
|
+
id: hold.id,
|
|
29
|
+
subjectType: hold.subjectType,
|
|
30
|
+
subjectId: hold.subjectId,
|
|
31
|
+
reason: hold.reason,
|
|
32
|
+
placedBy: hold.placedBy,
|
|
33
|
+
placedAt: hold.placedAt.toISOString(),
|
|
34
|
+
state: hold.state,
|
|
35
|
+
releasedAt: hold.releasedAt?.toISOString() ?? null,
|
|
36
|
+
releasedBy: hold.releasedBy,
|
|
37
|
+
releaseReason: hold.releaseReason,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The audit event for a placement or a release: the subject as the resource, the hold and the reason beside it. */
|
|
42
|
+
async function auditHold(c: Context<PithyHonoEnv>, action: string, hold: LegalHold, reason: string): Promise<void> {
|
|
43
|
+
const context = caller(c);
|
|
44
|
+
await safeEmit(
|
|
45
|
+
c.var.emit,
|
|
46
|
+
{
|
|
47
|
+
action,
|
|
48
|
+
outcome: "success",
|
|
49
|
+
severity: "warning",
|
|
50
|
+
actorType: "control-plane",
|
|
51
|
+
actorId: context.subject,
|
|
52
|
+
resourceType: hold.subjectType === "audit" ? "audit_trail" : hold.subjectType,
|
|
53
|
+
resourceId: hold.subjectId,
|
|
54
|
+
requestId: c.req.header("cf-ray"),
|
|
55
|
+
ip: c.req.header("cf-connecting-ip"),
|
|
56
|
+
userAgent: c.req.header("user-agent"),
|
|
57
|
+
metadata: { holdId: hold.id, subjectType: hold.subjectType, reason },
|
|
58
|
+
},
|
|
59
|
+
c.var.log,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `POST /control-plane/holds` — place a hold. */
|
|
64
|
+
export function placeHoldHandler(deps: ControlPlaneHandlerDeps) {
|
|
65
|
+
return async (c: Context<PithyHonoEnv>, body: PlaceHoldRequest) => {
|
|
66
|
+
const context = caller(c);
|
|
67
|
+
const hold = await placeHold(deps.database(c), {
|
|
68
|
+
subjectType: body.subjectType,
|
|
69
|
+
subjectId: body.subjectId,
|
|
70
|
+
reason: body.reason,
|
|
71
|
+
actor: { subject: context.subject },
|
|
72
|
+
at: deps.now(),
|
|
73
|
+
});
|
|
74
|
+
await auditHold(c, ControlPlaneAuditActions.holdPlaced, hold, body.reason);
|
|
75
|
+
return c.json({ hold: holdView(hold) } satisfies ControlPlaneHoldResponse, 201);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `POST /control-plane/holds/:holdId/release` — release a hold, with a reason. */
|
|
80
|
+
export function releaseHoldHandler(deps: ControlPlaneHandlerDeps) {
|
|
81
|
+
return async (c: Context<PithyHonoEnv>, params: HoldParams, body: ReleaseHoldRequest) => {
|
|
82
|
+
const context = caller(c);
|
|
83
|
+
const hold = await releaseHold(deps.database(c), {
|
|
84
|
+
id: params.holdId,
|
|
85
|
+
reason: body.reason,
|
|
86
|
+
actor: { subject: context.subject },
|
|
87
|
+
at: deps.now(),
|
|
88
|
+
});
|
|
89
|
+
await auditHold(c, ControlPlaneAuditActions.holdReleased, hold, body.reason);
|
|
90
|
+
return c.json({ hold: holdView(hold) } satisfies ControlPlaneHoldResponse);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `GET /control-plane/holds` — every hold, standing first. Read-only. */
|
|
95
|
+
export function listHoldsHandler(deps: ControlPlaneHandlerDeps) {
|
|
96
|
+
return async (c: Context<PithyHonoEnv>, query: HoldListQuery) => {
|
|
97
|
+
caller(c);
|
|
98
|
+
const db = deps.database(c);
|
|
99
|
+
const holds = await listHolds(db, {
|
|
100
|
+
...(query.state ? { state: query.state } : {}),
|
|
101
|
+
limit: query.limit ?? DEFAULT_HOLD_LIMIT,
|
|
102
|
+
});
|
|
103
|
+
return c.json({
|
|
104
|
+
active: await countActiveHolds(db),
|
|
105
|
+
holds: holds.map(holdView),
|
|
106
|
+
} satisfies ControlPlaneHoldsResponse);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { Context, MiddlewareHandler } from "hono";
|
|
5
|
+
import type { PithyHonoEnv } from "../../capability/capability";
|
|
6
|
+
import { NotFoundError } from "../../error/pithyError";
|
|
7
|
+
import type { PresenceTicket } from "../../presence/http/responses";
|
|
8
|
+
import { issuePresenceTicket, roomKind } from "../../presence/http/routes";
|
|
9
|
+
import type { ControlPlanePresenceTicketRequest } from "../../presence/http/schemas";
|
|
10
|
+
import { ANY_VERIFIED_CALLER, type ControlPlaneRequirement } from "../scope/scope";
|
|
11
|
+
import { type ControlPlaneRequirementResolver, requireControlPlaneFor } from "./guard";
|
|
12
|
+
import type { ControlPlaneHandlerDeps } from "./handlers";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `POST /control-plane/presence/tickets` — a ticket into any room this Worker composes, for a management
|
|
16
|
+
* client.
|
|
17
|
+
*
|
|
18
|
+
* **The scope is the room kind's, and the gate checks it.** Which scope a call needs is named in its body, so
|
|
19
|
+
* {@link presenceTicketRequirement} reads the kind from a copy of the body before the gate verifies, and the
|
|
20
|
+
* gate then verifies against that kind's scope exactly as it would against a route's own: one allowed row
|
|
21
|
+
* when it covers, one denial row naming the kind's scope when it does not, never both. Presence adds no scope
|
|
22
|
+
* of its own: a connection that may read a record may see who else has it open.
|
|
23
|
+
*
|
|
24
|
+
* A body that names no declared kind needs only a verified caller, so an unverified one is refused before any
|
|
25
|
+
* kind is looked at, and a verified one then gets the handler's 404 — the manifest lists every kind, so its
|
|
26
|
+
* existence is no secret.
|
|
27
|
+
*/
|
|
28
|
+
export function presenceTicketRequirement(deps: ControlPlaneHandlerDeps): ControlPlaneRequirementResolver {
|
|
29
|
+
return async (c) => {
|
|
30
|
+
const kind = await bodyKind(c.req.raw);
|
|
31
|
+
const presence = kind === undefined ? undefined : deps.composedPresence().get(kind);
|
|
32
|
+
if (kind === undefined || !presence) return ANY_VERIFIED_CALLER;
|
|
33
|
+
return roomKind(presence, kind).scope as ControlPlaneRequirement;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The gate the route wears: {@link requireControlPlaneFor} over the room kind's scope. */
|
|
38
|
+
export function presenceTicketGate(deps: ControlPlaneHandlerDeps): MiddlewareHandler<PithyHonoEnv> {
|
|
39
|
+
return requireControlPlaneFor(presenceTicketRequirement(deps));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The body's `kind`, read from a copy so the route's validator still reads the original. Only to choose a
|
|
44
|
+
* scope: the validator is what parses the body, and anything unreadable here is simply no kind.
|
|
45
|
+
*/
|
|
46
|
+
async function bodyKind(request: Request): Promise<string | undefined> {
|
|
47
|
+
try {
|
|
48
|
+
const body: unknown = await request.clone().json();
|
|
49
|
+
const kind = typeof body === "object" && body !== null ? (body as { kind?: unknown }).kind : undefined;
|
|
50
|
+
return typeof kind === "string" ? kind : undefined;
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The handler, behind {@link presenceTicketGate}: the kind's scope is already verified, so it mints. */
|
|
57
|
+
export function presenceTicketHandler(deps: ControlPlaneHandlerDeps) {
|
|
58
|
+
return async (c: Context<PithyHonoEnv>, body: ControlPlanePresenceTicketRequest): Promise<Response> => {
|
|
59
|
+
const presence = deps.composedPresence().get(body.kind);
|
|
60
|
+
if (!presence) throw new NotFoundError({ message: "That room kind does not exist.", params: { kind: body.kind } });
|
|
61
|
+
const room = roomKind(presence, body.kind);
|
|
62
|
+
const ticket = await issuePresenceTicket(c, presence, {
|
|
63
|
+
kind: room.kind,
|
|
64
|
+
recordId: body.recordId,
|
|
65
|
+
ref: body.participant.ref,
|
|
66
|
+
name: body.participant.name,
|
|
67
|
+
origin: "control-plane",
|
|
68
|
+
});
|
|
69
|
+
return c.json(ticket satisfies PresenceTicket, 201);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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 { readProcessingRecord } from "../processing/read";
|
|
7
|
+
import { type ControlPlaneHandlerDeps, caller } from "./handlers";
|
|
8
|
+
import type { ControlPlaneProcessingResponse } from "./responses";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `GET /control-plane/processing` — the record of processing activities, GDPR Article 30. Each activity's purpose,
|
|
12
|
+
* classes, legal basis, recipients, retention and measures, and every gap named. Read-only.
|
|
13
|
+
*/
|
|
14
|
+
export function processingHandler(deps: ControlPlaneHandlerDeps) {
|
|
15
|
+
return async (c: Context<PithyHonoEnv>) => {
|
|
16
|
+
const context = caller(c);
|
|
17
|
+
return c.json((await readProcessingRecord(c, deps, context)) satisfies ControlPlaneProcessingResponse);
|
|
18
|
+
};
|
|
19
|
+
}
|