@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.
- package/dist/controlPlane/capability.d.ts.map +1 -1
- package/dist/controlPlane/capability.js +6 -4
- package/dist/controlPlane/data/connection.d.ts +10 -0
- package/dist/controlPlane/data/connection.d.ts.map +1 -1
- package/dist/controlPlane/data/connection.js +34 -1
- package/dist/controlPlane/data/connections.d.ts.map +1 -1
- package/dist/controlPlane/data/connections.js +4 -4
- package/dist/controlPlane/evidence/pack.js +2 -2
- package/dist/controlPlane/migrations/0006_connection_nulls.d.ts +14 -0
- package/dist/controlPlane/migrations/0006_connection_nulls.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0006_connection_nulls.js +28 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +1 -1
- package/src/controlPlane/capability.ts +5 -3
- package/src/controlPlane/data/connection.ts +36 -0
- package/src/controlPlane/data/connections.ts +9 -4
- package/src/controlPlane/evidence/pack.ts +2 -2
- package/src/controlPlane/migrations/0006_connection_nulls.ts +32 -0
- package/src/version.generated.ts +1 -1
|
@@ -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;
|
|
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 {
|
|
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 ?
|
|
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 ?
|
|
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;
|
|
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":"AAKA,OAAO,
|
|
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) =>
|
|
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 ?
|
|
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:
|
|
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 {
|
|
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 =
|
|
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.
|
|
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.
|
|
7
|
+
const PACKAGE_VERSION = "0.16.1";
|
|
8
8
|
//#endregion
|
|
9
9
|
export { PACKAGE_NAME, PACKAGE_VERSION };
|
package/package.json
CHANGED
|
@@ -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 {
|
|
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 ?
|
|
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 ?
|
|
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 {
|
|
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) =>
|
|
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 ?
|
|
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:
|
|
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 {
|
|
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 =
|
|
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
|
+
};
|
package/src/version.generated.ts
CHANGED