@pithy-sh/core 0.13.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/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 +433 -3
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
- package/dist/cli/commandCatalog.generated.js +449 -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 +482 -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,125 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { D1Database } from "@cloudflare/workers-types";
|
|
5
|
+
import { type Expression, type ExpressionBuilder, type Kysely, type SqlBool, sql } from "kysely";
|
|
6
|
+
import type { z } from "zod";
|
|
7
|
+
import { createDatabase } from "../data/db";
|
|
8
|
+
import { type ErasingPurgeContribution, exportRows, type PurgeContext, purgeBinding } from "./purge";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The purge contribution most capabilities need, written once (#814): rows in D1 that name a person by a column,
|
|
12
|
+
* exported on an access request and deleted by a purge.
|
|
13
|
+
*
|
|
14
|
+
* `@pithy-sh/rating`'s ratings, `@pithy-sh/leaderboard`'s entries, `@pithy-sh/matchmaking`'s friendships — each is
|
|
15
|
+
* a table whose rows carry the person's id in a column, and nothing else to do with them than read them out and
|
|
16
|
+
* delete them. A capability with more to do — bytes in R2, an address rather than an id, a row that must be
|
|
17
|
+
* anonymized — writes its own contribution with the same types.
|
|
18
|
+
*
|
|
19
|
+
* **One transaction.** The deletes are one `d1.batch()`, in the order the tables are listed, so a failure leaves
|
|
20
|
+
* every row where it was and the retried step erases the lot. **Idempotent**: a second run deletes nothing.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** One table a person's rows are in, and how they are found and exported. */
|
|
24
|
+
export interface PersonTable {
|
|
25
|
+
/** The table's camelCase name, as the capability's tables map keys it. */
|
|
26
|
+
readonly table: string;
|
|
27
|
+
/** Its row schema. Rows are decoded through it, so the export carries the app shape and never SQLite's. */
|
|
28
|
+
readonly schema: z.ZodObject;
|
|
29
|
+
/** The columns that name the person. A row is theirs when any of them is the subject's id. */
|
|
30
|
+
readonly columns: readonly string[];
|
|
31
|
+
/**
|
|
32
|
+
* Columns a row must also hold exactly — `{ subjectType: "user" }`, where one id column names people and
|
|
33
|
+
* organizations both and only a person's rows are theirs.
|
|
34
|
+
*/
|
|
35
|
+
readonly match?: Readonly<Record<string, string>>;
|
|
36
|
+
/** The key the rows are exported under — `ratings`, `entries`. */
|
|
37
|
+
readonly as: string;
|
|
38
|
+
/** A column to read them in, so an export reads the same twice. */
|
|
39
|
+
readonly orderBy?: string;
|
|
40
|
+
/** Columns the export leaves out — a credential, or another person's data the row carries beside theirs. */
|
|
41
|
+
readonly omit?: readonly string[];
|
|
42
|
+
/**
|
|
43
|
+
* A per-row redaction, for a credential that sits in a column only some rows hold it in — a Google Play purchase
|
|
44
|
+
* token keys a purchase row on that rail alone. It is handed the decoded row, before `omit` drops any column, and
|
|
45
|
+
* returns the row to export.
|
|
46
|
+
*/
|
|
47
|
+
readonly redact?: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** What {@link personRowsPurge} is built from. */
|
|
51
|
+
export interface PersonRowsPurgeInput {
|
|
52
|
+
/** The capability, for the fault a missing binding raises. */
|
|
53
|
+
readonly capability: string;
|
|
54
|
+
/** The one sentence the purge records with this step. */
|
|
55
|
+
readonly summary: string;
|
|
56
|
+
/** The D1 binding the tables live in. */
|
|
57
|
+
readonly database?: string;
|
|
58
|
+
/** The tables, in the order a purge deletes from them — children before the rows they reference. */
|
|
59
|
+
readonly tables: readonly PersonTable[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** An untyped Kysely: each table is named by the input, not by a typed map. */
|
|
63
|
+
type Untyped = Kysely<Record<string, Record<string, unknown>>>;
|
|
64
|
+
|
|
65
|
+
/** `(column = id OR column = id …) AND match…` over a table's person columns. */
|
|
66
|
+
function theirs(
|
|
67
|
+
{ columns, match }: Pick<PersonTable, "columns" | "match">,
|
|
68
|
+
id: string,
|
|
69
|
+
): (eb: ExpressionBuilder<Record<string, Record<string, unknown>>, string>) => Expression<SqlBool> {
|
|
70
|
+
return (eb) =>
|
|
71
|
+
eb.and([
|
|
72
|
+
eb.or(columns.map((column) => eb(sql.ref(column), "=", id))),
|
|
73
|
+
...Object.entries(match ?? {}).map(([column, value]) => eb(sql.ref(column), "=", value)),
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The export half alone: every row naming the person in `tables`, decoded, under each table's key. For a
|
|
79
|
+
* contribution that **retains** its rows (#814) — `@pithy-sh/payments` keeps a purchase for tax law, and still
|
|
80
|
+
* answers for it.
|
|
81
|
+
*/
|
|
82
|
+
export function personRowsExport(
|
|
83
|
+
input: Omit<PersonRowsPurgeInput, "summary">,
|
|
84
|
+
): (context: PurgeContext) => Promise<Record<string, unknown>> {
|
|
85
|
+
const database = input.database ?? "DB";
|
|
86
|
+
return async ({ subject, env }) => {
|
|
87
|
+
const db = createDatabase(purgeBinding<D1Database>(env, database, input.capability), {}) as unknown as Untyped;
|
|
88
|
+
const read = await Promise.all(
|
|
89
|
+
input.tables.map(async (table) => {
|
|
90
|
+
let query = db.selectFrom(table.table).selectAll().where(theirs(table, subject.id));
|
|
91
|
+
if (table.orderBy) query = query.orderBy(sql.ref(table.orderBy));
|
|
92
|
+
const rows = (await query.execute()).map((row) => {
|
|
93
|
+
const parsed = table.schema.parse(row) as Record<string, unknown>;
|
|
94
|
+
const decoded = table.redact ? table.redact(parsed) : parsed;
|
|
95
|
+
for (const column of table.omit ?? []) delete decoded[column];
|
|
96
|
+
return decoded;
|
|
97
|
+
});
|
|
98
|
+
return [table.as, exportRows(rows)] as const;
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
return Object.fromEntries(read);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Build a user contribution that exports, then on a purge deletes, every row naming the person in `tables`. */
|
|
106
|
+
export function personRowsPurge(input: PersonRowsPurgeInput): ErasingPurgeContribution {
|
|
107
|
+
const database = input.database ?? "DB";
|
|
108
|
+
const open = (env: Record<string, unknown>) => {
|
|
109
|
+
const d1 = purgeBinding<D1Database>(env, database, input.capability);
|
|
110
|
+
return { d1, db: createDatabase(d1, {}) as unknown as Untyped };
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
subject: "user",
|
|
114
|
+
summary: input.summary,
|
|
115
|
+
erase: async ({ subject, env }) => {
|
|
116
|
+
const { d1, db } = open(env);
|
|
117
|
+
const statements = input.tables.map((table) => {
|
|
118
|
+
const compiled = db.deleteFrom(table.table).where(theirs(table, subject.id)).compile();
|
|
119
|
+
return d1.prepare(compiled.sql).bind(...(compiled.parameters as unknown[]));
|
|
120
|
+
});
|
|
121
|
+
if (statements.length > 0) await d1.batch(statements);
|
|
122
|
+
},
|
|
123
|
+
export: personRowsExport(input),
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/capability/purge.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { ValidationError } from "../error/pithyError";
|
|
5
|
+
import { InternalError, ValidationError } from "../error/pithyError";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* How a capability says what erasing one subject means for its own rows — the purge contribution.
|
|
@@ -31,6 +31,13 @@ import { ValidationError } from "../error/pithyError";
|
|
|
31
31
|
* contribution with no `export` is listed as holding rows it did not say how to export, which is the honest
|
|
32
32
|
* answer and the one an operator can act on.
|
|
33
33
|
*
|
|
34
|
+
* ## Or it keeps them, and says why (#814)
|
|
35
|
+
*
|
|
36
|
+
* Not every row about a person should go. A payment record is kept for tax law, a suppression has to outlive the
|
|
37
|
+
* person or the unsubscribe stops holding, and the audit trail is the record of who acted. So a contribution has a
|
|
38
|
+
* third disposition beside erase and anonymize: `retain`, with a one-sentence basis written for the person. The
|
|
39
|
+
* purge records that step as retained and erases nothing of it; the export carries the rows and the basis.
|
|
40
|
+
*
|
|
34
41
|
* ## An adopter declares one too
|
|
35
42
|
*
|
|
36
43
|
* On their own `app` capability, for their own tables, with the same types. The contract is exported for
|
|
@@ -55,30 +62,73 @@ export interface PurgeContext {
|
|
|
55
62
|
readonly subject: { readonly type: PurgeSubject; readonly id: string };
|
|
56
63
|
/** The Worker's bindings for this run, from which the contribution opens its own database. */
|
|
57
64
|
readonly env: Record<string, unknown>;
|
|
65
|
+
/**
|
|
66
|
+
* The subject's email address, read on demand from the capability that owns the subject — `null` when it has
|
|
67
|
+
* none, or no longer has one (#814). For a capability that holds rows by address rather than by id: a tester's
|
|
68
|
+
* membership, a suppression, an invitation.
|
|
69
|
+
*
|
|
70
|
+
* **A function, never a value**, so the address is read inside the step that needs it and is never carried in
|
|
71
|
+
* a Workflow's journal, where a purge would otherwise leave it behind. Absent where nobody can say, and a
|
|
72
|
+
* contribution that needs it then finds nothing by address.
|
|
73
|
+
*/
|
|
74
|
+
readonly address?: () => Promise<string | null>;
|
|
58
75
|
}
|
|
59
76
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
77
|
+
/** What every contribution says, whichever its disposition. */
|
|
78
|
+
interface PurgeContributionBase {
|
|
62
79
|
/** The kind of subject this erases. */
|
|
63
80
|
readonly subject: PurgeSubject;
|
|
64
81
|
/**
|
|
65
|
-
* One sentence on what is erased or
|
|
82
|
+
* One sentence on what is erased, anonymized or kept — `Deletes the user's uploads and their stored bytes.`
|
|
66
83
|
* Recorded with the step that ran it, so the trail says what a purge did and not only that it ran.
|
|
67
84
|
*/
|
|
68
85
|
readonly summary: string;
|
|
86
|
+
/**
|
|
87
|
+
* What this capability holds about the subject, as one JSON object — the read side of `erase` (#622). It
|
|
88
|
+
* answers an access request, so it carries the subject's own data and never a credential: no session token,
|
|
89
|
+
* no provider token, no push token. Optional on an erasing contribution: one without it is listed in the
|
|
90
|
+
* export as rows it erases and did not say how to export. A throw costs this capability's entry, never the
|
|
91
|
+
* document.
|
|
92
|
+
*/
|
|
93
|
+
readonly export?: (context: PurgeContext) => Promise<Record<string, unknown>>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A contribution that erases or anonymizes the subject's rows. */
|
|
97
|
+
export interface ErasingPurgeContribution extends PurgeContributionBase {
|
|
69
98
|
/**
|
|
70
99
|
* Erase or anonymize this subject's rows. **Idempotent**: run again after it succeeded, or after a failure
|
|
71
100
|
* that came once the rows were gone, it must succeed having found nothing to do. Throwing fails this step
|
|
72
101
|
* and only this step.
|
|
73
102
|
*/
|
|
74
103
|
readonly erase: (context: PurgeContext) => Promise<void>;
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
104
|
+
/** Never set on an erasing contribution. */
|
|
105
|
+
readonly retain?: undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A contribution that **keeps** the subject's rows through a purge, and says why (#814). A payment record kept
|
|
110
|
+
* for tax law, a suppression that must outlive the person or the unsubscribe stops holding, the audit trail of
|
|
111
|
+
* who acted. The purge records the step as retained and never erases a row of it; the export carries the rows
|
|
112
|
+
* and the basis, because rows that are kept are rows that are answered for.
|
|
113
|
+
*/
|
|
114
|
+
export interface RetainingPurgeContribution extends PurgeContributionBase {
|
|
115
|
+
/** Why the rows are kept, in one sentence written for the person — `Kept for tax records.` */
|
|
116
|
+
readonly retain: { readonly basis: string };
|
|
117
|
+
/** Required: what is kept is exported. */
|
|
118
|
+
readonly export: (context: PurgeContext) => Promise<Record<string, unknown>>;
|
|
119
|
+
/** Never set on a retaining contribution. */
|
|
120
|
+
readonly erase?: undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One capability's answer to "what happens to this subject's rows": erased, anonymized, or kept and why. */
|
|
124
|
+
export type PurgeContribution = ErasingPurgeContribution | RetainingPurgeContribution;
|
|
125
|
+
|
|
126
|
+
/** What a purge does with a contribution's rows: `erase` (or anonymize), or `retain`. */
|
|
127
|
+
export type PurgeDisposition = "erase" | "retain";
|
|
128
|
+
|
|
129
|
+
/** Whether a contribution keeps the subject's rows rather than erasing them. */
|
|
130
|
+
export function isRetained(contribution: PurgeContribution): contribution is RetainingPurgeContribution {
|
|
131
|
+
return contribution.retain !== undefined;
|
|
82
132
|
}
|
|
83
133
|
|
|
84
134
|
/** One step of a purge: which capability it runs, and the contribution that capability declared. */
|
|
@@ -123,15 +173,43 @@ export function assertPurgeContributions(capability: string, contributions: read
|
|
|
123
173
|
action: "Give it an `export` that answers what this capability holds about the subject, or leave it out.",
|
|
124
174
|
});
|
|
125
175
|
}
|
|
176
|
+
if (contribution.retain !== undefined) {
|
|
177
|
+
assertRetention(capability, subject.data, contribution);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
126
180
|
if (typeof contribution.erase !== "function") {
|
|
127
181
|
throw new ValidationError({
|
|
128
182
|
message: `The ${capability} capability's ${subject.data} purge has no erase function.`,
|
|
129
|
-
action:
|
|
183
|
+
action:
|
|
184
|
+
"Give it an `erase` that removes or anonymizes the subject's rows, or a `retain` that says why it keeps them.",
|
|
130
185
|
});
|
|
131
186
|
}
|
|
132
187
|
}
|
|
133
188
|
}
|
|
134
189
|
|
|
190
|
+
/** Refuse a retaining contribution the person could not be answered with: no basis, no export, or an erase too. */
|
|
191
|
+
function assertRetention(capability: string, subject: PurgeSubject, contribution: PurgeContribution): void {
|
|
192
|
+
const basis = (contribution.retain as { basis?: unknown } | undefined)?.basis;
|
|
193
|
+
if (typeof basis !== "string" || basis.trim() === "") {
|
|
194
|
+
throw new ValidationError({
|
|
195
|
+
message: `The ${capability} capability's ${subject} purge retains the rows with no basis.`,
|
|
196
|
+
action: "Say in one sentence, written for the person, why they are kept — `Kept for tax records.`",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (typeof contribution.export !== "function") {
|
|
200
|
+
throw new ValidationError({
|
|
201
|
+
message: `The ${capability} capability's ${subject} purge retains the rows and has no export.`,
|
|
202
|
+
action: "Give it an `export`: rows that are kept are rows an access request is answered with.",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (contribution.erase !== undefined) {
|
|
206
|
+
throw new ValidationError({
|
|
207
|
+
message: `The ${capability} capability's ${subject} purge both retains and erases the rows.`,
|
|
208
|
+
action: "Declare one disposition: `retain` with its basis, or `erase`.",
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
135
213
|
/** The steps a purge of `subject` runs, in composition order — libraries first, the adopter's `app` last. */
|
|
136
214
|
export function purgeSteps(
|
|
137
215
|
capabilities: readonly { readonly name: string; readonly purge?: readonly PurgeContribution[] }[],
|
|
@@ -145,16 +223,21 @@ export function purgeSteps(
|
|
|
145
223
|
return steps;
|
|
146
224
|
}
|
|
147
225
|
|
|
226
|
+
/** What every export entry says: the capability, its sentence, and what a purge does with its rows. */
|
|
227
|
+
interface PurgeExportEntryBase {
|
|
228
|
+
readonly capability: string;
|
|
229
|
+
readonly summary: string;
|
|
230
|
+
/** `erase` — a purge erases or anonymizes these rows — or `retain`, a purge keeps them (#814). */
|
|
231
|
+
readonly disposition: PurgeDisposition;
|
|
232
|
+
/** For `retain`, why the rows are kept, in the sentence the contribution wrote for the person. */
|
|
233
|
+
readonly basis?: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
148
236
|
/** What one step's export answered: its data, that it declared none, or that it could not be read. */
|
|
149
237
|
export type PurgeExportEntry =
|
|
150
|
-
| {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
readonly state: "exported";
|
|
154
|
-
readonly data: Record<string, unknown>;
|
|
155
|
-
}
|
|
156
|
-
| { readonly capability: string; readonly summary: string; readonly state: "not_exported" }
|
|
157
|
-
| { readonly capability: string; readonly summary: string; readonly state: "unavailable" };
|
|
238
|
+
| (PurgeExportEntryBase & { readonly state: "exported"; readonly data: Record<string, unknown> })
|
|
239
|
+
| (PurgeExportEntryBase & { readonly state: "not_exported" })
|
|
240
|
+
| (PurgeExportEntryBase & { readonly state: "unavailable" });
|
|
158
241
|
|
|
159
242
|
/** Whether an export answered what the contract promises: one plain object. */
|
|
160
243
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -173,7 +256,9 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
173
256
|
export async function purgeExports(steps: readonly PurgeStep[], context: PurgeContext): Promise<PurgeExportEntry[]> {
|
|
174
257
|
return Promise.all(
|
|
175
258
|
steps.map(async ({ step, contribution }): Promise<PurgeExportEntry> => {
|
|
176
|
-
const base
|
|
259
|
+
const base: PurgeExportEntryBase = isRetained(contribution)
|
|
260
|
+
? { capability: step, summary: contribution.summary, disposition: "retain", basis: contribution.retain.basis }
|
|
261
|
+
: { capability: step, summary: contribution.summary, disposition: "erase" };
|
|
177
262
|
const read = contribution.export;
|
|
178
263
|
if (read === undefined) return { ...base, state: "not_exported" };
|
|
179
264
|
try {
|
|
@@ -185,3 +270,55 @@ export async function purgeExports(steps: readonly PurgeStep[], context: PurgeCo
|
|
|
185
270
|
}),
|
|
186
271
|
);
|
|
187
272
|
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* One binding a contribution opens off a run's env — or a wiring fault naming it and the capability (#814). The
|
|
276
|
+
* purge and the export both run in the adopter's Worker, so a missing binding is that Worker's wiring, and the
|
|
277
|
+
* fault says which binding to add rather than failing deep inside a query.
|
|
278
|
+
*/
|
|
279
|
+
export function purgeBinding<T>(
|
|
280
|
+
env: Record<string, unknown>,
|
|
281
|
+
name: string,
|
|
282
|
+
capability: string,
|
|
283
|
+
kind = "D1 database",
|
|
284
|
+
): T {
|
|
285
|
+
const binding = env[name];
|
|
286
|
+
if (typeof binding !== "object" || binding === null) {
|
|
287
|
+
throw new InternalError({
|
|
288
|
+
message: `The ${capability} capability's ${kind} is not bound, so its purge step could not run.`,
|
|
289
|
+
action: `Bind the ${kind} named ${name} to the Worker that runs the purge.`,
|
|
290
|
+
detail: `${capability} purge contribution found no ${name} binding on the env`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return binding as T;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Rows as an export carries them (#814): plain JSON, a `Date` as its ISO-8601 string — the shape the export
|
|
298
|
+
* document crosses the wire in, so what a contribution answers is what a reader parses, and a test compares
|
|
299
|
+
* exactly that.
|
|
300
|
+
*/
|
|
301
|
+
export function exportRows<T>(rows: readonly T[]): Record<string, unknown>[] {
|
|
302
|
+
return JSON.parse(JSON.stringify(rows)) as Record<string, unknown>[];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* A field name that holds a credential — a token, a secret, a password, a digest of one, a sealed value. What an
|
|
307
|
+
* export never carries (#814): an access request is answered with what is held, never with the means to act as
|
|
308
|
+
* the person or to reach what they shared.
|
|
309
|
+
*/
|
|
310
|
+
const CREDENTIAL_FIELD = /secret|token|password|credential|digest|private_?key|encrypted/i;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Every path in an export's data whose field name holds a credential — empty for an export that carries none.
|
|
314
|
+
* Each capability's export test asserts it empty, so a column added later with a token in it fails the build
|
|
315
|
+
* rather than reaching an access request (#814).
|
|
316
|
+
*/
|
|
317
|
+
export function credentialFields(value: unknown, path = ""): string[] {
|
|
318
|
+
if (Array.isArray(value)) return value.flatMap((item, index) => credentialFields(item, `${path}[${index}]`));
|
|
319
|
+
if (typeof value !== "object" || value === null) return [];
|
|
320
|
+
return Object.entries(value).flatMap(([key, inner]) => {
|
|
321
|
+
const here = path ? `${path}.${key}` : key;
|
|
322
|
+
return [...(CREDENTIAL_FIELD.test(key) ? [here] : []), ...credentialFields(inner, here)];
|
|
323
|
+
});
|
|
324
|
+
}
|
|
@@ -169,6 +169,13 @@ export interface SettingsAccountReader {
|
|
|
169
169
|
* Asked by secrets of the manager token the runtime token superseded, which nothing revokes for you.
|
|
170
170
|
*/
|
|
171
171
|
accountToken(name: string): Promise<boolean>;
|
|
172
|
+
/**
|
|
173
|
+
* **Whether an R2 bucket's objects under `prefix` are under an indefinite lock (#822)** — `locked`, `unlocked`, or
|
|
174
|
+
* `missing` when no bucket of that name exists on the account. Asked by audit, whose checkpoints are evidence only
|
|
175
|
+
* while nothing can replace them. Optional, so a reader built before the question existed still compiles; a
|
|
176
|
+
* check asking a reader without it reports the environment unanswered rather than guessing.
|
|
177
|
+
*/
|
|
178
|
+
r2BucketLock?(request: { bucket: string; prefix: string }): Promise<"locked" | "unlocked" | "missing">;
|
|
172
179
|
}
|
|
173
180
|
|
|
174
181
|
/** What {@link SettingsAccountReader.appRows} is asked. */
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { InternalError } from "../error/pithyError";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A Vectorize index a capability owns and re-embeds itself — the contribution `pithy vector reprocess` reads.
|
|
8
|
+
*
|
|
9
|
+
* ## Why a contribution
|
|
10
|
+
*
|
|
11
|
+
* `@pithy-sh/vector` re-embeds the indexes an adopter declares in `vector({ indexes })`. A capability that
|
|
12
|
+
* builds an index of its own — `@pithy-sh/support`'s semantic message index — uses vector's public modules to
|
|
13
|
+
* embed and to re-embed, and runs vector's reprocess orchestration in a Workflow it hosts, over its own corpus.
|
|
14
|
+
* What it cannot do is teach vector's CLI that the index exists: capabilities depend on core seams, never on
|
|
15
|
+
* each other. So it says so here, on its own `Capability`, the way it states its migrations and its purge, and
|
|
16
|
+
* `pithy vector reprocess` reads every composed declaration — in a project that never composes `vector()` too.
|
|
17
|
+
*
|
|
18
|
+
* ## What the CLI does with one
|
|
19
|
+
*
|
|
20
|
+
* It starts `reprocessJob`, one of this capability's own `workflows`, under the deployed name every Workflow
|
|
21
|
+
* gets — `<project>-<env>-<capability>-<job>` — with the parameters every reprocess takes: `{ index, all }`.
|
|
22
|
+
* A model change, a repair and a full rebuild (`--all`) are then the same three cases they are for any index.
|
|
23
|
+
*/
|
|
24
|
+
export interface VectorIndexContribution {
|
|
25
|
+
/**
|
|
26
|
+
* The index's name as `pithy vector reprocess --index` takes it — `support-messages`. Unique across the
|
|
27
|
+
* project, and never one of the adopter's own `vector()` index keys.
|
|
28
|
+
*/
|
|
29
|
+
readonly name: string;
|
|
30
|
+
/** The job, in this capability's `workflows`, that re-embeds the index. */
|
|
31
|
+
readonly reprocessJob: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Refuse a declaration no reprocess could run: a duplicate name, or a job the capability does not register.
|
|
36
|
+
* Called by `defineCapability`, so the mistake is named where it is written rather than on the day somebody
|
|
37
|
+
* changes a model.
|
|
38
|
+
*/
|
|
39
|
+
export function assertVectorIndexContributions(
|
|
40
|
+
capability: string,
|
|
41
|
+
indexes: readonly VectorIndexContribution[],
|
|
42
|
+
jobs: readonly string[],
|
|
43
|
+
): void {
|
|
44
|
+
const seen = new Set<string>();
|
|
45
|
+
for (const index of indexes) {
|
|
46
|
+
if (seen.has(index.name)) {
|
|
47
|
+
throw new InternalError({
|
|
48
|
+
message: `The ${capability} capability declares the vector index \`${index.name}\` twice.`,
|
|
49
|
+
detail: `vectorIndexes on ${capability} repeats ${index.name}`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
seen.add(index.name);
|
|
53
|
+
if (!jobs.includes(index.reprocessJob)) {
|
|
54
|
+
throw new InternalError({
|
|
55
|
+
message: `The ${capability} capability's vector index \`${index.name}\` names a reprocess job it does not register.`,
|
|
56
|
+
detail: `vectorIndexes on ${capability}: no workflow job ${index.reprocessJob}; registered: ${jobs.join(", ") || "none"}`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|