@pithy-sh/core 0.16.0 → 0.16.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../../src/controlPlane/capability.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAuC,MAAM,0BAA0B,CAAC;AAChG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAc,MAAM,kBAAkB,CAAC;AAWvE,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAqBnF,OAAO,EACL,wBAAwB,EAIzB,MAAM,kBAAkB,CAAC;AAG1B;;;;;;;;;GASG;AACH,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG,uBAAuB,CAAC;AAE1D,+GAA+G;AAC/G,eAAO,MAAM,uBAAuB;aAClC,GAAG,EAAE,aAAa;aAClB,IAAI,EAAE,OAAO;aACb,MAAM;aACN,KAAK;aACL,IAAI,EAAE,SAAS;aACf,OAAO,EAAE,qGAAqG;aAC9G,OAAO;CACC,CAAC;AAEX,uEAAuE;AACvE,MAAM,WAAW,sBAAuB,SAAQ,UAAU,CAAC,eAAe,EAAE,kBAAkB,EAAE,cAAc,CAAC;IAC7G,gCAAgC;IAChC,kBAAkB,EAAE,kBAAkB,CAAC;CACxC;AAED,qGAAqG;AACrG,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,IAAI,sBAAsB,CAErG;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,sBAAsB,CAsPtF;AAED,4GAA4G;AAC5G,OAAO,EAAE,wBAAwB,EAAE,CAAC"}
1
+ {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../../src/controlPlane/capability.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAuC,MAAM,0BAA0B,CAAC;AAChG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAc,MAAM,kBAAkB,CAAC;AAWvE,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAsBnF,OAAO,EACL,wBAAwB,EAIzB,MAAM,kBAAkB,CAAC;AAG1B;;;;;;;;;GASG;AACH,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG,uBAAuB,CAAC;AAE1D,+GAA+G;AAC/G,eAAO,MAAM,uBAAuB;aAClC,GAAG,EAAE,aAAa;aAClB,IAAI,EAAE,OAAO;aACb,MAAM;aACN,KAAK;aACL,IAAI,EAAE,SAAS;aACf,OAAO,EAAE,qGAAqG;aAC9G,OAAO;CACC,CAAC;AAEX,uEAAuE;AACvE,MAAM,WAAW,sBAAuB,SAAQ,UAAU,CAAC,eAAe,EAAE,kBAAkB,EAAE,cAAc,CAAC;IAC7G,gCAAgC;IAChC,kBAAkB,EAAE,kBAAkB,CAAC;CACxC;AAED,qGAAqG;AACrG,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,IAAI,sBAAsB,CAErG;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,sBAAsB,CAuPtF;AAED,4GAA4G;AAC5G,OAAO,EAAE,wBAAwB,EAAE,CAAC"}
@@ -8,7 +8,7 @@ import { describePresenceRooms } from "../presence/http/routes.js";
8
8
  import { readRetirement } from "../privacy/fieldRotation.js";
9
9
  import { lazyPrivacyInventory } from "../privacy/inventory.js";
10
10
  import { composedRetention } from "../retention/composed.js";
11
- import { ControlPlaneConnection } from "./data/connection.js";
11
+ import { parseConnectionRow } from "./data/connection.js";
12
12
  import { CONTROL_PLANE_CONNECTIONS_TABLE, controlPlaneTables } from "./data/tables.js";
13
13
  import { standingHolds } from "../retention/holds.js";
14
14
  import { holdsUserPurge } from "../retention/holdsPurge.js";
@@ -25,6 +25,7 @@ import { controlplane_0002_management_origin } from "./migrations/0002_managemen
25
25
  import { controlplane_0003_connection_principal } from "./migrations/0003_connection_principal.js";
26
26
  import { controlplane_0004_connection_trusts } from "./migrations/0004_connection_trusts.js";
27
27
  import { controlplane_0005_holds } from "./migrations/0005_holds.js";
28
+ import { controlplane_0006_connection_nulls } from "./migrations/0006_connection_nulls.js";
28
29
  import { d1ReplayGuard } from "./replay/d1Guard.js";
29
30
  import { CONTROL_PLANE_KV_BINDING, controlPlaneKvNamespaces, kvReplayGuard } from "./replay/kvGuard.js";
30
31
  //#region src/controlPlane/capability.ts
@@ -79,7 +80,8 @@ function controlplane(options = {}) {
79
80
  "0002_management_origin": controlplane_0002_management_origin,
80
81
  "0003_connection_principal": controlplane_0003_connection_principal,
81
82
  "0004_connection_trusts": controlplane_0004_connection_trusts,
82
- "0005_holds": controlplane_0005_holds
83
+ "0005_holds": controlplane_0005_holds,
84
+ "0006_connection_nulls": controlplane_0006_connection_nulls
83
85
  };
84
86
  const requiredBindings = [{
85
87
  type: "d1",
@@ -165,7 +167,7 @@ function controlplane(options = {}) {
165
167
  c.set("controlPlaneVerifier", createControlPlaneVerifier({
166
168
  loadConnection: async (connectionId) => {
167
169
  const row = await db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().where("id", "=", connectionId).executeTakeFirst();
168
- return row ? ControlPlaneConnection.parse(row) : null;
170
+ return row ? parseConnectionRow(row) : null;
169
171
  },
170
172
  countConnections: async () => {
171
173
  return (await db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).select("id").limit(1).execute()).length;
@@ -177,7 +179,7 @@ function controlplane(options = {}) {
177
179
  }, {
178
180
  loadEnvironmentConnection: async () => {
179
181
  const row = await db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().where("environment", "=", environment).executeTakeFirst();
180
- return row ? ControlPlaneConnection.parse(row) : null;
182
+ return row ? parseConnectionRow(row) : null;
181
183
  },
182
184
  environment,
183
185
  now,
@@ -135,4 +135,14 @@ export declare const ControlPlaneConnection: z.ZodObject<{
135
135
  }, z.core.$strip>;
136
136
  export type ControlPlaneConnection = z.output<typeof ControlPlaneConnection>;
137
137
  export type ControlPlaneConnectionRow = z.input<typeof ControlPlaneConnection>;
138
+ /** The nullable columns a NULL in which means "not recorded", and which `pithy migrate` repairs when they hold `''`. */
139
+ export declare const CONNECTION_NULLABLE_COLUMNS: readonly ["managementOrigin", "authorizedBy", "actorKind", "purpose"];
140
+ /**
141
+ * Decode one stored connection row, or refuse it with the columns that would not parse.
142
+ *
143
+ * Every reader of the table goes through this, the Worker's verifier and the CLI's registry alike, so a row
144
+ * nobody can read is named the same way wherever it is met, never surfaced as a bare `ZodError`. The message is
145
+ * the default, because the verifier answers before a caller is authenticated.
146
+ */
147
+ export declare function parseConnectionRow(row: unknown): ControlPlaneConnection;
138
148
  //# sourceMappingURL=connection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/connection.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;;;;;GASG;AAEH,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB;;;;iBAgBoF,CAAC;AAClH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEjE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;iBAoBoF,CAAC;AAC/G,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB;;;;EAI/B,CAAC;AACJ,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,YAK7B,CAAC;AACJ,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEvE,8GAA8G;AAC9G,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAiB1C;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,aAW3B,CAAC;AACJ,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE,8DAA8D;AAC9D,eAAO,MAAM,wBAAwB;;;;;EAIlC,CAAC;AACJ,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuEhC,CAAC;AACJ,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC7E,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/connection.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB;;;;;;;;;GASG;AAEH,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB;;;;iBAgBoF,CAAC;AAClH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEjE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;iBAoBoF,CAAC;AAC/G,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB;;;;EAI/B,CAAC;AACJ,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,YAK7B,CAAC;AACJ,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEvE,8GAA8G;AAC9G,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAiB1C;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,aAW3B,CAAC;AACJ,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE,8DAA8D;AAC9D,eAAO,MAAM,wBAAwB;;;;;EAIlC,CAAC;AACJ,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuEhC,CAAC;AACJ,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC7E,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAQ/E,wHAAwH;AACxH,eAAO,MAAM,2BAA2B,YAAI,kBAAkB,EAAE,cAAc,EAAE,WAAW,EAAE,SAAS,CAAU,CAAC;AAEjH;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,sBAAsB,CAgBvE"}
@@ -1,5 +1,7 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
+ import { InternalError } from "../../error/pithyError.js";
4
+ import { commandName } from "../../cli/command.js";
3
5
  import { ControlPlaneScope } from "../scope/scope.js";
4
6
  import { JsonDate, SQLiteDate, sqliteJson } from "../../data/codecs.js";
5
7
  import { OidcTrust } from "./oidcTrust.js";
@@ -117,5 +119,36 @@ const ControlPlaneConnection = z.object({
117
119
  createdAt: SQLiteDate.describe("When the connection was registered. Ms-epoch in SQLite, a Date in TypeScript."),
118
120
  updatedAt: SQLiteDate.describe("When the row last changed — a key registered or expired, or the Worker URL re-pointed.")
119
121
  }).describe("One management-client connection: its identity, the environment and URL it is bound to, the operations it was granted, and its versioned public keys.");
122
+ /** The SQLite column a schema path names: `authorizedBy` → `authorized_by`, or `row` for the row itself. */
123
+ function columnOf(path) {
124
+ const field = path[0];
125
+ return field === void 0 ? "row" : String(field).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
126
+ }
127
+ /** The nullable columns a NULL in which means "not recorded", and which `pithy migrate` repairs when they hold `''`. */
128
+ const CONNECTION_NULLABLE_COLUMNS = [
129
+ "managementOrigin",
130
+ "authorizedBy",
131
+ "actorKind",
132
+ "purpose"
133
+ ];
134
+ /**
135
+ * Decode one stored connection row, or refuse it with the columns that would not parse.
136
+ *
137
+ * Every reader of the table goes through this, the Worker's verifier and the CLI's registry alike, so a row
138
+ * nobody can read is named the same way wherever it is met, never surfaced as a bare `ZodError`. The message is
139
+ * the default, because the verifier answers before a caller is authenticated.
140
+ */
141
+ function parseConnectionRow(row) {
142
+ const parsed = ControlPlaneConnection.safeParse(row);
143
+ if (parsed.success) return parsed.data;
144
+ const columns = [...new Set(parsed.error.issues.map((issue) => columnOf(issue.path)))];
145
+ const repairable = CONNECTION_NULLABLE_COLUMNS.map((column) => columnOf([column]));
146
+ const id = typeof row === "object" && row !== null && "id" in row ? String(row.id) : "unknown";
147
+ const repair = columns.some((column) => repairable.includes(column)) ? ` ${repairable.join(", ")} read NULL as not recorded, and an empty string in one is not NULL. Run ${commandName("migrate")} for its environment, which puts it back.` : "";
148
+ throw new InternalError({
149
+ action: `${columns.join(", ")} in pithy_controlplane_connections ${columns.length === 1 ? "holds a value" : "hold values"} this release cannot read.${repair}`,
150
+ detail: `Connection ${id}: ${parsed.error.issues.map((issue) => `${columnOf(issue.path)}: ${issue.message}`).join("; ")}`
151
+ }, { cause: parsed.error });
152
+ }
120
153
  //#endregion
121
- export { ConnectionCredentialKind, ConnectionPrincipal, ConnectionPurpose, ControlPlaneActorKind, ControlPlaneConnection, Ed25519PublicJwk, MAX_CONNECTION_PURPOSE, RegisteredKey };
154
+ export { CONNECTION_NULLABLE_COLUMNS, ConnectionCredentialKind, ConnectionPrincipal, ConnectionPurpose, ControlPlaneActorKind, ControlPlaneConnection, Ed25519PublicJwk, MAX_CONNECTION_PURPOSE, RegisteredKey, parseConnectionRow };
@@ -1 +1 @@
1
- {"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/connections.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/data/connections.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { ConflictError } from "../../error/pithyError.js";
4
4
  import { activeTrusts } from "./oidcTrust.js";
5
- import { ControlPlaneConnection } from "./connection.js";
5
+ import { ControlPlaneConnection, parseConnectionRow } from "./connection.js";
6
6
  import { CONTROL_PLANE_CONNECTIONS_TABLE } from "./tables.js";
7
7
  import { activeKeys } from "./keyLifecycle.js";
8
8
  import { sql } from "kysely";
@@ -74,7 +74,7 @@ function authorizedByPrincipal(connection, principal) {
74
74
  async function listConnections(db, filter = {}) {
75
75
  let query = db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().orderBy("createdAt").orderBy("id");
76
76
  if (filter.environment !== void 0) query = query.where("environment", "=", filter.environment);
77
- const rows = (await query.execute()).map((row) => ControlPlaneConnection.parse(row));
77
+ const rows = (await query.execute()).map((row) => parseConnectionRow(row));
78
78
  const { principal } = filter;
79
79
  return {
80
80
  connections: rows.filter((row) => row.authorizedBy !== null && (principal === void 0 || authorizedByPrincipal(row, principal))),
@@ -84,7 +84,7 @@ async function listConnections(db, filter = {}) {
84
84
  /** Read one connection by id, or null. */
85
85
  async function readConnection(db, id) {
86
86
  const row = await db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().where("id", "=", id).executeTakeFirst();
87
- return row ? ControlPlaneConnection.parse(row) : null;
87
+ return row ? parseConnectionRow(row) : null;
88
88
  }
89
89
  /** Read one connection by id for a write, or null. With `environment`, null for a connection bound to another. */
90
90
  async function readConnectionForWrite(db, id, options = {}) {
@@ -93,7 +93,7 @@ async function readConnectionForWrite(db, id, options = {}) {
93
93
  const row = await query.executeTakeFirst();
94
94
  if (!row) return null;
95
95
  return {
96
- connection: ControlPlaneConnection.parse(row),
96
+ connection: parseConnectionRow(row),
97
97
  storedKeys: row.keys,
98
98
  storedTrusts: row.trusts
99
99
  };
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { noopEmit } from "../../audit/recorder.js";
4
4
  import { MIGRATIONS_READ_SCOPE } from "../scope/scope.js";
5
- import { ControlPlaneConnection } from "../data/connection.js";
5
+ import { parseConnectionRow } from "../data/connection.js";
6
6
  import { CONTROL_PLANE_CONNECTIONS_TABLE } from "../data/tables.js";
7
7
  import { workerVersionMetadata } from "../../worker/identity.js";
8
8
  import { readCapabilityHealth } from "../discovery/health.js";
@@ -17,7 +17,7 @@ import { deriveAuditLogging, deriveChangeHistory, deriveCsrf, deriveDataProtecti
17
17
  async function readConnections(c, sources, environment) {
18
18
  try {
19
19
  return (await sources.database(c).selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().where("environment", "=", environment).orderBy("createdAt").execute()).map((row) => {
20
- const connection = ControlPlaneConnection.parse(row);
20
+ const connection = parseConnectionRow(row);
21
21
  return {
22
22
  id: connection.id,
23
23
  authorizedBy: connection.authorizedBy,
@@ -0,0 +1,14 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { Migration } from "kysely/migration";
4
+ /**
5
+ * An empty nullable column reads as NULL again.
6
+ *
7
+ * `''` is not a value any of them accepts: not a URL, not an email, not an actor kind, shorter than a purpose.
8
+ * A connection holding one cannot be read, so every call on it is refused, and the only writer of one bound NULL
9
+ * as an empty string. So `''` in any of them is NULL, put back.
10
+ *
11
+ * `down` writes nothing. An empty string is not a state to return to: it is the one that refuses every call.
12
+ */
13
+ export declare const controlplane_0006_connection_nulls: Migration;
14
+ //# sourceMappingURL=0006_connection_nulls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"0006_connection_nulls.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/migrations/0006_connection_nulls.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAKlD;;;;;;;;GAQG;AACH,eAAO,MAAM,kCAAkC,EAAE,SAahD,CAAC"}
@@ -0,0 +1,28 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ //#region src/controlPlane/migrations/0006_connection_nulls.ts
4
+ /** The nullable columns `''` cannot satisfy, as they stood when this migration was written. */
5
+ const NULLABLE_COLUMNS = [
6
+ "managementOrigin",
7
+ "authorizedBy",
8
+ "actorKind",
9
+ "purpose"
10
+ ];
11
+ /**
12
+ * An empty nullable column reads as NULL again.
13
+ *
14
+ * `''` is not a value any of them accepts: not a URL, not an email, not an actor kind, shorter than a purpose.
15
+ * A connection holding one cannot be read, so every call on it is refused, and the only writer of one bound NULL
16
+ * as an empty string. So `''` in any of them is NULL, put back.
17
+ *
18
+ * `down` writes nothing. An empty string is not a state to return to: it is the one that refuses every call.
19
+ */
20
+ const controlplane_0006_connection_nulls = {
21
+ up: async (db) => {
22
+ const table = db;
23
+ for (const column of NULLABLE_COLUMNS) await table.updateTable("pithyControlplaneConnections").set({ [column]: null }).where(column, "=", "").execute();
24
+ },
25
+ down: async () => {}
26
+ };
27
+ //#endregion
28
+ export { controlplane_0006_connection_nulls };
@@ -3,5 +3,5 @@
3
3
  /** This package's npm name — the join key against a release feed. */
4
4
  export declare const PACKAGE_NAME = "@pithy-sh/core";
5
5
  /** This package's version, stamped from its own package.json at generation time. */
6
- export declare const PACKAGE_VERSION = "0.16.0";
6
+ export declare const PACKAGE_VERSION = "0.16.1";
7
7
  //# sourceMappingURL=version.generated.d.ts.map
@@ -4,6 +4,6 @@
4
4
  /** This package's npm name — the join key against a release feed. */
5
5
  const PACKAGE_NAME = "@pithy-sh/core";
6
6
  /** This package's version, stamped from its own package.json at generation time. */
7
- const PACKAGE_VERSION = "0.16.0";
7
+ const PACKAGE_VERSION = "0.16.1";
8
8
  //#endregion
9
9
  export { PACKAGE_NAME, PACKAGE_VERSION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pithy-sh/core",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,7 @@ import { holdsUserPurge } from "../retention/holdsPurge";
19
19
  import { PACKAGE_NAME, PACKAGE_VERSION } from "../version.generated";
20
20
  import { ENVIRONMENT_VAR } from "../worker/identity";
21
21
  import { ControlPlaneConfig, type ControlPlaneConfigInput } from "./config/config";
22
- import { ControlPlaneConnection } from "./data/connection";
22
+ import { parseConnectionRow } from "./data/connection";
23
23
  import { CONTROL_PLANE_CONNECTIONS_TABLE, type ControlPlaneDatabase, controlPlaneTables } from "./data/tables";
24
24
  import { recordTrustFirstCall } from "./data/trustFirstCall";
25
25
  import type { CapabilityDeclaration } from "./discovery/adminRoute";
@@ -37,6 +37,7 @@ import { controlplane_0002_management_origin } from "./migrations/0002_managemen
37
37
  import { controlplane_0003_connection_principal } from "./migrations/0003_connection_principal";
38
38
  import { controlplane_0004_connection_trusts } from "./migrations/0004_connection_trusts";
39
39
  import { controlplane_0005_holds } from "./migrations/0005_holds";
40
+ import { controlplane_0006_connection_nulls } from "./migrations/0006_connection_nulls";
40
41
  import { d1ReplayGuard } from "./replay/d1Guard";
41
42
  import type { ReplayGuard } from "./replay/guard";
42
43
  import {
@@ -113,6 +114,7 @@ export function controlplane(options: ControlPlaneOptions = {}): ControlPlaneCap
113
114
  "0003_connection_principal": controlplane_0003_connection_principal,
114
115
  "0004_connection_trusts": controlplane_0004_connection_trusts,
115
116
  "0005_holds": controlplane_0005_holds,
117
+ "0006_connection_nulls": controlplane_0006_connection_nulls,
116
118
  };
117
119
  const requiredBindings: BindingSpecInput[] = [
118
120
  // The app database — the connections table lives here, beside auth's and audit's. Not the secrets
@@ -291,7 +293,7 @@ export function controlplane(options: ControlPlaneOptions = {}): ControlPlaneCap
291
293
  .selectAll()
292
294
  .where("id", "=", connectionId)
293
295
  .executeTakeFirst();
294
- return row ? ControlPlaneConnection.parse(row) : null;
296
+ return row ? parseConnectionRow(row) : null;
295
297
  },
296
298
  countConnections: async () => {
297
299
  const rows = await db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).select("id").limit(1).execute();
@@ -313,7 +315,7 @@ export function controlplane(options: ControlPlaneOptions = {}): ControlPlaneCap
313
315
  .selectAll()
314
316
  .where("environment", "=", environment)
315
317
  .executeTakeFirst();
316
- return row ? ControlPlaneConnection.parse(row) : null;
318
+ return row ? parseConnectionRow(row) : null;
317
319
  },
318
320
  environment,
319
321
  now,
@@ -2,7 +2,9 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
4
  import { z } from "zod";
5
+ import { commandName } from "../../cli/command";
5
6
  import { JsonDate, SQLiteDate, sqliteJson } from "../../data/codecs";
7
+ import { InternalError } from "../../error/pithyError";
6
8
  import { ControlPlaneScope } from "../scope/scope";
7
9
  import { OidcTrust } from "./oidcTrust";
8
10
 
@@ -232,3 +234,37 @@ export const ControlPlaneConnection = z
232
234
  );
233
235
  export type ControlPlaneConnection = z.output<typeof ControlPlaneConnection>;
234
236
  export type ControlPlaneConnectionRow = z.input<typeof ControlPlaneConnection>;
237
+
238
+ /** The SQLite column a schema path names: `authorizedBy` → `authorized_by`, or `row` for the row itself. */
239
+ function columnOf(path: readonly PropertyKey[]): string {
240
+ const field = path[0];
241
+ return field === undefined ? "row" : String(field).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
242
+ }
243
+
244
+ /** The nullable columns a NULL in which means "not recorded", and which `pithy migrate` repairs when they hold `''`. */
245
+ export const CONNECTION_NULLABLE_COLUMNS = ["managementOrigin", "authorizedBy", "actorKind", "purpose"] as const;
246
+
247
+ /**
248
+ * Decode one stored connection row, or refuse it with the columns that would not parse.
249
+ *
250
+ * Every reader of the table goes through this, the Worker's verifier and the CLI's registry alike, so a row
251
+ * nobody can read is named the same way wherever it is met, never surfaced as a bare `ZodError`. The message is
252
+ * the default, because the verifier answers before a caller is authenticated.
253
+ */
254
+ export function parseConnectionRow(row: unknown): ControlPlaneConnection {
255
+ const parsed = ControlPlaneConnection.safeParse(row);
256
+ if (parsed.success) return parsed.data;
257
+ const columns = [...new Set(parsed.error.issues.map((issue) => columnOf(issue.path)))];
258
+ const repairable = CONNECTION_NULLABLE_COLUMNS.map((column) => columnOf([column]));
259
+ const id = typeof row === "object" && row !== null && "id" in row ? String(row.id) : "unknown";
260
+ const repair = columns.some((column) => repairable.includes(column))
261
+ ? ` ${repairable.join(", ")} read NULL as not recorded, and an empty string in one is not NULL. Run ${commandName("migrate")} for its environment, which puts it back.`
262
+ : "";
263
+ throw new InternalError(
264
+ {
265
+ action: `${columns.join(", ")} in pithy_controlplane_connections ${columns.length === 1 ? "holds a value" : "hold values"} this release cannot read.${repair}`,
266
+ detail: `Connection ${id}: ${parsed.error.issues.map((issue) => `${columnOf(issue.path)}: ${issue.message}`).join("; ")}`,
267
+ },
268
+ { cause: parsed.error },
269
+ );
270
+ }
@@ -3,7 +3,12 @@
3
3
 
4
4
  import { sql } from "kysely";
5
5
  import { ConflictError } from "../../error/pithyError";
6
- import { type ConnectionCredentialKind, ControlPlaneConnection, type RegisteredKey } from "./connection";
6
+ import {
7
+ type ConnectionCredentialKind,
8
+ ControlPlaneConnection,
9
+ parseConnectionRow,
10
+ type RegisteredKey,
11
+ } from "./connection";
7
12
  import { activeKeys } from "./keyLifecycle";
8
13
  import { activeTrusts, type OidcTrust } from "./oidcTrust";
9
14
  import { CONTROL_PLANE_CONNECTIONS_TABLE, type ControlPlaneDatabase } from "./tables";
@@ -104,7 +109,7 @@ export async function listConnections(
104
109
  ): Promise<ConnectionListing> {
105
110
  let query = db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().orderBy("createdAt").orderBy("id");
106
111
  if (filter.environment !== undefined) query = query.where("environment", "=", filter.environment);
107
- const rows = (await query.execute()).map((row) => ControlPlaneConnection.parse(row));
112
+ const rows = (await query.execute()).map((row) => parseConnectionRow(row));
108
113
  const { principal } = filter;
109
114
  return {
110
115
  connections: rows.filter(
@@ -117,7 +122,7 @@ export async function listConnections(
117
122
  /** Read one connection by id, or null. */
118
123
  export async function readConnection(db: ControlPlaneDatabase, id: string): Promise<ControlPlaneConnection | null> {
119
124
  const row = await db.selectFrom(CONTROL_PLANE_CONNECTIONS_TABLE).selectAll().where("id", "=", id).executeTakeFirst();
120
- return row ? ControlPlaneConnection.parse(row) : null;
125
+ return row ? parseConnectionRow(row) : null;
121
126
  }
122
127
 
123
128
  /**
@@ -142,7 +147,7 @@ export async function readConnectionForWrite(
142
147
  if (options.environment !== undefined) query = query.where("environment", "=", options.environment);
143
148
  const row = await query.executeTakeFirst();
144
149
  if (!row) return null;
145
- return { connection: ControlPlaneConnection.parse(row), storedKeys: row.keys, storedTrusts: row.trusts };
150
+ return { connection: parseConnectionRow(row), storedKeys: row.keys, storedTrusts: row.trusts };
146
151
  }
147
152
 
148
153
  /**
@@ -12,7 +12,7 @@ import type { PrivacyInventory } from "../../privacy/report";
12
12
  import type { RetirementVerdict } from "../../privacy/retirement";
13
13
  import { workerVersionMetadata } from "../../worker/identity";
14
14
  import type { ControlPlaneContext } from "../context";
15
- import { ControlPlaneConnection } from "../data/connection";
15
+ import { parseConnectionRow } from "../data/connection";
16
16
  import { CONTROL_PLANE_CONNECTIONS_TABLE, type ControlPlaneDatabase } from "../data/tables";
17
17
  import { type CapabilityHealthSource, readCapabilityHealth } from "../discovery/health";
18
18
  import type { ControlPlaneEvidenceResponse } from "../http/responses";
@@ -82,7 +82,7 @@ async function readConnections(
82
82
  .execute();
83
83
  return rows.map((row) => {
84
84
  // Only the fields the pack reports and the key windows it ages. The public halves stay in the row.
85
- const connection = ControlPlaneConnection.parse(row);
85
+ const connection = parseConnectionRow(row);
86
86
  return {
87
87
  id: connection.id,
88
88
  authorizedBy: connection.authorizedBy,
@@ -0,0 +1,32 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { Kysely } from "kysely";
5
+ import type { Migration } from "kysely/migration";
6
+
7
+ /** The nullable columns `''` cannot satisfy, as they stood when this migration was written. */
8
+ const NULLABLE_COLUMNS = ["managementOrigin", "authorizedBy", "actorKind", "purpose"] as const;
9
+
10
+ /**
11
+ * An empty nullable column reads as NULL again.
12
+ *
13
+ * `''` is not a value any of them accepts: not a URL, not an email, not an actor kind, shorter than a purpose.
14
+ * A connection holding one cannot be read, so every call on it is refused, and the only writer of one bound NULL
15
+ * as an empty string. So `''` in any of them is NULL, put back.
16
+ *
17
+ * `down` writes nothing. An empty string is not a state to return to: it is the one that refuses every call.
18
+ */
19
+ export const controlplane_0006_connection_nulls: Migration = {
20
+ up: async (db: Kysely<unknown>): Promise<void> => {
21
+ const table = db as unknown as Kysely<{ pithyControlplaneConnections: Record<string, string | null> }>;
22
+ for (const column of NULLABLE_COLUMNS) {
23
+ await table
24
+ .updateTable("pithyControlplaneConnections")
25
+ .set({ [column]: null })
26
+ .where(column, "=", "")
27
+ .execute();
28
+ }
29
+ },
30
+
31
+ down: async (): Promise<void> => {},
32
+ };
@@ -13,4 +13,4 @@
13
13
  export const PACKAGE_NAME = "@pithy-sh/core";
14
14
 
15
15
  /** This package's version, stamped from its own package.json at generation time. */
16
- export const PACKAGE_VERSION = "0.16.0";
16
+ export const PACKAGE_VERSION = "0.16.1";