@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
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { RETENTION_READ_SCOPE } from "./scope/scope.js";
|
|
3
4
|
import { defineCapability } from "../capability/capability.js";
|
|
5
|
+
import { memoryJwksCache } from "../http/oidcWebhook.js";
|
|
6
|
+
import { routeGroupsOf } from "../openapi/routeGroups.js";
|
|
7
|
+
import { describePresenceRooms } from "../presence/http/routes.js";
|
|
4
8
|
import { readRetirement } from "../privacy/fieldRotation.js";
|
|
5
9
|
import { lazyPrivacyInventory } from "../privacy/inventory.js";
|
|
10
|
+
import { composedRetention } from "../retention/composed.js";
|
|
11
|
+
import { ControlPlaneConnection } from "./data/connection.js";
|
|
12
|
+
import { CONTROL_PLANE_CONNECTIONS_TABLE, controlPlaneTables } from "./data/tables.js";
|
|
13
|
+
import { standingHolds } from "../retention/holds.js";
|
|
14
|
+
import { holdsUserPurge } from "../retention/holdsPurge.js";
|
|
6
15
|
import { PACKAGE_NAME, PACKAGE_VERSION } from "../version.generated.js";
|
|
7
16
|
import "../worker/identity.js";
|
|
8
17
|
import { ControlPlaneConfig } from "./config/config.js";
|
|
9
|
-
import {
|
|
10
|
-
import { CONTROL_PLANE_CONNECTIONS_TABLE, controlPlaneTables } from "./data/tables.js";
|
|
18
|
+
import { recordTrustFirstCall } from "./data/trustFirstCall.js";
|
|
11
19
|
import { capabilityHealthSources, defineCapabilityHealth } from "./discovery/health.js";
|
|
12
20
|
import { createControlPlaneVerifier } from "./http/guard.js";
|
|
13
21
|
import { MIGRATION_HEALTH_KEYS, composeMigrationDatabases, readMigrationHealth } from "./migrationState/migrationState.js";
|
|
@@ -15,6 +23,8 @@ import { controlPlaneRouteDescriptors, registerControlPlaneRoutes } from "./http
|
|
|
15
23
|
import { controlplane_0001_init } from "./migrations/0001_init.js";
|
|
16
24
|
import { controlplane_0002_management_origin } from "./migrations/0002_management_origin.js";
|
|
17
25
|
import { controlplane_0003_connection_principal } from "./migrations/0003_connection_principal.js";
|
|
26
|
+
import { controlplane_0004_connection_trusts } from "./migrations/0004_connection_trusts.js";
|
|
27
|
+
import { controlplane_0005_holds } from "./migrations/0005_holds.js";
|
|
18
28
|
import { d1ReplayGuard } from "./replay/d1Guard.js";
|
|
19
29
|
import { CONTROL_PLANE_KV_BINDING, controlPlaneKvNamespaces, kvReplayGuard } from "./replay/kvGuard.js";
|
|
20
30
|
//#region src/controlPlane/capability.ts
|
|
@@ -29,6 +39,16 @@ import { CONTROL_PLANE_KV_BINDING, controlPlaneKvNamespaces, kvReplayGuard } fro
|
|
|
29
39
|
* composed key and make Kysely re-run an applied migration.
|
|
30
40
|
*/
|
|
31
41
|
const CONTROLPLANE_MIGRATION_ORDER = 1100;
|
|
42
|
+
/** The standing legal holds, on the seam's own manifest entry. Behind the scope the holds listing requires. */
|
|
43
|
+
const ACTIVE_HOLDS_HEALTH_KEY = {
|
|
44
|
+
key: "activeHolds",
|
|
45
|
+
kind: "count",
|
|
46
|
+
states: null,
|
|
47
|
+
scope: RETENTION_READ_SCOPE,
|
|
48
|
+
cost: "indexed",
|
|
49
|
+
summary: "Legal holds standing. While one stands, no purge, prune or inactivity deletion touches its subject.",
|
|
50
|
+
nominal: null
|
|
51
|
+
};
|
|
32
52
|
/** Narrow a composed capability to this one — the CLI and tests use it to find the seam's config. */
|
|
33
53
|
function isControlPlaneCapability(capability) {
|
|
34
54
|
return capability.name === "controlplane";
|
|
@@ -57,7 +77,9 @@ function controlplane(options = {}) {
|
|
|
57
77
|
const migrations = {
|
|
58
78
|
"0001_init": controlplane_0001_init,
|
|
59
79
|
"0002_management_origin": controlplane_0002_management_origin,
|
|
60
|
-
"0003_connection_principal": controlplane_0003_connection_principal
|
|
80
|
+
"0003_connection_principal": controlplane_0003_connection_principal,
|
|
81
|
+
"0004_connection_trusts": controlplane_0004_connection_trusts,
|
|
82
|
+
"0005_holds": controlplane_0005_holds
|
|
61
83
|
};
|
|
62
84
|
const requiredBindings = [{
|
|
63
85
|
type: "d1",
|
|
@@ -75,6 +97,7 @@ function controlplane(options = {}) {
|
|
|
75
97
|
*/
|
|
76
98
|
const database = (c) => c.var.db.app;
|
|
77
99
|
const now = () => /* @__PURE__ */ new Date();
|
|
100
|
+
const jwksCache = memoryJwksCache({ now });
|
|
78
101
|
/**
|
|
79
102
|
* The replay guard for this request, chosen once by config.
|
|
80
103
|
*
|
|
@@ -90,6 +113,7 @@ function controlplane(options = {}) {
|
|
|
90
113
|
let health = /* @__PURE__ */ new Map();
|
|
91
114
|
let migrationDatabases = [];
|
|
92
115
|
let composedCapabilities = [];
|
|
116
|
+
let presence = /* @__PURE__ */ new Map();
|
|
93
117
|
const privacy = lazyPrivacyInventory(() => composedCapabilities);
|
|
94
118
|
return {
|
|
95
119
|
...defineCapability({
|
|
@@ -106,9 +130,17 @@ function controlplane(options = {}) {
|
|
|
106
130
|
} },
|
|
107
131
|
kvNamespaces: usesKvReplay ? controlPlaneKvNamespaces(config.jtiTtlSeconds) : {},
|
|
108
132
|
adminRoutes: controlPlaneRouteDescriptors(config.basePath),
|
|
133
|
+
purge: [holdsUserPurge()],
|
|
109
134
|
health: defineCapabilityHealth({
|
|
110
|
-
keys: MIGRATION_HEALTH_KEYS,
|
|
111
|
-
read: (c) =>
|
|
135
|
+
keys: [...MIGRATION_HEALTH_KEYS, ACTIVE_HOLDS_HEALTH_KEY],
|
|
136
|
+
read: async (c) => {
|
|
137
|
+
const env = c.env;
|
|
138
|
+
const [migrations, activeHolds] = await Promise.all([readMigrationHealth(env, migrationDatabases), standingHolds(env)]);
|
|
139
|
+
return {
|
|
140
|
+
...migrations,
|
|
141
|
+
activeHolds
|
|
142
|
+
};
|
|
143
|
+
}
|
|
112
144
|
}),
|
|
113
145
|
compose: ({ capabilities }) => {
|
|
114
146
|
composed = capabilities.map((cap) => ({
|
|
@@ -118,8 +150,10 @@ function controlplane(options = {}) {
|
|
|
118
150
|
adminRoutes: [...cap.adminRoutes ?? []],
|
|
119
151
|
healthKeys: [...cap.health?.keys ?? []],
|
|
120
152
|
configKeys: [...cap.manifestConfig?.keys ?? []],
|
|
121
|
-
config: { ...cap.manifestConfig?.values ?? {} }
|
|
153
|
+
config: { ...cap.manifestConfig?.values ?? {} },
|
|
154
|
+
presenceRooms: cap.presence ? describePresenceRooms(cap.presence) : []
|
|
122
155
|
}));
|
|
156
|
+
presence = new Map(capabilities.flatMap((cap) => cap.presence ? cap.presence.rooms.map((room) => [room.kind, cap.presence]) : []));
|
|
123
157
|
health = capabilityHealthSources(capabilities);
|
|
124
158
|
migrationDatabases = composeMigrationDatabases(capabilities);
|
|
125
159
|
composedCapabilities = [...capabilities];
|
|
@@ -127,6 +161,7 @@ function controlplane(options = {}) {
|
|
|
127
161
|
middleware: [(app) => {
|
|
128
162
|
app.use("*", async (c, next) => {
|
|
129
163
|
const db = database(c);
|
|
164
|
+
const environment = String(c.env["ENVIRONMENT"] ?? "");
|
|
130
165
|
c.set("controlPlaneVerifier", createControlPlaneVerifier({
|
|
131
166
|
loadConnection: async (connectionId) => {
|
|
132
167
|
const row = await db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().where("id", "=", connectionId).executeTakeFirst();
|
|
@@ -136,9 +171,18 @@ function controlplane(options = {}) {
|
|
|
136
171
|
return (await db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).select("id").limit(1).execute()).length;
|
|
137
172
|
},
|
|
138
173
|
replay: replay(c),
|
|
139
|
-
environment
|
|
174
|
+
environment,
|
|
140
175
|
config,
|
|
141
176
|
now
|
|
177
|
+
}, {
|
|
178
|
+
loadEnvironmentConnection: async () => {
|
|
179
|
+
const row = await db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().where("environment", "=", environment).executeTakeFirst();
|
|
180
|
+
return row ? ControlPlaneConnection.parse(row) : null;
|
|
181
|
+
},
|
|
182
|
+
environment,
|
|
183
|
+
now,
|
|
184
|
+
jwksCache,
|
|
185
|
+
recordFirstCall: (connectionId, trustId, at) => recordTrustFirstCall(db, connectionId, trustId, at)
|
|
142
186
|
}));
|
|
143
187
|
await next();
|
|
144
188
|
});
|
|
@@ -152,6 +196,10 @@ function controlplane(options = {}) {
|
|
|
152
196
|
composedMigrations: () => migrationDatabases,
|
|
153
197
|
privacyInventory: privacy,
|
|
154
198
|
retirement: (env) => readRetirement(env, composedCapabilities),
|
|
199
|
+
composedPresence: () => presence,
|
|
200
|
+
composition: () => composedCapabilities,
|
|
201
|
+
routeGroups: () => routeGroupsOf(app),
|
|
202
|
+
retention: composedRetention,
|
|
155
203
|
now
|
|
156
204
|
});
|
|
157
205
|
}
|
|
@@ -160,4 +208,4 @@ function controlplane(options = {}) {
|
|
|
160
208
|
};
|
|
161
209
|
}
|
|
162
210
|
//#endregion
|
|
163
|
-
export { CONTROLPLANE_MIGRATION_ORDER, CONTROL_PLANE_KV_BINDING, controlplane, isControlPlaneCapability };
|
|
211
|
+
export { ACTIVE_HOLDS_HEALTH_KEY, CONTROLPLANE_MIGRATION_ORDER, CONTROL_PLANE_KV_BINDING, controlplane, isControlPlaneCapability };
|
|
@@ -24,6 +24,10 @@ export declare const ControlPlaneContext: z.ZodObject<{
|
|
|
24
24
|
grantedScopes: z.ZodArray<z.ZodString>;
|
|
25
25
|
keyId: z.ZodString;
|
|
26
26
|
tokenId: z.ZodString;
|
|
27
|
+
credentialKind: z.ZodOptional<z.ZodEnum<{
|
|
28
|
+
key: "key";
|
|
29
|
+
trust: "trust";
|
|
30
|
+
}>>;
|
|
27
31
|
actorKind: z.ZodNullable<z.ZodEnum<{
|
|
28
32
|
agent: "agent";
|
|
29
33
|
dashboard: "dashboard";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/controlPlane/context.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/controlPlane/context.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;iBAoD7B,CAAC;AACJ,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
|
@@ -25,8 +25,9 @@ const ControlPlaneContext = z.object({
|
|
|
25
25
|
subject: z.string().describe("The management client's own user id from the token's `sub` — who, on their side, is acting. Recorded as the audit `actorId` so the trail answers 'which person at the dashboard did this', not just 'the dashboard'."),
|
|
26
26
|
scope: ControlPlaneScope.describe("The single scope this token was minted for. One call, one operation — a token carrying the whole grant would make every call as dangerous as the most dangerous one."),
|
|
27
27
|
grantedScopes: z.array(ControlPlaneScope).describe("Every scope the adopter granted this connection, read from their own row. The authority: a scope absent here is denied however the token is written."),
|
|
28
|
-
keyId: z.string().describe("Which registered key verified this call. Recorded so a rotation can be traced through the trail, and so a
|
|
29
|
-
tokenId: z.string().describe("The token's `jti`, already claimed in the replay set. Recorded to tie one audit event to one call."),
|
|
28
|
+
keyId: z.string().describe("Which registered key verified this call — or, for a call under an OIDC trust, which trust. Recorded so a rotation can be traced through the trail, and so a credential still in use is visible before it is retired."),
|
|
29
|
+
tokenId: z.string().describe("The token's `jti`, already claimed in the replay set. Recorded to tie one audit event to one call. For a call under an OIDC trust, the provider token's own `jti` when it carries one, else empty: such a token is verified per call and never spent."),
|
|
30
|
+
credentialKind: z.enum(["key", "trust"]).optional().describe("Which kind of credential verified this call: a registered `key`, or an OIDC `trust` (#722), in which case `keyId` names the trust. Absent reads as a key, which is every call made before trusts existed."),
|
|
30
31
|
actorKind: ControlPlaneActorKind.nullable().describe("What kind of client the connection is for — `agent`, `dashboard` or `script` — read from the adopter's own row, never from the token. The gate stamps it onto every control-plane audit row this call writes. Null for a connection registered before the kind was recorded (#721).")
|
|
31
32
|
}).describe("The verified control-plane caller for one request. Never an AuthContext: a management client is not a user of the adopter's app.");
|
|
32
33
|
//#endregion
|
|
@@ -38,7 +38,7 @@ export declare const RegisteredKey: z.ZodObject<{
|
|
|
38
38
|
}, z.core.$strip>;
|
|
39
39
|
export type RegisteredKey = z.output<typeof RegisteredKey>;
|
|
40
40
|
/**
|
|
41
|
-
* What kind of client a connection is for
|
|
41
|
+
* What kind of client a connection is for: `agent`, `dashboard` or `script`.
|
|
42
42
|
*
|
|
43
43
|
* The audit trail's `actorType: "control-plane"` names all three identically, and the questions asked
|
|
44
44
|
* after an incident are exactly the ones that tell them apart — was it a person at the dashboard, a
|
|
@@ -69,7 +69,7 @@ export type ConnectionPrincipal = z.output<typeof ConnectionPrincipal>;
|
|
|
69
69
|
/** The longest purpose a connection may carry. One line on a review screen, with room to say what it does. */
|
|
70
70
|
export declare const MAX_CONNECTION_PURPOSE = 200;
|
|
71
71
|
/**
|
|
72
|
-
* What a connection is for, in the words of whoever registered it
|
|
72
|
+
* What a connection is for, in the words of whoever registered it.
|
|
73
73
|
*
|
|
74
74
|
* **Required at registration, and refused when absent**, because a review listing credentials by key id
|
|
75
75
|
* cannot support the decision it exists for. **Untrusted input**: a customer writes it and something else
|
|
@@ -77,6 +77,14 @@ export declare const MAX_CONNECTION_PURPOSE = 200;
|
|
|
77
77
|
*/
|
|
78
78
|
export declare const ConnectionPurpose: z.ZodString;
|
|
79
79
|
export type ConnectionPurpose = z.output<typeof ConnectionPurpose>;
|
|
80
|
+
/** Which credential kinds a connection holds, live or not. */
|
|
81
|
+
export declare const ConnectionCredentialKind: z.ZodEnum<{
|
|
82
|
+
both: "both";
|
|
83
|
+
key: "key";
|
|
84
|
+
none: "none";
|
|
85
|
+
trust: "trust";
|
|
86
|
+
}>;
|
|
87
|
+
export type ConnectionCredentialKind = z.infer<typeof ConnectionCredentialKind>;
|
|
80
88
|
/**
|
|
81
89
|
* One management-client connection — the row in `pithy_controlplane_connections`.
|
|
82
90
|
*
|
|
@@ -111,6 +119,17 @@ export declare const ControlPlaneConnection: z.ZodObject<{
|
|
|
111
119
|
validUntil: z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>>;
|
|
112
120
|
revokedAt: z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>>;
|
|
113
121
|
}, z.core.$strip>>>;
|
|
122
|
+
trusts: z.ZodOptional<import("../../data/codecs").SqliteJson<z.ZodArray<z.ZodObject<{
|
|
123
|
+
trustId: z.ZodString;
|
|
124
|
+
issuer: z.ZodURL;
|
|
125
|
+
jwksUrl: z.ZodURL;
|
|
126
|
+
audience: z.ZodString;
|
|
127
|
+
claims: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
128
|
+
authorizedBy: z.ZodEmail;
|
|
129
|
+
validFrom: z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>;
|
|
130
|
+
revokedAt: z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>>;
|
|
131
|
+
firstCallAt: z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>>;
|
|
132
|
+
}, z.core.$strip>>>>;
|
|
114
133
|
createdAt: z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>;
|
|
115
134
|
updatedAt: z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>;
|
|
116
135
|
}, z.core.$strip>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/connection.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/connection.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;;;;;GASG;AAEH,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB;;;;iBAgBoF,CAAC;AAClH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEjE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;iBAoBoF,CAAC;AAC/G,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB;;;;EAI/B,CAAC;AACJ,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,YAK7B,CAAC;AACJ,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEvE,8GAA8G;AAC9G,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAiB1C;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,aAW3B,CAAC;AACJ,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE,8DAA8D;AAC9D,eAAO,MAAM,wBAAwB;;;;;EAIlC,CAAC;AACJ,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuEhC,CAAC;AACJ,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC7E,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import { JsonDate, SQLiteDate, sqliteJson } from "../../data/codecs.js";
|
|
4
3
|
import { ControlPlaneScope } from "../scope/scope.js";
|
|
4
|
+
import { JsonDate, SQLiteDate, sqliteJson } from "../../data/codecs.js";
|
|
5
|
+
import { OidcTrust } from "./oidcTrust.js";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
//#region src/controlPlane/data/connection.ts
|
|
7
8
|
/**
|
|
@@ -35,7 +36,7 @@ const RegisteredKey = z.object({
|
|
|
35
36
|
revokedAt: JsonDate.nullable().describe("When this key was revoked outright, or null. Revocation ignores `validUntil` and takes effect immediately — it is the adopter's unilateral control, and it must not wait for a window to close.")
|
|
36
37
|
}).describe("One registered public key with its validity window. Two live at once during a rotation overlap.");
|
|
37
38
|
/**
|
|
38
|
-
* What kind of client a connection is for
|
|
39
|
+
* What kind of client a connection is for: `agent`, `dashboard` or `script`.
|
|
39
40
|
*
|
|
40
41
|
* The audit trail's `actorType: "control-plane"` names all three identically, and the questions asked
|
|
41
42
|
* after an incident are exactly the ones that tell them apart — was it a person at the dashboard, a
|
|
@@ -78,13 +79,20 @@ const MAX_CONNECTION_PURPOSE = 200;
|
|
|
78
79
|
*/
|
|
79
80
|
const UNRENDERABLE = /[\p{Cc}\p{Bidi_Control}\u2028\u2029\u200B\u2060\uFEFF]/u;
|
|
80
81
|
/**
|
|
81
|
-
* What a connection is for, in the words of whoever registered it
|
|
82
|
+
* What a connection is for, in the words of whoever registered it.
|
|
82
83
|
*
|
|
83
84
|
* **Required at registration, and refused when absent**, because a review listing credentials by key id
|
|
84
85
|
* cannot support the decision it exists for. **Untrusted input**: a customer writes it and something else
|
|
85
86
|
* renders it, so it is validated on the way in, and again on every read of the row.
|
|
86
87
|
*/
|
|
87
88
|
const ConnectionPurpose = z.string().trim().min(1, "Say what this client is for.").max(200, `A purpose is 200 characters at most.`).refine((purpose) => !UNRENDERABLE.test(purpose), "A purpose is one line of plain text: no control, bidi or invisible characters.").describe(`What this connection's client is for, in the registering operator's words — one line of plain text, 200 characters at most, with no control, bidi or invisible characters. Written by a customer and rendered elsewhere, so validated at the boundary.`);
|
|
89
|
+
/** Which credential kinds a connection holds, live or not. */
|
|
90
|
+
const ConnectionCredentialKind = z.enum([
|
|
91
|
+
"key",
|
|
92
|
+
"trust",
|
|
93
|
+
"both",
|
|
94
|
+
"none"
|
|
95
|
+
]).describe("Which credential kinds the connection holds, live or not: `key` (registered Ed25519 keys), `trust` (OIDC trusts), `both`, or `none`.");
|
|
88
96
|
/**
|
|
89
97
|
* One management-client connection — the row in `pithy_controlplane_connections`.
|
|
90
98
|
*
|
|
@@ -105,8 +113,9 @@ const ControlPlaneConnection = z.object({
|
|
|
105
113
|
purpose: ConnectionPurpose.nullable().default(null).describe("What this connection's client is for, in the registering operator's words. Required at registration and editable with `pithy dashboard connect --update --purpose`; null only for a connection registered before it was recorded."),
|
|
106
114
|
scopes: sqliteJson(z.array(ControlPlaneScope)).describe("The operations this connection was granted, stored and enforced on the adopter's side. Enforced only by the caller, a scope would not be a limit at all."),
|
|
107
115
|
keys: sqliteJson(z.array(RegisteredKey)).describe("Every key this connection may sign with, current and superseded. One JSON column rather than a child table, so a rotation is one atomic UPDATE and verification is one read."),
|
|
116
|
+
trusts: sqliteJson(z.array(OidcTrust)).optional().describe("Every OIDC trust this connection honors, live and revoked (#722) — the second credential kind beside `keys`, so a connection may hold a trust, a key, or both. Optional on the type, and absent reads as none: a connection value built before trusts existed still describes a connection, and the column itself defaults to an empty list."),
|
|
108
117
|
createdAt: SQLiteDate.describe("When the connection was registered. Ms-epoch in SQLite, a Date in TypeScript."),
|
|
109
118
|
updatedAt: SQLiteDate.describe("When the row last changed — a key registered or expired, or the Worker URL re-pointed.")
|
|
110
119
|
}).describe("One management-client connection: its identity, the environment and URL it is bound to, the operations it was granted, and its versioned public keys.");
|
|
111
120
|
//#endregion
|
|
112
|
-
export { ConnectionPrincipal, ConnectionPurpose, ControlPlaneActorKind, ControlPlaneConnection, Ed25519PublicJwk, MAX_CONNECTION_PURPOSE, RegisteredKey };
|
|
121
|
+
export { ConnectionCredentialKind, ConnectionPrincipal, ConnectionPurpose, ControlPlaneActorKind, ControlPlaneConnection, Ed25519PublicJwk, MAX_CONNECTION_PURPOSE, RegisteredKey };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { type ConnectionCredentialKind, ControlPlaneConnection, type RegisteredKey } from "./connection";
|
|
4
|
+
import { type OidcTrust } from "./oidcTrust";
|
|
5
|
+
import { type ControlPlaneDatabase } from "./tables";
|
|
6
|
+
/**
|
|
7
|
+
* Every connection in one database, read for a review: which person authorized each, and which still answer.
|
|
8
|
+
*
|
|
9
|
+
* `GET {basePath}/connections` and `pithy dashboard connections` both read through {@link listConnections}, so the
|
|
10
|
+
* Worker's answer and the CLI's cannot list different things.
|
|
11
|
+
*/
|
|
12
|
+
/** Which credential kinds a connection holds, live or not. */
|
|
13
|
+
export declare function connectionCredentialKind(connection: ControlPlaneConnection): ConnectionCredentialKind;
|
|
14
|
+
/** Whether any key or trust on the connection can verify a call at `now`. */
|
|
15
|
+
export declare function connectionLive(connection: ControlPlaneConnection, now: Date): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* When the connection was revoked: the latest revocation among its credentials, once none is live and every one of
|
|
18
|
+
* them was revoked. Null while anything is live, and for a connection whose keys merely expired.
|
|
19
|
+
*/
|
|
20
|
+
export declare function connectionRevokedAt(connection: ControlPlaneConnection, now: Date): Date | null;
|
|
21
|
+
/** What {@link revokeCredentials} changed. */
|
|
22
|
+
export interface RevokedCredentials {
|
|
23
|
+
/** The connection with every key and trust not yet revoked revoked at `now`. */
|
|
24
|
+
readonly connection: ControlPlaneConnection;
|
|
25
|
+
readonly revokedKeyIds: readonly string[];
|
|
26
|
+
readonly revokedTrustIds: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Revoke every key and trust not yet revoked, whatever its window. One that has not started will verify once its
|
|
30
|
+
* window opens, and the revoker's clock may run behind the Worker that stamped it, so it is revoked too. A credential
|
|
31
|
+
* already revoked keeps its instant.
|
|
32
|
+
*/
|
|
33
|
+
export declare function revokeCredentials(connection: ControlPlaneConnection, now: Date): RevokedCredentials;
|
|
34
|
+
/** Whether a connection was authorized by `principal`. An email, compared without regard to case. */
|
|
35
|
+
export declare function authorizedByPrincipal(connection: ControlPlaneConnection, principal: string): boolean;
|
|
36
|
+
/** Which connections {@link listConnections} reads. */
|
|
37
|
+
export interface ConnectionFilter {
|
|
38
|
+
/** Only connections this email authorized. Unattributed connections are listed apart either way. */
|
|
39
|
+
readonly principal?: string | undefined;
|
|
40
|
+
/** Only connections bound to this environment. */
|
|
41
|
+
readonly environment?: string | undefined;
|
|
42
|
+
}
|
|
43
|
+
/** The connections a filter selects, with the ones no principal was recorded for kept apart. */
|
|
44
|
+
export interface ConnectionListing {
|
|
45
|
+
/** Connections with a recorded principal, matching the filter, oldest first. */
|
|
46
|
+
readonly connections: readonly ControlPlaneConnection[];
|
|
47
|
+
/** Connections registered before principals were recorded, oldest first. Never matched by a principal. */
|
|
48
|
+
readonly unattributed: readonly ControlPlaneConnection[];
|
|
49
|
+
}
|
|
50
|
+
/** Every connection in the database, attributed ones matching the filter, unattributed ones apart. */
|
|
51
|
+
export declare function listConnections(db: ControlPlaneDatabase, filter?: ConnectionFilter): Promise<ConnectionListing>;
|
|
52
|
+
/** Read one connection by id, or null. */
|
|
53
|
+
export declare function readConnection(db: ControlPlaneDatabase, id: string): Promise<ControlPlaneConnection | null>;
|
|
54
|
+
/**
|
|
55
|
+
* A connection read for a write to its credential columns, with those columns exactly as stored. A write made from it
|
|
56
|
+
* lands only while they are unchanged, so a revocation landing in between is never overwritten.
|
|
57
|
+
*/
|
|
58
|
+
export interface ConnectionForWrite {
|
|
59
|
+
readonly connection: ControlPlaneConnection;
|
|
60
|
+
/** The keys column as stored, compared as stored. */
|
|
61
|
+
readonly storedKeys: unknown;
|
|
62
|
+
/** The trusts column as stored, compared as stored. */
|
|
63
|
+
readonly storedTrusts: unknown;
|
|
64
|
+
}
|
|
65
|
+
/** Read one connection by id for a write, or null. With `environment`, null for a connection bound to another. */
|
|
66
|
+
export declare function readConnectionForWrite(db: ControlPlaneDatabase, id: string, options?: {
|
|
67
|
+
readonly environment?: string | undefined;
|
|
68
|
+
}): Promise<ConnectionForWrite | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Write a connection's keys and stamp `updatedAt`, only while its keys column still holds what `read` saw. False
|
|
71
|
+
* when it no longer does: another write, a revocation among them, landed in between and nothing was written.
|
|
72
|
+
*/
|
|
73
|
+
export declare function writeKeys(db: ControlPlaneDatabase, read: ConnectionForWrite, keys: readonly RegisteredKey[], now: Date): Promise<boolean>;
|
|
74
|
+
/** The credential columns a {@link writeCredentials} call replaces. A column left out is not written. */
|
|
75
|
+
export interface CredentialColumns {
|
|
76
|
+
readonly keys?: readonly RegisteredKey[] | undefined;
|
|
77
|
+
readonly trusts?: readonly OidcTrust[] | undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Write the given credential columns and stamp `updatedAt`, only while both the keys and the trusts columns still hold
|
|
81
|
+
* what `read` saw. False when either changed in between, and nothing was written.
|
|
82
|
+
*/
|
|
83
|
+
export declare function writeCredentials(db: ControlPlaneDatabase, read: ConnectionForWrite, columns: CredentialColumns, now: Date): Promise<boolean>;
|
|
84
|
+
/**
|
|
85
|
+
* Revoke every key and trust of one connection not yet revoked, or answer null when no connection has that id. With
|
|
86
|
+
* `environment`, a connection bound to another environment answers null too.
|
|
87
|
+
*
|
|
88
|
+
* Writes the two credential columns and the stamp and nothing else on the row, and only while both columns still
|
|
89
|
+
* hold what it read; when another write landed in between, it reads again.
|
|
90
|
+
*/
|
|
91
|
+
export declare function revokeConnection(db: ControlPlaneDatabase, id: string, now: Date, options?: {
|
|
92
|
+
readonly environment?: string | undefined;
|
|
93
|
+
}): Promise<RevokedCredentials | null>;
|
|
94
|
+
//# sourceMappingURL=connections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/connections.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,wBAAwB,EAAE,sBAAsB,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAEzG,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAmC,KAAK,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAEtF;;;;;GAKG;AAEH,8DAA8D;AAC9D,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,sBAAsB,GAAG,wBAAwB,CAOrG;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,UAAU,EAAE,sBAAsB,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAErF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,sBAAsB,EAAE,GAAG,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAU9F;AAED,8CAA8C;AAC9C,MAAM,WAAW,kBAAkB;IACjC,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,sBAAsB,CAAC;IAC5C,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,sBAAsB,EAAE,GAAG,EAAE,IAAI,GAAG,kBAAkB,CAenG;AAED,qGAAqG;AACrG,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,sBAAsB,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAEpG;AAED,uDAAuD;AACvD,MAAM,WAAW,gBAAgB;IAC/B,oGAAoG;IACpG,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,kDAAkD;IAClD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3C;AAED,gGAAgG;AAChG,MAAM,WAAW,iBAAiB;IAChC,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,SAAS,sBAAsB,EAAE,CAAC;IACxD,0GAA0G;IAC1G,QAAQ,CAAC,YAAY,EAAE,SAAS,sBAAsB,EAAE,CAAC;CAC1D;AAED,sGAAsG;AACtG,wBAAsB,eAAe,CACnC,EAAE,EAAE,oBAAoB,EACxB,MAAM,GAAE,gBAAqB,GAC5B,OAAO,CAAC,iBAAiB,CAAC,CAW5B;AAED,0CAA0C;AAC1C,wBAAsB,cAAc,CAAC,EAAE,EAAE,oBAAoB,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAGjH;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,UAAU,EAAE,sBAAsB,CAAC;IAC5C,qDAAqD;IACrD,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAED,kHAAkH;AAClH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,oBAAoB,EACxB,EAAE,EAAE,MAAM,EACV,OAAO,GAAE;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO,GAC1D,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAMpC;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAC7B,EAAE,EAAE,oBAAoB,EACxB,IAAI,EAAE,kBAAkB,EACxB,IAAI,EAAE,SAAS,aAAa,EAAE,EAC9B,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,OAAO,CAAC,CAWlB;AAED,yGAAyG;AACzG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,CAAC;CACpD;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,oBAAoB,EACxB,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,iBAAiB,EAC1B,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,OAAO,CAAC,CAelB;AAKD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,oBAAoB,EACxB,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,IAAI,EACT,OAAO,GAAE;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO,GAC1D,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAcpC"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { ConflictError } from "../../error/pithyError.js";
|
|
4
|
+
import { activeTrusts } from "./oidcTrust.js";
|
|
5
|
+
import { ControlPlaneConnection } from "./connection.js";
|
|
6
|
+
import { CONTROL_PLANE_CONNECTIONS_TABLE } from "./tables.js";
|
|
7
|
+
import { activeKeys } from "./keyLifecycle.js";
|
|
8
|
+
import { sql } from "kysely";
|
|
9
|
+
//#region src/controlPlane/data/connections.ts
|
|
10
|
+
/**
|
|
11
|
+
* Every connection in one database, read for a review: which person authorized each, and which still answer.
|
|
12
|
+
*
|
|
13
|
+
* `GET {basePath}/connections` and `pithy dashboard connections` both read through {@link listConnections}, so the
|
|
14
|
+
* Worker's answer and the CLI's cannot list different things.
|
|
15
|
+
*/
|
|
16
|
+
/** Which credential kinds a connection holds, live or not. */
|
|
17
|
+
function connectionCredentialKind(connection) {
|
|
18
|
+
const keys = connection.keys.length > 0;
|
|
19
|
+
const trusts = (connection.trusts ?? []).length > 0;
|
|
20
|
+
if (keys && trusts) return "both";
|
|
21
|
+
if (keys) return "key";
|
|
22
|
+
if (trusts) return "trust";
|
|
23
|
+
return "none";
|
|
24
|
+
}
|
|
25
|
+
/** Whether any key or trust on the connection can verify a call at `now`. */
|
|
26
|
+
function connectionLive(connection, now) {
|
|
27
|
+
return activeKeys(connection.keys, now).length > 0 || activeTrusts(connection.trusts ?? [], now).length > 0;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* When the connection was revoked: the latest revocation among its credentials, once none is live and every one of
|
|
31
|
+
* them was revoked. Null while anything is live, and for a connection whose keys merely expired.
|
|
32
|
+
*/
|
|
33
|
+
function connectionRevokedAt(connection, now) {
|
|
34
|
+
if (connectionLive(connection, now)) return null;
|
|
35
|
+
const credentials = [...connection.keys, ...connection.trusts ?? []];
|
|
36
|
+
if (credentials.length === 0) return null;
|
|
37
|
+
let latest = null;
|
|
38
|
+
for (const credential of credentials) {
|
|
39
|
+
if (credential.revokedAt === null) return null;
|
|
40
|
+
if (latest === null || credential.revokedAt.getTime() > latest.getTime()) latest = credential.revokedAt;
|
|
41
|
+
}
|
|
42
|
+
return latest;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Revoke every key and trust not yet revoked, whatever its window. One that has not started will verify once its
|
|
46
|
+
* window opens, and the revoker's clock may run behind the Worker that stamped it, so it is revoked too. A credential
|
|
47
|
+
* already revoked keeps its instant.
|
|
48
|
+
*/
|
|
49
|
+
function revokeCredentials(connection, now) {
|
|
50
|
+
const keyIds = connection.keys.filter((key) => key.revokedAt === null).map((key) => key.keyId);
|
|
51
|
+
const trustIds = (connection.trusts ?? []).filter((trust) => trust.revokedAt === null).map((trust) => trust.trustId);
|
|
52
|
+
return {
|
|
53
|
+
connection: {
|
|
54
|
+
...connection,
|
|
55
|
+
keys: connection.keys.map((key) => key.revokedAt === null ? {
|
|
56
|
+
...key,
|
|
57
|
+
revokedAt: now
|
|
58
|
+
} : key),
|
|
59
|
+
trusts: (connection.trusts ?? []).map((trust) => trust.revokedAt === null ? {
|
|
60
|
+
...trust,
|
|
61
|
+
revokedAt: now
|
|
62
|
+
} : trust),
|
|
63
|
+
updatedAt: keyIds.length + trustIds.length > 0 ? now : connection.updatedAt
|
|
64
|
+
},
|
|
65
|
+
revokedKeyIds: keyIds,
|
|
66
|
+
revokedTrustIds: trustIds
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Whether a connection was authorized by `principal`. An email, compared without regard to case. */
|
|
70
|
+
function authorizedByPrincipal(connection, principal) {
|
|
71
|
+
return connection.authorizedBy !== null && connection.authorizedBy.toLowerCase() === principal.toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
/** Every connection in the database, attributed ones matching the filter, unattributed ones apart. */
|
|
74
|
+
async function listConnections(db, filter = {}) {
|
|
75
|
+
let query = db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().orderBy("createdAt").orderBy("id");
|
|
76
|
+
if (filter.environment !== void 0) query = query.where("environment", "=", filter.environment);
|
|
77
|
+
const rows = (await query.execute()).map((row) => ControlPlaneConnection.parse(row));
|
|
78
|
+
const { principal } = filter;
|
|
79
|
+
return {
|
|
80
|
+
connections: rows.filter((row) => row.authorizedBy !== null && (principal === void 0 || authorizedByPrincipal(row, principal))),
|
|
81
|
+
unattributed: rows.filter((row) => row.authorizedBy === null)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Read one connection by id, or null. */
|
|
85
|
+
async function readConnection(db, id) {
|
|
86
|
+
const row = await db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().where("id", "=", id).executeTakeFirst();
|
|
87
|
+
return row ? ControlPlaneConnection.parse(row) : null;
|
|
88
|
+
}
|
|
89
|
+
/** Read one connection by id for a write, or null. With `environment`, null for a connection bound to another. */
|
|
90
|
+
async function readConnectionForWrite(db, id, options = {}) {
|
|
91
|
+
let query = db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().where("id", "=", id);
|
|
92
|
+
if (options.environment !== void 0) query = query.where("environment", "=", options.environment);
|
|
93
|
+
const row = await query.executeTakeFirst();
|
|
94
|
+
if (!row) return null;
|
|
95
|
+
return {
|
|
96
|
+
connection: ControlPlaneConnection.parse(row),
|
|
97
|
+
storedKeys: row.keys,
|
|
98
|
+
storedTrusts: row.trusts
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Write a connection's keys and stamp `updatedAt`, only while its keys column still holds what `read` saw. False
|
|
103
|
+
* when it no longer does: another write, a revocation among them, landed in between and nothing was written.
|
|
104
|
+
*/
|
|
105
|
+
async function writeKeys(db, read, keys, now) {
|
|
106
|
+
const result = await db.updateTable(CONTROL_PLANE_CONNECTIONS_TABLE).set({
|
|
107
|
+
keys: ControlPlaneConnection.shape.keys.encode([...keys]),
|
|
108
|
+
updatedAt: ControlPlaneConnection.shape.updatedAt.encode(now)
|
|
109
|
+
}).where("id", "=", read.connection.id).where(sql`${sql.ref("keys")} = ${read.storedKeys}`).executeTakeFirst();
|
|
110
|
+
return Number(result.numUpdatedRows) > 0;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Write the given credential columns and stamp `updatedAt`, only while both the keys and the trusts columns still hold
|
|
114
|
+
* what `read` saw. False when either changed in between, and nothing was written.
|
|
115
|
+
*/
|
|
116
|
+
async function writeCredentials(db, read, columns, now) {
|
|
117
|
+
const result = await db.updateTable(CONTROL_PLANE_CONNECTIONS_TABLE).set({
|
|
118
|
+
...columns.keys === void 0 ? {} : { keys: ControlPlaneConnection.shape.keys.encode([...columns.keys]) },
|
|
119
|
+
...columns.trusts === void 0 ? {} : { trusts: ControlPlaneConnection.shape.trusts.encode([...columns.trusts]) },
|
|
120
|
+
updatedAt: ControlPlaneConnection.shape.updatedAt.encode(now)
|
|
121
|
+
}).where("id", "=", read.connection.id).where(sql`${sql.ref("keys")} = ${read.storedKeys}`).where(sql`${sql.ref("trusts")} = ${read.storedTrusts}`).executeTakeFirst();
|
|
122
|
+
return Number(result.numUpdatedRows) > 0;
|
|
123
|
+
}
|
|
124
|
+
/** How many times {@link revokeConnection} reads again after a write landed between its read and its own. */
|
|
125
|
+
const REVOKE_ATTEMPTS = 5;
|
|
126
|
+
/**
|
|
127
|
+
* Revoke every key and trust of one connection not yet revoked, or answer null when no connection has that id. With
|
|
128
|
+
* `environment`, a connection bound to another environment answers null too.
|
|
129
|
+
*
|
|
130
|
+
* Writes the two credential columns and the stamp and nothing else on the row, and only while both columns still
|
|
131
|
+
* hold what it read; when another write landed in between, it reads again.
|
|
132
|
+
*/
|
|
133
|
+
async function revokeConnection(db, id, now, options = {}) {
|
|
134
|
+
for (let attempt = 0; attempt < REVOKE_ATTEMPTS; attempt++) {
|
|
135
|
+
const read = await readConnectionForWrite(db, id, options);
|
|
136
|
+
if (read === null) return null;
|
|
137
|
+
const revoked = revokeCredentials(read.connection, now);
|
|
138
|
+
if (revoked.revokedKeyIds.length + revoked.revokedTrustIds.length === 0) return revoked;
|
|
139
|
+
if (await writeCredentials(db, read, {
|
|
140
|
+
keys: revoked.connection.keys,
|
|
141
|
+
trusts: revoked.connection.trusts ?? []
|
|
142
|
+
}, now)) return revoked;
|
|
143
|
+
}
|
|
144
|
+
throw new ConflictError({
|
|
145
|
+
message: "The connection kept changing while it was being revoked.",
|
|
146
|
+
action: "Revoke it again.",
|
|
147
|
+
detail: `connection ${id} changed between read and write ${REVOKE_ATTEMPTS} times`
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
export { authorizedByPrincipal, connectionCredentialKind, connectionLive, connectionRevokedAt, listConnections, readConnection, readConnectionForWrite, revokeConnection, revokeCredentials, writeCredentials, writeKeys };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* **A legal hold: one subject preserved, with the reason it was placed and, once released, why.**
|
|
6
|
+
*
|
|
7
|
+
* Placed and released over the control plane, behind `retention:hold`, because a hold is operational and urgent:
|
|
8
|
+
* counsel says preserve everything about this customer, and the next scheduled purge is tonight. While a hold stands
|
|
9
|
+
* no purge, prune or inactivity deletion touches its subject. Released holds are kept — the row is the record that
|
|
10
|
+
* one stood, from when to when, and why it ended — so nothing here is ever deleted.
|
|
11
|
+
*/
|
|
12
|
+
/** What a hold preserves: one user, one organization, or one tenant's audit trail. */
|
|
13
|
+
export declare const HoldSubjectType: z.ZodEnum<{
|
|
14
|
+
audit: "audit";
|
|
15
|
+
organization: "organization";
|
|
16
|
+
user: "user";
|
|
17
|
+
}>;
|
|
18
|
+
export type HoldSubjectType = z.infer<typeof HoldSubjectType>;
|
|
19
|
+
/** Whether a hold is in force. */
|
|
20
|
+
export declare const HoldState: z.ZodEnum<{
|
|
21
|
+
active: "active";
|
|
22
|
+
released: "released";
|
|
23
|
+
}>;
|
|
24
|
+
export type HoldState = z.infer<typeof HoldState>;
|
|
25
|
+
/** One hold, as `pithy_controlplane_holds` stores it. */
|
|
26
|
+
export declare const LegalHold: z.ZodObject<{
|
|
27
|
+
id: z.ZodString;
|
|
28
|
+
subjectType: z.ZodEnum<{
|
|
29
|
+
audit: "audit";
|
|
30
|
+
organization: "organization";
|
|
31
|
+
user: "user";
|
|
32
|
+
}>;
|
|
33
|
+
subjectId: z.ZodNullable<z.ZodString>;
|
|
34
|
+
reason: z.ZodString;
|
|
35
|
+
placedBy: z.ZodString;
|
|
36
|
+
placedAt: z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>;
|
|
37
|
+
state: z.ZodEnum<{
|
|
38
|
+
active: "active";
|
|
39
|
+
released: "released";
|
|
40
|
+
}>;
|
|
41
|
+
releasedAt: z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>>;
|
|
42
|
+
releasedBy: z.ZodNullable<z.ZodString>;
|
|
43
|
+
releaseReason: z.ZodNullable<z.ZodString>;
|
|
44
|
+
}, z.core.$strip>;
|
|
45
|
+
export type LegalHold = z.output<typeof LegalHold>;
|
|
46
|
+
//# sourceMappingURL=hold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hold.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/hold.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;GAOG;AAEH,sFAAsF;AACtF,eAAO,MAAM,eAAe;;;;EAIzB,CAAC;AACJ,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,kCAAkC;AAClC,eAAO,MAAM,SAAS;;;EAE6E,CAAC;AACpG,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAElD,yDAAyD;AACzD,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;iBAoBgE,CAAC;AACvF,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { SQLiteDate } from "../../data/codecs.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/controlPlane/data/hold.ts
|
|
6
|
+
/**
|
|
7
|
+
* **A legal hold: one subject preserved, with the reason it was placed and, once released, why.**
|
|
8
|
+
*
|
|
9
|
+
* Placed and released over the control plane, behind `retention:hold`, because a hold is operational and urgent:
|
|
10
|
+
* counsel says preserve everything about this customer, and the next scheduled purge is tonight. While a hold stands
|
|
11
|
+
* no purge, prune or inactivity deletion touches its subject. Released holds are kept — the row is the record that
|
|
12
|
+
* one stood, from when to when, and why it ended — so nothing here is ever deleted.
|
|
13
|
+
*/
|
|
14
|
+
/** What a hold preserves: one user, one organization, or one tenant's audit trail. */
|
|
15
|
+
const HoldSubjectType = z.enum([
|
|
16
|
+
"user",
|
|
17
|
+
"organization",
|
|
18
|
+
"audit"
|
|
19
|
+
]).describe("What the hold preserves: `user`, one account and everything its purge would erase; `organization`, one tenant and everything its purge would erase; `audit`, one tenant's audit trail, which the prune then leaves whole.");
|
|
20
|
+
/** Whether a hold is in force. */
|
|
21
|
+
const HoldState = z.enum(["active", "released"]).describe("`active` while the hold stands; `released` once it has been lifted, with the reason.");
|
|
22
|
+
/** One hold, as `pithy_controlplane_holds` stores it. */
|
|
23
|
+
const LegalHold = z.object({
|
|
24
|
+
id: z.string().min(1).describe("The hold's id. Text, never a sequence: it is named on a release route."),
|
|
25
|
+
subjectType: HoldSubjectType,
|
|
26
|
+
subjectId: z.string().min(1).max(255).nullable().describe("The held user's or organization's id, or the tenant whose audit trail is held. Null only for an `audit` hold on the untenanted trail."),
|
|
27
|
+
reason: z.string().min(1).max(500).describe("Why the hold was placed, in whoever placed it's words."),
|
|
28
|
+
placedBy: z.string().describe("The management client's own user id that placed it — the token's `sub`."),
|
|
29
|
+
placedAt: SQLiteDate.describe("When the hold was placed. Ms-epoch in SQLite."),
|
|
30
|
+
state: HoldState,
|
|
31
|
+
releasedAt: SQLiteDate.nullable().describe("When the hold was released, or null while it stands."),
|
|
32
|
+
releasedBy: z.string().nullable().describe("Who released it, or null while it stands."),
|
|
33
|
+
releaseReason: z.string().max(500).nullable().describe("Why it was released, or null while it stands.")
|
|
34
|
+
}).describe("A legal hold on one user, one organization or one tenant's audit trail.");
|
|
35
|
+
//#endregion
|
|
36
|
+
export { HoldState, HoldSubjectType, LegalHold };
|