@pithy-sh/core 0.12.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/audit/composed.d.ts +18 -0
- package/dist/audit/composed.d.ts.map +1 -0
- package/dist/audit/composed.js +28 -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 +133 -15
- 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 +113 -4
- package/dist/capability/purge.d.ts.map +1 -1
- package/dist/capability/purge.js +138 -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 +434 -4
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
- package/dist/cli/commandCatalog.generated.js +455 -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 +103 -7
- 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 +14 -1
- package/dist/createEntrypoint.d.ts.map +1 -1
- package/dist/createEntrypoint.js +19 -5
- package/dist/error/http.d.ts +216 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +432 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +144 -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/validation.d.ts +11 -2
- package/dist/http/validation.d.ts.map +1 -1
- package/dist/http/validation.js +20 -3
- 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 +126 -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/audit/composed.ts +30 -0
- package/src/capability/bindings.ts +61 -0
- package/src/capability/capability.ts +166 -18
- 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 +208 -8
- package/src/capability/settings.ts +7 -0
- package/src/capability/vectorIndex.ts +60 -0
- package/src/cli/commandCatalog.generated.ts +488 -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 +178 -10
- 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 +34 -2
- package/src/error/payload.ts +302 -0
- package/src/error/pithyError.ts +42 -0
- package/src/http/routeSpec.ts +75 -5
- package/src/http/validation.ts +35 -3
- 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,124 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { noopEmit } from "../../audit/recorder.js";
|
|
4
|
+
import { MIGRATIONS_READ_SCOPE } from "../scope/scope.js";
|
|
5
|
+
import { ControlPlaneConnection } from "../data/connection.js";
|
|
6
|
+
import { CONTROL_PLANE_CONNECTIONS_TABLE } from "../data/tables.js";
|
|
7
|
+
import { workerVersionMetadata } from "../../worker/identity.js";
|
|
8
|
+
import { readCapabilityHealth } from "../discovery/health.js";
|
|
9
|
+
import { privacyAttestation } from "../../privacy/posture.js";
|
|
10
|
+
import { causeMessage } from "../../error/cause.js";
|
|
11
|
+
import { describedOperations } from "../../openapi/operations.js";
|
|
12
|
+
import { readDatabaseMigrations } from "../migrationState/migrationState.js";
|
|
13
|
+
import { EVIDENCE_CONTROL_IDS, SECRETS_CAPABILITY } from "./mapping.js";
|
|
14
|
+
import { deriveAuditLogging, deriveChangeHistory, deriveCsrf, deriveDataProtection, deriveKeyRotation, deriveManagementAccess, deriveRouteVerification, evidenceEntry } from "./derive.js";
|
|
15
|
+
//#region src/controlPlane/evidence/pack.ts
|
|
16
|
+
/** Every connection registered for this environment, or null where the table could not be read. */
|
|
17
|
+
async function readConnections(c, sources, environment) {
|
|
18
|
+
try {
|
|
19
|
+
return (await sources.database(c).selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().where("environment", "=", environment).orderBy("createdAt").execute()).map((row) => {
|
|
20
|
+
const connection = ControlPlaneConnection.parse(row);
|
|
21
|
+
return {
|
|
22
|
+
id: connection.id,
|
|
23
|
+
authorizedBy: connection.authorizedBy,
|
|
24
|
+
actorKind: connection.actorKind,
|
|
25
|
+
purpose: connection.purpose,
|
|
26
|
+
scopes: connection.scopes,
|
|
27
|
+
keys: connection.keys.map(({ validFrom, validUntil, revokedAt }) => ({
|
|
28
|
+
validFrom,
|
|
29
|
+
validUntil,
|
|
30
|
+
revokedAt
|
|
31
|
+
}))
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
} catch (cause) {
|
|
35
|
+
c.var.log.warn("evidence: connections unreadable", { reason: causeMessage(cause) ?? String(cause) });
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** The composed audit recorder and whether each binding it writes through is present, or null for none. */
|
|
40
|
+
function auditRecorder(capabilities, env) {
|
|
41
|
+
const recorder = capabilities.find((capability) => capability.auditRecorder !== void 0);
|
|
42
|
+
if (!recorder) return null;
|
|
43
|
+
const bindings = [...new Set(Object.values(recorder.databases ?? {}).map((database) => database.binding))];
|
|
44
|
+
return {
|
|
45
|
+
capability: recorder.name,
|
|
46
|
+
bindings: bindings.map((binding) => ({
|
|
47
|
+
binding,
|
|
48
|
+
present: env[binding] != null
|
|
49
|
+
}))
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Secrets' rotation status for this caller, read exactly as the manifest reads it. */
|
|
53
|
+
async function secretsRotation(c, sources, context) {
|
|
54
|
+
if (!sources.composition().some((capability) => capability.name === "secrets")) return { state: "not-composed" };
|
|
55
|
+
const source = sources.composedHealth().get(SECRETS_CAPABILITY);
|
|
56
|
+
const report = await readCapabilityHealth(source, context.grantedScopes, (found) => found.read(c));
|
|
57
|
+
return {
|
|
58
|
+
state: "composed",
|
|
59
|
+
keys: source?.keys ?? [],
|
|
60
|
+
report
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** The privacy inventory, or null where the walk threw. */
|
|
64
|
+
function inventoryOf(c, sources) {
|
|
65
|
+
try {
|
|
66
|
+
return sources.privacyInventory();
|
|
67
|
+
} catch (cause) {
|
|
68
|
+
c.var.log.warn("evidence: privacy inventory unreadable", { reason: causeMessage(cause) ?? String(cause) });
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Read everything the pack derives from, and derive it. */
|
|
73
|
+
async function readEvidencePack(c, sources, context) {
|
|
74
|
+
const env = c.env;
|
|
75
|
+
const now = sources.now();
|
|
76
|
+
const groups = sources.routeGroups();
|
|
77
|
+
const operations = groups === void 0 ? void 0 : describedOperations(groups);
|
|
78
|
+
const migrationsGranted = context.grantedScopes.includes(MIGRATIONS_READ_SCOPE);
|
|
79
|
+
const [connections, secrets, retirement, databases] = await Promise.all([
|
|
80
|
+
readConnections(c, sources, context.environment),
|
|
81
|
+
secretsRotation(c, sources, context),
|
|
82
|
+
sources.retirement(env),
|
|
83
|
+
migrationsGranted ? Promise.all(sources.composedMigrations().map((entry) => readDatabaseMigrations(env, entry, (failed, cause) => c.var.log.warn("evidence: migration ledger unreadable", {
|
|
84
|
+
database: failed.database,
|
|
85
|
+
binding: failed.binding,
|
|
86
|
+
reason: causeMessage(cause) ?? String(cause)
|
|
87
|
+
})))) : Promise.resolve([])
|
|
88
|
+
]);
|
|
89
|
+
const derived = {
|
|
90
|
+
"audit-logging": deriveAuditLogging({
|
|
91
|
+
recorder: auditRecorder(sources.composition(), env),
|
|
92
|
+
emitting: c.var.emit !== noopEmit
|
|
93
|
+
}),
|
|
94
|
+
"route-verification": deriveRouteVerification(operations),
|
|
95
|
+
"management-access": deriveManagementAccess(connections),
|
|
96
|
+
"key-rotation": deriveKeyRotation({
|
|
97
|
+
connections,
|
|
98
|
+
now,
|
|
99
|
+
secrets,
|
|
100
|
+
retirement
|
|
101
|
+
}),
|
|
102
|
+
"change-history": deriveChangeHistory({
|
|
103
|
+
granted: migrationsGranted,
|
|
104
|
+
databases
|
|
105
|
+
}),
|
|
106
|
+
"data-protection": deriveDataProtection({
|
|
107
|
+
inventory: inventoryOf(c, sources),
|
|
108
|
+
attestation: privacyAttestation(env)
|
|
109
|
+
}),
|
|
110
|
+
csrf: deriveCsrf({
|
|
111
|
+
operations,
|
|
112
|
+
sameOriginPublished: c.var.sameOrigin !== null
|
|
113
|
+
})
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
environment: context.environment,
|
|
117
|
+
generatedAt: now.toISOString(),
|
|
118
|
+
version: workerVersionMetadata(c.env).id,
|
|
119
|
+
mappingVersion: 1,
|
|
120
|
+
controls: EVIDENCE_CONTROL_IDS.map((control) => evidenceEntry(control, derived[control]))
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { readEvidencePack };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { Context } from "hono";
|
|
4
|
+
import type { PithyHonoEnv } from "../../capability/capability";
|
|
5
|
+
import { ConflictError } from "../../error/pithyError";
|
|
6
|
+
import type { ControlPlaneConnection } from "../data/connection";
|
|
7
|
+
import { type ControlPlaneHandlerDeps } from "./handlers";
|
|
8
|
+
import type { ConnectionView } from "./responses";
|
|
9
|
+
import type { ConnectionListQuery, ConnectionParams, RevokeConnectionRequest } from "./schemas";
|
|
10
|
+
/** A connection as the listing shows it at `now`: who, what for, which credentials, never key material. */
|
|
11
|
+
export declare function connectionView(connection: ControlPlaneConnection, now: Date): ConnectionView;
|
|
12
|
+
/**
|
|
13
|
+
* `GET /control-plane/connections` — every connection bound to this Worker's environment, the caller's own among
|
|
14
|
+
* them. A database shared with another environment holds that environment's rows too, and they are never listed.
|
|
15
|
+
* Read-only.
|
|
16
|
+
*/
|
|
17
|
+
export declare function listConnectionsHandler(deps: ControlPlaneHandlerDeps): (c: Context<PithyHonoEnv>, query: ConnectionListQuery) => Promise<Response & import("hono").TypedResponse<{
|
|
18
|
+
connections: {
|
|
19
|
+
id: string;
|
|
20
|
+
environment: string;
|
|
21
|
+
actorKind: "agent" | "dashboard" | "script" | null;
|
|
22
|
+
purpose: string | null;
|
|
23
|
+
authorizedBy: string | null;
|
|
24
|
+
credentialKind: "both" | "key" | "none" | "trust";
|
|
25
|
+
scopes: string[];
|
|
26
|
+
trusts: {
|
|
27
|
+
trustId: string;
|
|
28
|
+
issuer: string;
|
|
29
|
+
claims: {
|
|
30
|
+
[x: string]: string;
|
|
31
|
+
};
|
|
32
|
+
revokedAt: string | null;
|
|
33
|
+
}[];
|
|
34
|
+
live: boolean;
|
|
35
|
+
registeredAt: string;
|
|
36
|
+
revokedAt: string | null;
|
|
37
|
+
}[];
|
|
38
|
+
unattributed: {
|
|
39
|
+
id: string;
|
|
40
|
+
environment: string;
|
|
41
|
+
actorKind: "agent" | "dashboard" | "script" | null;
|
|
42
|
+
purpose: string | null;
|
|
43
|
+
authorizedBy: string | null;
|
|
44
|
+
credentialKind: "both" | "key" | "none" | "trust";
|
|
45
|
+
scopes: string[];
|
|
46
|
+
trusts: {
|
|
47
|
+
trustId: string;
|
|
48
|
+
issuer: string;
|
|
49
|
+
claims: {
|
|
50
|
+
[x: string]: string;
|
|
51
|
+
};
|
|
52
|
+
revokedAt: string | null;
|
|
53
|
+
}[];
|
|
54
|
+
live: boolean;
|
|
55
|
+
registeredAt: string;
|
|
56
|
+
revokedAt: string | null;
|
|
57
|
+
}[];
|
|
58
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">>;
|
|
59
|
+
/**
|
|
60
|
+
* The refusal of a connection asking to revoke itself. The action names the environment it is bound to, since
|
|
61
|
+
* `dashboard disconnect` would otherwise end the connection of the environment its `--env` defaults to.
|
|
62
|
+
*/
|
|
63
|
+
export declare function selfRevokeRefusal(connectionId: string, environment: string): ConflictError;
|
|
64
|
+
/**
|
|
65
|
+
* `POST /control-plane/connections/:connectionId/revoke` — revoke every key and trust another connection in this
|
|
66
|
+
* Worker's environment holds that is not yet revoked. A connection bound to another environment is answered as
|
|
67
|
+
* though it did not exist.
|
|
68
|
+
*
|
|
69
|
+
* Refused on the caller's own connection: a client ending its own access does it through `disconnect`, which also
|
|
70
|
+
* removes what it holds locally.
|
|
71
|
+
*/
|
|
72
|
+
export declare function revokeConnectionHandler(deps: ControlPlaneHandlerDeps): (c: Context<PithyHonoEnv>, params: ConnectionParams, body: RevokeConnectionRequest) => Promise<Response & import("hono").TypedResponse<{
|
|
73
|
+
connection: {
|
|
74
|
+
id: string;
|
|
75
|
+
environment: string;
|
|
76
|
+
actorKind: "agent" | "dashboard" | "script" | null;
|
|
77
|
+
purpose: string | null;
|
|
78
|
+
authorizedBy: string | null;
|
|
79
|
+
credentialKind: "both" | "key" | "none" | "trust";
|
|
80
|
+
scopes: string[];
|
|
81
|
+
trusts: {
|
|
82
|
+
trustId: string;
|
|
83
|
+
issuer: string;
|
|
84
|
+
claims: {
|
|
85
|
+
[x: string]: string;
|
|
86
|
+
};
|
|
87
|
+
revokedAt: string | null;
|
|
88
|
+
}[];
|
|
89
|
+
live: boolean;
|
|
90
|
+
registeredAt: string;
|
|
91
|
+
revokedAt: string | null;
|
|
92
|
+
};
|
|
93
|
+
revokedKeyIds: string[];
|
|
94
|
+
revokedTrustIds: string[];
|
|
95
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">>;
|
|
96
|
+
//# sourceMappingURL=connections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/connections.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEhE,OAAO,EAAE,aAAa,EAAiB,MAAM,wBAAwB,CAAC;AAEtE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAQjE,OAAO,EAAE,KAAK,uBAAuB,EAAU,MAAM,YAAY,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAA6D,MAAM,aAAa,CAAC;AAC7G,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAEhG,2GAA2G;AAC3G,wBAAgB,cAAc,CAAC,UAAU,EAAE,sBAAsB,EAAE,GAAG,EAAE,IAAI,GAAG,cAAc,CAmB5F;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,uBAAuB,OACjD,OAAO,CAAC,YAAY,CAAC,SAAS,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mEAYnE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,aAAa,CAM1F;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,uBAAuB,OAClD,OAAO,CAAC,YAAY,CAAC,UAAU,gBAAgB,QAAQ,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;mEA6ChG"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { ConflictError, NotFoundError } from "../../error/pithyError.js";
|
|
4
|
+
import { personDecision } from "../../cli/command.js";
|
|
5
|
+
import { ControlPlaneAuditActions, safeEmit } from "../audit/actions.js";
|
|
6
|
+
import { connectionCredentialKind, connectionLive, connectionRevokedAt, listConnections, revokeConnection } from "../data/connections.js";
|
|
7
|
+
import { caller } from "./handlers.js";
|
|
8
|
+
//#region src/controlPlane/http/connections.ts
|
|
9
|
+
/** A connection as the listing shows it at `now`: who, what for, which credentials, never key material. */
|
|
10
|
+
function connectionView(connection, now) {
|
|
11
|
+
return {
|
|
12
|
+
id: connection.id,
|
|
13
|
+
environment: connection.environment,
|
|
14
|
+
actorKind: connection.actorKind,
|
|
15
|
+
purpose: connection.purpose,
|
|
16
|
+
authorizedBy: connection.authorizedBy,
|
|
17
|
+
credentialKind: connectionCredentialKind(connection),
|
|
18
|
+
scopes: [...connection.scopes],
|
|
19
|
+
trusts: (connection.trusts ?? []).map((trust) => ({
|
|
20
|
+
trustId: trust.trustId,
|
|
21
|
+
issuer: trust.issuer,
|
|
22
|
+
claims: { ...trust.claims },
|
|
23
|
+
revokedAt: trust.revokedAt?.toISOString() ?? null
|
|
24
|
+
})),
|
|
25
|
+
live: connectionLive(connection, now),
|
|
26
|
+
registeredAt: connection.createdAt.toISOString(),
|
|
27
|
+
revokedAt: connectionRevokedAt(connection, now)?.toISOString() ?? null
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `GET /control-plane/connections` — every connection bound to this Worker's environment, the caller's own among
|
|
32
|
+
* them. A database shared with another environment holds that environment's rows too, and they are never listed.
|
|
33
|
+
* Read-only.
|
|
34
|
+
*/
|
|
35
|
+
function listConnectionsHandler(deps) {
|
|
36
|
+
return async (c, query) => {
|
|
37
|
+
const context = caller(c);
|
|
38
|
+
const now = deps.now();
|
|
39
|
+
const listing = await listConnections(deps.database(c), {
|
|
40
|
+
principal: query.principal,
|
|
41
|
+
environment: context.environment
|
|
42
|
+
});
|
|
43
|
+
return c.json({
|
|
44
|
+
connections: listing.connections.map((connection) => connectionView(connection, now)),
|
|
45
|
+
unattributed: listing.unattributed.map((connection) => connectionView(connection, now))
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The refusal of a connection asking to revoke itself. The action names the environment it is bound to, since
|
|
51
|
+
* `dashboard disconnect` would otherwise end the connection of the environment its `--env` defaults to.
|
|
52
|
+
*/
|
|
53
|
+
function selfRevokeRefusal(connectionId, environment) {
|
|
54
|
+
return new ConflictError({
|
|
55
|
+
message: "A connection cannot revoke itself here.",
|
|
56
|
+
action: `End this connection from the CLI instead. ${personDecision("dashboard disconnect", { flags: { "--env": environment } }, { runtime: true })}`,
|
|
57
|
+
detail: `connection ${connectionId} asked to revoke itself`
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `POST /control-plane/connections/:connectionId/revoke` — revoke every key and trust another connection in this
|
|
62
|
+
* Worker's environment holds that is not yet revoked. A connection bound to another environment is answered as
|
|
63
|
+
* though it did not exist.
|
|
64
|
+
*
|
|
65
|
+
* Refused on the caller's own connection: a client ending its own access does it through `disconnect`, which also
|
|
66
|
+
* removes what it holds locally.
|
|
67
|
+
*/
|
|
68
|
+
function revokeConnectionHandler(deps) {
|
|
69
|
+
return async (c, params, body) => {
|
|
70
|
+
const context = caller(c);
|
|
71
|
+
if (params.connectionId === context.connectionId) throw selfRevokeRefusal(context.connectionId, context.environment);
|
|
72
|
+
const db = deps.database(c);
|
|
73
|
+
const now = deps.now();
|
|
74
|
+
const revoked = await revokeConnection(db, params.connectionId, now, { environment: context.environment });
|
|
75
|
+
if (revoked === null) throw new NotFoundError({
|
|
76
|
+
message: "No connection in this environment has that id.",
|
|
77
|
+
detail: `connection ${params.connectionId} not found in ${context.environment}`
|
|
78
|
+
});
|
|
79
|
+
const target = revoked.connection;
|
|
80
|
+
await safeEmit(c.var.emit, {
|
|
81
|
+
action: ControlPlaneAuditActions.connectionRevoked,
|
|
82
|
+
outcome: "success",
|
|
83
|
+
severity: "warning",
|
|
84
|
+
actorType: "control-plane",
|
|
85
|
+
actorId: context.subject,
|
|
86
|
+
resourceType: "controlplane_connection",
|
|
87
|
+
resourceId: target.id,
|
|
88
|
+
requestId: c.req.header("cf-ray"),
|
|
89
|
+
ip: c.req.header("cf-connecting-ip"),
|
|
90
|
+
userAgent: c.req.header("user-agent"),
|
|
91
|
+
metadata: {
|
|
92
|
+
callerConnectionId: context.connectionId,
|
|
93
|
+
connectionId: target.id,
|
|
94
|
+
connectionEnvironment: target.environment,
|
|
95
|
+
authorizedBy: target.authorizedBy,
|
|
96
|
+
reason: body.reason,
|
|
97
|
+
revokedKeyIds: [...revoked.revokedKeyIds],
|
|
98
|
+
revokedTrustIds: [...revoked.revokedTrustIds]
|
|
99
|
+
}
|
|
100
|
+
}, c.var.log);
|
|
101
|
+
return c.json({
|
|
102
|
+
connection: connectionView(target, now),
|
|
103
|
+
revokedKeyIds: [...revoked.revokedKeyIds],
|
|
104
|
+
revokedTrustIds: [...revoked.revokedTrustIds]
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { connectionView, listConnectionsHandler, revokeConnectionHandler, selfRevokeRefusal };
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import type { MiddlewareHandler } from "hono";
|
|
4
|
-
import type
|
|
4
|
+
import { type AuditEmit } from "../../audit/recorder";
|
|
5
5
|
import type { PithyHonoEnv } from "../../capability/capability";
|
|
6
6
|
import type { ControlPlaneContext } from "../context";
|
|
7
7
|
import type { ControlPlaneActorKind } from "../data/connection";
|
|
8
8
|
import type { ControlPlaneRequirement } from "../scope/scope";
|
|
9
9
|
import { type ControlPlaneVerifyDeps } from "./verify";
|
|
10
|
+
import { type ControlPlaneTrustVerifyDeps } from "./verifyTrust";
|
|
10
11
|
/**
|
|
11
12
|
* `requireControlPlane(scope)` — the gate every control-plane route wears, including the ones
|
|
12
13
|
* capabilities contribute.
|
|
@@ -43,7 +44,12 @@ export type ControlPlaneVerifier = (request: Request, requirement: ControlPlaneR
|
|
|
43
44
|
* Build the verifier the seam's middleware publishes. Split out from the middleware so the wiring is
|
|
44
45
|
* testable without a Worker, and so `verify.ts` stays free of Hono.
|
|
45
46
|
*/
|
|
46
|
-
export declare function createControlPlaneVerifier(deps: ControlPlaneVerifyDeps
|
|
47
|
+
export declare function createControlPlaneVerifier(deps: ControlPlaneVerifyDeps,
|
|
48
|
+
/**
|
|
49
|
+
* The trust path's own dependencies (#722). Absent, a call presenting an OIDC token is refused like any other
|
|
50
|
+
* credential this Worker cannot verify — the seam fails closed rather than waving an unverifiable token through.
|
|
51
|
+
*/
|
|
52
|
+
trusts?: ControlPlaneTrustVerifyDeps): ControlPlaneVerifier;
|
|
47
53
|
/**
|
|
48
54
|
* What this Worker may say about its own build, as headers — everything the binding handed it, and
|
|
49
55
|
* nothing else.
|
|
@@ -78,6 +84,18 @@ export declare const CONTROL_PLANE_GUARD: unique symbol;
|
|
|
78
84
|
/** Whether a mounted handler is a control-plane gate. */
|
|
79
85
|
export declare function isControlPlaneGuard(handler: unknown): boolean;
|
|
80
86
|
export declare function requireControlPlane(requirement: ControlPlaneRequirement): MiddlewareHandler<PithyHonoEnv>;
|
|
87
|
+
/**
|
|
88
|
+
* The scope a call needs, resolved from the call itself. Never throws: whatever it cannot read resolves to
|
|
89
|
+
* a requirement, and `ANY_VERIFIED_CALLER` is the least it may resolve to — never no gate at all.
|
|
90
|
+
*/
|
|
91
|
+
export type ControlPlaneRequirementResolver = (c: Parameters<MiddlewareHandler<PithyHonoEnv>>[0]) => Promise<ControlPlaneRequirement>;
|
|
92
|
+
/**
|
|
93
|
+
* {@link requireControlPlane} for a route whose scope is named by the request, which is the presence ticket
|
|
94
|
+
* route's case (#815): the room kind in its body decides the scope. Resolving it **inside the gate, before
|
|
95
|
+
* verifying**, is what keeps a refused call to the one denial row every other route writes — a handler that
|
|
96
|
+
* checked the scope after the gate let the call through would leave an allowed row ahead of its denial.
|
|
97
|
+
*/
|
|
98
|
+
export declare function requireControlPlaneFor(resolve: ControlPlaneRequirementResolver): MiddlewareHandler<PithyHonoEnv>;
|
|
81
99
|
/**
|
|
82
100
|
* The recorder a verified call's handlers write through: the request's own, with the connection's actor
|
|
83
101
|
* kind added to every `control-plane` event that does not already carry one.
|
|
@@ -85,6 +103,9 @@ export declare function requireControlPlane(requirement: ControlPlaneRequirement
|
|
|
85
103
|
* Only `control-plane` events, because the kind describes a management client and nothing else — a
|
|
86
104
|
* handler recording what happened to a user is recording a user. An event that states a kind keeps it.
|
|
87
105
|
* A connection with no recorded kind adds nothing, so its rows read exactly as they did before.
|
|
106
|
+
*
|
|
107
|
+
* The no-op comes back as itself: there is nothing to stamp, and a wrapper around it would hide that nothing is
|
|
108
|
+
* recorded from every reader past this gate — the evidence pack's `controlPlaneCallsRecorded` among them (#821).
|
|
88
109
|
*/
|
|
89
110
|
export declare function stampActorKind(emit: AuditEmit, actorKind: ControlPlaneActorKind | null): AuditEmit;
|
|
90
111
|
//# sourceMappingURL=guard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/guard.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/guard.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAMhE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAMhE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAS9D,OAAO,EAAE,KAAK,sBAAsB,EAA0B,MAAM,UAAU,CAAC;AAC/E,OAAO,EAAE,KAAK,2BAA2B,EAA+B,MAAM,eAAe,CAAC;AAE9F;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG,CACjC,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,uBAAuB,KACjC,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAElC;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,sBAAsB;AAC5B;;;GAGG;AACH,MAAM,CAAC,EAAE,2BAA2B,GACnC,oBAAoB,CAwCtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMzE;AAED;;;;;;GAMG;AACH;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,eAAwC,CAAC;AAEzE,yDAAyD;AACzD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAE7D;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,uBAAuB,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAEzG;AAED;;;GAGG;AACH,MAAM,MAAM,+BAA+B,GAAG,CAC5C,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,KAC9C,OAAO,CAAC,uBAAuB,CAAC,CAAC;AAEtC;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,+BAA+B,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAwFhH;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,CAIlG"}
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { noopEmit } from "../../audit/recorder.js";
|
|
3
4
|
import { PithyError } from "../../error/pithyError.js";
|
|
4
5
|
import { commandName, remedy } from "../../cli/command.js";
|
|
6
|
+
import { ControlPlaneInsufficientScopeError, ControlPlaneInvalidCredentialError, ControlPlaneNotConnectedError } from "../error/errors.js";
|
|
5
7
|
import { workerVersionMetadata } from "../../worker/identity.js";
|
|
6
8
|
import { compositionEnvironment } from "../../env/ambient.js";
|
|
7
9
|
import { ControlPlaneAuditActions, safeEmit } from "../audit/actions.js";
|
|
8
|
-
import {
|
|
9
|
-
import { CONTROL_PLANE_VERSION_CREATED_HEADER, CONTROL_PLANE_VERSION_HEADER } from "../wire.js";
|
|
10
|
+
import { CONTROL_PLANE_OIDC_HEADER, CONTROL_PLANE_VERSION_CREATED_HEADER, CONTROL_PLANE_VERSION_HEADER } from "../wire.js";
|
|
10
11
|
import { verifyControlPlaneCall } from "./verify.js";
|
|
12
|
+
import { verifyControlPlaneTrustCall } from "./verifyTrust.js";
|
|
11
13
|
//#region src/controlPlane/http/guard.ts
|
|
12
14
|
/**
|
|
13
15
|
* Build the verifier the seam's middleware publishes. Split out from the middleware so the wiring is
|
|
14
16
|
* testable without a Worker, and so `verify.ts` stays free of Hono.
|
|
15
17
|
*/
|
|
16
|
-
function createControlPlaneVerifier(deps) {
|
|
18
|
+
function createControlPlaneVerifier(deps, trusts) {
|
|
17
19
|
return async (request, requirement) => {
|
|
20
|
+
const oidc = request.headers.get(CONTROL_PLANE_OIDC_HEADER);
|
|
21
|
+
if (oidc !== null) {
|
|
22
|
+
if (request.headers.get("pithy-control-plane") !== null) throw new ControlPlaneInvalidCredentialError({ detail: "a call presented both a signed token and an OIDC token" });
|
|
23
|
+
if (!trusts) throw new ControlPlaneInvalidCredentialError({ detail: "an OIDC token was presented and this Worker verifies none" });
|
|
24
|
+
return await verifyControlPlaneTrustCall({
|
|
25
|
+
token: oidc,
|
|
26
|
+
trustId: request.headers.get("pithy-control-plane-trust") ?? void 0,
|
|
27
|
+
scope: request.headers.get("pithy-control-plane-scope") ?? void 0,
|
|
28
|
+
requirement
|
|
29
|
+
}, trusts);
|
|
30
|
+
}
|
|
18
31
|
const body = new Uint8Array(await request.clone().arrayBuffer());
|
|
19
32
|
return await verifyControlPlaneCall({
|
|
20
33
|
token: request.headers.get("pithy-control-plane") ?? void 0,
|
|
@@ -65,7 +78,17 @@ function isControlPlaneGuard(handler) {
|
|
|
65
78
|
return typeof handler === "function" && handler[CONTROL_PLANE_GUARD] === true;
|
|
66
79
|
}
|
|
67
80
|
function requireControlPlane(requirement) {
|
|
81
|
+
return requireControlPlaneFor(async () => requirement);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* {@link requireControlPlane} for a route whose scope is named by the request, which is the presence ticket
|
|
85
|
+
* route's case (#815): the room kind in its body decides the scope. Resolving it **inside the gate, before
|
|
86
|
+
* verifying**, is what keeps a refused call to the one denial row every other route writes — a handler that
|
|
87
|
+
* checked the scope after the gate let the call through would leave an allowed row ahead of its denial.
|
|
88
|
+
*/
|
|
89
|
+
function requireControlPlaneFor(resolve) {
|
|
68
90
|
const guard = async (c, next) => {
|
|
91
|
+
const requirement = await resolve(c);
|
|
69
92
|
for (const [name, value] of Object.entries(workerVersionHeaders(c.env))) c.header(name, value);
|
|
70
93
|
const verify = c.var.controlPlaneVerifier;
|
|
71
94
|
if (!verify) {
|
|
@@ -101,11 +124,18 @@ function requireControlPlane(requirement) {
|
|
|
101
124
|
connectionEnvironment: context.environment,
|
|
102
125
|
scope: context.scope,
|
|
103
126
|
keyId: context.keyId,
|
|
127
|
+
...context.credentialKind === "trust" ? { credentialKind: "trust" } : {},
|
|
104
128
|
method: c.req.method,
|
|
105
129
|
path: c.req.path
|
|
106
130
|
}
|
|
107
131
|
}, c.var.log);
|
|
108
|
-
|
|
132
|
+
try {
|
|
133
|
+
await next();
|
|
134
|
+
} catch (error) {
|
|
135
|
+
await auditHandlerDenial(c, requirement, context, error);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
await auditHandlerDenial(c, requirement, context, c.error);
|
|
109
139
|
};
|
|
110
140
|
guard[CONTROL_PLANE_GUARD] = true;
|
|
111
141
|
return guard;
|
|
@@ -117,9 +147,12 @@ function requireControlPlane(requirement) {
|
|
|
117
147
|
* Only `control-plane` events, because the kind describes a management client and nothing else — a
|
|
118
148
|
* handler recording what happened to a user is recording a user. An event that states a kind keeps it.
|
|
119
149
|
* A connection with no recorded kind adds nothing, so its rows read exactly as they did before.
|
|
150
|
+
*
|
|
151
|
+
* The no-op comes back as itself: there is nothing to stamp, and a wrapper around it would hide that nothing is
|
|
152
|
+
* recorded from every reader past this gate — the evidence pack's `controlPlaneCallsRecorded` among them (#821).
|
|
120
153
|
*/
|
|
121
154
|
function stampActorKind(emit, actorKind) {
|
|
122
|
-
if (actorKind === null) return emit;
|
|
155
|
+
if (actorKind === null || emit === noopEmit) return emit;
|
|
123
156
|
return (event) => emit(event.actorType === "control-plane" && event.actorKind == null ? {
|
|
124
157
|
...event,
|
|
125
158
|
actorKind
|
|
@@ -136,23 +169,86 @@ function stampActorKind(emit, actorKind) {
|
|
|
136
169
|
* the exact verification step, and the audit trail is queryable and long-lived. The step belongs in the
|
|
137
170
|
* log, which is bounded and already carries the whole payload.
|
|
138
171
|
*
|
|
172
|
+
* **Attributed exactly as far as it is proven (#812).** A call that failed verification — no connection, a bad
|
|
173
|
+
* signature, an unknown or expired key — names nobody, because nothing about it is proven. A call whose credential
|
|
174
|
+
* verified and whose scope was not granted is a known connection asking for an operation it lacks, which is what
|
|
175
|
+
* scope probing looks like, so its row carries the subject as `actorId`, the connection as its resource, the key
|
|
176
|
+
* in `metadata.keyId` (and `credentialKind: "trust"` when that key is a trust), and the connection's `actorKind` — the same four the allowed row carries. Only the first presentation of a token is: the verifier
|
|
177
|
+
* spends the jti of a scope refusal and names no caller on a token already spent, because a signature proves who minted
|
|
178
|
+
* a token, not who presented it. Only the row changes; the error thrown is the same.
|
|
179
|
+
*
|
|
139
180
|
* Nothing here can turn a correct denial into a 500 — `safeEmit` swallows, and the caller rethrows the
|
|
140
181
|
* original error regardless.
|
|
141
182
|
*/
|
|
142
183
|
async function auditDenial(c, requirement, cause) {
|
|
143
184
|
const code = cause instanceof PithyError ? cause.payload.code : "core/internal";
|
|
185
|
+
const proven = cause instanceof ControlPlaneInsufficientScopeError ? cause.caller : null;
|
|
186
|
+
const refusedTrust = cause instanceof ControlPlaneInvalidCredentialError ? cause.refusedTrust : null;
|
|
187
|
+
const metadata = {
|
|
188
|
+
reason: code,
|
|
189
|
+
required: String(requirement),
|
|
190
|
+
method: c.req.method,
|
|
191
|
+
path: c.req.path,
|
|
192
|
+
...refusedTrust ? {
|
|
193
|
+
connectionId: refusedTrust.connectionId,
|
|
194
|
+
trustId: refusedTrust.trustId,
|
|
195
|
+
observedClaims: { ...refusedTrust.observedClaims }
|
|
196
|
+
} : {}
|
|
197
|
+
};
|
|
198
|
+
await safeEmit(c.var.emit, {
|
|
199
|
+
action: ControlPlaneAuditActions.callDenied,
|
|
200
|
+
outcome: "denied",
|
|
201
|
+
severity: "warning",
|
|
202
|
+
actorType: "control-plane",
|
|
203
|
+
...proven?.actorKind ? { actorKind: proven.actorKind } : {},
|
|
204
|
+
actorId: proven?.subject ?? null,
|
|
205
|
+
...proven ? {
|
|
206
|
+
resourceType: "controlplane_connection",
|
|
207
|
+
resourceId: proven.connectionId
|
|
208
|
+
} : {},
|
|
209
|
+
requestId: c.req.header("cf-ray"),
|
|
210
|
+
ip: c.req.header("cf-connecting-ip"),
|
|
211
|
+
userAgent: c.req.header("user-agent"),
|
|
212
|
+
metadata: proven ? {
|
|
213
|
+
...metadata,
|
|
214
|
+
keyId: proven.keyId,
|
|
215
|
+
...proven.credentialKind === "trust" ? { credentialKind: "trust" } : {}
|
|
216
|
+
} : metadata
|
|
217
|
+
}, c.var.log);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Record a scope refusal a handler raised after the gate let the call in.
|
|
221
|
+
*
|
|
222
|
+
* Only `controlplane/insufficient_scope`: every other refusal past the gate is the handler's own to
|
|
223
|
+
* record, and a 404 or a validation error is not a credential reaching beyond its grant. The same row as
|
|
224
|
+
* {@link auditDenial}, with two differences that follow from the call having been verified: the actor is
|
|
225
|
+
* the proven subject and its connection, and `refusedBy` says the gate passed it — `required` is the
|
|
226
|
+
* gate's requirement, which this call met; what it lacked is in the log, with the rest of `detail`.
|
|
227
|
+
* `scope`, `keyId` and `credentialKind` name the refused credential exactly as the gate's `call_allowed` row does, so a
|
|
228
|
+
* read of one key's reach (#828) finds its refusals too, even on a request with no `cf-ray` to join by.
|
|
229
|
+
*/
|
|
230
|
+
async function auditHandlerDenial(c, requirement, context, cause) {
|
|
231
|
+
const code = cause?.payload?.code;
|
|
232
|
+
if (code !== "controlplane/insufficient_scope") return;
|
|
144
233
|
await safeEmit(c.var.emit, {
|
|
145
234
|
action: ControlPlaneAuditActions.callDenied,
|
|
146
235
|
outcome: "denied",
|
|
147
236
|
severity: "warning",
|
|
148
237
|
actorType: "control-plane",
|
|
149
|
-
actorId:
|
|
238
|
+
actorId: context.subject,
|
|
239
|
+
resourceType: "controlplane_connection",
|
|
240
|
+
resourceId: context.connectionId,
|
|
150
241
|
requestId: c.req.header("cf-ray"),
|
|
151
242
|
ip: c.req.header("cf-connecting-ip"),
|
|
152
243
|
userAgent: c.req.header("user-agent"),
|
|
153
244
|
metadata: {
|
|
154
245
|
reason: code,
|
|
155
246
|
required: String(requirement),
|
|
247
|
+
refusedBy: "handler",
|
|
248
|
+
connectionId: context.connectionId,
|
|
249
|
+
scope: context.scope,
|
|
250
|
+
keyId: context.keyId,
|
|
251
|
+
...context.credentialKind === "trust" ? { credentialKind: "trust" } : {},
|
|
156
252
|
method: c.req.method,
|
|
157
253
|
path: c.req.path
|
|
158
254
|
}
|
|
@@ -167,4 +263,4 @@ function connectStep(env) {
|
|
|
167
263
|
return remedy("dashboard connect", { flags: { "--env": env } }, { runtime: true }).say((command) => `run ${command}`);
|
|
168
264
|
}
|
|
169
265
|
//#endregion
|
|
170
|
-
export { CONTROL_PLANE_GUARD, createControlPlaneVerifier, isControlPlaneGuard, requireControlPlane, stampActorKind, workerVersionHeaders };
|
|
266
|
+
export { CONTROL_PLANE_GUARD, createControlPlaneVerifier, isControlPlaneGuard, requireControlPlane, requireControlPlaneFor, stampActorKind, workerVersionHeaders };
|