@pithy-sh/core 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/audit/auditEvent.js +1 -1
- package/dist/audit/chain.d.ts +289 -0
- package/dist/audit/chain.d.ts.map +1 -0
- package/dist/audit/chain.js +400 -0
- package/dist/audit/citations.d.ts +52 -0
- package/dist/audit/citations.d.ts.map +1 -0
- package/dist/audit/citations.js +13 -0
- package/dist/audit/composed.d.ts +18 -0
- package/dist/audit/composed.d.ts.map +1 -0
- package/dist/audit/composed.js +28 -0
- package/dist/capability/bindings.d.ts +22 -0
- package/dist/capability/bindings.d.ts.map +1 -1
- package/dist/capability/bindings.js +32 -2
- package/dist/capability/capability.d.ts +133 -15
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/capability.js +17 -2
- package/dist/capability/erasure.d.ts +75 -0
- package/dist/capability/erasure.d.ts.map +1 -0
- package/dist/capability/erasure.js +2 -0
- package/dist/capability/manifest.d.ts +5 -0
- package/dist/capability/manifest.d.ts.map +1 -1
- package/dist/capability/personColumns.d.ts +28 -0
- package/dist/capability/personColumns.d.ts.map +1 -0
- package/dist/capability/personColumns.js +66 -0
- package/dist/capability/personRows.d.ts +62 -0
- package/dist/capability/personRows.d.ts.map +1 -0
- package/dist/capability/personRows.js +59 -0
- package/dist/capability/purge.d.ts +113 -4
- package/dist/capability/purge.d.ts.map +1 -1
- package/dist/capability/purge.js +138 -5
- package/dist/capability/settings.d.ts +10 -0
- package/dist/capability/settings.d.ts.map +1 -1
- package/dist/capability/vectorIndex.d.ts +36 -0
- package/dist/capability/vectorIndex.d.ts.map +1 -0
- package/dist/capability/vectorIndex.js +25 -0
- package/dist/cli/commandCatalog.generated.d.ts +434 -4
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
- package/dist/cli/commandCatalog.generated.js +455 -2
- package/dist/cli/commandFacts.d.ts +8 -5
- package/dist/cli/commandFacts.d.ts.map +1 -1
- package/dist/cli/commandFacts.js +12 -3
- package/dist/consent/purpose.d.ts +70 -0
- package/dist/consent/purpose.d.ts.map +1 -0
- package/dist/consent/purpose.js +97 -0
- package/dist/consent/reader.d.ts +99 -0
- package/dist/consent/reader.d.ts.map +1 -0
- package/dist/consent/reader.js +33 -0
- package/dist/controlPlane/audit/actions.d.ts +15 -4
- package/dist/controlPlane/audit/actions.d.ts.map +1 -1
- package/dist/controlPlane/audit/actions.js +16 -5
- package/dist/controlPlane/capability.d.ts +10 -0
- package/dist/controlPlane/capability.d.ts.map +1 -1
- package/dist/controlPlane/capability.js +56 -8
- package/dist/controlPlane/context.d.ts +4 -0
- package/dist/controlPlane/context.d.ts.map +1 -1
- package/dist/controlPlane/context.js +3 -2
- package/dist/controlPlane/data/connection.d.ts +21 -2
- package/dist/controlPlane/data/connection.d.ts.map +1 -1
- package/dist/controlPlane/data/connection.js +13 -4
- package/dist/controlPlane/data/connections.d.ts +94 -0
- package/dist/controlPlane/data/connections.d.ts.map +1 -0
- package/dist/controlPlane/data/connections.js +151 -0
- package/dist/controlPlane/data/hold.d.ts +46 -0
- package/dist/controlPlane/data/hold.d.ts.map +1 -0
- package/dist/controlPlane/data/hold.js +36 -0
- package/dist/controlPlane/data/oidcTrust.d.ts +122 -0
- package/dist/controlPlane/data/oidcTrust.d.ts.map +1 -0
- package/dist/controlPlane/data/oidcTrust.js +173 -0
- package/dist/controlPlane/data/tables.d.ts +4 -0
- package/dist/controlPlane/data/tables.d.ts.map +1 -1
- package/dist/controlPlane/data/tables.js +9 -4
- package/dist/controlPlane/data/trustFirstCall.d.ts +16 -0
- package/dist/controlPlane/data/trustFirstCall.d.ts.map +1 -0
- package/dist/controlPlane/data/trustFirstCall.js +25 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts +165 -2
- package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -1
- package/dist/controlPlane/discovery/adminRoute.js +32 -8
- package/dist/controlPlane/discovery/routeInput.d.ts +23 -0
- package/dist/controlPlane/discovery/routeInput.d.ts.map +1 -0
- package/dist/controlPlane/discovery/routeInput.js +80 -0
- package/dist/controlPlane/discovery/toolName.d.ts +21 -0
- package/dist/controlPlane/discovery/toolName.d.ts.map +1 -0
- package/dist/controlPlane/discovery/toolName.js +25 -0
- package/dist/controlPlane/erasure/attest.d.ts +78 -0
- package/dist/controlPlane/erasure/attest.d.ts.map +1 -0
- package/dist/controlPlane/erasure/attest.js +164 -0
- package/dist/controlPlane/erasure/read.d.ts +39 -0
- package/dist/controlPlane/erasure/read.d.ts.map +1 -0
- package/dist/controlPlane/erasure/read.js +140 -0
- package/dist/controlPlane/error/errors.d.ts +61 -2
- package/dist/controlPlane/error/errors.d.ts.map +1 -1
- package/dist/controlPlane/error/errors.js +13 -0
- package/dist/controlPlane/evidence/derive.d.ts +124 -0
- package/dist/controlPlane/evidence/derive.d.ts.map +1 -0
- package/dist/controlPlane/evidence/derive.js +343 -0
- package/dist/controlPlane/evidence/mapping.d.ts +85 -0
- package/dist/controlPlane/evidence/mapping.d.ts.map +1 -0
- package/dist/controlPlane/evidence/mapping.js +114 -0
- package/dist/controlPlane/evidence/pack.d.ts +46 -0
- package/dist/controlPlane/evidence/pack.d.ts.map +1 -0
- package/dist/controlPlane/evidence/pack.js +124 -0
- package/dist/controlPlane/http/connections.d.ts +96 -0
- package/dist/controlPlane/http/connections.d.ts.map +1 -0
- package/dist/controlPlane/http/connections.js +109 -0
- package/dist/controlPlane/http/guard.d.ts +23 -2
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +103 -7
- package/dist/controlPlane/http/handlers.d.ts +212 -10
- package/dist/controlPlane/http/handlers.d.ts.map +1 -1
- package/dist/controlPlane/http/handlers.js +117 -26
- package/dist/controlPlane/http/holds.d.ts +57 -0
- package/dist/controlPlane/http/holds.d.ts.map +1 -0
- package/dist/controlPlane/http/holds.js +98 -0
- package/dist/controlPlane/http/presence.d.ts +27 -0
- package/dist/controlPlane/http/presence.d.ts.map +1 -0
- package/dist/controlPlane/http/presence.js +67 -0
- package/dist/controlPlane/http/processing.d.ts +73 -0
- package/dist/controlPlane/http/processing.d.ts.map +1 -0
- package/dist/controlPlane/http/processing.js +17 -0
- package/dist/controlPlane/http/responses.d.ts +927 -3
- package/dist/controlPlane/http/responses.d.ts.map +1 -1
- package/dist/controlPlane/http/responses.js +283 -7
- package/dist/controlPlane/http/routes.d.ts +12 -2
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +182 -5
- package/dist/controlPlane/http/schemas.d.ts +63 -0
- package/dist/controlPlane/http/schemas.d.ts.map +1 -1
- package/dist/controlPlane/http/schemas.js +39 -1
- package/dist/controlPlane/http/verify.d.ts.map +1 -1
- package/dist/controlPlane/http/verify.js +13 -1
- package/dist/controlPlane/http/verifyTrust.d.ts +76 -0
- package/dist/controlPlane/http/verifyTrust.d.ts.map +1 -0
- package/dist/controlPlane/http/verifyTrust.js +75 -0
- package/dist/controlPlane/migrations/0004_connection_trusts.d.ts +15 -0
- package/dist/controlPlane/migrations/0004_connection_trusts.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0004_connection_trusts.js +23 -0
- package/dist/controlPlane/migrations/0005_holds.d.ts +12 -0
- package/dist/controlPlane/migrations/0005_holds.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0005_holds.js +26 -0
- package/dist/controlPlane/processing/derive.d.ts +77 -0
- package/dist/controlPlane/processing/derive.d.ts.map +1 -0
- package/dist/controlPlane/processing/derive.js +206 -0
- package/dist/controlPlane/processing/read.d.ts +10 -0
- package/dist/controlPlane/processing/read.d.ts.map +1 -0
- package/dist/controlPlane/processing/read.js +89 -0
- package/dist/controlPlane/scope/kind.d.ts +46 -0
- package/dist/controlPlane/scope/kind.d.ts.map +1 -0
- package/dist/controlPlane/scope/kind.js +52 -0
- package/dist/controlPlane/scope/scope.d.ts +58 -3
- package/dist/controlPlane/scope/scope.d.ts.map +1 -1
- package/dist/controlPlane/scope/scope.js +59 -4
- package/dist/controlPlane/token/jws.js +1 -1
- package/dist/controlPlane/token/mint.js +1 -1
- package/dist/controlPlane/wire.d.ts +23 -0
- package/dist/controlPlane/wire.d.ts.map +1 -1
- package/dist/controlPlane/wire.js +24 -1
- package/dist/createBackend.d.ts +13 -0
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +11 -3
- package/dist/createEntrypoint.d.ts +14 -1
- package/dist/createEntrypoint.d.ts.map +1 -1
- package/dist/createEntrypoint.js +19 -5
- package/dist/error/http.d.ts +216 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +432 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +144 -0
- package/dist/error/pithyError.d.ts +22 -0
- package/dist/error/pithyError.d.ts.map +1 -1
- package/dist/error/pithyError.js +31 -1
- package/dist/http/routeSpec.d.ts +46 -1
- package/dist/http/routeSpec.d.ts.map +1 -1
- package/dist/http/routeSpec.js +39 -5
- package/dist/http/validation.d.ts +11 -2
- package/dist/http/validation.d.ts.map +1 -1
- package/dist/http/validation.js +20 -3
- package/dist/http/verification.d.ts.map +1 -1
- package/dist/http/verification.js +1 -1
- package/dist/i18n/translator.d.ts +2 -1
- package/dist/i18n/translator.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/logger/record.d.ts +126 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/mcp/annotations.d.ts +20 -0
- package/dist/mcp/annotations.d.ts.map +1 -0
- package/dist/mcp/annotations.js +23 -0
- package/dist/mcp/tools.d.ts +75 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +118 -0
- package/dist/openapi/document.d.ts.map +1 -1
- package/dist/openapi/document.js +8 -26
- package/dist/openapi/operations.d.ts +35 -0
- package/dist/openapi/operations.d.ts.map +1 -0
- package/dist/openapi/operations.js +37 -0
- package/dist/presence/channel.d.ts +11 -0
- package/dist/presence/channel.d.ts.map +1 -0
- package/dist/presence/channel.js +65 -0
- package/dist/presence/declaration.d.ts +154 -0
- package/dist/presence/declaration.d.ts.map +1 -0
- package/dist/presence/declaration.js +164 -0
- package/dist/presence/durableObject.d.ts +61 -0
- package/dist/presence/durableObject.d.ts.map +1 -0
- package/dist/presence/durableObject.js +257 -0
- package/dist/presence/frames.d.ts +166 -0
- package/dist/presence/frames.d.ts.map +1 -0
- package/dist/presence/frames.js +90 -0
- package/dist/presence/http/guard.d.ts +11 -0
- package/dist/presence/http/guard.d.ts.map +1 -0
- package/dist/presence/http/guard.js +20 -0
- package/dist/presence/http/responses.d.ts +31 -0
- package/dist/presence/http/responses.d.ts.map +1 -0
- package/dist/presence/http/responses.js +34 -0
- package/dist/presence/http/routes.d.ts +24 -0
- package/dist/presence/http/routes.d.ts.map +1 -0
- package/dist/presence/http/routes.js +127 -0
- package/dist/presence/http/schemas.d.ts +29 -0
- package/dist/presence/http/schemas.d.ts.map +1 -0
- package/dist/presence/http/schemas.js +28 -0
- package/dist/presence/names.d.ts +23 -0
- package/dist/presence/names.d.ts.map +1 -0
- package/dist/presence/names.js +22 -0
- package/dist/presence/room.d.ts +95 -0
- package/dist/presence/room.d.ts.map +1 -0
- package/dist/presence/room.js +105 -0
- package/dist/presence/ticket.d.ts +55 -0
- package/dist/presence/ticket.d.ts.map +1 -0
- package/dist/presence/ticket.js +109 -0
- package/dist/privacy/classify.js +1 -1
- package/dist/privacy/encrypted.js +1 -1
- package/dist/processing/activity.d.ts +134 -0
- package/dist/processing/activity.d.ts.map +1 -0
- package/dist/processing/activity.js +161 -0
- package/dist/processing/composed.d.ts +18 -0
- package/dist/processing/composed.d.ts.map +1 -0
- package/dist/processing/composed.js +28 -0
- package/dist/processing/vocabulary.d.ts +26 -0
- package/dist/processing/vocabulary.d.ts.map +1 -0
- package/dist/processing/vocabulary.js +35 -0
- package/dist/queue/consumer.d.ts +59 -0
- package/dist/queue/consumer.d.ts.map +1 -0
- package/dist/queue/consumer.js +46 -0
- package/dist/retention/composed.d.ts +37 -0
- package/dist/retention/composed.d.ts.map +1 -0
- package/dist/retention/composed.js +66 -0
- package/dist/retention/holds.d.ts +61 -0
- package/dist/retention/holds.d.ts.map +1 -0
- package/dist/retention/holds.js +107 -0
- package/dist/retention/holdsPurge.d.ts +22 -0
- package/dist/retention/holdsPurge.d.ts.map +1 -0
- package/dist/retention/holdsPurge.js +50 -0
- package/dist/retention/policy.d.ts +132 -0
- package/dist/retention/policy.d.ts.map +1 -0
- package/dist/retention/policy.js +197 -0
- package/dist/session/rotated.d.ts +34 -0
- package/dist/session/rotated.d.ts.map +1 -0
- package/dist/session/rotated.js +25 -0
- package/dist/test-utils/connectionFixtures.d.ts +13 -0
- package/dist/test-utils/connectionFixtures.d.ts.map +1 -0
- package/dist/test-utils/connectionFixtures.js +58 -0
- package/dist/test-utils/interleave.d.ts +12 -0
- package/dist/test-utils/interleave.d.ts.map +1 -0
- package/dist/test-utils/interleave.js +26 -0
- package/dist/testWorker.d.ts +8 -0
- package/dist/testWorker.d.ts.map +1 -0
- package/dist/testWorker.js +9 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workflow/dispatchRoute.js +1 -1
- package/package.json +1 -1
- package/src/audit/auditEvent.ts +1 -1
- package/src/audit/chain.ts +551 -0
- package/src/audit/citations.ts +63 -0
- package/src/audit/composed.ts +30 -0
- package/src/capability/bindings.ts +61 -0
- package/src/capability/capability.ts +166 -18
- package/src/capability/erasure.ts +82 -0
- package/src/capability/personColumns.ts +83 -0
- package/src/capability/personRows.ts +125 -0
- package/src/capability/purge.ts +208 -8
- package/src/capability/settings.ts +7 -0
- package/src/capability/vectorIndex.ts +60 -0
- package/src/cli/commandCatalog.generated.ts +488 -2
- package/src/cli/commandFacts.ts +12 -3
- package/src/cloudflare-test.d.ts +3 -1
- package/src/consent/purpose.ts +160 -0
- package/src/consent/reader.ts +134 -0
- package/src/controlPlane/audit/actions.ts +15 -4
- package/src/controlPlane/capability.ts +99 -20
- package/src/controlPlane/context.ts +10 -2
- package/src/controlPlane/data/connection.ts +16 -2
- package/src/controlPlane/data/connections.ts +231 -0
- package/src/controlPlane/data/hold.ts +52 -0
- package/src/controlPlane/data/oidcTrust.ts +249 -0
- package/src/controlPlane/data/tables.ts +7 -0
- package/src/controlPlane/data/trustFirstCall.ts +37 -0
- package/src/controlPlane/discovery/adminRoute.ts +71 -9
- package/src/controlPlane/discovery/routeInput.ts +86 -0
- package/src/controlPlane/discovery/toolName.ts +23 -0
- package/src/controlPlane/erasure/attest.ts +205 -0
- package/src/controlPlane/erasure/read.ts +169 -0
- package/src/controlPlane/error/errors.ts +69 -2
- package/src/controlPlane/evidence/derive.ts +426 -0
- package/src/controlPlane/evidence/mapping.ts +118 -0
- package/src/controlPlane/evidence/pack.ts +190 -0
- package/src/controlPlane/http/connections.ts +129 -0
- package/src/controlPlane/http/guard.ts +178 -10
- package/src/controlPlane/http/handlers.ts +172 -40
- package/src/controlPlane/http/holds.ts +108 -0
- package/src/controlPlane/http/presence.ts +71 -0
- package/src/controlPlane/http/processing.ts +19 -0
- package/src/controlPlane/http/responses.ts +590 -7
- package/src/controlPlane/http/routes.ts +302 -3
- package/src/controlPlane/http/schemas.ts +126 -0
- package/src/controlPlane/http/verify.ts +19 -2
- package/src/controlPlane/http/verifyTrust.ts +176 -0
- package/src/controlPlane/migrations/0004_connection_trusts.ts +30 -0
- package/src/controlPlane/migrations/0005_holds.ts +40 -0
- package/src/controlPlane/processing/derive.ts +359 -0
- package/src/controlPlane/processing/read.ts +102 -0
- package/src/controlPlane/scope/kind.ts +63 -0
- package/src/controlPlane/scope/scope.ts +65 -3
- package/src/controlPlane/wire.ts +26 -0
- package/src/createBackend.ts +31 -6
- package/src/createEntrypoint.ts +34 -2
- package/src/error/payload.ts +302 -0
- package/src/error/pithyError.ts +42 -0
- package/src/http/routeSpec.ts +75 -5
- package/src/http/validation.ts +35 -3
- package/src/http/verification.ts +3 -1
- package/src/i18n/translator.ts +2 -1
- package/src/index.ts +2 -0
- package/src/mcp/annotations.ts +38 -0
- package/src/mcp/tools.ts +220 -0
- package/src/openapi/document.ts +8 -28
- package/src/openapi/operations.ts +62 -0
- package/src/presence/channel.ts +46 -0
- package/src/presence/declaration.ts +283 -0
- package/src/presence/durableObject.ts +287 -0
- package/src/presence/frames.ts +153 -0
- package/src/presence/http/guard.ts +23 -0
- package/src/presence/http/responses.ts +63 -0
- package/src/presence/http/routes.ts +165 -0
- package/src/presence/http/schemas.ts +39 -0
- package/src/presence/names.ts +45 -0
- package/src/presence/room.ts +145 -0
- package/src/presence/ticket.ts +145 -0
- package/src/processing/activity.ts +289 -0
- package/src/processing/composed.ts +33 -0
- package/src/processing/vocabulary.ts +43 -0
- package/src/queue/consumer.ts +104 -0
- package/src/retention/composed.ts +75 -0
- package/src/retention/holds.ts +175 -0
- package/src/retention/holdsPurge.ts +50 -0
- package/src/retention/policy.ts +305 -0
- package/src/session/rotated.ts +53 -0
- package/src/test-utils/connectionFixtures.ts +58 -0
- package/src/test-utils/interleave.ts +31 -0
- package/src/testWorker.ts +13 -0
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* An OIDC trust: the second credential kind a connection may hold, beside a registered key
|
|
6
|
+
* (`docs/AGENT-ACCESS.md` §6.2, #722).
|
|
7
|
+
*
|
|
8
|
+
* A key is a bearer: one client holds the private half. A trust is a **policy**: a trusted issuer, the
|
|
9
|
+
* audience its tokens must name, and a matcher over the claims that say which workload a token was minted
|
|
10
|
+
* for. Nobody holds a secret. The workload asks its own identity provider for a short-lived token, and the
|
|
11
|
+
* Worker checks it against the issuer's published keys with the verifier the webhooks already use,
|
|
12
|
+
* `core/src/http/oidcWebhook.ts`.
|
|
13
|
+
*
|
|
14
|
+
* ## The claims become data here, and that is a deliberate departure
|
|
15
|
+
*
|
|
16
|
+
* In a webhook the claims check is a predicate the **route** holds, because whoever wrote the route knows
|
|
17
|
+
* the legitimate sender. A connection's trust is per adopter, decided at registration by the same person who
|
|
18
|
+
* grants the scopes, so the matcher is stored with the trust and validated on the way in. That is the whole
|
|
19
|
+
* reason {@link OidcClaimMatch} exists rather than a function.
|
|
20
|
+
*
|
|
21
|
+
* ## The claims check is the boundary, not the signature
|
|
22
|
+
*
|
|
23
|
+
* An identity provider signs for everybody with the same keys. GitHub signs a token for every repository on
|
|
24
|
+
* the planet. A trust whose matcher constrains nothing accepts all of them, so {@link OidcClaimMatch} refuses
|
|
25
|
+
* one at registration — structurally, not as a warning — and refuses the claims that name the token rather
|
|
26
|
+
* than the workload ({@link RESERVED_OIDC_CLAIMS}), since a matcher made of those constrains nothing either.
|
|
27
|
+
*
|
|
28
|
+
* ## Named claims, not the `sub` string
|
|
29
|
+
*
|
|
30
|
+
* A `sub` is a concatenation whose composition belongs to the provider and to the adopter's own template.
|
|
31
|
+
* The claims beneath it — GitHub's `repository_id`, `environment`, `ref`; GitLab's `project_id` — are
|
|
32
|
+
* published individually and are stable. So the matcher is a map of claim names to values, and `sub` is one
|
|
33
|
+
* claim among the others rather than a special case.
|
|
34
|
+
*
|
|
35
|
+
* ## No rotation, and revocation changes shape
|
|
36
|
+
*
|
|
37
|
+
* There is no key to append, prove and expire. A token cannot be revoked; the trust is, and every token
|
|
38
|
+
* already minted stays live until it expires — {@link TRUST_REVOCATION_LATENCY} is the sentence that says
|
|
39
|
+
* so at the moment it matters.
|
|
40
|
+
*
|
|
41
|
+
* Nothing in this module reads a clock, a binding or D1. `now` is injected wherever time matters.
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* Claims that name the token rather than the workload: who minted it, for whom, and when. A matcher built
|
|
45
|
+
* from these alone would accept every workload the issuer serves, so none of them may be registered.
|
|
46
|
+
* `iss` and `aud` are checked anyway, by the trust's own issuer and audience.
|
|
47
|
+
*/
|
|
48
|
+
export declare const RESERVED_OIDC_CLAIMS: readonly ["iss", "aud", "exp", "nbf", "iat", "jti"];
|
|
49
|
+
/** The most claims one trust may match on. Far above any real constraint; a bound on a stored row. */
|
|
50
|
+
export declare const MAX_TRUST_CLAIMS = 16;
|
|
51
|
+
/** The most claims a refusal records, so a token carrying a hundred cannot fill the audit trail. */
|
|
52
|
+
export declare const MAX_OBSERVED_CLAIMS = 32;
|
|
53
|
+
/** The longest claim value a matcher stores or a refusal records. */
|
|
54
|
+
export declare const MAX_CLAIM_VALUE = 256;
|
|
55
|
+
/**
|
|
56
|
+
* One claim name in a matcher. A name, not a path: nested claims are not matched, because a matcher that
|
|
57
|
+
* walks a structure is a matcher with more ways to be wrong on the check the whole trust rests on.
|
|
58
|
+
*/
|
|
59
|
+
export declare const OidcClaimName: z.ZodString;
|
|
60
|
+
export type OidcClaimName = z.infer<typeof OidcClaimName>;
|
|
61
|
+
/** One claim value in a matcher: compared as a string against the token's claim of the same name. */
|
|
62
|
+
export declare const OidcClaimValue: z.ZodString;
|
|
63
|
+
export type OidcClaimValue = z.infer<typeof OidcClaimValue>;
|
|
64
|
+
/**
|
|
65
|
+
* Which workloads a trust accepts: every named claim must equal its value.
|
|
66
|
+
*
|
|
67
|
+
* **At least one, and that is the refusal the whole credential kind rests on.** An issuer with no claim
|
|
68
|
+
* constraint accepts every workload it serves.
|
|
69
|
+
*/
|
|
70
|
+
export declare const OidcClaimMatch: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
71
|
+
export type OidcClaimMatch = z.infer<typeof OidcClaimMatch>;
|
|
72
|
+
/**
|
|
73
|
+
* One registered OIDC trust, an element of the connection's `trusts` column.
|
|
74
|
+
*
|
|
75
|
+
* Stored beside `keys` rather than inside it: the two kinds share nothing but the connection they grant
|
|
76
|
+
* through, and a union array would make every key reader learn about trusts.
|
|
77
|
+
*/
|
|
78
|
+
export declare const OidcTrust: z.ZodObject<{
|
|
79
|
+
trustId: z.ZodString;
|
|
80
|
+
issuer: z.ZodURL;
|
|
81
|
+
jwksUrl: z.ZodURL;
|
|
82
|
+
audience: z.ZodString;
|
|
83
|
+
claims: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
84
|
+
authorizedBy: z.ZodEmail;
|
|
85
|
+
validFrom: z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>;
|
|
86
|
+
revokedAt: z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>>;
|
|
87
|
+
firstCallAt: z.ZodNullable<z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>>;
|
|
88
|
+
}, z.core.$strip>;
|
|
89
|
+
export type OidcTrust = z.output<typeof OidcTrust>;
|
|
90
|
+
/**
|
|
91
|
+
* What a caller should expect after a trust is revoked, said at the moment it is revoked rather than
|
|
92
|
+
* discovered afterwards.
|
|
93
|
+
*/
|
|
94
|
+
export declare const TRUST_REVOCATION_LATENCY = "A token cannot be revoked, only the trust. Every call naming this trust is refused from now on, but a token already minted stays in its holder's hands until it expires \u2014 the issuer's own lifetime, typically minutes.";
|
|
95
|
+
/** Every trust that may verify a call at `now`: not revoked, and started. */
|
|
96
|
+
export declare function activeTrusts(trusts: readonly OidcTrust[], now: Date): OidcTrust[];
|
|
97
|
+
/** The trust a call names, if it is live at `now`, or null. The revocation check lives here so no caller forgets it. */
|
|
98
|
+
export declare function findVerifyingTrust(trusts: readonly OidcTrust[], trustId: string, now: Date): OidcTrust | null;
|
|
99
|
+
/** Register a trust beside the others. A duplicate id is refused: the id is what a call addresses. */
|
|
100
|
+
export declare function appendTrust(trusts: readonly OidcTrust[], trust: OidcTrust): OidcTrust[];
|
|
101
|
+
/**
|
|
102
|
+
* Revoke one trust at `now`, or answer null when no trust answers to that id. A trust already revoked keeps
|
|
103
|
+
* the instant it was first revoked: the window a call was refused under must not move.
|
|
104
|
+
*/
|
|
105
|
+
export declare function revokeTrust(trusts: readonly OidcTrust[], trustId: string, now: Date): OidcTrust[] | null;
|
|
106
|
+
/**
|
|
107
|
+
* Whether a token's claims satisfy a trust's matcher: every registered claim present and equal.
|
|
108
|
+
*
|
|
109
|
+
* An empty matcher matches nothing. Registration refuses one, and this refuses it again rather than letting
|
|
110
|
+
* a hand-edited row vacuously admit every workload.
|
|
111
|
+
*/
|
|
112
|
+
export declare function claimsMatch(match: Readonly<Record<string, string>>, claims: Readonly<Record<string, unknown>>): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* The claims a refused token carried, as a refusal records them: the workload's claims, as strings, capped.
|
|
115
|
+
*
|
|
116
|
+
* This is what makes a custom issuer workable and a profiled one self-correcting — register, let the
|
|
117
|
+
* workload run once, read what it actually sent. Only ever called once the signature, issuer and audience
|
|
118
|
+
* have verified, so the caller demonstrably holds a genuine token for this audience and naming its claims
|
|
119
|
+
* leaks nothing it does not already hold.
|
|
120
|
+
*/
|
|
121
|
+
export declare function observedClaims(claims: Readonly<Record<string, unknown>>): Record<string, string>;
|
|
122
|
+
//# sourceMappingURL=oidcTrust.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oidcTrust.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/oidcTrust.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,YAAI,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAU,CAAC;AAExF,sGAAsG;AACtG,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,qEAAqE;AACrE,eAAO,MAAM,eAAe,MAAM,CAAC;AAUnC;;;GAGG;AACH,eAAO,MAAM,aAAa,aAQvB,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,qGAAqG;AACrG,eAAO,MAAM,cAAc,aAOxB,CAAC;AACJ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D;;;;;GAKG;AACH,eAAO,MAAM,cAAc,uCAUxB,CAAC;AACJ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAK5D;;;;;GAKG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;iBAuCnB,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC;AAEnD;;;GAGG;AACH,eAAO,MAAM,wBAAwB,iOACsL,CAAC;AAE5N,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,GAAG,EAAE,IAAI,GAAG,SAAS,EAAE,CAEjF;AAED,wHAAwH;AACxH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,SAAS,GAAG,IAAI,CAE7G;AAED,sGAAsG;AACtG,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,GAAG,SAAS,EAAE,CASvF;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,SAAS,EAAE,GAAG,IAAI,CAKxG;AAUD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACvC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACxC,OAAO,CAIT;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUhG"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { ControlPlaneKeyConflictError } from "../error/errors.js";
|
|
4
|
+
import { JsonDate } from "../../data/codecs.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region src/controlPlane/data/oidcTrust.ts
|
|
7
|
+
/**
|
|
8
|
+
* An OIDC trust: the second credential kind a connection may hold, beside a registered key
|
|
9
|
+
* (`docs/AGENT-ACCESS.md` §6.2, #722).
|
|
10
|
+
*
|
|
11
|
+
* A key is a bearer: one client holds the private half. A trust is a **policy**: a trusted issuer, the
|
|
12
|
+
* audience its tokens must name, and a matcher over the claims that say which workload a token was minted
|
|
13
|
+
* for. Nobody holds a secret. The workload asks its own identity provider for a short-lived token, and the
|
|
14
|
+
* Worker checks it against the issuer's published keys with the verifier the webhooks already use,
|
|
15
|
+
* `core/src/http/oidcWebhook.ts`.
|
|
16
|
+
*
|
|
17
|
+
* ## The claims become data here, and that is a deliberate departure
|
|
18
|
+
*
|
|
19
|
+
* In a webhook the claims check is a predicate the **route** holds, because whoever wrote the route knows
|
|
20
|
+
* the legitimate sender. A connection's trust is per adopter, decided at registration by the same person who
|
|
21
|
+
* grants the scopes, so the matcher is stored with the trust and validated on the way in. That is the whole
|
|
22
|
+
* reason {@link OidcClaimMatch} exists rather than a function.
|
|
23
|
+
*
|
|
24
|
+
* ## The claims check is the boundary, not the signature
|
|
25
|
+
*
|
|
26
|
+
* An identity provider signs for everybody with the same keys. GitHub signs a token for every repository on
|
|
27
|
+
* the planet. A trust whose matcher constrains nothing accepts all of them, so {@link OidcClaimMatch} refuses
|
|
28
|
+
* one at registration — structurally, not as a warning — and refuses the claims that name the token rather
|
|
29
|
+
* than the workload ({@link RESERVED_OIDC_CLAIMS}), since a matcher made of those constrains nothing either.
|
|
30
|
+
*
|
|
31
|
+
* ## Named claims, not the `sub` string
|
|
32
|
+
*
|
|
33
|
+
* A `sub` is a concatenation whose composition belongs to the provider and to the adopter's own template.
|
|
34
|
+
* The claims beneath it — GitHub's `repository_id`, `environment`, `ref`; GitLab's `project_id` — are
|
|
35
|
+
* published individually and are stable. So the matcher is a map of claim names to values, and `sub` is one
|
|
36
|
+
* claim among the others rather than a special case.
|
|
37
|
+
*
|
|
38
|
+
* ## No rotation, and revocation changes shape
|
|
39
|
+
*
|
|
40
|
+
* There is no key to append, prove and expire. A token cannot be revoked; the trust is, and every token
|
|
41
|
+
* already minted stays live until it expires — {@link TRUST_REVOCATION_LATENCY} is the sentence that says
|
|
42
|
+
* so at the moment it matters.
|
|
43
|
+
*
|
|
44
|
+
* Nothing in this module reads a clock, a binding or D1. `now` is injected wherever time matters.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* Claims that name the token rather than the workload: who minted it, for whom, and when. A matcher built
|
|
48
|
+
* from these alone would accept every workload the issuer serves, so none of them may be registered.
|
|
49
|
+
* `iss` and `aud` are checked anyway, by the trust's own issuer and audience.
|
|
50
|
+
*/
|
|
51
|
+
const RESERVED_OIDC_CLAIMS = [
|
|
52
|
+
"iss",
|
|
53
|
+
"aud",
|
|
54
|
+
"exp",
|
|
55
|
+
"nbf",
|
|
56
|
+
"iat",
|
|
57
|
+
"jti"
|
|
58
|
+
];
|
|
59
|
+
/** The most claims one trust may match on. Far above any real constraint; a bound on a stored row. */
|
|
60
|
+
const MAX_TRUST_CLAIMS = 16;
|
|
61
|
+
/** The most claims a refusal records, so a token carrying a hundred cannot fill the audit trail. */
|
|
62
|
+
const MAX_OBSERVED_CLAIMS = 32;
|
|
63
|
+
/** The longest claim value a matcher stores or a refusal records. */
|
|
64
|
+
const MAX_CLAIM_VALUE = 256;
|
|
65
|
+
/** A claim name: the plain alphabet providers use, plus `.`, `:` and `-` for a customized template. */
|
|
66
|
+
const CLAIM_NAME = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
67
|
+
/** Every C0 and C1 control, the line separators, and the bidi controls: a value is one line of plain text. */
|
|
68
|
+
const UNRENDERABLE = /[\p{Cc}\p{Bidi_Control}\u2028\u2029]/u;
|
|
69
|
+
const reserved = new Set(RESERVED_OIDC_CLAIMS);
|
|
70
|
+
/**
|
|
71
|
+
* One claim name in a matcher. A name, not a path: nested claims are not matched, because a matcher that
|
|
72
|
+
* walks a structure is a matcher with more ways to be wrong on the check the whole trust rests on.
|
|
73
|
+
*/
|
|
74
|
+
const OidcClaimName = z.string().regex(CLAIM_NAME, "A claim name is letters, digits, `_`, `.`, `:` or `-`, at most 64.").refine((name) => !reserved.has(name), { message: `${RESERVED_OIDC_CLAIMS.join(", ")} name the token, not the workload, so they cannot constrain a trust.` }).describe("A claim the token must carry, by name — `repository_id`, `environment`, `project_id`, `sub`. Never one of the claims that name the token itself.");
|
|
75
|
+
/** One claim value in a matcher: compared as a string against the token's claim of the same name. */
|
|
76
|
+
const OidcClaimValue = z.string().min(1, "A claim value cannot be empty.").max(256, `A claim value is 256 characters at most.`).refine((value) => !UNRENDERABLE.test(value), "A claim value is one line of plain text.").describe("The exact value that claim must have. A numeric or boolean claim is compared by its string form, so `1262274726` matches a `repository_id` whichever way the provider typed it.");
|
|
77
|
+
/**
|
|
78
|
+
* Which workloads a trust accepts: every named claim must equal its value.
|
|
79
|
+
*
|
|
80
|
+
* **At least one, and that is the refusal the whole credential kind rests on.** An issuer with no claim
|
|
81
|
+
* constraint accepts every workload it serves.
|
|
82
|
+
*/
|
|
83
|
+
const OidcClaimMatch = z.record(OidcClaimName, OidcClaimValue).refine((claims) => Object.keys(claims).length > 0, { message: "A trust needs at least one claim constraint. An issuer alone accepts every workload it serves." }).refine((claims) => Object.keys(claims).length <= 16, { message: `A trust matches on 16 claims at most.` }).describe("The subject constraint: every named claim must equal its value. At least one, because an issuer alone accepts every workload it serves.");
|
|
84
|
+
/** An https URL. A trust's issuer and key endpoint are fetched from, so plain http is refused. */
|
|
85
|
+
const HttpsUrl = z.url({ protocol: /^https$/ });
|
|
86
|
+
/**
|
|
87
|
+
* One registered OIDC trust, an element of the connection's `trusts` column.
|
|
88
|
+
*
|
|
89
|
+
* Stored beside `keys` rather than inside it: the two kinds share nothing but the connection they grant
|
|
90
|
+
* through, and a union array would make every key reader learn about trusts.
|
|
91
|
+
*/
|
|
92
|
+
const OidcTrust = z.object({
|
|
93
|
+
trustId: z.string().min(1).max(64).describe("The trust's id, named by a call on the `pithy-control-plane-trust` header the way a token's `kid` names a key. Not a secret."),
|
|
94
|
+
issuer: HttpsUrl.describe("The exact `iss` this trust accepts — `https://token.actions.githubusercontent.com`, `https://gitlab.com`. A token from any other issuer is refused before its claims are read."),
|
|
95
|
+
jwksUrl: HttpsUrl.describe("Where the issuer publishes the keys that sign its tokens, read from its discovery document at registration. Public, so configuration and not a secret."),
|
|
96
|
+
audience: z.string().min(1).max(512).describe("The `aud` a token must name. Defaults to this environment's own Worker origin, which is what stops a staging token being replayed against production."),
|
|
97
|
+
claims: OidcClaimMatch,
|
|
98
|
+
authorizedBy: z.email().max(254).describe("Who sanctioned this trust: the signed-in session's email at registration. Never who may use it — a trust's users are whoever its claims admit."),
|
|
99
|
+
validFrom: JsonDate.describe("When the trust was registered and began to be honored."),
|
|
100
|
+
revokedAt: JsonDate.nullable().describe("When the trust was revoked, or null. Takes effect on the next call; a token already minted is refused from then on, but stays in its holder's hands until it expires."),
|
|
101
|
+
firstCallAt: JsonDate.nullable().describe("When a call first verified under this trust, or null while it is awaiting a first call. Nothing can prove a trust at registration — there is no private key here and no workload present — so this is the only proof there is.")
|
|
102
|
+
}).describe("One OIDC trust: a trusted issuer, the audience its tokens must name, and the claims that say which workloads it admits.");
|
|
103
|
+
/**
|
|
104
|
+
* What a caller should expect after a trust is revoked, said at the moment it is revoked rather than
|
|
105
|
+
* discovered afterwards.
|
|
106
|
+
*/
|
|
107
|
+
const TRUST_REVOCATION_LATENCY = "A token cannot be revoked, only the trust. Every call naming this trust is refused from now on, but a token already minted stays in its holder's hands until it expires — the issuer's own lifetime, typically minutes.";
|
|
108
|
+
/** Every trust that may verify a call at `now`: not revoked, and started. */
|
|
109
|
+
function activeTrusts(trusts, now) {
|
|
110
|
+
return trusts.filter((trust) => trust.revokedAt === null && trust.validFrom.getTime() <= now.getTime());
|
|
111
|
+
}
|
|
112
|
+
/** The trust a call names, if it is live at `now`, or null. The revocation check lives here so no caller forgets it. */
|
|
113
|
+
function findVerifyingTrust(trusts, trustId, now) {
|
|
114
|
+
return activeTrusts(trusts, now).find((trust) => trust.trustId === trustId) ?? null;
|
|
115
|
+
}
|
|
116
|
+
/** Register a trust beside the others. A duplicate id is refused: the id is what a call addresses. */
|
|
117
|
+
function appendTrust(trusts, trust) {
|
|
118
|
+
if (trusts.some((existing) => existing.trustId === trust.trustId)) throw new ControlPlaneKeyConflictError({
|
|
119
|
+
message: "That trust id is already registered on this connection.",
|
|
120
|
+
action: "Register the trust under a new id.",
|
|
121
|
+
detail: `duplicate trustId ${trust.trustId}`
|
|
122
|
+
});
|
|
123
|
+
return [...trusts, trust];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Revoke one trust at `now`, or answer null when no trust answers to that id. A trust already revoked keeps
|
|
127
|
+
* the instant it was first revoked: the window a call was refused under must not move.
|
|
128
|
+
*/
|
|
129
|
+
function revokeTrust(trusts, trustId, now) {
|
|
130
|
+
if (!trusts.some((trust) => trust.trustId === trustId)) return null;
|
|
131
|
+
return trusts.map((trust) => trust.trustId === trustId && trust.revokedAt === null ? {
|
|
132
|
+
...trust,
|
|
133
|
+
revokedAt: now
|
|
134
|
+
} : trust);
|
|
135
|
+
}
|
|
136
|
+
/** A claim's string form, or undefined for a value that is not a string, number or boolean. */
|
|
137
|
+
function claimString(value) {
|
|
138
|
+
if (typeof value === "string") return value;
|
|
139
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
140
|
+
if (typeof value === "boolean") return String(value);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Whether a token's claims satisfy a trust's matcher: every registered claim present and equal.
|
|
144
|
+
*
|
|
145
|
+
* An empty matcher matches nothing. Registration refuses one, and this refuses it again rather than letting
|
|
146
|
+
* a hand-edited row vacuously admit every workload.
|
|
147
|
+
*/
|
|
148
|
+
function claimsMatch(match, claims) {
|
|
149
|
+
const entries = Object.entries(match);
|
|
150
|
+
if (entries.length === 0) return false;
|
|
151
|
+
return entries.every(([name, expected]) => claimString(claims[name]) === expected);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The claims a refused token carried, as a refusal records them: the workload's claims, as strings, capped.
|
|
155
|
+
*
|
|
156
|
+
* This is what makes a custom issuer workable and a profiled one self-correcting — register, let the
|
|
157
|
+
* workload run once, read what it actually sent. Only ever called once the signature, issuer and audience
|
|
158
|
+
* have verified, so the caller demonstrably holds a genuine token for this audience and naming its claims
|
|
159
|
+
* leaks nothing it does not already hold.
|
|
160
|
+
*/
|
|
161
|
+
function observedClaims(claims) {
|
|
162
|
+
const recorded = {};
|
|
163
|
+
for (const [name, value] of Object.entries(claims)) {
|
|
164
|
+
if (Object.keys(recorded).length >= 32) break;
|
|
165
|
+
if (reserved.has(name) || !CLAIM_NAME.test(name)) continue;
|
|
166
|
+
const text = claimString(value);
|
|
167
|
+
if (text === void 0) continue;
|
|
168
|
+
recorded[name] = text.slice(0, 256);
|
|
169
|
+
}
|
|
170
|
+
return recorded;
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
export { MAX_CLAIM_VALUE, MAX_OBSERVED_CLAIMS, MAX_TRUST_CLAIMS, OidcClaimMatch, OidcClaimName, OidcClaimValue, OidcTrust, RESERVED_OIDC_CLAIMS, TRUST_REVOCATION_LATENCY, activeTrusts, appendTrust, claimsMatch, findVerifyingTrust, observedClaims, revokeTrust };
|
|
@@ -5,6 +5,7 @@ import type { Kysely } from "kysely";
|
|
|
5
5
|
import type { z } from "zod";
|
|
6
6
|
import { type DatabaseSchema } from "../../data/db";
|
|
7
7
|
import { ControlPlaneConnection } from "./connection";
|
|
8
|
+
import { LegalHold } from "./hold";
|
|
8
9
|
import { ControlPlaneReplay } from "./replay";
|
|
9
10
|
/**
|
|
10
11
|
* The connections table. `CamelCasePlugin` snake-cases it to `pithy_controlplane_connections` — the
|
|
@@ -19,6 +20,8 @@ export declare const CONTROL_PLANE_CONNECTIONS_TABLE = "pithyControlplaneConnect
|
|
|
19
20
|
* makes the claim a primary-key insert and therefore strongly consistent. See `../replay/guard.ts`.
|
|
20
21
|
*/
|
|
21
22
|
export declare const CONTROL_PLANE_REPLAYS_TABLE = "pithyControlplaneReplays";
|
|
23
|
+
/** Legal holds, kept after release as the record that one stood. */
|
|
24
|
+
export declare const CONTROL_PLANE_HOLDS_TABLE = "pithyControlplaneHolds";
|
|
22
25
|
/**
|
|
23
26
|
* The control-plane tables map. Both are always present: a Worker that composes the seam and has never
|
|
24
27
|
* been connected still needs somewhere for the answer "nobody is connected" to come from, and the
|
|
@@ -29,6 +32,7 @@ export declare function controlPlaneTables(): Record<string, z.ZodObject>;
|
|
|
29
32
|
export type ControlPlaneTables = {
|
|
30
33
|
[CONTROL_PLANE_CONNECTIONS_TABLE]: typeof ControlPlaneConnection;
|
|
31
34
|
[CONTROL_PLANE_REPLAYS_TABLE]: typeof ControlPlaneReplay;
|
|
35
|
+
[CONTROL_PLANE_HOLDS_TABLE]: typeof LegalHold;
|
|
32
36
|
};
|
|
33
37
|
export type ControlPlaneDatabase = Kysely<DatabaseSchema<ControlPlaneTables>>;
|
|
34
38
|
/** Build the control-plane database from the `DB` binding (CamelCasePlugin installed). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/tables.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,iCAAiC,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,6BAA6B,CAAC;AAEtE;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/tables.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,iCAAiC,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,6BAA6B,CAAC;AAEtE,oEAAoE;AACpE,eAAO,MAAM,yBAAyB,2BAA2B,CAAC;AAElE;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAMhE;AAED,+DAA+D;AAC/D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,CAAC,+BAA+B,CAAC,EAAE,OAAO,sBAAsB,CAAC;IACjE,CAAC,2BAA2B,CAAC,EAAE,OAAO,kBAAkB,CAAC;IACzD,CAAC,yBAAyB,CAAC,EAAE,OAAO,SAAS,CAAC;CAC/C,CAAC;AACF,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAE9E,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,UAAU,GAAG,oBAAoB,CAMzE"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import {
|
|
3
|
+
import { LegalHold } from "./hold.js";
|
|
4
4
|
import { createDatabase } from "../../data/db.js";
|
|
5
|
+
import { ControlPlaneConnection } from "./connection.js";
|
|
5
6
|
import { ControlPlaneReplay } from "./replay.js";
|
|
6
7
|
//#region src/controlPlane/data/tables.ts
|
|
7
8
|
/**
|
|
@@ -17,6 +18,8 @@ const CONTROL_PLANE_CONNECTIONS_TABLE = "pithyControlplaneConnections";
|
|
|
17
18
|
* makes the claim a primary-key insert and therefore strongly consistent. See `../replay/guard.ts`.
|
|
18
19
|
*/
|
|
19
20
|
const CONTROL_PLANE_REPLAYS_TABLE = "pithyControlplaneReplays";
|
|
21
|
+
/** Legal holds, kept after release as the record that one stood. */
|
|
22
|
+
const CONTROL_PLANE_HOLDS_TABLE = "pithyControlplaneHolds";
|
|
20
23
|
/**
|
|
21
24
|
* The control-plane tables map. Both are always present: a Worker that composes the seam and has never
|
|
22
25
|
* been connected still needs somewhere for the answer "nobody is connected" to come from, and the
|
|
@@ -25,15 +28,17 @@ const CONTROL_PLANE_REPLAYS_TABLE = "pithyControlplaneReplays";
|
|
|
25
28
|
function controlPlaneTables() {
|
|
26
29
|
return {
|
|
27
30
|
[CONTROL_PLANE_CONNECTIONS_TABLE]: ControlPlaneConnection,
|
|
28
|
-
[CONTROL_PLANE_REPLAYS_TABLE]: ControlPlaneReplay
|
|
31
|
+
[CONTROL_PLANE_REPLAYS_TABLE]: ControlPlaneReplay,
|
|
32
|
+
[CONTROL_PLANE_HOLDS_TABLE]: LegalHold
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
/** Build the control-plane database from the `DB` binding (CamelCasePlugin installed). */
|
|
32
36
|
function controlPlaneDatabase(d1) {
|
|
33
37
|
return createDatabase(d1, {
|
|
34
38
|
[CONTROL_PLANE_CONNECTIONS_TABLE]: ControlPlaneConnection,
|
|
35
|
-
[CONTROL_PLANE_REPLAYS_TABLE]: ControlPlaneReplay
|
|
39
|
+
[CONTROL_PLANE_REPLAYS_TABLE]: ControlPlaneReplay,
|
|
40
|
+
[CONTROL_PLANE_HOLDS_TABLE]: LegalHold
|
|
36
41
|
});
|
|
37
42
|
}
|
|
38
43
|
//#endregion
|
|
39
|
-
export { CONTROL_PLANE_CONNECTIONS_TABLE, CONTROL_PLANE_REPLAYS_TABLE, controlPlaneDatabase, controlPlaneTables };
|
|
44
|
+
export { CONTROL_PLANE_CONNECTIONS_TABLE, CONTROL_PLANE_HOLDS_TABLE, CONTROL_PLANE_REPLAYS_TABLE, controlPlaneDatabase, controlPlaneTables };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { type ControlPlaneDatabase } from "./tables";
|
|
4
|
+
/**
|
|
5
|
+
* Record the first call a trust answered (#722), so a listing stops saying *awaiting a first call*.
|
|
6
|
+
*
|
|
7
|
+
* **One statement that touches one field, never a read-modify-write of the column.** The verifier read the row
|
|
8
|
+
* moments ago, and an operator may have revoked the trust since. Writing back the whole `trusts` array as the
|
|
9
|
+
* verifier saw it would resurrect that trust — a revocation undone by the very call it was meant to stop. So the
|
|
10
|
+
* write is `json_set` on the matching element's `firstCallAt` alone, and only while that element is still
|
|
11
|
+
* unrevoked and still uncalled: a revoked trust is left exactly as revoked, and a second first call changes nothing.
|
|
12
|
+
*
|
|
13
|
+
* The instant is written as ISO-8601, the shape `JsonDate` stores inside the column.
|
|
14
|
+
*/
|
|
15
|
+
export declare function recordTrustFirstCall(db: ControlPlaneDatabase, connectionId: string, trustId: string, at: Date): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=trustFirstCall.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trustFirstCall.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/trustFirstCall.ts"],"names":[],"mappings":"AAIA,OAAO,EAAmC,KAAK,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAEtF;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,oBAAoB,EACxB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,IAAI,GACP,OAAO,CAAC,IAAI,CAAC,CAcf"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { CONTROL_PLANE_CONNECTIONS_TABLE } from "./tables.js";
|
|
4
|
+
import { sql } from "kysely";
|
|
5
|
+
//#region src/controlPlane/data/trustFirstCall.ts
|
|
6
|
+
/**
|
|
7
|
+
* Record the first call a trust answered (#722), so a listing stops saying *awaiting a first call*.
|
|
8
|
+
*
|
|
9
|
+
* **One statement that touches one field, never a read-modify-write of the column.** The verifier read the row
|
|
10
|
+
* moments ago, and an operator may have revoked the trust since. Writing back the whole `trusts` array as the
|
|
11
|
+
* verifier saw it would resurrect that trust — a revocation undone by the very call it was meant to stop. So the
|
|
12
|
+
* write is `json_set` on the matching element's `firstCallAt` alone, and only while that element is still
|
|
13
|
+
* unrevoked and still uncalled: a revoked trust is left exactly as revoked, and a second first call changes nothing.
|
|
14
|
+
*
|
|
15
|
+
* The instant is written as ISO-8601, the shape `JsonDate` stores inside the column.
|
|
16
|
+
*/
|
|
17
|
+
async function recordTrustFirstCall(db, connectionId, trustId, at) {
|
|
18
|
+
const trusts = sql.ref("trusts");
|
|
19
|
+
const uncalled = (alias) => sql`json_extract(${sql.ref(`${alias}.value`)}, '$.trustId') = ${trustId}
|
|
20
|
+
and json_extract(${sql.ref(`${alias}.value`)}, '$.revokedAt') is null
|
|
21
|
+
and json_extract(${sql.ref(`${alias}.value`)}, '$.firstCallAt') is null`;
|
|
22
|
+
await db.updateTable(CONTROL_PLANE_CONNECTIONS_TABLE).set({ trusts: sql`json_set(${trusts}, '$[' || (select ${sql.ref("t.key")} from json_each(${trusts}) as t where ${uncalled("t")} limit 1) || '].firstCallAt', ${at.toISOString()})` }).where("id", "=", connectionId).where(sql`exists (select 1 from json_each(${trusts}) as u where ${uncalled("u")})`).execute();
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { recordTrustFirstCall };
|