@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,289 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { ConsentPurposeId } from "../consent/purpose";
|
|
6
|
+
import { fromZodError, ValidationError } from "../error/pithyError";
|
|
7
|
+
import { PrivacyClassName } from "../privacy/classes";
|
|
8
|
+
import { type BasisSource, LegalBasis } from "./vocabulary";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Processing activities: what a Worker does with personal data, why, and on what legal basis — the declarations the
|
|
12
|
+
* GDPR Article 30 record is derived from.
|
|
13
|
+
*
|
|
14
|
+
* ## Declared where the processing is
|
|
15
|
+
*
|
|
16
|
+
* A capability states the activities it performs, under its own `name` (`auth.account`), the rule consent purposes,
|
|
17
|
+
* tables and error codes follow. The adopter adds their own in the `processing` block of a Worker's
|
|
18
|
+
* `pithy.config.ts`, and may replace a capability's legal basis there.
|
|
19
|
+
*
|
|
20
|
+
* ## A missing basis is a gap, never a guess
|
|
21
|
+
*
|
|
22
|
+
* `legalBasis` is optional. An activity without one is reported as a gap in the record, and nothing fills it in.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** An activity id: `<domain>.<name>`, lowercase, the domain being the declaring capability's name. */
|
|
26
|
+
export const ProcessingActivityId = z
|
|
27
|
+
.string()
|
|
28
|
+
.regex(
|
|
29
|
+
/^[a-z][a-z0-9-]*\.[a-z][a-z0-9_.-]*$/,
|
|
30
|
+
"A processing activity id is `<domain>.<name>`: lowercase, the domain being the declaring capability's name.",
|
|
31
|
+
)
|
|
32
|
+
.max(128)
|
|
33
|
+
.describe(
|
|
34
|
+
"A processing activity id, `<domain>.<name>` — `auth.account`. The domain is the declaring capability's name, or the adopter's own domain for an activity declared in `pithy.config.ts`.",
|
|
35
|
+
);
|
|
36
|
+
export type ProcessingActivityId = z.infer<typeof ProcessingActivityId>;
|
|
37
|
+
|
|
38
|
+
/** Someone outside the Worker's own code who receives personal data for an activity. */
|
|
39
|
+
export const ProcessingRecipient = z
|
|
40
|
+
.object({
|
|
41
|
+
name: z
|
|
42
|
+
.string()
|
|
43
|
+
.min(1)
|
|
44
|
+
.max(120)
|
|
45
|
+
.describe("Who receives it, as an auditor would name them — `Stripe`, `Cloudflare Email Service`."),
|
|
46
|
+
receives: z
|
|
47
|
+
.string()
|
|
48
|
+
.min(1)
|
|
49
|
+
.max(500)
|
|
50
|
+
.describe("What they receive, in one sentence — `The buyer's email address and what they bought.`"),
|
|
51
|
+
classes: z
|
|
52
|
+
.array(PrivacyClassName)
|
|
53
|
+
.default([])
|
|
54
|
+
.describe("The data classes in what they receive — `pii`, `phi`, or an adopter's own."),
|
|
55
|
+
})
|
|
56
|
+
.describe("One recipient of personal data: who it is, what it receives, and the classes in it.");
|
|
57
|
+
export type ProcessingRecipient = z.output<typeof ProcessingRecipient>;
|
|
58
|
+
export type ProcessingRecipientInput = z.input<typeof ProcessingRecipient>;
|
|
59
|
+
|
|
60
|
+
/** A consent purpose belongs with a consent basis and nowhere else. */
|
|
61
|
+
function consentPurposeNeedsConsent(
|
|
62
|
+
value: { legalBasis?: LegalBasis | undefined; consentPurpose?: string | undefined },
|
|
63
|
+
ctx: z.RefinementCtx,
|
|
64
|
+
): void {
|
|
65
|
+
if (value.consentPurpose !== undefined && value.legalBasis !== "consent") {
|
|
66
|
+
ctx.addIssue({
|
|
67
|
+
code: "custom",
|
|
68
|
+
path: ["consentPurpose"],
|
|
69
|
+
message:
|
|
70
|
+
"`consentPurpose` names the purpose a `consent` basis rests on. Set `legalBasis` to `consent`, or drop it.",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One processing activity, as a capability or the adopter declares it. */
|
|
76
|
+
export const ProcessingActivity = z
|
|
77
|
+
.object({
|
|
78
|
+
id: ProcessingActivityId,
|
|
79
|
+
purpose: z
|
|
80
|
+
.string()
|
|
81
|
+
.min(1)
|
|
82
|
+
.max(500)
|
|
83
|
+
.describe(
|
|
84
|
+
"Why the data is processed, in one sentence an auditor reads — `Keep an account and sign its holder in.`",
|
|
85
|
+
),
|
|
86
|
+
classes: z
|
|
87
|
+
.array(PrivacyClassName)
|
|
88
|
+
.default([])
|
|
89
|
+
.describe(
|
|
90
|
+
"The data classes the activity uses — `pii`, `phi`, or an adopter's own. Cross-checked against the classes the Worker's schemas store.",
|
|
91
|
+
),
|
|
92
|
+
legalBasis: LegalBasis.optional().describe(
|
|
93
|
+
"The lawful basis it rests on. Absent is reported as a gap in the processing record, never filled in.",
|
|
94
|
+
),
|
|
95
|
+
consentPurpose: ConsentPurposeId.optional().describe(
|
|
96
|
+
"For a `consent` basis, the consent purpose the activity rests on — `email.marketing`. The record cites its grant counts.",
|
|
97
|
+
),
|
|
98
|
+
recipients: z
|
|
99
|
+
.array(ProcessingRecipient)
|
|
100
|
+
.default([])
|
|
101
|
+
.describe(
|
|
102
|
+
"Who receives the data beyond Cloudflare, which the record always lists — a payment rail, a mail provider. Built from the resolved config, so a rail that is off is not named.",
|
|
103
|
+
),
|
|
104
|
+
})
|
|
105
|
+
.superRefine(consentPurposeNeedsConsent)
|
|
106
|
+
.describe("One processing activity: its purpose, the data classes it uses, its legal basis and its recipients.");
|
|
107
|
+
export type ProcessingActivity = z.output<typeof ProcessingActivity>;
|
|
108
|
+
export type ProcessingActivityInput = z.input<typeof ProcessingActivity>;
|
|
109
|
+
|
|
110
|
+
/** The adopter's replacement for a capability's legal basis. */
|
|
111
|
+
export const ProcessingOverride = z
|
|
112
|
+
.object({
|
|
113
|
+
legalBasis: LegalBasis.describe("The basis that replaces the capability's own."),
|
|
114
|
+
consentPurpose: ConsentPurposeId.optional().describe(
|
|
115
|
+
"For a `consent` basis, the consent purpose it rests on. Left out, a capability's own purpose is kept.",
|
|
116
|
+
),
|
|
117
|
+
})
|
|
118
|
+
.strict()
|
|
119
|
+
.superRefine(consentPurposeNeedsConsent)
|
|
120
|
+
.describe("A legal basis the adopter states for a capability's activity, replacing the one it declares.");
|
|
121
|
+
export type ProcessingOverride = z.infer<typeof ProcessingOverride>;
|
|
122
|
+
|
|
123
|
+
/** The `processing` block, as a Worker's `pithy.config.ts` writes it. */
|
|
124
|
+
export const ProcessingPolicy = z
|
|
125
|
+
.object({
|
|
126
|
+
activities: z
|
|
127
|
+
.array(ProcessingActivity)
|
|
128
|
+
.optional()
|
|
129
|
+
.describe(
|
|
130
|
+
"Your own processing activities, beside the ones composed capabilities declare. Each under your own domain.",
|
|
131
|
+
),
|
|
132
|
+
overrides: z
|
|
133
|
+
.record(ProcessingActivityId, ProcessingOverride)
|
|
134
|
+
.optional()
|
|
135
|
+
.describe(
|
|
136
|
+
'A legal basis per capability activity, replacing the one it declares: `{ "auth.account": { legalBasis: "legitimate-interests" } }`.',
|
|
137
|
+
),
|
|
138
|
+
})
|
|
139
|
+
.strict()
|
|
140
|
+
.describe("What this Worker does with personal data beyond what its capabilities declare, and the bases you state.");
|
|
141
|
+
export type ProcessingPolicy = z.output<typeof ProcessingPolicy>;
|
|
142
|
+
export type ProcessingPolicyInput = z.input<typeof ProcessingPolicy>;
|
|
143
|
+
|
|
144
|
+
/** The domain half of an activity id: `auth` for `auth.account`. */
|
|
145
|
+
export function activityDomain(id: string): string {
|
|
146
|
+
const dot = id.indexOf(".");
|
|
147
|
+
return dot === -1 ? id : id.slice(0, dot);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parse one declarer's activities and refuse what the record could not attribute: an invalid activity, one under a
|
|
152
|
+
* domain that is not the declarer's, or two with one id. `domain` is the declaring capability's name, or `null` for
|
|
153
|
+
* the adopter's own, which may use any domain but a reserved one.
|
|
154
|
+
*/
|
|
155
|
+
export function parseProcessingActivities(
|
|
156
|
+
declarer: string,
|
|
157
|
+
activities: readonly ProcessingActivityInput[],
|
|
158
|
+
domain: string | null,
|
|
159
|
+
reserved: ReadonlySet<string> = new Set(),
|
|
160
|
+
): ProcessingActivity[] {
|
|
161
|
+
const seen = new Set<string>();
|
|
162
|
+
return activities.map((input) => {
|
|
163
|
+
const parsed = ProcessingActivity.safeParse(input);
|
|
164
|
+
if (!parsed.success) {
|
|
165
|
+
throw new ValidationError({
|
|
166
|
+
message: `${declarer} declares a processing activity that is not valid: ${parsed.error.issues[0]?.message ?? "invalid"}.`,
|
|
167
|
+
action:
|
|
168
|
+
"Give it an id of the form `<domain>.<name>` and a purpose; name a consent purpose only under a consent basis.",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const activity = parsed.data;
|
|
172
|
+
const own = activityDomain(activity.id);
|
|
173
|
+
if (domain !== null && own !== domain) {
|
|
174
|
+
throw new ValidationError({
|
|
175
|
+
message: `The ${declarer} capability declares the processing activity "${activity.id}" outside its own domain.`,
|
|
176
|
+
action: `Name it under ${domain}. — a capability declares activities only under its own name.`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (domain === null && reserved.has(own)) {
|
|
180
|
+
throw new ValidationError({
|
|
181
|
+
message: `The processing activity "${activity.id}" is under ${own}., which a composed capability owns.`,
|
|
182
|
+
action: "Name your own activities under your own domain, such as your app's name.",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (seen.has(activity.id)) {
|
|
186
|
+
throw new ValidationError({
|
|
187
|
+
message: `${declarer} declares the processing activity "${activity.id}" twice.`,
|
|
188
|
+
action: "Declare each activity once.",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
seen.add(activity.id);
|
|
192
|
+
return activity;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Parse a `processing` block, or refuse it naming what is wrong. `undefined` is the empty policy. */
|
|
197
|
+
export function parseProcessingPolicy(input: unknown): ProcessingPolicy {
|
|
198
|
+
if (input === undefined) return {};
|
|
199
|
+
const parsed = ProcessingPolicy.safeParse(input);
|
|
200
|
+
if (!parsed.success) {
|
|
201
|
+
throw fromZodError(parsed.error, {
|
|
202
|
+
message: "The processing block in pithy.config.ts does not parse.",
|
|
203
|
+
action: "Fix the field named, then deploy again.",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return parsed.data;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** One activity as it takes effect: who declared it, its basis after any override, and where that came from. */
|
|
210
|
+
export interface ResolvedActivity {
|
|
211
|
+
readonly id: string;
|
|
212
|
+
readonly purpose: string;
|
|
213
|
+
readonly classes: readonly string[];
|
|
214
|
+
readonly legalBasis: LegalBasis | null;
|
|
215
|
+
readonly basisSource: BasisSource | null;
|
|
216
|
+
readonly consentPurpose: string | null;
|
|
217
|
+
readonly recipients: readonly ProcessingRecipient[];
|
|
218
|
+
/** The capability that declared it, or `config` for the adopter's own. */
|
|
219
|
+
readonly declaredBy: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** What resolving reads off a composed capability. */
|
|
223
|
+
interface ActivityDeclarer {
|
|
224
|
+
readonly name: string;
|
|
225
|
+
readonly processing?: readonly ProcessingActivity[];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** One declared activity, before any override. */
|
|
229
|
+
function resolved(activity: ProcessingActivity, declaredBy: string, source: BasisSource): ResolvedActivity {
|
|
230
|
+
return {
|
|
231
|
+
id: activity.id,
|
|
232
|
+
purpose: activity.purpose,
|
|
233
|
+
classes: [...activity.classes],
|
|
234
|
+
legalBasis: activity.legalBasis ?? null,
|
|
235
|
+
basisSource: activity.legalBasis === undefined ? null : source,
|
|
236
|
+
consentPurpose: activity.consentPurpose ?? null,
|
|
237
|
+
recipients: activity.recipients.map((recipient) => ({ ...recipient, classes: [...recipient.classes] })),
|
|
238
|
+
declaredBy,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Every activity as it takes effect over a composition: each capability's, in composition order, then the adopter's,
|
|
244
|
+
* with the block's overrides applied. Refuses an adopter activity under a composed capability's domain, an id declared
|
|
245
|
+
* twice, and an override naming an activity no composed capability declares. Called when a Worker assembles.
|
|
246
|
+
*/
|
|
247
|
+
export function resolveProcessing(
|
|
248
|
+
policy: ProcessingPolicy,
|
|
249
|
+
capabilities: readonly ActivityDeclarer[],
|
|
250
|
+
): ResolvedActivity[] {
|
|
251
|
+
const reserved = new Set(capabilities.map((capability) => capability.name));
|
|
252
|
+
const own = parseProcessingActivities("pithy.config.ts", policy.activities ?? [], null, reserved);
|
|
253
|
+
const declared = capabilities.flatMap((capability) =>
|
|
254
|
+
(capability.processing ?? []).map((activity) => resolved(activity, capability.name, "capability")),
|
|
255
|
+
);
|
|
256
|
+
const ids = new Set<string>();
|
|
257
|
+
for (const activity of declared) {
|
|
258
|
+
if (ids.has(activity.id)) {
|
|
259
|
+
throw new ValidationError({
|
|
260
|
+
message: `The processing activity "${activity.id}" is declared twice.`,
|
|
261
|
+
action: "Declare each activity once.",
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
ids.add(activity.id);
|
|
265
|
+
}
|
|
266
|
+
const overrides = policy.overrides ?? {};
|
|
267
|
+
for (const id of Object.keys(overrides)) {
|
|
268
|
+
if (own.some((activity) => activity.id === id)) {
|
|
269
|
+
throw new ValidationError({
|
|
270
|
+
message: `The processing block overrides "${id}", which is declared in the same block.`,
|
|
271
|
+
action: `State its legal basis on the activity itself.`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (!ids.has(id)) {
|
|
275
|
+
throw new ValidationError({
|
|
276
|
+
message: `The processing block overrides "${id}", which no composed capability declares.`,
|
|
277
|
+
action: "Name an activity a composed capability declares. `GET /control-plane/processing` lists them.",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const overridden = declared.map((activity): ResolvedActivity => {
|
|
282
|
+
const override = overrides[activity.id];
|
|
283
|
+
if (override === undefined) return activity;
|
|
284
|
+
const consentPurpose =
|
|
285
|
+
override.legalBasis === "consent" ? (override.consentPurpose ?? activity.consentPurpose) : null;
|
|
286
|
+
return { ...activity, legalBasis: override.legalBasis, basisSource: "config", consentPurpose };
|
|
287
|
+
});
|
|
288
|
+
return [...overridden, ...own.map((activity) => resolved(activity, "config", "config"))];
|
|
289
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { Capability } from "../capability/capability";
|
|
5
|
+
import { parseProcessingPolicy, type ResolvedActivity, resolveProcessing } from "./activity";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The processing activities as this Worker resolved them at assembly, reachable from module scope, so the control
|
|
9
|
+
* plane reads the same record `createBackend` refused or accepted. One Worker assembles one backend; a second assembly
|
|
10
|
+
* in one isolate replaces the first wholesale, as `../retention/composed.ts` does for the retention policy.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Parse and resolve a Worker's `processing` block over what it composes, or refuse it. */
|
|
14
|
+
export function effectiveProcessing(block: unknown, capabilities: readonly Capability[]): ResolvedActivity[] {
|
|
15
|
+
return resolveProcessing(parseProcessingPolicy(block), capabilities);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let recorded: readonly ResolvedActivity[] | null = null;
|
|
19
|
+
|
|
20
|
+
/** Record the resolved activities. Called once by `createBackend`. */
|
|
21
|
+
export function recordProcessing(activities: readonly ResolvedActivity[]): void {
|
|
22
|
+
recorded = activities;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Forget them. For a test that needs the un-assembled state back. */
|
|
26
|
+
export function forgetProcessing(): void {
|
|
27
|
+
recorded = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The resolved activities, or `null` before any backend has been assembled in this isolate. */
|
|
31
|
+
export function composedProcessing(): readonly ResolvedActivity[] | null {
|
|
32
|
+
return recorded;
|
|
33
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
/** The processing record's closed vocabularies, apart from the walk that fills them, so a browser may import them. */
|
|
7
|
+
|
|
8
|
+
/** What kind of gap the record names. */
|
|
9
|
+
export const PROCESSING_GAP_KINDS = [
|
|
10
|
+
"no-legal-basis",
|
|
11
|
+
"no-consent-purpose",
|
|
12
|
+
"class-not-stored",
|
|
13
|
+
"class-unclaimed",
|
|
14
|
+
"phi-not-covered",
|
|
15
|
+
"no-standing-grant",
|
|
16
|
+
] as const;
|
|
17
|
+
export type ProcessingGapKindId = (typeof PROCESSING_GAP_KINDS)[number];
|
|
18
|
+
|
|
19
|
+
/** Where a recipient came from. */
|
|
20
|
+
export const RECIPIENT_SOURCES = ["platform", "binding", "declared"] as const;
|
|
21
|
+
export type RecipientSource = (typeof RECIPIENT_SOURCES)[number];
|
|
22
|
+
|
|
23
|
+
/** Where a legal basis came from: the declaring capability, or the `processing` block. */
|
|
24
|
+
export const BASIS_SOURCES = ["capability", "config"] as const;
|
|
25
|
+
export type BasisSource = (typeof BASIS_SOURCES)[number];
|
|
26
|
+
|
|
27
|
+
/** The six lawful bases of GDPR Article 6(1), (a) to (f), in that order. */
|
|
28
|
+
export const LEGAL_BASES = [
|
|
29
|
+
"consent",
|
|
30
|
+
"contract",
|
|
31
|
+
"legal-obligation",
|
|
32
|
+
"vital-interests",
|
|
33
|
+
"public-task",
|
|
34
|
+
"legitimate-interests",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
/** One lawful basis for processing. */
|
|
38
|
+
export const LegalBasis = z
|
|
39
|
+
.enum(LEGAL_BASES)
|
|
40
|
+
.describe(
|
|
41
|
+
"The lawful basis of GDPR Article 6(1): `consent` (a), `contract` (b), `legal-obligation` (c), `vital-interests` (d), `public-task` (e) or `legitimate-interests` (f). A `consent` basis names the consent purpose it rests on.",
|
|
42
|
+
);
|
|
43
|
+
export type LegalBasis = z.infer<typeof LegalBasis>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { ExecutionContext } from "@cloudflare/workers-types";
|
|
5
|
+
import type { Logger } from "../logger/logger";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* **A capability's queue consumer, and how one Worker's `queue` export finds it (#824).**
|
|
9
|
+
*
|
|
10
|
+
* A Worker has one `queue` handler for every queue it consumes, the way it has one `email` and one `scheduled`.
|
|
11
|
+
* Which capability a message belongs to cannot be read off the batch: `batch.queue` is the provisioned name,
|
|
12
|
+
* `<project>-<env>-<thing>`, which no capability knows at runtime and which a long name truncates. So the message
|
|
13
|
+
* says it: every body a capability sends carries its own name under {@link QUEUE_CAPABILITY_FIELD}, built with
|
|
14
|
+
* {@link queueBody}, and the dispatcher hands each capability exactly the messages that name it.
|
|
15
|
+
*
|
|
16
|
+
* **A message no composed capability claims is acknowledged and logged**, never retried. Retrying cannot make a
|
|
17
|
+
* capability appear, so a retry would only walk the message to its dead-letter queue and count it as a delivery
|
|
18
|
+
* failure it never was.
|
|
19
|
+
*
|
|
20
|
+
* **A handler that throws costs its own messages, and nothing else.** Every other capability's messages are still
|
|
21
|
+
* handed over; the first failure is rethrown after the pass, so the invocation is recorded as failed and the runtime
|
|
22
|
+
* retries what was neither acknowledged nor retried.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* One delivered message, as much of the runtime's as a consumer touches. Stated here rather than imported, so the
|
|
27
|
+
* Workers types' `Message` — a word half the kit uses for other things — never becomes a name the kit reaches for.
|
|
28
|
+
*/
|
|
29
|
+
export interface QueueMessage {
|
|
30
|
+
/** The runtime's id for this delivery. */
|
|
31
|
+
readonly id: string;
|
|
32
|
+
/** The body as sent. */
|
|
33
|
+
readonly body: unknown;
|
|
34
|
+
/** How many times this message has been delivered, this one included. Starts at 1. */
|
|
35
|
+
readonly attempts: number;
|
|
36
|
+
/** Settle it: delivered, never to be seen again. */
|
|
37
|
+
ack(): void;
|
|
38
|
+
/** Settle it: deliver it again after the delay, or to the dead-letter queue once retries are spent. */
|
|
39
|
+
retry(options?: { delaySeconds?: number }): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One batch the runtime hands a Worker's `queue` export. */
|
|
43
|
+
export interface QueueBatch {
|
|
44
|
+
/** The provisioned queue's name. */
|
|
45
|
+
readonly queue: string;
|
|
46
|
+
/** Its messages. */
|
|
47
|
+
readonly messages: readonly QueueMessage[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The body field that names the capability a queue message belongs to. */
|
|
51
|
+
export const QUEUE_CAPABILITY_FIELD = "capability";
|
|
52
|
+
|
|
53
|
+
/** A capability's queue handler: the messages that name it, from one batch. */
|
|
54
|
+
export type CapabilityQueueHandler = (
|
|
55
|
+
messages: readonly QueueMessage[],
|
|
56
|
+
env: Record<string, unknown>,
|
|
57
|
+
ctx: ExecutionContext,
|
|
58
|
+
) => Promise<void>;
|
|
59
|
+
|
|
60
|
+
/** A message body, stamped with the capability it belongs to so the Worker's one `queue` export can route it. */
|
|
61
|
+
export function queueBody<const Capability extends string, const Body extends Record<string, unknown>>(
|
|
62
|
+
capability: Capability,
|
|
63
|
+
body: Body,
|
|
64
|
+
): Body & { capability: Capability } {
|
|
65
|
+
return { ...body, capability };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The capability a message body names, or null when it names none. */
|
|
69
|
+
export function queueMessageCapability(body: unknown): string | null {
|
|
70
|
+
if (typeof body !== "object" || body === null) return null;
|
|
71
|
+
const value = (body as Record<string, unknown>)[QUEUE_CAPABILITY_FIELD];
|
|
72
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Hand each composed capability the messages of one batch that name it. */
|
|
76
|
+
export async function dispatchQueueBatch(
|
|
77
|
+
batch: QueueBatch,
|
|
78
|
+
handlers: ReadonlyMap<string, CapabilityQueueHandler>,
|
|
79
|
+
env: Record<string, unknown>,
|
|
80
|
+
ctx: ExecutionContext,
|
|
81
|
+
log?: Pick<Logger, "warn">,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
const claimed = new Map<string, QueueMessage[]>();
|
|
84
|
+
for (const message of batch.messages) {
|
|
85
|
+
const capability = queueMessageCapability(message.body);
|
|
86
|
+
if (capability === null || !handlers.has(capability)) {
|
|
87
|
+
log?.warn("queue message unclaimed", { queue: batch.queue, messageId: message.id, capability });
|
|
88
|
+
message.ack();
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const list = claimed.get(capability) ?? [];
|
|
92
|
+
list.push(message);
|
|
93
|
+
claimed.set(capability, list);
|
|
94
|
+
}
|
|
95
|
+
const failures: unknown[] = [];
|
|
96
|
+
for (const [capability, messages] of claimed) {
|
|
97
|
+
try {
|
|
98
|
+
await handlers.get(capability)?.(messages, env, ctx);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
failures.push(error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (failures[0] !== undefined) throw failures[0];
|
|
104
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { Capability } from "../capability/capability";
|
|
5
|
+
import type { PurgeSubject } from "../capability/purge";
|
|
6
|
+
import { privacyInventory } from "../privacy/inventory";
|
|
7
|
+
import { type EffectiveRetention, parseRetentionPolicy, type RetentionComposition, resolveRetention } from "./policy";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* **The retention policy as this Worker resolved it, reachable from module scope** — so a purge, a prune and
|
|
11
|
+
* an inactivity pass, each running in a Workflow the runtime constructs with `env` alone, read the same policy the
|
|
12
|
+
* routes do. The reasoning, and the cost, are `../capability/composition.ts`'s: `createBackend` runs at module load,
|
|
13
|
+
* one Worker assembles one backend, and a second assembly in one isolate replaces the first wholesale.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What resolving reads off a composition: each capability's window, and every lifecycle action it audits — the
|
|
18
|
+
* transitions it declares, then the ones its erasure records.
|
|
19
|
+
*/
|
|
20
|
+
export function retentionComposition(capabilities: readonly Capability[]): RetentionComposition {
|
|
21
|
+
return {
|
|
22
|
+
purgeWindows: capabilities.flatMap((capability) => (capability.purgeWindow ? [capability.purgeWindow] : [])),
|
|
23
|
+
lifecycleActions: capabilities.flatMap((capability) => {
|
|
24
|
+
const actions = capability.erasureRecords?.auditActions;
|
|
25
|
+
const erasure = actions ? [...actions.deleted, actions.step, actions.failed, actions.purged] : [];
|
|
26
|
+
return [...(capability.lifecycleAuditActions ?? []), ...erasure];
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse and resolve a Worker's `retention` block over what it composes, or refuse it. What `createBackend` calls at
|
|
33
|
+
* assembly and `pithy doctor` calls on the same config, so both report — or refuse — one policy.
|
|
34
|
+
*/
|
|
35
|
+
export function effectiveRetention(block: unknown, capabilities: readonly Capability[]): EffectiveRetention {
|
|
36
|
+
return resolveRetention(parseRetentionPolicy(block), retentionComposition(capabilities));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The resolved policy, or null before any backend has been assembled in this isolate. */
|
|
40
|
+
let recorded: EffectiveRetention | null = null;
|
|
41
|
+
|
|
42
|
+
/** Record the resolved policy. Called once by `createBackend`, beside `recordComposition`. */
|
|
43
|
+
export function recordRetention(retention: EffectiveRetention): void {
|
|
44
|
+
recorded = retention;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Forget it. For a test that needs the un-assembled state back. */
|
|
48
|
+
export function forgetRetention(): void {
|
|
49
|
+
recorded = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The resolved policy — or, before any assembly, `null`. A caller with a capability's own default in hand falls back
|
|
54
|
+
* to it, which is exactly what an empty block means.
|
|
55
|
+
*/
|
|
56
|
+
export function composedRetention(): EffectiveRetention | null {
|
|
57
|
+
return recorded;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The privacy classes a subject's rows hold: every class marked on a schema of a capability that erases that
|
|
62
|
+
* subject in its purge — the capabilities that hold rows keyed to the subject. What the class ages are read against:
|
|
63
|
+
* an account holds `pii` when a capability holding its rows marks a field `pii`.
|
|
64
|
+
*/
|
|
65
|
+
export function subjectClasses(capabilities: readonly Capability[], subject: PurgeSubject): string[] {
|
|
66
|
+
const holders = new Set(
|
|
67
|
+
capabilities
|
|
68
|
+
.filter((capability) => capability.purge?.some((contribution) => contribution.subject === subject))
|
|
69
|
+
.map((capability) => capability.name),
|
|
70
|
+
);
|
|
71
|
+
const classes = privacyInventory(capabilities)
|
|
72
|
+
.schemas.filter((schema) => holders.has(schema.capability))
|
|
73
|
+
.flatMap((schema) => schema.fields.map((field) => field.class));
|
|
74
|
+
return [...new Set(classes)].sort();
|
|
75
|
+
}
|