@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,95 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* **What a room knows, and where it keeps it: on its sockets, and nowhere else.**
|
|
6
|
+
*
|
|
7
|
+
* A room is one Durable Object on the Hibernation API. When it hibernates its memory goes, so everything it
|
|
8
|
+
* must still know when it wakes rides in each socket's attachment, read back from `getWebSockets()`: the
|
|
9
|
+
* participant, the version the socket negotiated, the room kind's rules, and which tickets have been spent.
|
|
10
|
+
* Nothing is written to D1, KV or Durable Object storage. When the last socket closes, the room is gone.
|
|
11
|
+
*
|
|
12
|
+
* This module is the logic of that, pure, so it is tested without a runtime; `./durableObject.ts` is the
|
|
13
|
+
* thin part that reads and writes attachments.
|
|
14
|
+
*/
|
|
15
|
+
/** A spent ticket: its id, and when it would have expired anyway — after which remembering it is pointless. */
|
|
16
|
+
export declare const SpentTicket: z.ZodObject<{
|
|
17
|
+
id: z.ZodString;
|
|
18
|
+
exp: z.ZodNumber;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
export type SpentTicket = z.infer<typeof SpentTicket>;
|
|
21
|
+
/**
|
|
22
|
+
* What the Worker's socket route tells the room, on a header only the Worker can set: a verified ticket's
|
|
23
|
+
* participant, the ticket itself, and the room kind's rules. The room trusts this and nothing a client sent.
|
|
24
|
+
*/
|
|
25
|
+
export declare const PresenceAdmission: z.ZodObject<{
|
|
26
|
+
kind: z.ZodString;
|
|
27
|
+
recordId: z.ZodString;
|
|
28
|
+
ref: z.ZodString;
|
|
29
|
+
name: z.ZodString;
|
|
30
|
+
origin: z.ZodEnum<{
|
|
31
|
+
"control-plane": "control-plane";
|
|
32
|
+
session: "session";
|
|
33
|
+
}>;
|
|
34
|
+
ticket: z.ZodObject<{
|
|
35
|
+
id: z.ZodString;
|
|
36
|
+
exp: z.ZodNumber;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
activities: z.ZodArray<z.ZodString>;
|
|
39
|
+
maxParticipants: z.ZodNumber;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
export type PresenceAdmission = z.infer<typeof PresenceAdmission>;
|
|
42
|
+
/** The server-set header carrying a {@link PresenceAdmission} into the room. */
|
|
43
|
+
export declare const PRESENCE_ADMISSION_HEADER = "x-pithy-presence-admission";
|
|
44
|
+
/** What one socket carries through hibernation. Kept far under the 16,384-byte attachment ceiling. */
|
|
45
|
+
export declare const PresenceAttachment: z.ZodObject<{
|
|
46
|
+
participant: z.ZodObject<{
|
|
47
|
+
id: z.ZodString;
|
|
48
|
+
ref: z.ZodString;
|
|
49
|
+
name: z.ZodString;
|
|
50
|
+
origin: z.ZodEnum<{
|
|
51
|
+
"control-plane": "control-plane";
|
|
52
|
+
session: "session";
|
|
53
|
+
}>;
|
|
54
|
+
activity: z.ZodString;
|
|
55
|
+
joinedAt: z.ZodISODateTime;
|
|
56
|
+
}, z.core.$strip>;
|
|
57
|
+
version: z.ZodString;
|
|
58
|
+
ticket: z.ZodObject<{
|
|
59
|
+
id: z.ZodString;
|
|
60
|
+
exp: z.ZodNumber;
|
|
61
|
+
}, z.core.$strip>;
|
|
62
|
+
activities: z.ZodArray<z.ZodString>;
|
|
63
|
+
spent: z.ZodArray<z.ZodObject<{
|
|
64
|
+
id: z.ZodString;
|
|
65
|
+
exp: z.ZodNumber;
|
|
66
|
+
}, z.core.$strip>>;
|
|
67
|
+
}, z.core.$strip>;
|
|
68
|
+
export type PresenceAttachment = z.infer<typeof PresenceAttachment>;
|
|
69
|
+
/** Every ticket a room still remembers as spent, from its sockets' attachments and its own memory. */
|
|
70
|
+
export declare function spentTickets(attachments: readonly PresenceAttachment[], remembered: readonly SpentTicket[], nowSeconds: number): Set<string>;
|
|
71
|
+
/** Why a room refuses an admission, or `null` to admit. */
|
|
72
|
+
export type AdmissionRefusal = "spent" | "full" | null;
|
|
73
|
+
/** Decide one admission against the room as it stands. */
|
|
74
|
+
export declare function admit(admission: PresenceAdmission, attachments: readonly PresenceAttachment[], remembered: readonly SpentTicket[], nowSeconds: number): AdmissionRefusal;
|
|
75
|
+
/**
|
|
76
|
+
* The most spent tickets one attachment carries. A ticket is honored for 60 seconds, so this is how many
|
|
77
|
+
* sockets a room may see redeemed and closed within a minute before it starts forgetting the soonest to
|
|
78
|
+
* expire; at about 60 bytes each it keeps an attachment far under its 16,384-byte ceiling.
|
|
79
|
+
*/
|
|
80
|
+
export declare const MAX_CARRIED_SPENT = 160;
|
|
81
|
+
/**
|
|
82
|
+
* The spent list a socket joining a room starts with: what the room remembers in memory, moved onto the
|
|
83
|
+
* socket, where it survives hibernation. A room's memory is only for the tickets of sockets that closed with
|
|
84
|
+
* no other socket left to carry them; the moment a socket is there again, it carries them.
|
|
85
|
+
*/
|
|
86
|
+
export declare function inheritSpent(remembered: readonly SpentTicket[], own: string, nowSeconds: number): SpentTicket[];
|
|
87
|
+
/**
|
|
88
|
+
* The spent list a surviving socket carries once another closes: its own, plus the closing socket's ticket
|
|
89
|
+
* and everything it carried, less what has expired. Returns `null` when nothing changed, so a room writes no
|
|
90
|
+
* attachment it does not need to.
|
|
91
|
+
*/
|
|
92
|
+
export declare function carrySpent(survivor: PresenceAttachment, closing: PresenceAttachment, nowSeconds: number): SpentTicket[] | null;
|
|
93
|
+
/** Whether a client may announce this activity in this room. */
|
|
94
|
+
export declare function declaresActivity(attachment: PresenceAttachment, activity: string): boolean;
|
|
95
|
+
//# sourceMappingURL=room.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"room.d.ts","sourceRoot":"","sources":["../../src/presence/room.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;GAUG;AAEH,+GAA+G;AAC/G,eAAO,MAAM,WAAW;;;iBAKsD,CAAC;AAC/E,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;iBAc4D,CAAC;AAC3F,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,+BAA+B,CAAC;AAEtE,sGAAsG;AACtG,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;iBAU0D,CAAC;AAC1F,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,sGAAsG;AACtG,wBAAgB,YAAY,CAC1B,WAAW,EAAE,SAAS,kBAAkB,EAAE,EAC1C,UAAU,EAAE,SAAS,WAAW,EAAE,EAClC,UAAU,EAAE,MAAM,GACjB,GAAG,CAAC,MAAM,CAAC,CAWb;AAED,2DAA2D;AAC3D,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,GAAG,IAAI,CAAC;AAEvD,0DAA0D;AAC1D,wBAAgB,KAAK,CACnB,SAAS,EAAE,iBAAiB,EAC5B,WAAW,EAAE,SAAS,kBAAkB,EAAE,EAC1C,UAAU,EAAE,SAAS,WAAW,EAAE,EAClC,UAAU,EAAE,MAAM,GACjB,gBAAgB,CAIlB;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAYrC;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,SAAS,WAAW,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,WAAW,EAAE,CAE/G;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,MAAM,GACjB,WAAW,EAAE,GAAG,IAAI,CAKtB;AAED,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1F"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { PresenceActivity, PresenceRecordId, PresenceRoomKindName } from "./names.js";
|
|
4
|
+
import { PresenceOrigin, PresenceParticipant, PresenceParticipantName, PresenceParticipantRef } from "./frames.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region src/presence/room.ts
|
|
7
|
+
/**
|
|
8
|
+
* **What a room knows, and where it keeps it: on its sockets, and nowhere else.**
|
|
9
|
+
*
|
|
10
|
+
* A room is one Durable Object on the Hibernation API. When it hibernates its memory goes, so everything it
|
|
11
|
+
* must still know when it wakes rides in each socket's attachment, read back from `getWebSockets()`: the
|
|
12
|
+
* participant, the version the socket negotiated, the room kind's rules, and which tickets have been spent.
|
|
13
|
+
* Nothing is written to D1, KV or Durable Object storage. When the last socket closes, the room is gone.
|
|
14
|
+
*
|
|
15
|
+
* This module is the logic of that, pure, so it is tested without a runtime; `./durableObject.ts` is the
|
|
16
|
+
* thin part that reads and writes attachments.
|
|
17
|
+
*/
|
|
18
|
+
/** A spent ticket: its id, and when it would have expired anyway — after which remembering it is pointless. */
|
|
19
|
+
const SpentTicket = z.object({
|
|
20
|
+
id: z.string().uuid().describe("The spent ticket's id."),
|
|
21
|
+
exp: z.number().int().describe("When the ticket would have expired, in seconds since the epoch.")
|
|
22
|
+
}).describe("A ticket a room has redeemed, kept until it would have expired.");
|
|
23
|
+
/**
|
|
24
|
+
* What the Worker's socket route tells the room, on a header only the Worker can set: a verified ticket's
|
|
25
|
+
* participant, the ticket itself, and the room kind's rules. The room trusts this and nothing a client sent.
|
|
26
|
+
*/
|
|
27
|
+
const PresenceAdmission = z.object({
|
|
28
|
+
kind: PresenceRoomKindName.describe("The room kind."),
|
|
29
|
+
recordId: PresenceRecordId.describe("The record the room is about."),
|
|
30
|
+
ref: PresenceParticipantRef,
|
|
31
|
+
name: PresenceParticipantName,
|
|
32
|
+
origin: PresenceOrigin,
|
|
33
|
+
ticket: SpentTicket.describe("The ticket this socket is redeeming."),
|
|
34
|
+
activities: z.array(PresenceActivity).min(1).describe("The room kind's declared activities. The first is what a participant starts as."),
|
|
35
|
+
maxParticipants: z.number().int().positive().describe("The room kind's participant cap.")
|
|
36
|
+
}).describe("A verified admission, forwarded from the Worker's socket route to the room.");
|
|
37
|
+
/** The server-set header carrying a {@link PresenceAdmission} into the room. */
|
|
38
|
+
const PRESENCE_ADMISSION_HEADER = "x-pithy-presence-admission";
|
|
39
|
+
/** What one socket carries through hibernation. Kept far under the 16,384-byte attachment ceiling. */
|
|
40
|
+
const PresenceAttachment = z.object({
|
|
41
|
+
participant: PresenceParticipant.describe("Who this socket is, and what they are doing."),
|
|
42
|
+
version: z.string().describe("The negotiated subprotocol."),
|
|
43
|
+
ticket: SpentTicket.describe("The ticket this socket redeemed."),
|
|
44
|
+
activities: z.array(PresenceActivity).min(1).describe("The room kind's declared activities."),
|
|
45
|
+
spent: z.array(SpentTicket).describe("Tickets redeemed by sockets that have since closed, carried so a hibernated room still refuses them.")
|
|
46
|
+
}).describe("The participant and room state a presence socket keeps across hibernation.");
|
|
47
|
+
/** Every ticket a room still remembers as spent, from its sockets' attachments and its own memory. */
|
|
48
|
+
function spentTickets(attachments, remembered, nowSeconds) {
|
|
49
|
+
const spent = /* @__PURE__ */ new Set();
|
|
50
|
+
const add = (ticket) => {
|
|
51
|
+
if (ticket.exp > nowSeconds) spent.add(ticket.id);
|
|
52
|
+
};
|
|
53
|
+
for (const attachment of attachments) {
|
|
54
|
+
add(attachment.ticket);
|
|
55
|
+
for (const ticket of attachment.spent) add(ticket);
|
|
56
|
+
}
|
|
57
|
+
for (const ticket of remembered) add(ticket);
|
|
58
|
+
return spent;
|
|
59
|
+
}
|
|
60
|
+
/** Decide one admission against the room as it stands. */
|
|
61
|
+
function admit(admission, attachments, remembered, nowSeconds) {
|
|
62
|
+
if (spentTickets(attachments, remembered, nowSeconds).has(admission.ticket.id)) return "spent";
|
|
63
|
+
if (attachments.length >= admission.maxParticipants) return "full";
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The most spent tickets one attachment carries. A ticket is honored for 60 seconds, so this is how many
|
|
68
|
+
* sockets a room may see redeemed and closed within a minute before it starts forgetting the soonest to
|
|
69
|
+
* expire; at about 60 bytes each it keeps an attachment far under its 16,384-byte ceiling.
|
|
70
|
+
*/
|
|
71
|
+
const MAX_CARRIED_SPENT = 160;
|
|
72
|
+
/** Unexpired, once each, never `own`, and at most {@link MAX_CARRIED_SPENT} — the latest to expire kept. */
|
|
73
|
+
function liveSpent(tickets, own, nowSeconds) {
|
|
74
|
+
const byId = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const ticket of tickets) if (ticket.exp > nowSeconds && ticket.id !== own) byId.set(ticket.id, ticket);
|
|
76
|
+
const live = [...byId.values()];
|
|
77
|
+
return live.length > 160 ? live.sort((a, b) => b.exp - a.exp).slice(0, 160) : live;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The spent list a socket joining a room starts with: what the room remembers in memory, moved onto the
|
|
81
|
+
* socket, where it survives hibernation. A room's memory is only for the tickets of sockets that closed with
|
|
82
|
+
* no other socket left to carry them; the moment a socket is there again, it carries them.
|
|
83
|
+
*/
|
|
84
|
+
function inheritSpent(remembered, own, nowSeconds) {
|
|
85
|
+
return liveSpent(remembered, own, nowSeconds);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The spent list a surviving socket carries once another closes: its own, plus the closing socket's ticket
|
|
89
|
+
* and everything it carried, less what has expired. Returns `null` when nothing changed, so a room writes no
|
|
90
|
+
* attachment it does not need to.
|
|
91
|
+
*/
|
|
92
|
+
function carrySpent(survivor, closing, nowSeconds) {
|
|
93
|
+
const next = liveSpent([
|
|
94
|
+
...survivor.spent,
|
|
95
|
+
closing.ticket,
|
|
96
|
+
...closing.spent
|
|
97
|
+
], survivor.ticket.id, nowSeconds);
|
|
98
|
+
return next.length === survivor.spent.length && next.every((ticket, index) => survivor.spent[index]?.id === ticket.id) ? null : next;
|
|
99
|
+
}
|
|
100
|
+
/** Whether a client may announce this activity in this room. */
|
|
101
|
+
function declaresActivity(attachment, activity) {
|
|
102
|
+
return attachment.activities.includes(activity);
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
export { MAX_CARRIED_SPENT, PRESENCE_ADMISSION_HEADER, PresenceAdmission, PresenceAttachment, SpentTicket, admit, carrySpent, declaresActivity, inheritSpent, spentTickets };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* **A presence ticket: single use, 60 seconds, one room, one participant.**
|
|
6
|
+
*
|
|
7
|
+
* `<claims>.<signature>`, both unpadded base64url, the signature an HMAC-SHA256 over the claims segment with
|
|
8
|
+
* the Worker's `presence-ticket-key` secret. The alphabet and the dot are all token characters, so a
|
|
9
|
+
* ticket rides in `Sec-WebSocket-Protocol` as `pithy.ticket.<ticket>` untouched.
|
|
10
|
+
*
|
|
11
|
+
* What a ticket proves is that **this Worker** minted it, behind a gate, for these claims. Verifying it
|
|
12
|
+
* here checks the signature, the expiry and the room; spending it is the room's job, because only the room
|
|
13
|
+
* the ticket names can redeem it, which is what makes "single use" decidable without a write anywhere.
|
|
14
|
+
*
|
|
15
|
+
* Every failure is the one {@link PresenceTicketInvalidError}, with the step in `detail` for the log: a caller
|
|
16
|
+
* holding someone else's ticket learns that it does not work, never why.
|
|
17
|
+
*/
|
|
18
|
+
/** The subprotocol prefix a ticket travels under, beside `pithy.presence.v1`. */
|
|
19
|
+
export declare const PRESENCE_TICKET_PROTOCOL_PREFIX = "pithy.ticket.";
|
|
20
|
+
/** What a ticket says, signed. */
|
|
21
|
+
export declare const PresenceTicketClaims: z.ZodObject<{
|
|
22
|
+
v: z.ZodLiteral<1>;
|
|
23
|
+
id: z.ZodString;
|
|
24
|
+
kind: z.ZodString;
|
|
25
|
+
recordId: z.ZodString;
|
|
26
|
+
ref: z.ZodString;
|
|
27
|
+
name: z.ZodString;
|
|
28
|
+
origin: z.ZodEnum<{
|
|
29
|
+
"control-plane": "control-plane";
|
|
30
|
+
session: "session";
|
|
31
|
+
}>;
|
|
32
|
+
exp: z.ZodNumber;
|
|
33
|
+
}, z.core.$strip>;
|
|
34
|
+
export type PresenceTicketClaims = z.infer<typeof PresenceTicketClaims>;
|
|
35
|
+
/** What a caller asks a ticket for: everything but the id and the expiry, which the minter decides. */
|
|
36
|
+
export type PresenceTicketRequest = Omit<PresenceTicketClaims, "v" | "id" | "exp">;
|
|
37
|
+
/** Mint a ticket. `keys[0]` signs. */
|
|
38
|
+
export declare function mintPresenceTicket(request: PresenceTicketRequest, keys: readonly string[], now?: Date): Promise<{
|
|
39
|
+
ticket: string;
|
|
40
|
+
claims: PresenceTicketClaims;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Verify a ticket for one room: the signature under any honored key, the expiry, and that it names this
|
|
44
|
+
* kind and this record. Returns the claims; spending them is the room's.
|
|
45
|
+
*/
|
|
46
|
+
export declare function verifyPresenceTicket(ticket: string, room: {
|
|
47
|
+
kind: string;
|
|
48
|
+
recordId: string;
|
|
49
|
+
}, keys: readonly string[], now?: Date): Promise<PresenceTicketClaims>;
|
|
50
|
+
/**
|
|
51
|
+
* The ticket from a subprotocol offer, or `undefined` when none rides in it. More than one is refused as
|
|
52
|
+
* none: an offer is one client's, and two tickets in it is not something a client means.
|
|
53
|
+
*/
|
|
54
|
+
export declare function ticketFromOffer(offered: string | null | undefined): string | undefined;
|
|
55
|
+
//# sourceMappingURL=ticket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticket.d.ts","sourceRoot":"","sources":["../../src/presence/ticket.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB;;;;;;;;;;;;;GAaG;AAEH,iFAAiF;AACjF,eAAO,MAAM,+BAA+B,kBAAkB,CAAC;AAE/D,kCAAkC;AAClC,eAAO,MAAM,oBAAoB;;;;;;;;;;;;iBAawD,CAAC;AAC1F,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,uGAAuG;AACvG,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,oBAAoB,EAAE,GAAG,GAAG,IAAI,GAAG,KAAK,CAAC,CAAC;AAWnF,sCAAsC;AACtC,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,qBAAqB,EAC9B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,GAAE,IAAiB,GACrB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,CAAC,CAY3D;AAWD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EACxC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,GAAE,IAAiB,GACrB,OAAO,CAAC,oBAAoB,CAAC,CAqC/B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOtF"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { PresenceTicketInvalidError } from "../error/pithyError.js";
|
|
4
|
+
import { PresenceRecordId, PresenceRoomKindName } from "./names.js";
|
|
5
|
+
import { PresenceOrigin, PresenceParticipantName, PresenceParticipantRef } from "./frames.js";
|
|
6
|
+
import "./declaration.js";
|
|
7
|
+
import { base64UrlDecode, base64UrlEncode } from "../controlPlane/token/base64url.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
//#region src/presence/ticket.ts
|
|
10
|
+
/**
|
|
11
|
+
* **A presence ticket: single use, 60 seconds, one room, one participant.**
|
|
12
|
+
*
|
|
13
|
+
* `<claims>.<signature>`, both unpadded base64url, the signature an HMAC-SHA256 over the claims segment with
|
|
14
|
+
* the Worker's `presence-ticket-key` secret. The alphabet and the dot are all token characters, so a
|
|
15
|
+
* ticket rides in `Sec-WebSocket-Protocol` as `pithy.ticket.<ticket>` untouched.
|
|
16
|
+
*
|
|
17
|
+
* What a ticket proves is that **this Worker** minted it, behind a gate, for these claims. Verifying it
|
|
18
|
+
* here checks the signature, the expiry and the room; spending it is the room's job, because only the room
|
|
19
|
+
* the ticket names can redeem it, which is what makes "single use" decidable without a write anywhere.
|
|
20
|
+
*
|
|
21
|
+
* Every failure is the one {@link PresenceTicketInvalidError}, with the step in `detail` for the log: a caller
|
|
22
|
+
* holding someone else's ticket learns that it does not work, never why.
|
|
23
|
+
*/
|
|
24
|
+
/** The subprotocol prefix a ticket travels under, beside `pithy.presence.v1`. */
|
|
25
|
+
const PRESENCE_TICKET_PROTOCOL_PREFIX = "pithy.ticket.";
|
|
26
|
+
/** What a ticket says, signed. */
|
|
27
|
+
const PresenceTicketClaims = z.object({
|
|
28
|
+
v: z.literal(1).describe("The ticket format. A different one is refused."),
|
|
29
|
+
id: z.string().uuid().describe("The ticket's own id — what the room records as spent."),
|
|
30
|
+
kind: PresenceRoomKindName.describe("The room kind the ticket admits to."),
|
|
31
|
+
recordId: PresenceRecordId.describe("The record the room is about."),
|
|
32
|
+
ref: PresenceParticipantRef.describe("Who the participant is: the client's opaque ref, or the signed-in user's id."),
|
|
33
|
+
name: PresenceParticipantName.describe("How other participants see them."),
|
|
34
|
+
origin: PresenceOrigin.describe("Which door the ticket was minted at."),
|
|
35
|
+
exp: z.number().int().positive().describe("When the ticket stops being honored, in seconds since the epoch.")
|
|
36
|
+
}).describe("The claims a presence ticket carries, signed by the Worker that minted it.");
|
|
37
|
+
const encoder = new TextEncoder();
|
|
38
|
+
async function hmacKey(secret) {
|
|
39
|
+
return crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
40
|
+
name: "HMAC",
|
|
41
|
+
hash: "SHA-256"
|
|
42
|
+
}, false, ["sign", "verify"]);
|
|
43
|
+
}
|
|
44
|
+
/** Mint a ticket. `keys[0]` signs. */
|
|
45
|
+
async function mintPresenceTicket(request, keys, now = /* @__PURE__ */ new Date()) {
|
|
46
|
+
const [signing] = keys;
|
|
47
|
+
if (signing === void 0) throw new PresenceTicketInvalidError({ detail: "no signing key" });
|
|
48
|
+
const claims = PresenceTicketClaims.parse({
|
|
49
|
+
v: 1,
|
|
50
|
+
id: crypto.randomUUID(),
|
|
51
|
+
...request,
|
|
52
|
+
exp: Math.floor(now.getTime() / 1e3) + 60
|
|
53
|
+
});
|
|
54
|
+
const body = base64UrlEncode(encoder.encode(JSON.stringify(claims)));
|
|
55
|
+
const signature = new Uint8Array(await crypto.subtle.sign("HMAC", await hmacKey(signing), encoder.encode(body)));
|
|
56
|
+
return {
|
|
57
|
+
ticket: `${body}.${base64UrlEncode(signature)}`,
|
|
58
|
+
claims
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Decode a segment, mapping the control-plane decoder's refusal onto this one. */
|
|
62
|
+
function decode(segment, what) {
|
|
63
|
+
try {
|
|
64
|
+
return base64UrlDecode(segment);
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
throw new PresenceTicketInvalidError({ detail: `${what} is not base64url` }, { cause });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Verify a ticket for one room: the signature under any honored key, the expiry, and that it names this
|
|
71
|
+
* kind and this record. Returns the claims; spending them is the room's.
|
|
72
|
+
*/
|
|
73
|
+
async function verifyPresenceTicket(ticket, room, keys, now = /* @__PURE__ */ new Date()) {
|
|
74
|
+
const parts = ticket.split(".");
|
|
75
|
+
const [body, signature] = parts;
|
|
76
|
+
if (parts.length !== 2 || !body || !signature) throw new PresenceTicketInvalidError({ detail: "not two segments" });
|
|
77
|
+
const signed = decode(signature, "signature");
|
|
78
|
+
let authentic = false;
|
|
79
|
+
for (const key of keys) if (await crypto.subtle.verify("HMAC", await hmacKey(key), signed, encoder.encode(body))) {
|
|
80
|
+
authentic = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (!authentic) throw new PresenceTicketInvalidError({ detail: "signature does not verify under any honored key" });
|
|
84
|
+
let json;
|
|
85
|
+
try {
|
|
86
|
+
json = JSON.parse(new TextDecoder().decode(decode(body, "claims")));
|
|
87
|
+
} catch (cause) {
|
|
88
|
+
if (cause instanceof PresenceTicketInvalidError) throw cause;
|
|
89
|
+
throw new PresenceTicketInvalidError({ detail: "claims are not JSON" }, { cause });
|
|
90
|
+
}
|
|
91
|
+
const parsed = PresenceTicketClaims.safeParse(json);
|
|
92
|
+
if (!parsed.success) throw new PresenceTicketInvalidError({ detail: "claims do not parse" });
|
|
93
|
+
const claims = parsed.data;
|
|
94
|
+
const seconds = Math.floor(now.getTime() / 1e3);
|
|
95
|
+
if (claims.exp <= seconds) throw new PresenceTicketInvalidError({ detail: `expired at ${claims.exp}` });
|
|
96
|
+
if (claims.exp > seconds + 60) throw new PresenceTicketInvalidError({ detail: `expiry ${claims.exp} is beyond the ticket lifetime` });
|
|
97
|
+
if (claims.kind !== room.kind || claims.recordId !== room.recordId) throw new PresenceTicketInvalidError({ detail: `minted for ${claims.kind}/${claims.recordId}, presented at ${room.kind}/${room.recordId}` });
|
|
98
|
+
return claims;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The ticket from a subprotocol offer, or `undefined` when none rides in it. More than one is refused as
|
|
102
|
+
* none: an offer is one client's, and two tickets in it is not something a client means.
|
|
103
|
+
*/
|
|
104
|
+
function ticketFromOffer(offered) {
|
|
105
|
+
const tickets = (offered ?? "").split(",").map((protocol) => protocol.trim()).filter((protocol) => protocol.startsWith(PRESENCE_TICKET_PROTOCOL_PREFIX)).map((protocol) => protocol.slice(13));
|
|
106
|
+
return tickets.length === 1 ? tickets[0] : void 0;
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { PRESENCE_TICKET_PROTOCOL_PREFIX, PresenceTicketClaims, mintPresenceTicket, ticketFromOffer, verifyPresenceTicket };
|
package/dist/privacy/classify.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import { SQLiteBoolean, SQLiteDate, isSqliteJson } from "../data/codecs.js";
|
|
4
3
|
import { ownPrivacyClasses, privacyClasses, rankPrivacyClasses, strongestPrivacyClass } from "./classes.js";
|
|
4
|
+
import { SQLiteBoolean, SQLiteDate, isSqliteJson } from "../data/codecs.js";
|
|
5
5
|
//#region src/privacy/classify.ts
|
|
6
6
|
/**
|
|
7
7
|
* **The one recursion limit**, shared by `redact`, `classifyPrivacy` and `Redacted<T>` (`./types` reads this constant's
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { ValidationError } from "../error/pithyError.js";
|
|
4
|
+
import { PRIVACY_META_KEY, rankPrivacyClasses } from "./classes.js";
|
|
4
5
|
import { EncryptedEnvelope } from "../crypto/envelope.js";
|
|
5
6
|
import { isSqliteJson, sqliteJson } from "../data/codecs.js";
|
|
6
|
-
import { PRIVACY_META_KEY, rankPrivacyClasses } from "./classes.js";
|
|
7
7
|
import { classifyPrivacy } from "./classify.js";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { CamelCasePlugin } from "kysely";
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { type BasisSource, LegalBasis } from "./vocabulary";
|
|
5
|
+
/**
|
|
6
|
+
* Processing activities: what a Worker does with personal data, why, and on what legal basis — the declarations the
|
|
7
|
+
* GDPR Article 30 record is derived from.
|
|
8
|
+
*
|
|
9
|
+
* ## Declared where the processing is
|
|
10
|
+
*
|
|
11
|
+
* A capability states the activities it performs, under its own `name` (`auth.account`), the rule consent purposes,
|
|
12
|
+
* tables and error codes follow. The adopter adds their own in the `processing` block of a Worker's
|
|
13
|
+
* `pithy.config.ts`, and may replace a capability's legal basis there.
|
|
14
|
+
*
|
|
15
|
+
* ## A missing basis is a gap, never a guess
|
|
16
|
+
*
|
|
17
|
+
* `legalBasis` is optional. An activity without one is reported as a gap in the record, and nothing fills it in.
|
|
18
|
+
*/
|
|
19
|
+
/** An activity id: `<domain>.<name>`, lowercase, the domain being the declaring capability's name. */
|
|
20
|
+
export declare const ProcessingActivityId: z.ZodString;
|
|
21
|
+
export type ProcessingActivityId = z.infer<typeof ProcessingActivityId>;
|
|
22
|
+
/** Someone outside the Worker's own code who receives personal data for an activity. */
|
|
23
|
+
export declare const ProcessingRecipient: z.ZodObject<{
|
|
24
|
+
name: z.ZodString;
|
|
25
|
+
receives: z.ZodString;
|
|
26
|
+
classes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
export type ProcessingRecipient = z.output<typeof ProcessingRecipient>;
|
|
29
|
+
export type ProcessingRecipientInput = z.input<typeof ProcessingRecipient>;
|
|
30
|
+
/** One processing activity, as a capability or the adopter declares it. */
|
|
31
|
+
export declare const ProcessingActivity: z.ZodObject<{
|
|
32
|
+
id: z.ZodString;
|
|
33
|
+
purpose: z.ZodString;
|
|
34
|
+
classes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
35
|
+
legalBasis: z.ZodOptional<z.ZodEnum<{
|
|
36
|
+
consent: "consent";
|
|
37
|
+
contract: "contract";
|
|
38
|
+
"legal-obligation": "legal-obligation";
|
|
39
|
+
"legitimate-interests": "legitimate-interests";
|
|
40
|
+
"public-task": "public-task";
|
|
41
|
+
"vital-interests": "vital-interests";
|
|
42
|
+
}>>;
|
|
43
|
+
consentPurpose: z.ZodOptional<z.ZodString>;
|
|
44
|
+
recipients: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
45
|
+
name: z.ZodString;
|
|
46
|
+
receives: z.ZodString;
|
|
47
|
+
classes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
48
|
+
}, z.core.$strip>>>;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
export type ProcessingActivity = z.output<typeof ProcessingActivity>;
|
|
51
|
+
export type ProcessingActivityInput = z.input<typeof ProcessingActivity>;
|
|
52
|
+
/** The adopter's replacement for a capability's legal basis. */
|
|
53
|
+
export declare const ProcessingOverride: z.ZodObject<{
|
|
54
|
+
legalBasis: z.ZodEnum<{
|
|
55
|
+
consent: "consent";
|
|
56
|
+
contract: "contract";
|
|
57
|
+
"legal-obligation": "legal-obligation";
|
|
58
|
+
"legitimate-interests": "legitimate-interests";
|
|
59
|
+
"public-task": "public-task";
|
|
60
|
+
"vital-interests": "vital-interests";
|
|
61
|
+
}>;
|
|
62
|
+
consentPurpose: z.ZodOptional<z.ZodString>;
|
|
63
|
+
}, z.core.$strict>;
|
|
64
|
+
export type ProcessingOverride = z.infer<typeof ProcessingOverride>;
|
|
65
|
+
/** The `processing` block, as a Worker's `pithy.config.ts` writes it. */
|
|
66
|
+
export declare const ProcessingPolicy: z.ZodObject<{
|
|
67
|
+
activities: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
68
|
+
id: z.ZodString;
|
|
69
|
+
purpose: z.ZodString;
|
|
70
|
+
classes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
71
|
+
legalBasis: z.ZodOptional<z.ZodEnum<{
|
|
72
|
+
consent: "consent";
|
|
73
|
+
contract: "contract";
|
|
74
|
+
"legal-obligation": "legal-obligation";
|
|
75
|
+
"legitimate-interests": "legitimate-interests";
|
|
76
|
+
"public-task": "public-task";
|
|
77
|
+
"vital-interests": "vital-interests";
|
|
78
|
+
}>>;
|
|
79
|
+
consentPurpose: z.ZodOptional<z.ZodString>;
|
|
80
|
+
recipients: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
81
|
+
name: z.ZodString;
|
|
82
|
+
receives: z.ZodString;
|
|
83
|
+
classes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
84
|
+
}, z.core.$strip>>>;
|
|
85
|
+
}, z.core.$strip>>>;
|
|
86
|
+
overrides: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
87
|
+
legalBasis: z.ZodEnum<{
|
|
88
|
+
consent: "consent";
|
|
89
|
+
contract: "contract";
|
|
90
|
+
"legal-obligation": "legal-obligation";
|
|
91
|
+
"legitimate-interests": "legitimate-interests";
|
|
92
|
+
"public-task": "public-task";
|
|
93
|
+
"vital-interests": "vital-interests";
|
|
94
|
+
}>;
|
|
95
|
+
consentPurpose: z.ZodOptional<z.ZodString>;
|
|
96
|
+
}, z.core.$strict>>>;
|
|
97
|
+
}, z.core.$strict>;
|
|
98
|
+
export type ProcessingPolicy = z.output<typeof ProcessingPolicy>;
|
|
99
|
+
export type ProcessingPolicyInput = z.input<typeof ProcessingPolicy>;
|
|
100
|
+
/** The domain half of an activity id: `auth` for `auth.account`. */
|
|
101
|
+
export declare function activityDomain(id: string): string;
|
|
102
|
+
/**
|
|
103
|
+
* Parse one declarer's activities and refuse what the record could not attribute: an invalid activity, one under a
|
|
104
|
+
* domain that is not the declarer's, or two with one id. `domain` is the declaring capability's name, or `null` for
|
|
105
|
+
* the adopter's own, which may use any domain but a reserved one.
|
|
106
|
+
*/
|
|
107
|
+
export declare function parseProcessingActivities(declarer: string, activities: readonly ProcessingActivityInput[], domain: string | null, reserved?: ReadonlySet<string>): ProcessingActivity[];
|
|
108
|
+
/** Parse a `processing` block, or refuse it naming what is wrong. `undefined` is the empty policy. */
|
|
109
|
+
export declare function parseProcessingPolicy(input: unknown): ProcessingPolicy;
|
|
110
|
+
/** One activity as it takes effect: who declared it, its basis after any override, and where that came from. */
|
|
111
|
+
export interface ResolvedActivity {
|
|
112
|
+
readonly id: string;
|
|
113
|
+
readonly purpose: string;
|
|
114
|
+
readonly classes: readonly string[];
|
|
115
|
+
readonly legalBasis: LegalBasis | null;
|
|
116
|
+
readonly basisSource: BasisSource | null;
|
|
117
|
+
readonly consentPurpose: string | null;
|
|
118
|
+
readonly recipients: readonly ProcessingRecipient[];
|
|
119
|
+
/** The capability that declared it, or `config` for the adopter's own. */
|
|
120
|
+
readonly declaredBy: string;
|
|
121
|
+
}
|
|
122
|
+
/** What resolving reads off a composed capability. */
|
|
123
|
+
interface ActivityDeclarer {
|
|
124
|
+
readonly name: string;
|
|
125
|
+
readonly processing?: readonly ProcessingActivity[];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Every activity as it takes effect over a composition: each capability's, in composition order, then the adopter's,
|
|
129
|
+
* with the block's overrides applied. Refuses an adopter activity under a composed capability's domain, an id declared
|
|
130
|
+
* twice, and an override naming an activity no composed capability declares. Called when a Worker assembles.
|
|
131
|
+
*/
|
|
132
|
+
export declare function resolveProcessing(policy: ProcessingPolicy, capabilities: readonly ActivityDeclarer[]): ResolvedActivity[];
|
|
133
|
+
export {};
|
|
134
|
+
//# sourceMappingURL=activity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/processing/activity.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAE,KAAK,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D;;;;;;;;;;;;;GAaG;AAEH,sGAAsG;AACtG,eAAO,MAAM,oBAAoB,aAS9B,CAAC;AACJ,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,wFAAwF;AACxF,eAAO,MAAM,mBAAmB;;;;iBAiBkE,CAAC;AACnG,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACvE,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAiB3E,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;iBA8BmF,CAAC;AACnH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;AACrE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEzE,gEAAgE;AAChE,eAAO,MAAM,kBAAkB;;;;;;;;;;kBAS4E,CAAC;AAC5G,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,yEAAyE;AACzE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAgByF,CAAC;AACvH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AACjE,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAErE,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,SAAS,uBAAuB,EAAE,EAC9C,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,kBAAkB,EAAE,CAkCtB;AAED,sGAAsG;AACtG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAUtE;AAED,gHAAgH;AAChH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACpD,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,sDAAsD;AACtD,UAAU,gBAAgB;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACrD;AAgBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,gBAAgB,EACxB,YAAY,EAAE,SAAS,gBAAgB,EAAE,GACxC,gBAAgB,EAAE,CAuCpB"}
|