@pithy-sh/core 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/audit/auditEvent.js +1 -1
- package/dist/audit/chain.d.ts +289 -0
- package/dist/audit/chain.d.ts.map +1 -0
- package/dist/audit/chain.js +400 -0
- package/dist/audit/citations.d.ts +52 -0
- package/dist/audit/citations.d.ts.map +1 -0
- package/dist/audit/citations.js +13 -0
- package/dist/capability/bindings.d.ts +22 -0
- package/dist/capability/bindings.d.ts.map +1 -1
- package/dist/capability/bindings.js +32 -2
- package/dist/capability/capability.d.ts +123 -16
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/capability.js +17 -2
- package/dist/capability/erasure.d.ts +75 -0
- package/dist/capability/erasure.d.ts.map +1 -0
- package/dist/capability/erasure.js +2 -0
- package/dist/capability/manifest.d.ts +5 -0
- package/dist/capability/manifest.d.ts.map +1 -1
- package/dist/capability/personColumns.d.ts +28 -0
- package/dist/capability/personColumns.d.ts.map +1 -0
- package/dist/capability/personColumns.js +66 -0
- package/dist/capability/personRows.d.ts +62 -0
- package/dist/capability/personRows.d.ts.map +1 -0
- package/dist/capability/personRows.js +59 -0
- package/dist/capability/purge.d.ts +86 -19
- package/dist/capability/purge.d.ts.map +1 -1
- package/dist/capability/purge.js +83 -5
- package/dist/capability/settings.d.ts +10 -0
- package/dist/capability/settings.d.ts.map +1 -1
- package/dist/capability/vectorIndex.d.ts +36 -0
- package/dist/capability/vectorIndex.d.ts.map +1 -0
- package/dist/capability/vectorIndex.js +25 -0
- package/dist/cli/commandCatalog.generated.d.ts +438 -3
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
- package/dist/cli/commandCatalog.generated.js +454 -2
- package/dist/cli/commandFacts.d.ts +8 -5
- package/dist/cli/commandFacts.d.ts.map +1 -1
- package/dist/cli/commandFacts.js +12 -3
- package/dist/consent/purpose.d.ts +70 -0
- package/dist/consent/purpose.d.ts.map +1 -0
- package/dist/consent/purpose.js +97 -0
- package/dist/consent/reader.d.ts +99 -0
- package/dist/consent/reader.d.ts.map +1 -0
- package/dist/consent/reader.js +33 -0
- package/dist/controlPlane/audit/actions.d.ts +15 -4
- package/dist/controlPlane/audit/actions.d.ts.map +1 -1
- package/dist/controlPlane/audit/actions.js +16 -5
- package/dist/controlPlane/capability.d.ts +10 -0
- package/dist/controlPlane/capability.d.ts.map +1 -1
- package/dist/controlPlane/capability.js +56 -8
- package/dist/controlPlane/context.d.ts +4 -0
- package/dist/controlPlane/context.d.ts.map +1 -1
- package/dist/controlPlane/context.js +3 -2
- package/dist/controlPlane/data/connection.d.ts +21 -2
- package/dist/controlPlane/data/connection.d.ts.map +1 -1
- package/dist/controlPlane/data/connection.js +13 -4
- package/dist/controlPlane/data/connections.d.ts +94 -0
- package/dist/controlPlane/data/connections.d.ts.map +1 -0
- package/dist/controlPlane/data/connections.js +151 -0
- package/dist/controlPlane/data/hold.d.ts +46 -0
- package/dist/controlPlane/data/hold.d.ts.map +1 -0
- package/dist/controlPlane/data/hold.js +36 -0
- package/dist/controlPlane/data/oidcTrust.d.ts +122 -0
- package/dist/controlPlane/data/oidcTrust.d.ts.map +1 -0
- package/dist/controlPlane/data/oidcTrust.js +173 -0
- package/dist/controlPlane/data/tables.d.ts +4 -0
- package/dist/controlPlane/data/tables.d.ts.map +1 -1
- package/dist/controlPlane/data/tables.js +9 -4
- package/dist/controlPlane/data/trustFirstCall.d.ts +16 -0
- package/dist/controlPlane/data/trustFirstCall.d.ts.map +1 -0
- package/dist/controlPlane/data/trustFirstCall.js +25 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts +165 -2
- package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -1
- package/dist/controlPlane/discovery/adminRoute.js +32 -8
- package/dist/controlPlane/discovery/routeInput.d.ts +23 -0
- package/dist/controlPlane/discovery/routeInput.d.ts.map +1 -0
- package/dist/controlPlane/discovery/routeInput.js +80 -0
- package/dist/controlPlane/discovery/toolName.d.ts +21 -0
- package/dist/controlPlane/discovery/toolName.d.ts.map +1 -0
- package/dist/controlPlane/discovery/toolName.js +25 -0
- package/dist/controlPlane/erasure/attest.d.ts +78 -0
- package/dist/controlPlane/erasure/attest.d.ts.map +1 -0
- package/dist/controlPlane/erasure/attest.js +164 -0
- package/dist/controlPlane/erasure/read.d.ts +39 -0
- package/dist/controlPlane/erasure/read.d.ts.map +1 -0
- package/dist/controlPlane/erasure/read.js +140 -0
- package/dist/controlPlane/error/errors.d.ts +61 -2
- package/dist/controlPlane/error/errors.d.ts.map +1 -1
- package/dist/controlPlane/error/errors.js +13 -0
- package/dist/controlPlane/evidence/derive.d.ts +124 -0
- package/dist/controlPlane/evidence/derive.d.ts.map +1 -0
- package/dist/controlPlane/evidence/derive.js +343 -0
- package/dist/controlPlane/evidence/mapping.d.ts +85 -0
- package/dist/controlPlane/evidence/mapping.d.ts.map +1 -0
- package/dist/controlPlane/evidence/mapping.js +114 -0
- package/dist/controlPlane/evidence/pack.d.ts +46 -0
- package/dist/controlPlane/evidence/pack.d.ts.map +1 -0
- package/dist/controlPlane/evidence/pack.js +124 -0
- package/dist/controlPlane/http/connections.d.ts +96 -0
- package/dist/controlPlane/http/connections.d.ts.map +1 -0
- package/dist/controlPlane/http/connections.js +109 -0
- package/dist/controlPlane/http/guard.d.ts +23 -2
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +69 -12
- package/dist/controlPlane/http/handlers.d.ts +212 -10
- package/dist/controlPlane/http/handlers.d.ts.map +1 -1
- package/dist/controlPlane/http/handlers.js +117 -26
- package/dist/controlPlane/http/holds.d.ts +57 -0
- package/dist/controlPlane/http/holds.d.ts.map +1 -0
- package/dist/controlPlane/http/holds.js +98 -0
- package/dist/controlPlane/http/presence.d.ts +27 -0
- package/dist/controlPlane/http/presence.d.ts.map +1 -0
- package/dist/controlPlane/http/presence.js +67 -0
- package/dist/controlPlane/http/processing.d.ts +73 -0
- package/dist/controlPlane/http/processing.d.ts.map +1 -0
- package/dist/controlPlane/http/processing.js +17 -0
- package/dist/controlPlane/http/responses.d.ts +927 -3
- package/dist/controlPlane/http/responses.d.ts.map +1 -1
- package/dist/controlPlane/http/responses.js +283 -7
- package/dist/controlPlane/http/routes.d.ts +12 -2
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +182 -5
- package/dist/controlPlane/http/schemas.d.ts +63 -0
- package/dist/controlPlane/http/schemas.d.ts.map +1 -1
- package/dist/controlPlane/http/schemas.js +39 -1
- package/dist/controlPlane/http/verify.d.ts.map +1 -1
- package/dist/controlPlane/http/verify.js +13 -1
- package/dist/controlPlane/http/verifyTrust.d.ts +76 -0
- package/dist/controlPlane/http/verifyTrust.d.ts.map +1 -0
- package/dist/controlPlane/http/verifyTrust.js +75 -0
- package/dist/controlPlane/migrations/0004_connection_trusts.d.ts +15 -0
- package/dist/controlPlane/migrations/0004_connection_trusts.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0004_connection_trusts.js +23 -0
- package/dist/controlPlane/migrations/0005_holds.d.ts +12 -0
- package/dist/controlPlane/migrations/0005_holds.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0005_holds.js +26 -0
- package/dist/controlPlane/processing/derive.d.ts +77 -0
- package/dist/controlPlane/processing/derive.d.ts.map +1 -0
- package/dist/controlPlane/processing/derive.js +206 -0
- package/dist/controlPlane/processing/read.d.ts +10 -0
- package/dist/controlPlane/processing/read.d.ts.map +1 -0
- package/dist/controlPlane/processing/read.js +89 -0
- package/dist/controlPlane/scope/kind.d.ts +46 -0
- package/dist/controlPlane/scope/kind.d.ts.map +1 -0
- package/dist/controlPlane/scope/kind.js +52 -0
- package/dist/controlPlane/scope/scope.d.ts +58 -3
- package/dist/controlPlane/scope/scope.d.ts.map +1 -1
- package/dist/controlPlane/scope/scope.js +59 -4
- package/dist/controlPlane/token/jws.js +1 -1
- package/dist/controlPlane/token/mint.js +1 -1
- package/dist/controlPlane/wire.d.ts +23 -0
- package/dist/controlPlane/wire.d.ts.map +1 -1
- package/dist/controlPlane/wire.js +24 -1
- package/dist/createBackend.d.ts +13 -0
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +11 -3
- package/dist/createEntrypoint.d.ts +7 -0
- package/dist/createEntrypoint.d.ts.map +1 -1
- package/dist/createEntrypoint.js +4 -0
- package/dist/error/http.d.ts +168 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +336 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +112 -0
- package/dist/error/pithyError.d.ts +22 -0
- package/dist/error/pithyError.d.ts.map +1 -1
- package/dist/error/pithyError.js +31 -1
- package/dist/http/routeSpec.d.ts +46 -1
- package/dist/http/routeSpec.d.ts.map +1 -1
- package/dist/http/routeSpec.js +39 -5
- package/dist/http/verification.d.ts.map +1 -1
- package/dist/http/verification.js +1 -1
- package/dist/i18n/translator.d.ts +2 -1
- package/dist/i18n/translator.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/logger/record.d.ts +98 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/mcp/annotations.d.ts +20 -0
- package/dist/mcp/annotations.d.ts.map +1 -0
- package/dist/mcp/annotations.js +23 -0
- package/dist/mcp/tools.d.ts +75 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +118 -0
- package/dist/openapi/document.d.ts.map +1 -1
- package/dist/openapi/document.js +8 -26
- package/dist/openapi/operations.d.ts +35 -0
- package/dist/openapi/operations.d.ts.map +1 -0
- package/dist/openapi/operations.js +37 -0
- package/dist/presence/channel.d.ts +11 -0
- package/dist/presence/channel.d.ts.map +1 -0
- package/dist/presence/channel.js +65 -0
- package/dist/presence/declaration.d.ts +154 -0
- package/dist/presence/declaration.d.ts.map +1 -0
- package/dist/presence/declaration.js +164 -0
- package/dist/presence/durableObject.d.ts +61 -0
- package/dist/presence/durableObject.d.ts.map +1 -0
- package/dist/presence/durableObject.js +257 -0
- package/dist/presence/frames.d.ts +166 -0
- package/dist/presence/frames.d.ts.map +1 -0
- package/dist/presence/frames.js +90 -0
- package/dist/presence/http/guard.d.ts +11 -0
- package/dist/presence/http/guard.d.ts.map +1 -0
- package/dist/presence/http/guard.js +20 -0
- package/dist/presence/http/responses.d.ts +31 -0
- package/dist/presence/http/responses.d.ts.map +1 -0
- package/dist/presence/http/responses.js +34 -0
- package/dist/presence/http/routes.d.ts +24 -0
- package/dist/presence/http/routes.d.ts.map +1 -0
- package/dist/presence/http/routes.js +127 -0
- package/dist/presence/http/schemas.d.ts +29 -0
- package/dist/presence/http/schemas.d.ts.map +1 -0
- package/dist/presence/http/schemas.js +28 -0
- package/dist/presence/names.d.ts +23 -0
- package/dist/presence/names.d.ts.map +1 -0
- package/dist/presence/names.js +22 -0
- package/dist/presence/room.d.ts +95 -0
- package/dist/presence/room.d.ts.map +1 -0
- package/dist/presence/room.js +105 -0
- package/dist/presence/ticket.d.ts +55 -0
- package/dist/presence/ticket.d.ts.map +1 -0
- package/dist/presence/ticket.js +109 -0
- package/dist/privacy/classify.js +1 -1
- package/dist/privacy/encrypted.js +1 -1
- package/dist/processing/activity.d.ts +134 -0
- package/dist/processing/activity.d.ts.map +1 -0
- package/dist/processing/activity.js +161 -0
- package/dist/processing/composed.d.ts +18 -0
- package/dist/processing/composed.d.ts.map +1 -0
- package/dist/processing/composed.js +28 -0
- package/dist/processing/vocabulary.d.ts +26 -0
- package/dist/processing/vocabulary.d.ts.map +1 -0
- package/dist/processing/vocabulary.js +35 -0
- package/dist/queue/consumer.d.ts +59 -0
- package/dist/queue/consumer.d.ts.map +1 -0
- package/dist/queue/consumer.js +46 -0
- package/dist/retention/composed.d.ts +37 -0
- package/dist/retention/composed.d.ts.map +1 -0
- package/dist/retention/composed.js +66 -0
- package/dist/retention/holds.d.ts +61 -0
- package/dist/retention/holds.d.ts.map +1 -0
- package/dist/retention/holds.js +107 -0
- package/dist/retention/holdsPurge.d.ts +22 -0
- package/dist/retention/holdsPurge.d.ts.map +1 -0
- package/dist/retention/holdsPurge.js +50 -0
- package/dist/retention/policy.d.ts +132 -0
- package/dist/retention/policy.d.ts.map +1 -0
- package/dist/retention/policy.js +197 -0
- package/dist/session/rotated.d.ts +34 -0
- package/dist/session/rotated.d.ts.map +1 -0
- package/dist/session/rotated.js +25 -0
- package/dist/test-utils/connectionFixtures.d.ts +13 -0
- package/dist/test-utils/connectionFixtures.d.ts.map +1 -0
- package/dist/test-utils/connectionFixtures.js +58 -0
- package/dist/test-utils/interleave.d.ts +12 -0
- package/dist/test-utils/interleave.d.ts.map +1 -0
- package/dist/test-utils/interleave.js +26 -0
- package/dist/testWorker.d.ts +8 -0
- package/dist/testWorker.d.ts.map +1 -0
- package/dist/testWorker.js +9 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workflow/dispatchRoute.js +1 -1
- package/package.json +1 -1
- package/src/audit/auditEvent.ts +1 -1
- package/src/audit/chain.ts +551 -0
- package/src/audit/citations.ts +63 -0
- package/src/capability/bindings.ts +61 -0
- package/src/capability/capability.ts +156 -19
- package/src/capability/erasure.ts +82 -0
- package/src/capability/personColumns.ts +83 -0
- package/src/capability/personRows.ts +125 -0
- package/src/capability/purge.ts +158 -21
- package/src/capability/settings.ts +7 -0
- package/src/capability/vectorIndex.ts +60 -0
- package/src/cli/commandCatalog.generated.ts +487 -2
- package/src/cli/commandFacts.ts +12 -3
- package/src/cloudflare-test.d.ts +3 -1
- package/src/consent/purpose.ts +160 -0
- package/src/consent/reader.ts +134 -0
- package/src/controlPlane/audit/actions.ts +15 -4
- package/src/controlPlane/capability.ts +99 -20
- package/src/controlPlane/context.ts +10 -2
- package/src/controlPlane/data/connection.ts +16 -2
- package/src/controlPlane/data/connections.ts +231 -0
- package/src/controlPlane/data/hold.ts +52 -0
- package/src/controlPlane/data/oidcTrust.ts +249 -0
- package/src/controlPlane/data/tables.ts +7 -0
- package/src/controlPlane/data/trustFirstCall.ts +37 -0
- package/src/controlPlane/discovery/adminRoute.ts +71 -9
- package/src/controlPlane/discovery/routeInput.ts +86 -0
- package/src/controlPlane/discovery/toolName.ts +23 -0
- package/src/controlPlane/erasure/attest.ts +205 -0
- package/src/controlPlane/erasure/read.ts +169 -0
- package/src/controlPlane/error/errors.ts +69 -2
- package/src/controlPlane/evidence/derive.ts +426 -0
- package/src/controlPlane/evidence/mapping.ts +118 -0
- package/src/controlPlane/evidence/pack.ts +190 -0
- package/src/controlPlane/http/connections.ts +129 -0
- package/src/controlPlane/http/guard.ts +118 -9
- package/src/controlPlane/http/handlers.ts +172 -40
- package/src/controlPlane/http/holds.ts +108 -0
- package/src/controlPlane/http/presence.ts +71 -0
- package/src/controlPlane/http/processing.ts +19 -0
- package/src/controlPlane/http/responses.ts +590 -7
- package/src/controlPlane/http/routes.ts +302 -3
- package/src/controlPlane/http/schemas.ts +126 -0
- package/src/controlPlane/http/verify.ts +19 -2
- package/src/controlPlane/http/verifyTrust.ts +176 -0
- package/src/controlPlane/migrations/0004_connection_trusts.ts +30 -0
- package/src/controlPlane/migrations/0005_holds.ts +40 -0
- package/src/controlPlane/processing/derive.ts +359 -0
- package/src/controlPlane/processing/read.ts +102 -0
- package/src/controlPlane/scope/kind.ts +63 -0
- package/src/controlPlane/scope/scope.ts +65 -3
- package/src/controlPlane/wire.ts +26 -0
- package/src/createBackend.ts +31 -6
- package/src/createEntrypoint.ts +17 -0
- package/src/error/payload.ts +234 -0
- package/src/error/pithyError.ts +42 -0
- package/src/http/routeSpec.ts +75 -5
- package/src/http/verification.ts +3 -1
- package/src/i18n/translator.ts +2 -1
- package/src/index.ts +2 -0
- package/src/mcp/annotations.ts +38 -0
- package/src/mcp/tools.ts +220 -0
- package/src/openapi/document.ts +8 -28
- package/src/openapi/operations.ts +62 -0
- package/src/presence/channel.ts +46 -0
- package/src/presence/declaration.ts +283 -0
- package/src/presence/durableObject.ts +287 -0
- package/src/presence/frames.ts +153 -0
- package/src/presence/http/guard.ts +23 -0
- package/src/presence/http/responses.ts +63 -0
- package/src/presence/http/routes.ts +165 -0
- package/src/presence/http/schemas.ts +39 -0
- package/src/presence/names.ts +45 -0
- package/src/presence/room.ts +145 -0
- package/src/presence/ticket.ts +145 -0
- package/src/processing/activity.ts +289 -0
- package/src/processing/composed.ts +33 -0
- package/src/processing/vocabulary.ts +43 -0
- package/src/queue/consumer.ts +104 -0
- package/src/retention/composed.ts +75 -0
- package/src/retention/holds.ts +175 -0
- package/src/retention/holdsPurge.ts +50 -0
- package/src/retention/policy.ts +305 -0
- package/src/session/rotated.ts +53 -0
- package/src/test-utils/connectionFixtures.ts +58 -0
- package/src/test-utils/interleave.ts +31 -0
- package/src/testWorker.ts +13 -0
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* Consent purposes: what a person is asked to agree to, declared where the processing is.
|
|
6
|
+
*
|
|
7
|
+
* ## Declared, never discovered
|
|
8
|
+
*
|
|
9
|
+
* A capability that processes under consent says so, the way it states its migrations: `@pithy-sh/email` declares
|
|
10
|
+
* `email.marketing` and `email.tracking`. The adopter adds their own to `consent({ purposes })`. The consent
|
|
11
|
+
* capability composes every declaration into one catalog, and `hasConsent` answers only for a purpose in it.
|
|
12
|
+
*
|
|
13
|
+
* ## A purpose says what it covers and what it demands
|
|
14
|
+
*
|
|
15
|
+
* Beside its notice version, a purpose names the data classes it covers, whether it is **explicit** (GDPR
|
|
16
|
+
* Article 9), the **minimum method** a grant needs, and whether a withdrawal must be made at least at the grade
|
|
17
|
+
* of the grant it withdraws, the way a HIPAA revocation must be in writing.
|
|
18
|
+
*
|
|
19
|
+
* ## The id is the capability's own
|
|
20
|
+
*
|
|
21
|
+
* `<domain>.<name>`, and a capability may declare purposes only under its own `name`, the rule `pithy_<name>_*`
|
|
22
|
+
* tables and `<name>/…` error codes follow, for the same reason: the domain is what keeps two declarations from
|
|
23
|
+
* colliding. `defineCapability` refuses anything else, naming the capability.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* How a grant was given, weakest first. The order is the grade: a purpose's `minimumMethod` refuses a grant below
|
|
27
|
+
* it, and a purpose with `writtenWithdrawal` refuses a withdrawal below the grant it withdraws.
|
|
28
|
+
*/
|
|
29
|
+
export declare const CONSENT_METHODS: readonly ["click", "typed", "drawn", "provider"];
|
|
30
|
+
/** How a consent was given or withdrawn. */
|
|
31
|
+
export declare const ConsentMethod: z.ZodEnum<{
|
|
32
|
+
click: "click";
|
|
33
|
+
drawn: "drawn";
|
|
34
|
+
provider: "provider";
|
|
35
|
+
typed: "typed";
|
|
36
|
+
}>;
|
|
37
|
+
export type ConsentMethod = z.output<typeof ConsentMethod>;
|
|
38
|
+
/** A method's grade: its place in {@link CONSENT_METHODS}, so `click` is 0 and `provider` is 3. */
|
|
39
|
+
export declare function methodRank(method: ConsentMethod): number;
|
|
40
|
+
/** Whether `method` meets `minimum`: at its grade or above. */
|
|
41
|
+
export declare function meetsMethod(method: ConsentMethod, minimum: ConsentMethod): boolean;
|
|
42
|
+
/** A purpose id: `<domain>.<name>`, lowercase, the domain being the declaring capability's name. */
|
|
43
|
+
export declare const ConsentPurposeId: z.ZodString;
|
|
44
|
+
export type ConsentPurposeId = z.output<typeof ConsentPurposeId>;
|
|
45
|
+
/** One purpose a capability or the adopter processes under consent. */
|
|
46
|
+
export declare const ConsentPurpose: z.ZodObject<{
|
|
47
|
+
id: z.ZodString;
|
|
48
|
+
summary: z.ZodString;
|
|
49
|
+
noticeVersion: z.ZodNumber;
|
|
50
|
+
classes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
51
|
+
explicit: z.ZodDefault<z.ZodBoolean>;
|
|
52
|
+
minimumMethod: z.ZodDefault<z.ZodEnum<{
|
|
53
|
+
click: "click";
|
|
54
|
+
drawn: "drawn";
|
|
55
|
+
provider: "provider";
|
|
56
|
+
typed: "typed";
|
|
57
|
+
}>>;
|
|
58
|
+
writtenWithdrawal: z.ZodDefault<z.ZodBoolean>;
|
|
59
|
+
}, z.core.$strip>;
|
|
60
|
+
export type ConsentPurpose = z.output<typeof ConsentPurpose>;
|
|
61
|
+
export type ConsentPurposeInput = z.input<typeof ConsentPurpose>;
|
|
62
|
+
/** The domain half of a purpose id: `email` for `email.marketing`. */
|
|
63
|
+
export declare function purposeDomain(id: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Parse one declarer's purposes and refuse what no consent capability could answer for: an id that is not a
|
|
66
|
+
* purpose id, one under a domain that is not the declarer's, or two with one id. `domain` is the declaring
|
|
67
|
+
* capability's name, or `null` for the adopter's own, which may use any domain but a reserved one.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseConsentPurposes(declarer: string, purposes: readonly ConsentPurposeInput[], domain: string | null, reserved?: ReadonlySet<string>): ConsentPurpose[];
|
|
70
|
+
//# sourceMappingURL=purpose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"purpose.d.ts","sourceRoot":"","sources":["../../src/consent/purpose.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe,YAAI,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,CAAU,CAAC;AAEhF,4CAA4C;AAC5C,eAAO,MAAM,aAAa;;;;;EAIvB,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D,mGAAmG;AACnG,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAExD;AAED,+DAA+D;AAC/D,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAElF;AAED,oGAAoG;AACpG,eAAO,MAAM,gBAAgB,aAS1B,CAAC;AACJ,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEjE,uEAAuE;AACvE,eAAO,MAAM,cAAc;;;;;;;;;;;;;iBAuCsF,CAAC;AAClH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAC7D,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEjE,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAGhD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,SAAS,mBAAmB,EAAE,EACxC,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,cAAc,EAAE,CAiClB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { ValidationError } from "../error/pithyError.js";
|
|
4
|
+
import { PrivacyClassName } from "../privacy/classes.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region src/consent/purpose.ts
|
|
7
|
+
/**
|
|
8
|
+
* Consent purposes: what a person is asked to agree to, declared where the processing is.
|
|
9
|
+
*
|
|
10
|
+
* ## Declared, never discovered
|
|
11
|
+
*
|
|
12
|
+
* A capability that processes under consent says so, the way it states its migrations: `@pithy-sh/email` declares
|
|
13
|
+
* `email.marketing` and `email.tracking`. The adopter adds their own to `consent({ purposes })`. The consent
|
|
14
|
+
* capability composes every declaration into one catalog, and `hasConsent` answers only for a purpose in it.
|
|
15
|
+
*
|
|
16
|
+
* ## A purpose says what it covers and what it demands
|
|
17
|
+
*
|
|
18
|
+
* Beside its notice version, a purpose names the data classes it covers, whether it is **explicit** (GDPR
|
|
19
|
+
* Article 9), the **minimum method** a grant needs, and whether a withdrawal must be made at least at the grade
|
|
20
|
+
* of the grant it withdraws, the way a HIPAA revocation must be in writing.
|
|
21
|
+
*
|
|
22
|
+
* ## The id is the capability's own
|
|
23
|
+
*
|
|
24
|
+
* `<domain>.<name>`, and a capability may declare purposes only under its own `name`, the rule `pithy_<name>_*`
|
|
25
|
+
* tables and `<name>/…` error codes follow, for the same reason: the domain is what keeps two declarations from
|
|
26
|
+
* colliding. `defineCapability` refuses anything else, naming the capability.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* How a grant was given, weakest first. The order is the grade: a purpose's `minimumMethod` refuses a grant below
|
|
30
|
+
* it, and a purpose with `writtenWithdrawal` refuses a withdrawal below the grant it withdraws.
|
|
31
|
+
*/
|
|
32
|
+
const CONSENT_METHODS = [
|
|
33
|
+
"click",
|
|
34
|
+
"typed",
|
|
35
|
+
"drawn",
|
|
36
|
+
"provider"
|
|
37
|
+
];
|
|
38
|
+
/** How a consent was given or withdrawn. */
|
|
39
|
+
const ConsentMethod = z.enum(CONSENT_METHODS).describe("How a consent was given or withdrawn, weakest first: `click` (a checkbox or a button), `typed` (the person typed their name), `drawn` (a drawn signature), or `provider` (an e-signature provider's completed envelope). The order is the grade a purpose's minimum is held against.");
|
|
40
|
+
/** A method's grade: its place in {@link CONSENT_METHODS}, so `click` is 0 and `provider` is 3. */
|
|
41
|
+
function methodRank(method) {
|
|
42
|
+
return CONSENT_METHODS.indexOf(method);
|
|
43
|
+
}
|
|
44
|
+
/** Whether `method` meets `minimum`: at its grade or above. */
|
|
45
|
+
function meetsMethod(method, minimum) {
|
|
46
|
+
return methodRank(method) >= methodRank(minimum);
|
|
47
|
+
}
|
|
48
|
+
/** A purpose id: `<domain>.<name>`, lowercase, the domain being the declaring capability's name. */
|
|
49
|
+
const ConsentPurposeId = z.string().regex(/^[a-z][a-z0-9-]*\.[a-z][a-z0-9_.-]*$/, "A consent purpose id is `<domain>.<name>`: lowercase, the domain being the declaring capability's name.").max(128).describe("A consent purpose id, `<domain>.<name>` — `email.marketing`. The domain is the declaring capability's name, or the adopter's own domain for a purpose declared in `consent({ purposes })`.");
|
|
50
|
+
/** One purpose a capability or the adopter processes under consent. */
|
|
51
|
+
const ConsentPurpose = z.object({
|
|
52
|
+
id: ConsentPurposeId,
|
|
53
|
+
summary: z.string().min(1).max(500).describe("One sentence, written for the person, on what agreeing allows — `Send me news and offers by email.` Shown on the consent settings screen."),
|
|
54
|
+
noticeVersion: z.number().int().positive().describe("The version of the notice a person agrees to. Bump it when the notice changes: every grant under an older version then needs renewing, and `hasConsent` answers no for it until it is."),
|
|
55
|
+
classes: z.array(PrivacyClassName).default([]).describe("The data classes this purpose covers — `pii`, `phi`, or an adopter's own. The processing record reads it to name a classed field with no consent behind it."),
|
|
56
|
+
explicit: z.boolean().default(false).describe("Whether this purpose needs explicit consent (GDPR Article 9), as processing health data does. Recorded so an auditor can see which purposes claim it."),
|
|
57
|
+
minimumMethod: ConsentMethod.default("click").describe("The weakest method a grant may be given by. A grant below it is refused, and `hasConsent` answers no for one recorded below it before the minimum was raised."),
|
|
58
|
+
writtenWithdrawal: z.boolean().default(false).describe("Whether a withdrawal must be made at least at the grade of the grant it withdraws, as a HIPAA revocation must be in writing. Off, a click withdraws any grant: withdrawing is then never harder than granting.")
|
|
59
|
+
}).describe("One purpose processed under consent: what it allows, its notice version, and what a grant demands.");
|
|
60
|
+
/** The domain half of a purpose id: `email` for `email.marketing`. */
|
|
61
|
+
function purposeDomain(id) {
|
|
62
|
+
const dot = id.indexOf(".");
|
|
63
|
+
return dot === -1 ? id : id.slice(0, dot);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Parse one declarer's purposes and refuse what no consent capability could answer for: an id that is not a
|
|
67
|
+
* purpose id, one under a domain that is not the declarer's, or two with one id. `domain` is the declaring
|
|
68
|
+
* capability's name, or `null` for the adopter's own, which may use any domain but a reserved one.
|
|
69
|
+
*/
|
|
70
|
+
function parseConsentPurposes(declarer, purposes, domain, reserved = /* @__PURE__ */ new Set()) {
|
|
71
|
+
const seen = /* @__PURE__ */ new Set();
|
|
72
|
+
return purposes.map((input) => {
|
|
73
|
+
const parsed = ConsentPurpose.safeParse(input);
|
|
74
|
+
if (!parsed.success) throw new ValidationError({
|
|
75
|
+
message: `${declarer} declares a consent purpose that is not valid: ${parsed.error.issues[0]?.message ?? "invalid"}.`,
|
|
76
|
+
action: "Give it an id of the form `<domain>.<name>`, a summary, and a positive notice version."
|
|
77
|
+
});
|
|
78
|
+
const purpose = parsed.data;
|
|
79
|
+
const own = purposeDomain(purpose.id);
|
|
80
|
+
if (domain !== null && own !== domain) throw new ValidationError({
|
|
81
|
+
message: `The ${declarer} capability declares the consent purpose "${purpose.id}" outside its own domain.`,
|
|
82
|
+
action: `Name it under ${domain}. — a capability declares purposes only under its own name.`
|
|
83
|
+
});
|
|
84
|
+
if (domain === null && reserved.has(own)) throw new ValidationError({
|
|
85
|
+
message: `The consent purpose "${purpose.id}" is under ${own}., which a composed capability owns.`,
|
|
86
|
+
action: "Name your own purposes under your own domain, such as your app's name."
|
|
87
|
+
});
|
|
88
|
+
if (seen.has(purpose.id)) throw new ValidationError({
|
|
89
|
+
message: `${declarer} declares the consent purpose "${purpose.id}" twice.`,
|
|
90
|
+
action: "Declare each purpose once."
|
|
91
|
+
});
|
|
92
|
+
seen.add(purpose.id);
|
|
93
|
+
return purpose;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { CONSENT_METHODS, ConsentMethod, ConsentPurpose, ConsentPurposeId, meetsMethod, methodRank, parseConsentPurposes, purposeDomain };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { Capability } from "../capability/capability";
|
|
4
|
+
import type { ConsentPurpose } from "./purpose";
|
|
5
|
+
/**
|
|
6
|
+
* The consent seam: the one question a capability asks before consent-based processing.
|
|
7
|
+
*
|
|
8
|
+
* `@pithy-sh/consent` answers it, and nothing imports that package to ask: a capability finds the composed reader
|
|
9
|
+
* through {@link composedConsent}, the shape `composedAuditEmit` gives the audit recorder, so `@pithy-sh/email`
|
|
10
|
+
* gates its tracking without depending on a sibling (principle 4).
|
|
11
|
+
*
|
|
12
|
+
* **Absent is not "no".** With no consent capability composed there is no record to consult, so
|
|
13
|
+
* {@link composedConsent} answers `null`, and a capability gates nothing on it. `pithy doctor`
|
|
14
|
+
* names that state, because processing that a law may require consent for is running with no record behind it.
|
|
15
|
+
*/
|
|
16
|
+
/** The subject a consent is recorded against: an account's user id, or a visitor's anonymous device id. */
|
|
17
|
+
export interface ConsentSubject {
|
|
18
|
+
/** `account` — a user id from `@pithy-sh/auth`; `visitor` — an anonymous device id held in a first-party cookie. */
|
|
19
|
+
readonly kind: "account" | "visitor";
|
|
20
|
+
/** The id in that kind. */
|
|
21
|
+
readonly id: string;
|
|
22
|
+
}
|
|
23
|
+
/** What the consent capability answers. */
|
|
24
|
+
export interface ConsentReader {
|
|
25
|
+
/**
|
|
26
|
+
* Whether `subject` holds consent to `purpose` now: a grant under the purpose's current notice version, at or
|
|
27
|
+
* above its minimum method, not withdrawn since, and not expired. An account's answer includes what the visitors
|
|
28
|
+
* merged into it granted. A purpose the catalog does not know answers no.
|
|
29
|
+
*/
|
|
30
|
+
hasConsent(subject: ConsentSubject, purpose: string): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Every purpose in the composed catalog, the adopter's included, with how many subjects stand where on it now. What
|
|
33
|
+
* the processing record cites for a consent-based activity. Optional on the seam: a reader without it leaves the
|
|
34
|
+
* counts unknown, never zero.
|
|
35
|
+
*/
|
|
36
|
+
standings?(): Promise<readonly ConsentPurposeStanding[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Every purpose in the composed catalog, the adopter's included, read from memory and never from the record. What
|
|
39
|
+
* the processing record checks a consent basis against when `standings` cannot answer. Optional on the seam: a
|
|
40
|
+
* reader without it leaves the catalog unknown, and the record then reports no gap that rests on the catalog.
|
|
41
|
+
*/
|
|
42
|
+
purposes?(): readonly ConsentPurpose[];
|
|
43
|
+
}
|
|
44
|
+
/** One purpose in the catalog, and where its subjects stand on it now. */
|
|
45
|
+
export interface ConsentPurposeStanding {
|
|
46
|
+
/** The purpose as the catalog composed it. */
|
|
47
|
+
readonly purpose: ConsentPurpose;
|
|
48
|
+
/** Subjects whose latest choice is a grant, under any notice version, expired or not. */
|
|
49
|
+
readonly grants: number;
|
|
50
|
+
/** Subjects whose latest choice is a withdrawal. */
|
|
51
|
+
readonly withdrawals: number;
|
|
52
|
+
/**
|
|
53
|
+
* Subjects `hasConsent` answers yes for now: a grant under the current notice version, at or above the purpose's
|
|
54
|
+
* minimum method, unexpired, and for a visitor not merged into an account, inside the visitor window.
|
|
55
|
+
*/
|
|
56
|
+
readonly standing: number;
|
|
57
|
+
}
|
|
58
|
+
/** One withdrawal, as a capability that keeps its own state in step with consent is told of it. */
|
|
59
|
+
export interface ConsentWithdrawal {
|
|
60
|
+
/** Who withdrew. */
|
|
61
|
+
readonly subject: ConsentSubject;
|
|
62
|
+
/** What they withdrew from. */
|
|
63
|
+
readonly purpose: string;
|
|
64
|
+
/** The Worker's bindings for this request. */
|
|
65
|
+
readonly env: Record<string, unknown>;
|
|
66
|
+
/**
|
|
67
|
+
* The subject's email address, read on demand from the capability that owns accounts, or `null` for a visitor
|
|
68
|
+
* or an account with none. A function, so a listener that needs no address reads none.
|
|
69
|
+
*/
|
|
70
|
+
readonly address: () => Promise<string | null>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Hear of a withdrawal from one of this capability's own purposes. `@pithy-sh/email` suppresses the address for
|
|
74
|
+
* marketing when `email.marketing` is withdrawn, so the suppression list and the consent record never disagree.
|
|
75
|
+
* Called after the withdrawal is recorded; a throw is the listener's own failure and never undoes the record.
|
|
76
|
+
*/
|
|
77
|
+
export type ConsentWithdrawnListener = (withdrawal: ConsentWithdrawal) => Promise<void>;
|
|
78
|
+
/** One grant that now stands, as a capability that keeps its own state in step with consent is told of it. */
|
|
79
|
+
export type ConsentGrant = ConsentWithdrawal;
|
|
80
|
+
/**
|
|
81
|
+
* Hear of a grant to one of this capability's own purposes, once it stands. The other half of
|
|
82
|
+
* {@link ConsentWithdrawnListener}: `@pithy-sh/email` lifts the marketing suppression a withdrawal wrote when
|
|
83
|
+
* `email.marketing` is granted again, and nothing else, so a person who changes their mind back is heard. Called
|
|
84
|
+
* after the grant is recorded and only when the subject's standing is `granted`; a throw never undoes the record.
|
|
85
|
+
*/
|
|
86
|
+
export type ConsentGrantedListener = (grant: ConsentGrant) => Promise<void>;
|
|
87
|
+
/** Every purpose the composed capabilities declare, in composition order. */
|
|
88
|
+
export declare function composedConsentPurposes(capabilities: readonly Capability[]): ConsentPurpose[];
|
|
89
|
+
/** The composed consent reader for this env, or `null` when no capability provides one. */
|
|
90
|
+
export declare function composedConsent(capabilities: readonly Capability[], env: Record<string, unknown>): ConsentReader | null;
|
|
91
|
+
/** Whether any composed capability provides the consent seam. */
|
|
92
|
+
export declare function providesConsent(capabilities: readonly Capability[]): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* The account's address, read through the capability that owns accounts (`@pithy-sh/auth`'s `addressOf`), or
|
|
95
|
+
* `null` where nothing composed can say. Never throws: a listener that cannot learn the address does less, and the
|
|
96
|
+
* withdrawal it heard of stands.
|
|
97
|
+
*/
|
|
98
|
+
export declare function composedAddress(capabilities: readonly Capability[], env: Record<string, unknown>, subject: ConsentSubject): Promise<string | null>;
|
|
99
|
+
//# sourceMappingURL=reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../../src/consent/reader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD;;;;;;;;;;GAUG;AAEH,2GAA2G;AAC3G,MAAM,WAAW,cAAc;IAC7B,oHAAoH;IACpH,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;IACrC,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,2CAA2C;AAC3C,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvE;;;;OAIG;IACH,SAAS,CAAC,IAAI,OAAO,CAAC,SAAS,sBAAsB,EAAE,CAAC,CAAC;IACzD;;;;OAIG;IACH,QAAQ,CAAC,IAAI,SAAS,cAAc,EAAE,CAAC;CACxC;AAED,0EAA0E;AAC1E,MAAM,WAAW,sBAAsB;IACrC,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,mGAAmG;AACnG,MAAM,WAAW,iBAAiB;IAChC,oBAAoB;IACpB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,+BAA+B;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAChD;AAED;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,UAAU,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAExF,8GAA8G;AAC9G,MAAM,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE5E,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,SAAS,UAAU,EAAE,GAAG,cAAc,EAAE,CAE7F;AAED,2FAA2F;AAC3F,wBAAgB,eAAe,CAC7B,YAAY,EAAE,SAAS,UAAU,EAAE,EACnC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,aAAa,GAAG,IAAI,CAGtB;AAED,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,YAAY,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,CAE5E;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,SAAS,UAAU,EAAE,EACnC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
//#region src/consent/reader.ts
|
|
4
|
+
/** Every purpose the composed capabilities declare, in composition order. */
|
|
5
|
+
function composedConsentPurposes(capabilities) {
|
|
6
|
+
return capabilities.flatMap((capability) => [...capability.consentPurposes ?? []]);
|
|
7
|
+
}
|
|
8
|
+
/** The composed consent reader for this env, or `null` when no capability provides one. */
|
|
9
|
+
function composedConsent(capabilities, env) {
|
|
10
|
+
const provider = capabilities.find((capability) => capability.consentReader !== void 0);
|
|
11
|
+
return provider?.consentReader ? provider.consentReader(env) : null;
|
|
12
|
+
}
|
|
13
|
+
/** Whether any composed capability provides the consent seam. */
|
|
14
|
+
function providesConsent(capabilities) {
|
|
15
|
+
return capabilities.some((capability) => capability.consentReader !== void 0);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The account's address, read through the capability that owns accounts (`@pithy-sh/auth`'s `addressOf`), or
|
|
19
|
+
* `null` where nothing composed can say. Never throws: a listener that cannot learn the address does less, and the
|
|
20
|
+
* withdrawal it heard of stands.
|
|
21
|
+
*/
|
|
22
|
+
async function composedAddress(capabilities, env, subject) {
|
|
23
|
+
if (subject.kind !== "account") return null;
|
|
24
|
+
const provider = capabilities.find((capability) => capability.addressOf !== void 0);
|
|
25
|
+
if (!provider?.addressOf) return null;
|
|
26
|
+
try {
|
|
27
|
+
return await provider.addressOf(env, subject.id);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { composedAddress, composedConsent, composedConsentPurposes, providesConsent };
|
|
@@ -22,8 +22,8 @@ import type { Logger } from "../../logger/logger";
|
|
|
22
22
|
* Emission goes through core's `emit` seam (`c.var.emit`), a no-op when the audit capability is
|
|
23
23
|
* absent, so the seam never imports audit at runtime either (principle 4).
|
|
24
24
|
*
|
|
25
|
-
* **
|
|
26
|
-
* lifecycle — registered, updated, removed — is written by `pithy dashboard` opening the adopter's D1
|
|
25
|
+
* **Most of these are emitted by a route; four are emitted by the CLI, one of them by a route as well.** The
|
|
26
|
+
* connection lifecycle — registered, updated, removed — is written by `pithy dashboard` opening the adopter's D1
|
|
27
27
|
* directly, because those operations never reach their Worker: a first connect has no key to sign with
|
|
28
28
|
* and the Worker may not be deployed, an update re-points an address the Worker never reads, and a
|
|
29
29
|
* disconnect withdraws trust, which must not depend on a working connection (docs/CONTROL-PLANE.md §15).
|
|
@@ -32,8 +32,12 @@ import type { Logger } from "../../logger/logger";
|
|
|
32
32
|
* `control-plane`, meaning the management client called in and proved it, while a CLI-side event carries
|
|
33
33
|
* the adopter's own operator and no session at all.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* `connectionRevoked` is the one written from both sides: by `POST {basePath}/connections/:connectionId/revoke`,
|
|
36
|
+
* with `actorType` `control-plane` and the caller's connection, and by `pithy dashboard connections --revoke`, with
|
|
37
|
+
* the operator as actor and no session.
|
|
38
|
+
*
|
|
39
|
+
* A declared code that nothing emits is a surface the trail promises and never delivers.
|
|
40
|
+
* `packages/cli/src/ci/auditActions.test.ts` fails the build on one.
|
|
37
41
|
*/
|
|
38
42
|
export declare const ControlPlaneAuditActions: {
|
|
39
43
|
/** A verified, in-scope management call was admitted. The trail of what the dashboard actually did. */
|
|
@@ -50,6 +54,13 @@ export declare const ControlPlaneAuditActions: {
|
|
|
50
54
|
readonly connectionUpdated: "controlplane/connection_updated";
|
|
51
55
|
/** A connection was removed. The seam returns to its shipped state: connected to nothing, denying everything. */
|
|
52
56
|
readonly connectionRemoved: "controlplane/connection_removed";
|
|
57
|
+
/**
|
|
58
|
+
* Another connection's keys and trusts were revoked, with a reason: by a management client over the seam, or by the
|
|
59
|
+
* operator at the CLI.
|
|
60
|
+
*/
|
|
61
|
+
readonly connectionRevoked: "controlplane/connection_revoked";
|
|
62
|
+
readonly holdPlaced: "controlplane/hold_placed";
|
|
63
|
+
readonly holdReleased: "controlplane/hold_released";
|
|
53
64
|
};
|
|
54
65
|
/** One of the control-plane audit action codes. */
|
|
55
66
|
export type ControlPlaneAuditAction = (typeof ControlPlaneAuditActions)[keyof typeof ControlPlaneAuditActions];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/audit/actions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAElD
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/audit/actions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,wBAAwB;IACnC,uGAAuG;aACvG,WAAW,EAAE,2BAA2B;IACxC,8GAA8G;aAC9G,UAAU,EAAE,0BAA0B;IACtC,mFAAmF;aACnF,aAAa,EAAE,6BAA6B;IAC5C,gFAAgF;aAChF,UAAU,EAAE,0BAA0B;IACtC,qGAAqG;aACrG,oBAAoB,EAAE,oCAAoC;IAC1D,+GAA+G;aAC/G,iBAAiB,EAAE,iCAAiC;IACpD,iHAAiH;aACjH,iBAAiB,EAAE,iCAAiC;IACpD;;;OAGG;aACH,iBAAiB,EAAE,iCAAiC;aACpD,UAAU,EAAE,0BAA0B;aACtC,YAAY,EAAE,4BAA4B;CAClC,CAAC;AAEX,mDAAmD;AACnD,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,OAAO,wBAAwB,CAAC,CAAC;AAE/G;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMnG"}
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* Emission goes through core's `emit` seam (`c.var.emit`), a no-op when the audit capability is
|
|
21
21
|
* absent, so the seam never imports audit at runtime either (principle 4).
|
|
22
22
|
*
|
|
23
|
-
* **
|
|
24
|
-
* lifecycle — registered, updated, removed — is written by `pithy dashboard` opening the adopter's D1
|
|
23
|
+
* **Most of these are emitted by a route; four are emitted by the CLI, one of them by a route as well.** The
|
|
24
|
+
* connection lifecycle — registered, updated, removed — is written by `pithy dashboard` opening the adopter's D1
|
|
25
25
|
* directly, because those operations never reach their Worker: a first connect has no key to sign with
|
|
26
26
|
* and the Worker may not be deployed, an update re-points an address the Worker never reads, and a
|
|
27
27
|
* disconnect withdraws trust, which must not depend on a working connection (docs/CONTROL-PLANE.md §15).
|
|
@@ -30,8 +30,12 @@
|
|
|
30
30
|
* `control-plane`, meaning the management client called in and proved it, while a CLI-side event carries
|
|
31
31
|
* the adopter's own operator and no session at all.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* `connectionRevoked` is the one written from both sides: by `POST {basePath}/connections/:connectionId/revoke`,
|
|
34
|
+
* with `actorType` `control-plane` and the caller's connection, and by `pithy dashboard connections --revoke`, with
|
|
35
|
+
* the operator as actor and no session.
|
|
36
|
+
*
|
|
37
|
+
* A declared code that nothing emits is a surface the trail promises and never delivers.
|
|
38
|
+
* `packages/cli/src/ci/auditActions.test.ts` fails the build on one.
|
|
35
39
|
*/
|
|
36
40
|
const ControlPlaneAuditActions = {
|
|
37
41
|
/** A verified, in-scope management call was admitted. The trail of what the dashboard actually did. */
|
|
@@ -47,7 +51,14 @@ const ControlPlaneAuditActions = {
|
|
|
47
51
|
/** An existing connection changed — its granted scopes, the Worker URL it points at, or the keys it trusts. */
|
|
48
52
|
connectionUpdated: "controlplane/connection_updated",
|
|
49
53
|
/** A connection was removed. The seam returns to its shipped state: connected to nothing, denying everything. */
|
|
50
|
-
connectionRemoved: "controlplane/connection_removed"
|
|
54
|
+
connectionRemoved: "controlplane/connection_removed",
|
|
55
|
+
/**
|
|
56
|
+
* Another connection's keys and trusts were revoked, with a reason: by a management client over the seam, or by the
|
|
57
|
+
* operator at the CLI.
|
|
58
|
+
*/
|
|
59
|
+
connectionRevoked: "controlplane/connection_revoked",
|
|
60
|
+
holdPlaced: "controlplane/hold_placed",
|
|
61
|
+
holdReleased: "controlplane/hold_released"
|
|
51
62
|
};
|
|
52
63
|
/**
|
|
53
64
|
* Emit an audit event and swallow any failure.
|
|
@@ -17,6 +17,16 @@ import { CONTROL_PLANE_KV_BINDING } from "./replay/kvGuard";
|
|
|
17
17
|
*/
|
|
18
18
|
export declare const CONTROLPLANE_MIGRATION_ORDER = 1100;
|
|
19
19
|
export type ControlPlaneOptions = ControlPlaneConfigInput;
|
|
20
|
+
/** The standing legal holds, on the seam's own manifest entry. Behind the scope the holds listing requires. */
|
|
21
|
+
export declare const ACTIVE_HOLDS_HEALTH_KEY: {
|
|
22
|
+
readonly key: "activeHolds";
|
|
23
|
+
readonly kind: "count";
|
|
24
|
+
readonly states: null;
|
|
25
|
+
readonly scope: string;
|
|
26
|
+
readonly cost: "indexed";
|
|
27
|
+
readonly summary: "Legal holds standing. While one stands, no purge, prune or inactivity deletion touches its subject.";
|
|
28
|
+
readonly nominal: null;
|
|
29
|
+
};
|
|
20
30
|
/** The control-plane capability, with its resolved config attached. */
|
|
21
31
|
export interface ControlPlaneCapability extends Capability<DatabaseSpecMap, KvNamespaceSpecMap, "controlplane"> {
|
|
22
32
|
/** The resolved seam config. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../../src/controlPlane/capability.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAuC,MAAM,0BAA0B,CAAC;AAChG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../../src/controlPlane/capability.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAuC,MAAM,0BAA0B,CAAC;AAChG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAc,MAAM,kBAAkB,CAAC;AAWvE,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAqBnF,OAAO,EACL,wBAAwB,EAIzB,MAAM,kBAAkB,CAAC;AAG1B;;;;;;;;;GASG;AACH,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG,uBAAuB,CAAC;AAE1D,+GAA+G;AAC/G,eAAO,MAAM,uBAAuB;aAClC,GAAG,EAAE,aAAa;aAClB,IAAI,EAAE,OAAO;aACb,MAAM;aACN,KAAK;aACL,IAAI,EAAE,SAAS;aACf,OAAO,EAAE,qGAAqG;aAC9G,OAAO;CACC,CAAC;AAEX,uEAAuE;AACvE,MAAM,WAAW,sBAAuB,SAAQ,UAAU,CAAC,eAAe,EAAE,kBAAkB,EAAE,cAAc,CAAC;IAC7G,gCAAgC;IAChC,kBAAkB,EAAE,kBAAkB,CAAC;CACxC;AAED,qGAAqG;AACrG,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,IAAI,sBAAsB,CAErG;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,sBAAsB,CAsPtF;AAED,4GAA4G;AAC5G,OAAO,EAAE,wBAAwB,EAAE,CAAC"}
|
|
@@ -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
|