@pithy-sh/core 0.10.0 → 0.11.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/dist/access/access.d.ts +172 -0
- package/dist/access/access.d.ts.map +1 -0
- package/dist/access/access.js +147 -0
- package/dist/access/acting.d.ts +49 -0
- package/dist/access/acting.d.ts.map +1 -0
- package/dist/access/acting.js +21 -0
- package/dist/access/policyCheck.d.ts +33 -0
- package/dist/access/policyCheck.d.ts.map +1 -0
- package/dist/access/policyCheck.js +48 -0
- package/dist/access/usage.d.ts +83 -0
- package/dist/access/usage.d.ts.map +1 -0
- package/dist/access/usage.js +104 -0
- package/dist/access/visibility.d.ts +66 -0
- package/dist/access/visibility.d.ts.map +1 -0
- package/dist/access/visibility.js +86 -0
- package/dist/audit/auditEvent.d.ts +5 -0
- package/dist/audit/auditEvent.d.ts.map +1 -1
- package/dist/audit/auditEvent.js +2 -0
- package/dist/capability/bindings.d.ts +47 -2
- package/dist/capability/bindings.d.ts.map +1 -1
- package/dist/capability/bindings.js +117 -7
- package/dist/capability/capability.d.ts +98 -28
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/manifest.d.ts +2 -1
- package/dist/capability/manifest.d.ts.map +1 -1
- package/dist/capability/manifest.js +1 -1
- package/dist/capability/settings.d.ts +86 -2
- package/dist/capability/settings.d.ts.map +1 -1
- package/dist/capability/settings.js +80 -1
- package/dist/cli/commandCatalog.generated.d.ts +184 -22
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
- package/dist/cli/commandCatalog.generated.js +196 -22
- package/dist/cli/commandFacts.d.ts +1 -1
- package/dist/cli/commandFacts.d.ts.map +1 -1
- package/dist/cli/commandFacts.js +1 -0
- package/dist/controlPlane/capability.d.ts.map +1 -1
- package/dist/controlPlane/capability.js +12 -2
- package/dist/controlPlane/context.d.ts +5 -0
- package/dist/controlPlane/context.d.ts.map +1 -1
- package/dist/controlPlane/context.js +3 -1
- package/dist/controlPlane/data/connection.d.ts +47 -0
- package/dist/controlPlane/data/connection.d.ts.map +1 -1
- package/dist/controlPlane/data/connection.js +55 -1
- package/dist/controlPlane/http/cors.d.ts +4 -4
- package/dist/controlPlane/http/cors.js +4 -4
- package/dist/controlPlane/http/guard.d.ts +11 -0
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +17 -1
- package/dist/controlPlane/http/handlers.d.ts +60 -0
- package/dist/controlPlane/http/handlers.d.ts.map +1 -1
- package/dist/controlPlane/http/handlers.js +31 -2
- package/dist/controlPlane/http/readiness.d.ts +33 -0
- package/dist/controlPlane/http/readiness.d.ts.map +1 -0
- package/dist/controlPlane/http/readiness.js +39 -0
- package/dist/controlPlane/http/responses.d.ts +147 -0
- package/dist/controlPlane/http/responses.d.ts.map +1 -1
- package/dist/controlPlane/http/responses.js +61 -4
- package/dist/controlPlane/http/routes.d.ts +1 -0
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +17 -3
- package/dist/controlPlane/http/verify.d.ts.map +1 -1
- package/dist/controlPlane/http/verify.js +2 -1
- package/dist/controlPlane/migrationState/migrationState.d.ts +102 -0
- package/dist/controlPlane/migrationState/migrationState.d.ts.map +1 -0
- package/dist/controlPlane/migrationState/migrationState.js +142 -0
- package/dist/controlPlane/migrations/0003_connection_principal.d.ts +21 -0
- package/dist/controlPlane/migrations/0003_connection_principal.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0003_connection_principal.js +33 -0
- package/dist/controlPlane/scope/scope.d.ts +13 -2
- package/dist/controlPlane/scope/scope.d.ts.map +1 -1
- package/dist/controlPlane/scope/scope.js +14 -3
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +7 -2
- package/dist/createEntrypoint.d.ts.map +1 -1
- package/dist/createEntrypoint.js +5 -3
- package/dist/entitlement/allowance.d.ts +25 -0
- package/dist/entitlement/allowance.d.ts.map +1 -0
- package/dist/entitlement/allowance.js +80 -0
- package/dist/entitlement/entitlement.d.ts +41 -0
- package/dist/entitlement/entitlement.d.ts.map +1 -1
- package/dist/entitlement/meter.d.ts +57 -0
- package/dist/entitlement/meter.d.ts.map +1 -0
- package/dist/entitlement/meter.js +13 -0
- package/dist/error/http.d.ts +144 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +288 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +100 -4
- package/dist/http/authContext.d.ts +7 -0
- package/dist/http/authContext.d.ts.map +1 -1
- package/dist/http/authContext.js +8 -1
- package/dist/http/scopeGates.d.ts +20 -0
- package/dist/http/scopeGates.d.ts.map +1 -0
- package/dist/http/scopeGates.js +36 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/logger/record.d.ts +84 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/migrations/capabilitySets.d.ts +23 -0
- package/dist/migrations/capabilitySets.d.ts.map +1 -0
- package/dist/migrations/capabilitySets.js +74 -0
- package/dist/migrations/registry.d.ts +26 -0
- package/dist/migrations/registry.d.ts.map +1 -1
- package/dist/migrations/registry.js +38 -7
- package/dist/migrations/state.d.ts +60 -0
- package/dist/migrations/state.d.ts.map +1 -0
- package/dist/migrations/state.js +46 -0
- package/dist/naming/feature.d.ts +27 -5
- package/dist/naming/feature.d.ts.map +1 -1
- package/dist/naming/feature.js +36 -6
- package/dist/naming/limits.d.ts +47 -3
- package/dist/naming/limits.d.ts.map +1 -1
- package/dist/naming/limits.js +47 -3
- package/dist/naming/provisionScope.d.ts +6 -0
- package/dist/naming/provisionScope.d.ts.map +1 -1
- package/dist/naming/provisionScope.js +8 -3
- package/dist/naming/resourceNames.d.ts +21 -2
- package/dist/naming/resourceNames.d.ts.map +1 -1
- package/dist/naming/resourceNames.js +21 -3
- package/dist/seed/writeD1.d.ts +7 -1
- package/dist/seed/writeD1.d.ts.map +1 -1
- package/dist/seed/writeD1.js +9 -3
- package/dist/seed/writeKv.d.ts +2 -0
- package/dist/seed/writeKv.d.ts.map +1 -1
- package/dist/seed/writeKv.js +6 -2
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/worker/serviceLoopback.d.ts +36 -0
- package/dist/worker/serviceLoopback.d.ts.map +1 -0
- package/dist/worker/serviceLoopback.js +74 -0
- package/dist/workflow/faults.d.ts +12 -1
- package/dist/workflow/faults.d.ts.map +1 -1
- package/dist/workflow/faults.js +22 -15
- package/package.json +2 -2
- package/src/access/access.ts +298 -0
- package/src/access/acting.ts +59 -0
- package/src/access/policyCheck.ts +77 -0
- package/src/access/usage.ts +141 -0
- package/src/access/visibility.ts +109 -0
- package/src/audit/auditEvent.ts +4 -0
- package/src/capability/bindings.ts +159 -13
- package/src/capability/capability.ts +99 -29
- package/src/capability/settings.ts +146 -2
- package/src/cli/commandCatalog.generated.ts +212 -22
- package/src/cli/commandFacts.ts +1 -0
- package/src/controlPlane/capability.ts +25 -1
- package/src/controlPlane/context.ts +4 -0
- package/src/controlPlane/data/connection.ts +90 -0
- package/src/controlPlane/http/cors.ts +4 -4
- package/src/controlPlane/http/guard.ts +21 -0
- package/src/controlPlane/http/handlers.ts +41 -0
- package/src/controlPlane/http/readiness.ts +50 -0
- package/src/controlPlane/http/responses.ts +140 -2
- package/src/controlPlane/http/routes.ts +25 -1
- package/src/controlPlane/http/verify.ts +2 -0
- package/src/controlPlane/migrationState/migrationState.ts +200 -0
- package/src/controlPlane/migrations/0003_connection_principal.ts +37 -0
- package/src/controlPlane/scope/scope.ts +14 -2
- package/src/createBackend.ts +24 -6
- package/src/createEntrypoint.ts +9 -3
- package/src/entitlement/allowance.ts +85 -0
- package/src/entitlement/entitlement.ts +38 -0
- package/src/entitlement/meter.ts +69 -0
- package/src/error/payload.ts +211 -4
- package/src/http/authContext.ts +13 -1
- package/src/http/scopeGates.ts +34 -0
- package/src/index.ts +1 -0
- package/src/migrations/capabilitySets.ts +87 -0
- package/src/migrations/registry.ts +46 -2
- package/src/migrations/state.ts +94 -0
- package/src/naming/feature.ts +46 -7
- package/src/naming/limits.ts +35 -3
- package/src/naming/provisionScope.ts +19 -1
- package/src/naming/resourceNames.ts +52 -5
- package/src/seed/writeD1.ts +13 -4
- package/src/seed/writeKv.ts +6 -2
- package/src/version.generated.ts +1 -1
- package/src/worker/serviceLoopback.ts +105 -0
- package/src/workflow/faults.ts +51 -24
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import type { MiddlewareHandler } from "hono";
|
|
4
|
+
import type { AuditEmit } from "../../audit/recorder";
|
|
4
5
|
import type { PithyHonoEnv } from "../../capability/capability";
|
|
5
6
|
import type { ControlPlaneContext } from "../context";
|
|
7
|
+
import type { ControlPlaneActorKind } from "../data/connection";
|
|
6
8
|
import type { ControlPlaneRequirement } from "../scope/scope";
|
|
7
9
|
import { type ControlPlaneVerifyDeps } from "./verify";
|
|
8
10
|
/**
|
|
@@ -76,4 +78,13 @@ export declare const CONTROL_PLANE_GUARD: unique symbol;
|
|
|
76
78
|
/** Whether a mounted handler is a control-plane gate. */
|
|
77
79
|
export declare function isControlPlaneGuard(handler: unknown): boolean;
|
|
78
80
|
export declare function requireControlPlane(requirement: ControlPlaneRequirement): MiddlewareHandler<PithyHonoEnv>;
|
|
81
|
+
/**
|
|
82
|
+
* The recorder a verified call's handlers write through: the request's own, with the connection's actor
|
|
83
|
+
* kind added to every `control-plane` event that does not already carry one.
|
|
84
|
+
*
|
|
85
|
+
* Only `control-plane` events, because the kind describes a management client and nothing else — a
|
|
86
|
+
* handler recording what happened to a user is recording a user. An event that states a kind keeps it.
|
|
87
|
+
* A connection with no recorded kind adds nothing, so its rows read exactly as they did before.
|
|
88
|
+
*/
|
|
89
|
+
export declare function stampActorKind(emit: AuditEmit, actorKind: ControlPlaneActorKind | null): AuditEmit;
|
|
79
90
|
//# sourceMappingURL=guard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/guard.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAMhE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/guard.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAMhE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAEhE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAE9D,OAAO,EAAE,KAAK,sBAAsB,EAA0B,MAAM,UAAU,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG,CACjC,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,uBAAuB,KACjC,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAElC;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,sBAAsB,GAAG,oBAAoB,CAgB7F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMzE;AAED;;;;;;GAMG;AACH;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,eAAwC,CAAC;AAEzE,yDAAyD;AACzD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAE7D;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,uBAAuB,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAuEzG;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,CAIlG"}
|
|
@@ -85,6 +85,7 @@ function requireControlPlane(requirement) {
|
|
|
85
85
|
throw cause;
|
|
86
86
|
}
|
|
87
87
|
c.set("controlPlane", context);
|
|
88
|
+
c.set("emit", stampActorKind(c.var.emit, context.actorKind));
|
|
88
89
|
await safeEmit(c.var.emit, {
|
|
89
90
|
action: ControlPlaneAuditActions.callAllowed,
|
|
90
91
|
outcome: "success",
|
|
@@ -110,6 +111,21 @@ function requireControlPlane(requirement) {
|
|
|
110
111
|
return guard;
|
|
111
112
|
}
|
|
112
113
|
/**
|
|
114
|
+
* The recorder a verified call's handlers write through: the request's own, with the connection's actor
|
|
115
|
+
* kind added to every `control-plane` event that does not already carry one.
|
|
116
|
+
*
|
|
117
|
+
* Only `control-plane` events, because the kind describes a management client and nothing else — a
|
|
118
|
+
* handler recording what happened to a user is recording a user. An event that states a kind keeps it.
|
|
119
|
+
* A connection with no recorded kind adds nothing, so its rows read exactly as they did before.
|
|
120
|
+
*/
|
|
121
|
+
function stampActorKind(emit, actorKind) {
|
|
122
|
+
if (actorKind === null) return emit;
|
|
123
|
+
return (event) => emit(event.actorType === "control-plane" && event.actorKind == null ? {
|
|
124
|
+
...event,
|
|
125
|
+
actorKind
|
|
126
|
+
} : event);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
113
129
|
* Record a refused call.
|
|
114
130
|
*
|
|
115
131
|
* **Denials are audited, not only successes.** `denied` is a first-class outcome precisely for this, and
|
|
@@ -151,4 +167,4 @@ function connectStep(env) {
|
|
|
151
167
|
return remedy("dashboard connect", { flags: { "--env": env } }, { runtime: true }).say((command) => `run ${command}`);
|
|
152
168
|
}
|
|
153
169
|
//#endregion
|
|
154
|
-
export { CONTROL_PLANE_GUARD, createControlPlaneVerifier, isControlPlaneGuard, requireControlPlane, workerVersionHeaders };
|
|
170
|
+
export { CONTROL_PLANE_GUARD, createControlPlaneVerifier, isControlPlaneGuard, requireControlPlane, stampActorKind, workerVersionHeaders };
|
|
@@ -6,6 +6,7 @@ import type { ControlPlaneConfig } from "../config/config";
|
|
|
6
6
|
import { type ControlPlaneDatabase } from "../data/tables";
|
|
7
7
|
import type { CapabilityDeclaration } from "../discovery/adminRoute";
|
|
8
8
|
import { type CapabilityHealthSource } from "../discovery/health";
|
|
9
|
+
import { type ComposedMigrationDatabase } from "../migrationState/migrationState";
|
|
9
10
|
import type { ExpireKeyParams, ExpireKeyRequest, RegisterKeyRequest } from "./schemas";
|
|
10
11
|
/** The pieces of the composed capability a handler needs. Passed in, so nothing reaches for a global. */
|
|
11
12
|
export interface ControlPlaneHandlerDeps {
|
|
@@ -31,6 +32,11 @@ export interface ControlPlaneHandlerDeps {
|
|
|
31
32
|
* value is not: the numbers are resolved per request, behind the scope each one is declared under.
|
|
32
33
|
*/
|
|
33
34
|
composedHealth: () => ReadonlyMap<string, CapabilityHealthSource>;
|
|
35
|
+
/**
|
|
36
|
+
* Every database this Worker binds, with the provider this build composes for it — captured by the same
|
|
37
|
+
* `compose` hook through `collectMigrationSets`, the function `pithy migrate` composes with (#729).
|
|
38
|
+
*/
|
|
39
|
+
composedMigrations: () => readonly ComposedMigrationDatabase[];
|
|
34
40
|
/** The clock, injected so a test can stand at any instant. */
|
|
35
41
|
now: () => Date;
|
|
36
42
|
}
|
|
@@ -108,6 +114,57 @@ export declare function manifestHandler(deps: ControlPlaneHandlerDeps): (c: Cont
|
|
|
108
114
|
}[];
|
|
109
115
|
grantedScopes: string[];
|
|
110
116
|
}, import("hono/utils/http-status").ContentfulStatusCode, "json">>;
|
|
117
|
+
/**
|
|
118
|
+
* `GET /control-plane/migrations` — where each database this Worker binds stands (#729).
|
|
119
|
+
*
|
|
120
|
+
* Read-only: it reports, and nothing over the seam runs, reverses or resets a migration.
|
|
121
|
+
*
|
|
122
|
+
* **One unreadable ledger costs one database its answer.** Each read catches its own failure and lands on
|
|
123
|
+
* `unreadable`, so `Promise.all` still resolves and the route answers 200 with what it has. The cause is
|
|
124
|
+
* logged here, for the operator, and never projected: a D1 error can name a table or a query.
|
|
125
|
+
*/
|
|
126
|
+
export declare function migrationsHandler(deps: ControlPlaneHandlerDeps): (c: Context<PithyHonoEnv>) => Promise<Response & import("hono").TypedResponse<{
|
|
127
|
+
environment: string;
|
|
128
|
+
databases: ({
|
|
129
|
+
state: "readable";
|
|
130
|
+
database: string;
|
|
131
|
+
binding: string;
|
|
132
|
+
applied: {
|
|
133
|
+
name: string;
|
|
134
|
+
namespace: string | null;
|
|
135
|
+
}[];
|
|
136
|
+
pending: {
|
|
137
|
+
name: string;
|
|
138
|
+
namespace: string | null;
|
|
139
|
+
}[];
|
|
140
|
+
undeclared: {
|
|
141
|
+
name: string;
|
|
142
|
+
namespace: string | null;
|
|
143
|
+
}[];
|
|
144
|
+
lastGroup: {
|
|
145
|
+
group: string;
|
|
146
|
+
appliedAt: string;
|
|
147
|
+
migrations: {
|
|
148
|
+
name: string;
|
|
149
|
+
namespace: string | null;
|
|
150
|
+
}[];
|
|
151
|
+
position: {
|
|
152
|
+
state: "top";
|
|
153
|
+
} | {
|
|
154
|
+
state: "buried";
|
|
155
|
+
above: string[];
|
|
156
|
+
ungrouped: {
|
|
157
|
+
name: string;
|
|
158
|
+
namespace: string | null;
|
|
159
|
+
}[];
|
|
160
|
+
};
|
|
161
|
+
} | null;
|
|
162
|
+
} | {
|
|
163
|
+
state: "unreadable";
|
|
164
|
+
database: string;
|
|
165
|
+
binding: string;
|
|
166
|
+
})[];
|
|
167
|
+
}, import("hono/utils/http-status").ContentfulStatusCode, "json">>;
|
|
111
168
|
/** `GET /control-plane/keys` — the registration state, so a client can surface a stale key. */
|
|
112
169
|
export declare function listKeysHandler(deps: ControlPlaneHandlerDeps): (c: Context<PithyHonoEnv>) => Promise<Response & import("hono").TypedResponse<{
|
|
113
170
|
connectionId: string;
|
|
@@ -123,6 +180,9 @@ export declare function listKeysHandler(deps: ControlPlaneHandlerDeps): (c: Cont
|
|
|
123
180
|
validUntil: string | null;
|
|
124
181
|
revokedAt: string | null;
|
|
125
182
|
}[];
|
|
183
|
+
authorizedBy: string | null;
|
|
184
|
+
actorKind: "agent" | "dashboard" | "script" | null;
|
|
185
|
+
purpose: string | null;
|
|
126
186
|
}, import("hono/utils/http-status").ContentfulStatusCode, "json">>;
|
|
127
187
|
/**
|
|
128
188
|
* `POST /control-plane/keys` — register a new public key, authenticated by the key it succeeds.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/handlers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/handlers.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAIhE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAI3D,OAAO,EAAmC,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC5F,OAAO,KAAK,EAAE,qBAAqB,EAA4B,MAAM,yBAAyB,CAAC;AAC/F,OAAO,EAAE,KAAK,sBAAsB,EAAwB,MAAM,qBAAqB,CAAC;AAGxF,OAAO,EAAE,KAAK,yBAAyB,EAA0B,MAAM,kCAAkC,CAAC;AAS1G,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAuBvF,yGAAyG;AACzG,MAAM,WAAW,uBAAuB;IACtC,yDAAyD;IACzD,MAAM,EAAE,kBAAkB,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,oBAAoB,CAAC;IAC7D;;;;;;;;OAQG;IACH,oBAAoB,EAAE,MAAM,SAAS,qBAAqB,EAAE,CAAC;IAC7D;;;;;;OAMG;IACH,cAAc,EAAE,MAAM,WAAW,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;IAClE;;;OAGG;IACH,kBAAkB,EAAE,MAAM,SAAS,yBAAyB,EAAE,CAAC;IAC/D,8DAA8D;IAC9D,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAgED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,uBAAuB,OAC5C,OAAO,CAAC,YAAY,CAAC;;;;;;kEAUjC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,uBAAuB,OAC1C,OAAO,CAAC,YAAY,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mEAyBvC;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,uBAAuB,OAC5C,OAAO,CAAC,YAAY,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mEAgBvC;AAED,+FAA+F;AAC/F,wBAAgB,eAAe,CAAC,IAAI,EAAE,uBAAuB,OAC1C,OAAO,CAAC,YAAY,CAAC;;;;;;;;;;;;;;;;;mEAcvC;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,uBAAuB,OAC7C,OAAO,CAAC,YAAY,CAAC,QAAQ,kBAAkB;;;;;;;;;;;;;;iBAoDjE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,uBAAuB,OAC3C,OAAO,CAAC,YAAY,CAAC,UAAU,eAAe,QAAQ,gBAAgB;;;;;;;;;;;;;;mEA8CxF"}
|
|
@@ -8,6 +8,8 @@ import { readCapabilityHealth } from "../discovery/health.js";
|
|
|
8
8
|
import { ControlPlaneAuditActions, safeEmit } from "../audit/actions.js";
|
|
9
9
|
import { ControlPlaneInvalidCredentialError, ControlPlaneKeyConflictError } from "../error/errors.js";
|
|
10
10
|
import { appendKey, expireKey, pruneKeys } from "../data/keyLifecycle.js";
|
|
11
|
+
import { causeMessage } from "../../error/cause.js";
|
|
12
|
+
import { readDatabaseMigrations } from "../migrationState/migrationState.js";
|
|
11
13
|
//#region src/controlPlane/http/handlers.ts
|
|
12
14
|
/**
|
|
13
15
|
* The seam's own route handlers.
|
|
@@ -121,6 +123,30 @@ function manifestHandler(deps) {
|
|
|
121
123
|
});
|
|
122
124
|
};
|
|
123
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* `GET /control-plane/migrations` — where each database this Worker binds stands (#729).
|
|
128
|
+
*
|
|
129
|
+
* Read-only: it reports, and nothing over the seam runs, reverses or resets a migration.
|
|
130
|
+
*
|
|
131
|
+
* **One unreadable ledger costs one database its answer.** Each read catches its own failure and lands on
|
|
132
|
+
* `unreadable`, so `Promise.all` still resolves and the route answers 200 with what it has. The cause is
|
|
133
|
+
* logged here, for the operator, and never projected: a D1 error can name a table or a query.
|
|
134
|
+
*/
|
|
135
|
+
function migrationsHandler(deps) {
|
|
136
|
+
return async (c) => {
|
|
137
|
+
const context = caller(c);
|
|
138
|
+
const env = c.env;
|
|
139
|
+
const databases = await Promise.all(deps.composedMigrations().map((entry) => readDatabaseMigrations(env, entry, (failed, cause) => c.var.log.warn("migration ledger unreadable", {
|
|
140
|
+
database: failed.database,
|
|
141
|
+
binding: failed.binding,
|
|
142
|
+
reason: causeMessage(cause) ?? String(cause)
|
|
143
|
+
}))));
|
|
144
|
+
return c.json({
|
|
145
|
+
environment: context.environment,
|
|
146
|
+
databases
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
}
|
|
124
150
|
/** `GET /control-plane/keys` — the registration state, so a client can surface a stale key. */
|
|
125
151
|
function listKeysHandler(deps) {
|
|
126
152
|
return async (c) => {
|
|
@@ -129,7 +155,10 @@ function listKeysHandler(deps) {
|
|
|
129
155
|
return c.json({
|
|
130
156
|
connectionId: connection.id,
|
|
131
157
|
environment: connection.environment,
|
|
132
|
-
keys: connection.keys.map(publicKeyView)
|
|
158
|
+
keys: connection.keys.map(publicKeyView),
|
|
159
|
+
authorizedBy: connection.authorizedBy,
|
|
160
|
+
actorKind: connection.actorKind,
|
|
161
|
+
purpose: connection.purpose
|
|
133
162
|
});
|
|
134
163
|
};
|
|
135
164
|
}
|
|
@@ -244,4 +273,4 @@ function expireKeyHandler(deps) {
|
|
|
244
273
|
};
|
|
245
274
|
}
|
|
246
275
|
//#endregion
|
|
247
|
-
export { expireKeyHandler, listKeysHandler, manifestHandler, pingHandler, registerKeyHandler };
|
|
276
|
+
export { expireKeyHandler, listKeysHandler, manifestHandler, migrationsHandler, pingHandler, registerKeyHandler };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { Hono } from "hono";
|
|
4
|
+
import type { PithyHonoEnv, PithyReadinessGuard } from "../../capability/capability";
|
|
5
|
+
/**
|
|
6
|
+
* Mount every capability's readiness guard, exempting the admin surface.
|
|
7
|
+
*
|
|
8
|
+
* **The control plane answers whatever else is wrong with the Worker.** It is the diagnostic surface: a
|
|
9
|
+
* management client reads the manifest, migration state and drift through it. A guard that refuses every
|
|
10
|
+
* request because an index drifted would take that surface down on exactly the fault it exists to report,
|
|
11
|
+
* and the client could not tell drift from a Worker that is simply down.
|
|
12
|
+
*
|
|
13
|
+
* **The exemption is the guard, not the middleware.** The seam itself runs on capability middleware —
|
|
14
|
+
* `controlplane()` publishes the verifier and `audit()` publishes `emit` — so skipping middleware on a
|
|
15
|
+
* seam request would trade a 500 for a seam that denies everything, or one that stops auditing without a
|
|
16
|
+
* word. Readiness guards are the only middleware that refuses because the Worker is broken, so they are
|
|
17
|
+
* the only thing skipped.
|
|
18
|
+
*
|
|
19
|
+
* **Hono decides what is on the surface, not a matcher written here.** One `app.use` per admin path marks
|
|
20
|
+
* the request, so a `:segment` matches exactly as it does for the route and the CORS layer, and a literal
|
|
21
|
+
* path never matches what is beneath it. The surface is the one `corsSurface(all)` computes, which
|
|
22
|
+
* `routeContract.test.ts` already holds to the registered routes, so there is no second list to drift.
|
|
23
|
+
*
|
|
24
|
+
* The mark is a `WeakSet` over the raw `Request`, closed over here. A context variable would put an
|
|
25
|
+
* internal flag on `PithyVars`, where every capability could read it and one could set it.
|
|
26
|
+
*
|
|
27
|
+
* Call after every capability's `middleware` and before any capability's `routes`. Before the routes,
|
|
28
|
+
* because a terminal handler never calls `next()` and a guard mounted after it never runs.
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerReadinessGuards(app: Hono<PithyHonoEnv>, surface: readonly {
|
|
31
|
+
path: string;
|
|
32
|
+
}[], guards: readonly PithyReadinessGuard[]): void;
|
|
33
|
+
//# sourceMappingURL=readiness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readiness.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/readiness.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAErF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,EACvB,OAAO,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,EACpC,MAAM,EAAE,SAAS,mBAAmB,EAAE,GACrC,IAAI,CAcN"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
//#region src/controlPlane/http/readiness.ts
|
|
4
|
+
/**
|
|
5
|
+
* Mount every capability's readiness guard, exempting the admin surface.
|
|
6
|
+
*
|
|
7
|
+
* **The control plane answers whatever else is wrong with the Worker.** It is the diagnostic surface: a
|
|
8
|
+
* management client reads the manifest, migration state and drift through it. A guard that refuses every
|
|
9
|
+
* request because an index drifted would take that surface down on exactly the fault it exists to report,
|
|
10
|
+
* and the client could not tell drift from a Worker that is simply down.
|
|
11
|
+
*
|
|
12
|
+
* **The exemption is the guard, not the middleware.** The seam itself runs on capability middleware —
|
|
13
|
+
* `controlplane()` publishes the verifier and `audit()` publishes `emit` — so skipping middleware on a
|
|
14
|
+
* seam request would trade a 500 for a seam that denies everything, or one that stops auditing without a
|
|
15
|
+
* word. Readiness guards are the only middleware that refuses because the Worker is broken, so they are
|
|
16
|
+
* the only thing skipped.
|
|
17
|
+
*
|
|
18
|
+
* **Hono decides what is on the surface, not a matcher written here.** One `app.use` per admin path marks
|
|
19
|
+
* the request, so a `:segment` matches exactly as it does for the route and the CORS layer, and a literal
|
|
20
|
+
* path never matches what is beneath it. The surface is the one `corsSurface(all)` computes, which
|
|
21
|
+
* `routeContract.test.ts` already holds to the registered routes, so there is no second list to drift.
|
|
22
|
+
*
|
|
23
|
+
* The mark is a `WeakSet` over the raw `Request`, closed over here. A context variable would put an
|
|
24
|
+
* internal flag on `PithyVars`, where every capability could read it and one could set it.
|
|
25
|
+
*
|
|
26
|
+
* Call after every capability's `middleware` and before any capability's `routes`. Before the routes,
|
|
27
|
+
* because a terminal handler never calls `next()` and a guard mounted after it never runs.
|
|
28
|
+
*/
|
|
29
|
+
function registerReadinessGuards(app, surface, guards) {
|
|
30
|
+
if (guards.length === 0) return;
|
|
31
|
+
const onSurface = /* @__PURE__ */ new WeakSet();
|
|
32
|
+
for (const path of new Set(surface.map((entry) => entry.path))) app.use(path, async (c, next) => {
|
|
33
|
+
onSurface.add(c.req.raw);
|
|
34
|
+
await next();
|
|
35
|
+
});
|
|
36
|
+
for (const guard of guards) app.use("*", (c, next) => onSurface.has(c.req.raw) ? next() : guard(c, next));
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { registerReadinessGuards };
|
|
@@ -62,6 +62,13 @@ export declare const ControlPlaneKeysResponse: z.ZodObject<{
|
|
|
62
62
|
validUntil: z.ZodNullable<z.ZodISODateTime>;
|
|
63
63
|
revokedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
64
64
|
}, z.core.$strip>>;
|
|
65
|
+
authorizedBy: z.ZodOptional<z.ZodNullable<z.ZodEmail>>;
|
|
66
|
+
actorKind: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
67
|
+
agent: "agent";
|
|
68
|
+
dashboard: "dashboard";
|
|
69
|
+
script: "script";
|
|
70
|
+
}>>>;
|
|
71
|
+
purpose: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
65
72
|
}, z.core.$strip>;
|
|
66
73
|
export type ControlPlaneKeysResponse = z.output<typeof ControlPlaneKeysResponse>;
|
|
67
74
|
/** `POST /control-plane/keys` — the appended key, and the set as it now stands. */
|
|
@@ -98,4 +105,144 @@ export declare const ExpireKeyResponse: z.ZodObject<{
|
|
|
98
105
|
}, z.core.$strip>>;
|
|
99
106
|
}, z.core.$strip>;
|
|
100
107
|
export type ExpireKeyResponse = z.output<typeof ExpireKeyResponse>;
|
|
108
|
+
/**
|
|
109
|
+
* One migration as `GET {basePath}/migrations` reports it: its composed name, and whose it is.
|
|
110
|
+
*
|
|
111
|
+
* The namespace is a capability's name, which the manifest already reports in `capabilities[]`. So a
|
|
112
|
+
* client tells an adopter's own migrations from a composed capability's by comparing the two — no
|
|
113
|
+
* convention, and no list kept anywhere.
|
|
114
|
+
*/
|
|
115
|
+
export declare const MigrationView: z.ZodObject<{
|
|
116
|
+
name: z.ZodString;
|
|
117
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
118
|
+
}, z.core.$strip>;
|
|
119
|
+
export type MigrationView = z.output<typeof MigrationView>;
|
|
120
|
+
/** Whether the last group is still reversible by name, or what is in its way. */
|
|
121
|
+
export declare const MigrationGroupPositionView: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
122
|
+
state: z.ZodLiteral<"top">;
|
|
123
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
124
|
+
state: z.ZodLiteral<"buried">;
|
|
125
|
+
above: z.ZodArray<z.ZodString>;
|
|
126
|
+
ungrouped: z.ZodArray<z.ZodObject<{
|
|
127
|
+
name: z.ZodString;
|
|
128
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
129
|
+
}, z.core.$strip>>;
|
|
130
|
+
}, z.core.$strip>], "state">;
|
|
131
|
+
export type MigrationGroupPositionView = z.output<typeof MigrationGroupPositionView>;
|
|
132
|
+
/** The run that last moved a database, and whether it can still be taken back. */
|
|
133
|
+
export declare const MigrationGroupView: z.ZodObject<{
|
|
134
|
+
group: z.ZodString;
|
|
135
|
+
appliedAt: z.ZodISODateTime;
|
|
136
|
+
migrations: z.ZodArray<z.ZodObject<{
|
|
137
|
+
name: z.ZodString;
|
|
138
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
139
|
+
}, z.core.$strip>>;
|
|
140
|
+
position: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
141
|
+
state: z.ZodLiteral<"top">;
|
|
142
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
143
|
+
state: z.ZodLiteral<"buried">;
|
|
144
|
+
above: z.ZodArray<z.ZodString>;
|
|
145
|
+
ungrouped: z.ZodArray<z.ZodObject<{
|
|
146
|
+
name: z.ZodString;
|
|
147
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
148
|
+
}, z.core.$strip>>;
|
|
149
|
+
}, z.core.$strip>], "state">;
|
|
150
|
+
}, z.core.$strip>;
|
|
151
|
+
export type MigrationGroupView = z.output<typeof MigrationGroupView>;
|
|
152
|
+
/**
|
|
153
|
+
* One database this Worker binds, read or not.
|
|
154
|
+
*
|
|
155
|
+
* **An unreadable database carries no list at all**, never empty ones. `pending: []` reads as up to date,
|
|
156
|
+
* and a database nobody read is not that. The state is on the value, so a client reaches a list only by
|
|
157
|
+
* narrowing.
|
|
158
|
+
*/
|
|
159
|
+
export declare const MigrationDatabaseView: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
160
|
+
state: z.ZodLiteral<"readable">;
|
|
161
|
+
database: z.ZodString;
|
|
162
|
+
binding: z.ZodString;
|
|
163
|
+
applied: z.ZodArray<z.ZodObject<{
|
|
164
|
+
name: z.ZodString;
|
|
165
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
166
|
+
}, z.core.$strip>>;
|
|
167
|
+
pending: z.ZodArray<z.ZodObject<{
|
|
168
|
+
name: z.ZodString;
|
|
169
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
170
|
+
}, z.core.$strip>>;
|
|
171
|
+
undeclared: z.ZodArray<z.ZodObject<{
|
|
172
|
+
name: z.ZodString;
|
|
173
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
174
|
+
}, z.core.$strip>>;
|
|
175
|
+
lastGroup: z.ZodNullable<z.ZodObject<{
|
|
176
|
+
group: z.ZodString;
|
|
177
|
+
appliedAt: z.ZodISODateTime;
|
|
178
|
+
migrations: z.ZodArray<z.ZodObject<{
|
|
179
|
+
name: z.ZodString;
|
|
180
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
181
|
+
}, z.core.$strip>>;
|
|
182
|
+
position: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
183
|
+
state: z.ZodLiteral<"top">;
|
|
184
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
185
|
+
state: z.ZodLiteral<"buried">;
|
|
186
|
+
above: z.ZodArray<z.ZodString>;
|
|
187
|
+
ungrouped: z.ZodArray<z.ZodObject<{
|
|
188
|
+
name: z.ZodString;
|
|
189
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
190
|
+
}, z.core.$strip>>;
|
|
191
|
+
}, z.core.$strip>], "state">;
|
|
192
|
+
}, z.core.$strip>>;
|
|
193
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
194
|
+
state: z.ZodLiteral<"unreadable">;
|
|
195
|
+
database: z.ZodString;
|
|
196
|
+
binding: z.ZodString;
|
|
197
|
+
}, z.core.$strip>], "state">;
|
|
198
|
+
export type MigrationDatabaseView = z.output<typeof MigrationDatabaseView>;
|
|
199
|
+
/**
|
|
200
|
+
* `GET /control-plane/migrations` — where every database this Worker binds stands.
|
|
201
|
+
*
|
|
202
|
+
* Asked of the Worker because the Worker *is* the deployed build. `pithy doctor` reads the project on disk,
|
|
203
|
+
* which is what the operator has and not necessarily what is deployed.
|
|
204
|
+
*/
|
|
205
|
+
export declare const ControlPlaneMigrationsResponse: z.ZodObject<{
|
|
206
|
+
environment: z.ZodString;
|
|
207
|
+
databases: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
208
|
+
state: z.ZodLiteral<"readable">;
|
|
209
|
+
database: z.ZodString;
|
|
210
|
+
binding: z.ZodString;
|
|
211
|
+
applied: z.ZodArray<z.ZodObject<{
|
|
212
|
+
name: z.ZodString;
|
|
213
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
214
|
+
}, z.core.$strip>>;
|
|
215
|
+
pending: z.ZodArray<z.ZodObject<{
|
|
216
|
+
name: z.ZodString;
|
|
217
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
218
|
+
}, z.core.$strip>>;
|
|
219
|
+
undeclared: z.ZodArray<z.ZodObject<{
|
|
220
|
+
name: z.ZodString;
|
|
221
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
222
|
+
}, z.core.$strip>>;
|
|
223
|
+
lastGroup: z.ZodNullable<z.ZodObject<{
|
|
224
|
+
group: z.ZodString;
|
|
225
|
+
appliedAt: z.ZodISODateTime;
|
|
226
|
+
migrations: z.ZodArray<z.ZodObject<{
|
|
227
|
+
name: z.ZodString;
|
|
228
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
229
|
+
}, z.core.$strip>>;
|
|
230
|
+
position: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
231
|
+
state: z.ZodLiteral<"top">;
|
|
232
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
233
|
+
state: z.ZodLiteral<"buried">;
|
|
234
|
+
above: z.ZodArray<z.ZodString>;
|
|
235
|
+
ungrouped: z.ZodArray<z.ZodObject<{
|
|
236
|
+
name: z.ZodString;
|
|
237
|
+
namespace: z.ZodNullable<z.ZodString>;
|
|
238
|
+
}, z.core.$strip>>;
|
|
239
|
+
}, z.core.$strip>], "state">;
|
|
240
|
+
}, z.core.$strip>>;
|
|
241
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
242
|
+
state: z.ZodLiteral<"unreadable">;
|
|
243
|
+
database: z.ZodString;
|
|
244
|
+
binding: z.ZodString;
|
|
245
|
+
}, z.core.$strip>], "state">>;
|
|
246
|
+
}, z.core.$strip>;
|
|
247
|
+
export type ControlPlaneMigrationsResponse = z.output<typeof ControlPlaneMigrationsResponse>;
|
|
101
248
|
//# sourceMappingURL=responses.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/responses.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;;;;;GAkBG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,aAAa;;;;;;;;;;iBAcmF,CAAC;AAC9G,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;;iBAQwB,CAAC;AAC9D,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEjF,+FAA+F;AAC/F,eAAO,MAAM,wBAAwB
|
|
1
|
+
{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/responses.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;;;;;GAkBG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,aAAa;;;;;;;;;;iBAcmF,CAAC;AAC9G,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;;iBAQwB,CAAC;AAC9D,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEjF,+FAA+F;AAC/F,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;iBAuBwE,CAAC;AAC9G,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEjF,mFAAmF;AACnF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;iBAQyB,CAAC;AAC1D,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEvE,gGAAgG;AAChG,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;iBAM6B,CAAC;AAC5D,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;iBAU+C,CAAC;AAC1E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D,iFAAiF;AACjF,eAAO,MAAM,0BAA0B;;;;;;;;;4BAyBkE,CAAC;AAC1G,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAErF,kFAAkF;AAClF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;iBAW8C,CAAC;AAC9E,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BA+BwE,CAAC;AAC3G,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO+D,CAAC;AAC3G,MAAM,MAAM,8BAA8B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,8BAA8B,CAAC,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import { Ed25519PublicJwk } from "../data/connection.js";
|
|
3
|
+
import { ConnectionPrincipal, ConnectionPurpose, ControlPlaneActorKind, Ed25519PublicJwk } from "../data/connection.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
//#region src/controlPlane/http/responses.ts
|
|
6
6
|
/**
|
|
@@ -47,8 +47,11 @@ const ControlPlanePingResponse = z.object({
|
|
|
47
47
|
const ControlPlaneKeysResponse = z.object({
|
|
48
48
|
connectionId: z.string().describe("The connection these keys belong to."),
|
|
49
49
|
environment: z.string().describe("The environment that connection is bound to."),
|
|
50
|
-
keys: z.array(PublicKeyView).describe("Every registered key, expired and revoked ones included.")
|
|
51
|
-
|
|
50
|
+
keys: z.array(PublicKeyView).describe("Every registered key, expired and revoked ones included."),
|
|
51
|
+
authorizedBy: ConnectionPrincipal.nullable().optional().describe("Who sanctioned this connection — the signed-in session's email at registration. Null for a connection registered before principals were recorded; absent from a Worker older than the field."),
|
|
52
|
+
actorKind: ControlPlaneActorKind.nullable().optional().describe("What kind of client this connection is for: `agent`, `dashboard` or `script`. Null for a connection registered before it was recorded; absent from an older Worker."),
|
|
53
|
+
purpose: ConnectionPurpose.nullable().optional().describe("What the client is for, in the registering operator's words. Untrusted text: render it escaped. Null for a connection registered before it was recorded; absent from an older Worker.")
|
|
54
|
+
}).describe("Every key registered against this connection, with its window, and who authorized it for what.");
|
|
52
55
|
/** `POST /control-plane/keys` — the appended key, and the set as it now stands. */
|
|
53
56
|
const RegisterKeyResponse = z.object({
|
|
54
57
|
keyId: z.string().describe("The key just registered."),
|
|
@@ -61,5 +64,59 @@ const ExpireKeyResponse = z.object({
|
|
|
61
64
|
validUntil: z.iso.datetime().describe("When its window closed, ISO-8601 — now, by construction."),
|
|
62
65
|
keys: z.array(PublicKeyView).describe("The connection's keys after the expiry.")
|
|
63
66
|
}).describe("The expired key, and the set it left behind.");
|
|
67
|
+
/**
|
|
68
|
+
* One migration as `GET {basePath}/migrations` reports it: its composed name, and whose it is.
|
|
69
|
+
*
|
|
70
|
+
* The namespace is a capability's name, which the manifest already reports in `capabilities[]`. So a
|
|
71
|
+
* client tells an adopter's own migrations from a composed capability's by comparing the two — no
|
|
72
|
+
* convention, and no list kept anywhere.
|
|
73
|
+
*/
|
|
74
|
+
const MigrationView = z.object({
|
|
75
|
+
name: z.string().describe("The composed migration name, exactly as the ledger records it: `0250_audit_0001_init`."),
|
|
76
|
+
namespace: z.string().nullable().describe("The capability this migration belongs to — its name, as `capabilities[]` in the manifest reports it. Null only for an undeclared migration whose name no Pithy registry could have composed.")
|
|
77
|
+
}).describe("One migration, and the capability namespace it belongs to.");
|
|
78
|
+
/** Whether the last group is still reversible by name, or what is in its way. */
|
|
79
|
+
const MigrationGroupPositionView = z.discriminatedUnion("state", [z.object({ state: z.literal("top").describe("The group's migrations are the contiguous tip of this database's chain. Not a promise that `pithy migrate --rollback --group <value>` runs: the CLI also refuses an undeclared migration in the ledger, and keeps back or refuses a database another environment binds.") }).describe("A group nothing on this database's ledger sits over."), z.object({
|
|
80
|
+
state: z.literal("buried").describe("Something sits over the group or inside it, so reversing it by name is refused."),
|
|
81
|
+
above: z.array(z.string()).describe("The groups in the way, newest first."),
|
|
82
|
+
ungrouped: z.array(MigrationView).describe("The migrations in the way that no group claims, newest first. Nothing can be reversed by group while one of them is at the tip.")
|
|
83
|
+
}).describe("A group with something in its way, and what that is.")]).describe("Where the last group sits in this database's chain: the top, or buried under named things.");
|
|
84
|
+
/** The run that last moved a database, and whether it can still be taken back. */
|
|
85
|
+
const MigrationGroupView = z.object({
|
|
86
|
+
group: z.string().describe("The group's value: the `--group` the run was given, or the ISO-8601 timestamp it generated."),
|
|
87
|
+
appliedAt: z.iso.datetime().describe("When this group last applied something here, ISO-8601."),
|
|
88
|
+
migrations: z.array(MigrationView).describe("What this group applied in this database and is still applied, in application order."),
|
|
89
|
+
position: MigrationGroupPositionView.describe("Whether the group is the top of the chain, or what is in its way.")
|
|
90
|
+
}).describe("The newest group with anything still applied in this database.");
|
|
91
|
+
/**
|
|
92
|
+
* One database this Worker binds, read or not.
|
|
93
|
+
*
|
|
94
|
+
* **An unreadable database carries no list at all**, never empty ones. `pending: []` reads as up to date,
|
|
95
|
+
* and a database nobody read is not that. The state is on the value, so a client reaches a list only by
|
|
96
|
+
* narrowing.
|
|
97
|
+
*/
|
|
98
|
+
const MigrationDatabaseView = z.discriminatedUnion("state", [z.object({
|
|
99
|
+
state: z.literal("readable").describe("This database's ledger was read."),
|
|
100
|
+
database: z.string().describe("The database name, as capabilities declare it in `databases`."),
|
|
101
|
+
binding: z.string().describe("The D1 binding it is read through, as `wrangler.jsonc` declares it."),
|
|
102
|
+
applied: z.array(MigrationView).describe("Every applied migration, in the order the ledger applied them."),
|
|
103
|
+
pending: z.array(MigrationView).describe("Declared by this build and not applied here, in the order they would run."),
|
|
104
|
+
undeclared: z.array(MigrationView).describe("Applied here and not declared by this Worker, sorted. Relative to this Worker: a database shared with another Worker lists that Worker's migrations here. Otherwise Kysely reads one as a corrupted chain and applies nothing."),
|
|
105
|
+
lastGroup: MigrationGroupView.nullable().describe("The newest group with anything applied here, or null when nothing applied here was ever grouped.")
|
|
106
|
+
}).describe("A database whose ledger was read."), z.object({
|
|
107
|
+
state: z.literal("unreadable").describe("This database's ledger could not be read on this call."),
|
|
108
|
+
database: z.string().describe("The database name, as capabilities declare it in `databases`."),
|
|
109
|
+
binding: z.string().describe("The D1 binding it would be read through.")
|
|
110
|
+
}).describe("A database that could not be read. No lists, so nothing about it reads as zero. The other databases still answer.")]).describe("One database this Worker binds: its migration state, or the fact that it could not be read.");
|
|
111
|
+
/**
|
|
112
|
+
* `GET /control-plane/migrations` — where every database this Worker binds stands.
|
|
113
|
+
*
|
|
114
|
+
* Asked of the Worker because the Worker *is* the deployed build. `pithy doctor` reads the project on disk,
|
|
115
|
+
* which is what the operator has and not necessarily what is deployed.
|
|
116
|
+
*/
|
|
117
|
+
const ControlPlaneMigrationsResponse = z.object({
|
|
118
|
+
environment: z.string().describe("The environment this Worker answers for."),
|
|
119
|
+
databases: z.array(MigrationDatabaseView).describe("One entry per database this Worker binds, sorted by database name. One unreadable costs only itself.")
|
|
120
|
+
}).describe("What is applied, pending and undeclared in each bound database, and which run put it there.");
|
|
64
121
|
//#endregion
|
|
65
|
-
export { ControlPlaneKeysResponse, ControlPlanePingResponse, ExpireKeyResponse, PublicKeyView, RegisterKeyResponse };
|
|
122
|
+
export { ControlPlaneKeysResponse, ControlPlaneMigrationsResponse, ControlPlanePingResponse, ExpireKeyResponse, MigrationDatabaseView, MigrationGroupPositionView, MigrationGroupView, MigrationView, PublicKeyView, RegisterKeyResponse };
|
|
@@ -12,6 +12,7 @@ import { type ControlPlaneHandlerDeps } from "./handlers";
|
|
|
12
12
|
* GET /control-plane/keys → the registration state (keys:rotate)
|
|
13
13
|
* POST /control-plane/keys → register a key (keys:rotate)
|
|
14
14
|
* POST /control-plane/keys/:keyId/expire → retire a superseded key (keys:rotate)
|
|
15
|
+
* GET /control-plane/migrations → where each database stands (migrations:read)
|
|
15
16
|
*
|
|
16
17
|
* Every one is `control-plane` and default-denied — with no connection registered they all answer 403,
|
|
17
18
|
* and that is the shipped state of a Worker nobody has connected.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/routes.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAGhE,OAAO,EAAE,KAAK,UAAU,EAAwB,MAAM,yBAAyB,CAAC;AAGhF,OAAO,EACL,KAAK,uBAAuB,
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/routes.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAGhE,OAAO,EAAE,KAAK,UAAU,EAAwB,MAAM,yBAAyB,CAAC;AAGhF,OAAO,EACL,KAAK,uBAAuB,EAO7B,MAAM,YAAY,CAAC;AAUpB;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAwC3E;AAED,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,EACvB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,uBAAuB,GAC5B,IAAI,CA6EN"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import { ANY_VERIFIED_CALLER, KEYS_ROTATE_SCOPE, MANIFEST_READ_SCOPE } from "../scope/scope.js";
|
|
3
|
+
import { ANY_VERIFIED_CALLER, KEYS_ROTATE_SCOPE, MANIFEST_READ_SCOPE, MIGRATIONS_READ_SCOPE } from "../scope/scope.js";
|
|
4
4
|
import { requireControlPlane } from "./guard.js";
|
|
5
5
|
import { describeRoute } from "../../http/routeSpec.js";
|
|
6
6
|
import { validate } from "../../http/validation.js";
|
|
7
7
|
import { ControlPlaneManifest } from "../discovery/adminRoute.js";
|
|
8
|
-
import { expireKeyHandler, listKeysHandler, manifestHandler, pingHandler, registerKeyHandler } from "./handlers.js";
|
|
9
|
-
import { ControlPlaneKeysResponse, ControlPlanePingResponse, ExpireKeyResponse, RegisterKeyResponse } from "./responses.js";
|
|
8
|
+
import { expireKeyHandler, listKeysHandler, manifestHandler, migrationsHandler, pingHandler, registerKeyHandler } from "./handlers.js";
|
|
9
|
+
import { ControlPlaneKeysResponse, ControlPlaneMigrationsResponse, ControlPlanePingResponse, ExpireKeyResponse, RegisterKeyResponse } from "./responses.js";
|
|
10
10
|
import { ExpireKeyParams, ExpireKeyRequest, RegisterKeyRequest } from "./schemas.js";
|
|
11
11
|
//#region src/controlPlane/http/routes.ts
|
|
12
12
|
/**
|
|
@@ -17,6 +17,7 @@ import { ExpireKeyParams, ExpireKeyRequest, RegisterKeyRequest } from "./schemas
|
|
|
17
17
|
* GET /control-plane/keys → the registration state (keys:rotate)
|
|
18
18
|
* POST /control-plane/keys → register a key (keys:rotate)
|
|
19
19
|
* POST /control-plane/keys/:keyId/expire → retire a superseded key (keys:rotate)
|
|
20
|
+
* GET /control-plane/migrations → where each database stands (migrations:read)
|
|
20
21
|
*
|
|
21
22
|
* Every one is `control-plane` and default-denied — with no connection registered they all answer 403,
|
|
22
23
|
* and that is the shipped state of a Worker nobody has connected.
|
|
@@ -73,6 +74,12 @@ function controlPlaneRouteDescriptors(basePath) {
|
|
|
73
74
|
path: `${basePath}/keys/:keyId/expire`,
|
|
74
75
|
scope: KEYS_ROTATE_SCOPE,
|
|
75
76
|
summary: "Retire a superseded key. Must be signed with the successor it names as proven."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
method: "GET",
|
|
80
|
+
path: `${basePath}/migrations`,
|
|
81
|
+
scope: MIGRATIONS_READ_SCOPE,
|
|
82
|
+
summary: "What each bound database has applied, what is pending, what is undeclared, and which run put it there. Undeclared is relative to this Worker."
|
|
76
83
|
}
|
|
77
84
|
];
|
|
78
85
|
}
|
|
@@ -107,6 +114,13 @@ function registerControlPlaneRoutes(app, basePath, deps) {
|
|
|
107
114
|
scopes: [KEYS_ROTATE_SCOPE],
|
|
108
115
|
responses: { 200: ExpireKeyResponse }
|
|
109
116
|
}), requireControlPlane(KEYS_ROTATE_SCOPE), validate("param", ExpireKeyParams), validate("json", ExpireKeyRequest), (c) => expireKeyHandler(deps)(c, c.req.valid("param"), c.req.valid("json")));
|
|
117
|
+
app.get(`${basePath}/migrations`, describeRoute({
|
|
118
|
+
summary: "Report where each database this Worker binds stands.",
|
|
119
|
+
description: "Applied, pending and undeclared migrations per database, each naming its namespace, and the last group with whether it is still reversible. Undeclared is relative to this Worker: a database shared with another Worker lists that Worker's migrations there. Read-only.",
|
|
120
|
+
verification: "control-plane",
|
|
121
|
+
scopes: [MIGRATIONS_READ_SCOPE],
|
|
122
|
+
responses: { 200: ControlPlaneMigrationsResponse }
|
|
123
|
+
}), requireControlPlane(MIGRATIONS_READ_SCOPE), migrationsHandler(deps));
|
|
110
124
|
}
|
|
111
125
|
//#endregion
|
|
112
126
|
export { controlPlaneRouteDescriptors, registerControlPlaneRoutes };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/verify.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAOjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,KAAK,uBAAuB,EAAe,MAAM,gBAAgB,CAAC;AAI3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH;;;;;;;;;GASG;AACH,OAAO,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AAE7E,0GAA0G;AAC1G,MAAM,WAAW,sBAAsB;IACrC,8FAA8F;IAC9F,cAAc,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;IACjF;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,sCAAsC;IACtC,MAAM,EAAE,WAAW,CAAC;IACpB,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,MAAM,EAAE,kBAAkB,CAAC;IAC3B,8DAA8D;IAC9D,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAED,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,iGAAiG;IACjG,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,4EAA4E;IAC5E,IAAI,EAAE,UAAU,CAAC;IACjB,4EAA4E;IAC5E,WAAW,EAAE,uBAAuB,CAAC;CACtC;AAOD;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,mBAAmB,CAAC,
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/verify.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAOjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,KAAK,uBAAuB,EAAe,MAAM,gBAAgB,CAAC;AAI3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH;;;;;;;;;GASG;AACH,OAAO,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AAE7E,0GAA0G;AAC1G,MAAM,WAAW,sBAAsB;IACrC,8FAA8F;IAC9F,cAAc,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;IACjF;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,sCAAsC;IACtC,MAAM,EAAE,WAAW,CAAC;IACpB,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,MAAM,EAAE,kBAAkB,CAAC;IAC3B,8DAA8D;IAC9D,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAED,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,iGAAiG;IACjG,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,4EAA4E;IAC5E,IAAI,EAAE,UAAU,CAAC;IACjB,4EAA4E;IAC5E,WAAW,EAAE,uBAAuB,CAAC;CACtC;AAOD;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CA6E9B"}
|