@pithy-sh/core 0.8.0 → 0.10.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.
Files changed (166) hide show
  1. package/dist/asyncapi/document.d.ts +46 -0
  2. package/dist/asyncapi/document.d.ts.map +1 -0
  3. package/dist/asyncapi/document.js +115 -0
  4. package/dist/capability/capability.d.ts +7 -0
  5. package/dist/capability/capability.d.ts.map +1 -1
  6. package/dist/capability/composition.d.ts.map +1 -1
  7. package/dist/capability/composition.js +5 -1
  8. package/dist/capability/settings.d.ts +10 -2
  9. package/dist/capability/settings.d.ts.map +1 -1
  10. package/dist/capability/settings.js +5 -3
  11. package/dist/cli/command.d.ts +364 -0
  12. package/dist/cli/command.d.ts.map +1 -0
  13. package/dist/cli/command.js +418 -0
  14. package/dist/cli/commandCatalog.generated.d.ts +2275 -0
  15. package/dist/cli/commandCatalog.generated.d.ts.map +1 -0
  16. package/dist/cli/commandCatalog.generated.js +2390 -0
  17. package/dist/cli/commandFacts.d.ts +164 -0
  18. package/dist/cli/commandFacts.d.ts.map +1 -0
  19. package/dist/cli/commandFacts.js +215 -0
  20. package/dist/controlPlane/error/errors.d.ts.map +1 -1
  21. package/dist/controlPlane/error/errors.js +4 -3
  22. package/dist/controlPlane/http/guard.d.ts.map +1 -1
  23. package/dist/controlPlane/http/guard.js +11 -1
  24. package/dist/controlPlane/http/routes.d.ts +1 -1
  25. package/dist/controlPlane/http/routes.d.ts.map +1 -1
  26. package/dist/controlPlane/http/routes.js +34 -7
  27. package/dist/createBackend.d.ts +7 -0
  28. package/dist/createBackend.d.ts.map +1 -1
  29. package/dist/createBackend.js +34 -4
  30. package/dist/entitlement/require.js +1 -1
  31. package/dist/error/cause.d.ts +16 -116
  32. package/dist/error/cause.d.ts.map +1 -1
  33. package/dist/error/cause.js +19 -153
  34. package/dist/error/http.d.ts +40 -0
  35. package/dist/error/http.d.ts.map +1 -1
  36. package/dist/error/payload.d.ts +80 -0
  37. package/dist/error/payload.d.ts.map +1 -1
  38. package/dist/error/payload.js +26 -0
  39. package/dist/error/pithyError.d.ts +44 -0
  40. package/dist/error/pithyError.d.ts.map +1 -1
  41. package/dist/error/pithyError.js +56 -1
  42. package/dist/error/unresolved.d.ts +265 -0
  43. package/dist/error/unresolved.d.ts.map +1 -0
  44. package/dist/error/unresolved.js +599 -0
  45. package/dist/error/wire.d.ts +20 -0
  46. package/dist/error/wire.d.ts.map +1 -0
  47. package/dist/error/wire.js +19 -0
  48. package/dist/http/oidcWebhook.d.ts +3 -3
  49. package/dist/http/oidcWebhook.js +3 -3
  50. package/dist/http/routeContract.js +1 -1
  51. package/dist/http/routeSpec.d.ts +66 -0
  52. package/dist/http/routeSpec.d.ts.map +1 -0
  53. package/dist/http/routeSpec.js +40 -0
  54. package/dist/http/sameOrigin.d.ts +1 -1
  55. package/dist/http/sameOrigin.js +1 -1
  56. package/dist/http/signedWebhook.d.ts +3 -3
  57. package/dist/http/signedWebhook.js +3 -3
  58. package/dist/http/socket.d.ts +52 -0
  59. package/dist/http/socket.d.ts.map +1 -0
  60. package/dist/http/socket.js +2 -0
  61. package/dist/http/subprotocol.d.ts +29 -0
  62. package/dist/http/subprotocol.d.ts.map +1 -0
  63. package/dist/http/subprotocol.js +37 -0
  64. package/dist/http/validation.d.ts +40 -8
  65. package/dist/http/validation.d.ts.map +1 -1
  66. package/dist/http/validation.js +25 -1
  67. package/dist/index.js +2 -2
  68. package/dist/logger/record.d.ts +23 -0
  69. package/dist/logger/record.d.ts.map +1 -1
  70. package/dist/migrations/bookkeeping.d.ts +21 -0
  71. package/dist/migrations/bookkeeping.d.ts.map +1 -1
  72. package/dist/migrations/bookkeeping.js +32 -3
  73. package/dist/migrations/groups.d.ts +140 -0
  74. package/dist/migrations/groups.d.ts.map +1 -0
  75. package/dist/migrations/groups.js +191 -0
  76. package/dist/migrations/migrateStep.d.ts +13 -0
  77. package/dist/migrations/migrateStep.d.ts.map +1 -0
  78. package/dist/migrations/migrateStep.js +20 -0
  79. package/dist/migrations/owner.d.ts +7 -0
  80. package/dist/migrations/owner.d.ts.map +1 -1
  81. package/dist/migrations/owner.js +11 -7
  82. package/dist/migrations/retained.js +2 -2
  83. package/dist/migrations/runner.d.ts +76 -5
  84. package/dist/migrations/runner.d.ts.map +1 -1
  85. package/dist/migrations/runner.js +130 -19
  86. package/dist/naming/domains.d.ts +30 -0
  87. package/dist/naming/domains.d.ts.map +1 -1
  88. package/dist/naming/domains.js +83 -4
  89. package/dist/naming/environment.d.ts.map +1 -1
  90. package/dist/naming/environment.js +2 -1
  91. package/dist/naming/packageName.d.ts +17 -0
  92. package/dist/naming/packageName.d.ts.map +1 -0
  93. package/dist/naming/packageName.js +22 -0
  94. package/dist/naming/provisionScope.d.ts.map +1 -1
  95. package/dist/naming/provisionScope.js +2 -1
  96. package/dist/openapi/document.d.ts +106 -0
  97. package/dist/openapi/document.d.ts.map +1 -0
  98. package/dist/openapi/document.js +314 -0
  99. package/dist/openapi/routeGroups.d.ts +8 -0
  100. package/dist/openapi/routeGroups.d.ts.map +1 -0
  101. package/dist/openapi/routeGroups.js +23 -0
  102. package/dist/seed/devLogin.d.ts +33 -0
  103. package/dist/seed/devLogin.d.ts.map +1 -1
  104. package/dist/seed/devLogin.js +28 -1
  105. package/dist/text/comments.js +66 -4
  106. package/dist/text/literals.d.ts +43 -0
  107. package/dist/text/literals.d.ts.map +1 -0
  108. package/dist/text/literals.js +293 -0
  109. package/dist/version.generated.d.ts +1 -1
  110. package/dist/version.generated.d.ts.map +1 -1
  111. package/dist/version.generated.js +1 -1
  112. package/dist/workflow/dispatchRoute.d.ts.map +1 -1
  113. package/dist/workflow/dispatchRoute.js +12 -6
  114. package/dist/workflow/faults.d.ts.map +1 -1
  115. package/dist/workflow/faults.js +2 -3
  116. package/dist/workflow/hostEnv.d.ts +1 -0
  117. package/dist/workflow/hostEnv.d.ts.map +1 -1
  118. package/dist/workflow/hostEnv.js +4 -2
  119. package/dist/workflow/loopback.d.ts.map +1 -1
  120. package/dist/workflow/loopback.js +2 -1
  121. package/package.json +3 -3
  122. package/src/asyncapi/document.ts +164 -0
  123. package/src/capability/capability.ts +7 -0
  124. package/src/capability/composition.ts +2 -1
  125. package/src/capability/settings.ts +19 -2
  126. package/src/cli/command.ts +865 -0
  127. package/src/cli/commandCatalog.generated.ts +2533 -0
  128. package/src/cli/commandFacts.ts +219 -0
  129. package/src/controlPlane/error/errors.ts +10 -3
  130. package/src/controlPlane/http/guard.ts +14 -2
  131. package/src/controlPlane/http/routes.ts +57 -9
  132. package/src/createBackend.ts +35 -2
  133. package/src/entitlement/require.ts +1 -1
  134. package/src/error/cause.ts +18 -181
  135. package/src/error/payload.ts +59 -0
  136. package/src/error/pithyError.ts +76 -0
  137. package/src/error/unresolved.ts +853 -0
  138. package/src/error/wire.ts +40 -0
  139. package/src/http/oidcWebhook.ts +3 -3
  140. package/src/http/routeContract.ts +1 -1
  141. package/src/http/routeSpec.ts +81 -0
  142. package/src/http/sameOrigin.ts +1 -1
  143. package/src/http/signedWebhook.ts +3 -3
  144. package/src/http/socket.ts +55 -0
  145. package/src/http/subprotocol.ts +47 -0
  146. package/src/http/validation.ts +50 -8
  147. package/src/migrations/bookkeeping.ts +39 -2
  148. package/src/migrations/groups.ts +295 -0
  149. package/src/migrations/migrateStep.ts +19 -0
  150. package/src/migrations/owner.ts +10 -6
  151. package/src/migrations/retained.ts +2 -2
  152. package/src/migrations/runner.ts +214 -22
  153. package/src/naming/domains.ts +97 -16
  154. package/src/naming/environment.ts +2 -1
  155. package/src/naming/packageName.ts +20 -0
  156. package/src/naming/provisionScope.ts +2 -1
  157. package/src/openapi/document.ts +412 -0
  158. package/src/openapi/routeGroups.ts +25 -0
  159. package/src/seed/devLogin.ts +31 -0
  160. package/src/text/comments.ts +56 -4
  161. package/src/text/literals.ts +294 -0
  162. package/src/version.generated.ts +1 -1
  163. package/src/workflow/dispatchRoute.ts +13 -6
  164. package/src/workflow/faults.ts +5 -3
  165. package/src/workflow/hostEnv.ts +9 -1
  166. package/src/workflow/loopback.ts +2 -1
@@ -234,8 +234,8 @@ async function verifyOidcToken(token, options) {
234
234
  return claims;
235
235
  }
236
236
  /**
237
- * The OIDC gate, as a route wears it: `app.post(path, requireOidcWebhook({…}), zValidator("json", Body,
238
- * validationHook), handler)`.
237
+ * The OIDC gate, as a route wears it: `app.post(path, requireOidcWebhook({…}), validate("json", Body),
238
+ * handler)`.
239
239
  *
240
240
  * ## Why the header is read before anything else
241
241
  *
@@ -271,7 +271,7 @@ async function verifyOidcToken(token, options) {
271
271
  * null-check.
272
272
  *
273
273
  * The body is never read here — unlike the HMAC gate, whose proof covers the bytes. An OIDC token proves who
274
- * sent the request and says nothing about what they sent, so the route's `zValidator` is the only thing that
274
+ * sent the request and says nothing about what they sent, so the route's `validate` is the only thing that
275
275
  * touches the body, and it sees an untouched stream.
276
276
  */
277
277
  function requireOidcWebhook(options) {
@@ -35,7 +35,7 @@ function probeContext(record) {
35
35
  set: () => {}
36
36
  };
37
37
  }
38
- /** Does this middleware read `c.req.param()` — i.e. is it a `zValidator("param", …)`? */
38
+ /** Does this middleware read `c.req.param()` — i.e. is it a `validate("param", …)`? */
39
39
  async function readsPathParams(handler) {
40
40
  let read = false;
41
41
  try {
@@ -0,0 +1,66 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { MiddlewareHandler } from "hono";
4
+ import type { z } from "zod";
5
+ import type { VerificationStrategy } from "./verification";
6
+ /**
7
+ * The third half of a route declaration. `validate` says what a caller may send and the guards say who
8
+ * may send it; this says what the route is for, how its caller is verified, and what comes back. It is
9
+ * the part the OpenAPI contract (`../openapi/document.ts`) cannot read from anywhere else: a guard is an
10
+ * anonymous closure, and a handler's `c.json(… satisfies Response)` is a compile-time check that leaves
11
+ * nothing behind at runtime.
12
+ *
13
+ * It sits first on the route line, ahead of the guards, and does nothing when it runs but call `next`.
14
+ * Its whole value is the spec it carries under {@link ROUTE_SPEC}, which a reader of the composed app
15
+ * finds by walking `app.routes`.
16
+ *
17
+ * ```ts
18
+ * app.get(
19
+ * `${base}/:board/top`,
20
+ * describeRoute({ summary: "Read the top of a board.", verification: ["bearer", "session"], responses: { 200: TopPage } }),
21
+ * requireAuth(),
22
+ * validate("param", BoardParam),
23
+ * handler,
24
+ * );
25
+ * ```
26
+ */
27
+ /** The key a {@link describeRoute} middleware carries its spec under. Global, so two copies of core agree. */
28
+ export declare const ROUTE_SPEC: unique symbol;
29
+ /** One response a route can give that is not a JSON body: bytes, a redirect, HTML, or nothing at all. */
30
+ export interface RouteResponse {
31
+ /** What this response means, in one sentence. */
32
+ readonly description: string;
33
+ /** The JSON body, when there is one. Its input side is the wire: what a client parses with it. */
34
+ readonly schema?: z.ZodType;
35
+ /** The media type when the body is not JSON, e.g. `text/html` or `application/octet-stream`. */
36
+ readonly contentType?: string;
37
+ }
38
+ /** The HTTP methods an `app.all` route is documented under. OpenAPI has no "any method". */
39
+ export type DocumentedMethod = "get" | "post" | "put" | "patch" | "delete" | "head" | "options";
40
+ /** What a route is for, how its caller is verified, and what it returns. */
41
+ export interface RouteSpec {
42
+ /** One sentence, in the brand voice. The operation's title in the contract. */
43
+ readonly summary: string;
44
+ /** Anything a client author must know that the schemas do not say. */
45
+ readonly description?: string;
46
+ /**
47
+ * How the caller is verified. A list when the route takes more than one — `requireAuth()` accepts a
48
+ * bearer token or a session, so most authenticated routes declare both.
49
+ */
50
+ readonly verification: VerificationStrategy | readonly VerificationStrategy[];
51
+ /** Scopes the caller's credential must carry, beyond being verified. */
52
+ readonly scopes?: readonly string[];
53
+ /**
54
+ * Every success response, by status. A Zod schema is a JSON body described by the schema itself; a
55
+ * {@link RouteResponse} covers everything else. Errors are not listed: every route can answer with the
56
+ * one `PithyError` body, and the contract says so once.
57
+ */
58
+ readonly responses: Readonly<Record<number, z.ZodType | RouteResponse>>;
59
+ /** For an `app.all` route only: the methods it is documented under. */
60
+ readonly methods?: readonly DocumentedMethod[];
61
+ }
62
+ /** Declare what a route is for, how its caller is verified, and what it returns. See {@link RouteSpec}. */
63
+ export declare function describeRoute(spec: RouteSpec): MiddlewareHandler;
64
+ /** The spec a {@link describeRoute} middleware carries, or `undefined` for any other handler. */
65
+ export declare function routeSpecOf(handler: unknown): RouteSpec | undefined;
66
+ //# sourceMappingURL=routeSpec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routeSpec.d.ts","sourceRoot":"","sources":["../../src/http/routeSpec.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,8GAA8G;AAC9G,eAAO,MAAM,UAAU,eAAgC,CAAC;AAExD,yGAAyG;AACzG,MAAM,WAAW,aAAa;IAC5B,iDAAiD;IACjD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kGAAkG;IAClG,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,4FAA4F;AAC5F,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhG,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,YAAY,EAAE,oBAAoB,GAAG,SAAS,oBAAoB,EAAE,CAAC;IAC9E,wEAAwE;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,GAAG,aAAa,CAAC,CAAC,CAAC;IACxE,uEAAuE;IACvE,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAChD;AAED,2GAA2G;AAC3G,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,iBAAiB,CAKhE;AAED,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAGnE"}
@@ -0,0 +1,40 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ //#region src/http/routeSpec.ts
4
+ /**
5
+ * The third half of a route declaration. `validate` says what a caller may send and the guards say who
6
+ * may send it; this says what the route is for, how its caller is verified, and what comes back. It is
7
+ * the part the OpenAPI contract (`../openapi/document.ts`) cannot read from anywhere else: a guard is an
8
+ * anonymous closure, and a handler's `c.json(… satisfies Response)` is a compile-time check that leaves
9
+ * nothing behind at runtime.
10
+ *
11
+ * It sits first on the route line, ahead of the guards, and does nothing when it runs but call `next`.
12
+ * Its whole value is the spec it carries under {@link ROUTE_SPEC}, which a reader of the composed app
13
+ * finds by walking `app.routes`.
14
+ *
15
+ * ```ts
16
+ * app.get(
17
+ * `${base}/:board/top`,
18
+ * describeRoute({ summary: "Read the top of a board.", verification: ["bearer", "session"], responses: { 200: TopPage } }),
19
+ * requireAuth(),
20
+ * validate("param", BoardParam),
21
+ * handler,
22
+ * );
23
+ * ```
24
+ */
25
+ /** The key a {@link describeRoute} middleware carries its spec under. Global, so two copies of core agree. */
26
+ const ROUTE_SPEC = Symbol.for("pithy.routeSpec");
27
+ /** Declare what a route is for, how its caller is verified, and what it returns. See {@link RouteSpec}. */
28
+ function describeRoute(spec) {
29
+ const middleware = async (_c, next) => {
30
+ await next();
31
+ };
32
+ return Object.assign(middleware, { [ROUTE_SPEC]: spec });
33
+ }
34
+ /** The spec a {@link describeRoute} middleware carries, or `undefined` for any other handler. */
35
+ function routeSpecOf(handler) {
36
+ if (typeof handler !== "function") return void 0;
37
+ return handler[ROUTE_SPEC];
38
+ }
39
+ //#endregion
40
+ export { ROUTE_SPEC, describeRoute, routeSpecOf };
@@ -40,7 +40,7 @@ export type SameOriginGate = MiddlewareHandler<PithyHonoEnv>;
40
40
  * Require this request to come from an origin the Worker trusts — the CSRF gate a cookie-authenticated
41
41
  * mutating route wears, beside the strategy that says who the caller is.
42
42
  *
43
- * app.post("/organizations", requireAuth(), requireSameOrigin(), zValidator(…), handler)
43
+ * app.post("/organizations", requireAuth(), requireSameOrigin(), validate(…), handler)
44
44
  *
45
45
  * A bearer request carries no ambient credential and is CSRF-exempt; that exemption belongs to the
46
46
  * published gate, since the capability that owns the credential model is the one that knows.
@@ -6,7 +6,7 @@ import { ForbiddenError } from "../error/pithyError.js";
6
6
  * Require this request to come from an origin the Worker trusts — the CSRF gate a cookie-authenticated
7
7
  * mutating route wears, beside the strategy that says who the caller is.
8
8
  *
9
- * app.post("/organizations", requireAuth(), requireSameOrigin(), zValidator(…), handler)
9
+ * app.post("/organizations", requireAuth(), requireSameOrigin(), validate(…), handler)
10
10
  *
11
11
  * A bearer request carries no ambient credential and is CSRF-exempt; that exemption belongs to the
12
12
  * published gate, since the capability that owns the credential model is the one that knows.
@@ -139,8 +139,8 @@ export declare function checkSignedWebhook(body: string, header: string | null,
139
139
  */
140
140
  export declare function verifySignedWebhook(body: string, header: string | null, options: VerifySignedWebhookOptions): Promise<void>;
141
141
  /**
142
- * The `signed-webhook` gate, as a route wears it: `app.post(path, requireSignedWebhook({…}), zValidator("json",
143
- * Body, validationHook), handler)`.
142
+ * The `signed-webhook` gate, as a route wears it: `app.post(path, requireSignedWebhook({…}), validate("json",
143
+ * Body), handler)`.
144
144
  *
145
145
  * ## Why the header is read before anything else
146
146
  *
@@ -164,7 +164,7 @@ export declare function verifySignedWebhook(body: string, header: string | null,
164
164
  * `c.req.text()` is not.
165
165
  *
166
166
  * `c.req.text()` rather than `c.req.raw.text()`, and the distinction is load-bearing. Hono caches its own
167
- * body reads and serves `c.req.json()` out of the cached text, so a `zValidator("json", …)` after this gate
167
+ * body reads and serves `c.req.json()` out of the cached text, so a `validate("json", …)` after this gate
168
168
  * parses identical bytes. Reading `c.req.raw` would consume the stream and leave the validator with nothing.
169
169
  */
170
170
  export declare function requireSignedWebhook(options: SignedWebhookGuardOptions): MiddlewareHandler<PithyHonoEnv>;
@@ -206,8 +206,8 @@ async function verifySignedWebhook(body, header, options) {
206
206
  if (refusal !== void 0) throw webhookRefusal(refusal, options);
207
207
  }
208
208
  /**
209
- * The `signed-webhook` gate, as a route wears it: `app.post(path, requireSignedWebhook({…}), zValidator("json",
210
- * Body, validationHook), handler)`.
209
+ * The `signed-webhook` gate, as a route wears it: `app.post(path, requireSignedWebhook({…}), validate("json",
210
+ * Body), handler)`.
211
211
  *
212
212
  * ## Why the header is read before anything else
213
213
  *
@@ -231,7 +231,7 @@ async function verifySignedWebhook(body, header, options) {
231
231
  * `c.req.text()` is not.
232
232
  *
233
233
  * `c.req.text()` rather than `c.req.raw.text()`, and the distinction is load-bearing. Hono caches its own
234
- * body reads and serves `c.req.json()` out of the cached text, so a `zValidator("json", …)` after this gate
234
+ * body reads and serves `c.req.json()` out of the cached text, so a `validate("json", …)` after this gate
235
235
  * parses identical bytes. Reading `c.req.raw` would consume the stream and leave the validator with nothing.
236
236
  */
237
237
  function requireSignedWebhook(options) {
@@ -0,0 +1,52 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { z } from "zod";
4
+ import type { SocketVersions } from "./subprotocol";
5
+ import type { VerificationStrategy } from "./verification";
6
+ /**
7
+ * **A WebSocket, described for the AsyncAPI contract.** The socket's counterpart of `describeRoute`.
8
+ *
9
+ * OpenAPI cannot describe a socket: after the upgrade there are no requests, only frames each way. So a
10
+ * capability that serves one declares it on its `sockets` contribution, built from its resolved config —
11
+ * the path it mounted, how a caller is verified, the versions it serves, and a Zod schema for every frame
12
+ * each side may send. `../asyncapi/document.ts` reads the declarations into an AsyncAPI document, the way
13
+ * `../openapi/document.ts` reads the route table into OpenAPI.
14
+ *
15
+ * The schemas are the ones the socket parses with, never a copy: a frame the Worker receives is parsed with
16
+ * the schema declared here, so the document and the socket cannot disagree about what is accepted.
17
+ */
18
+ /** One frame one side of a socket may send. */
19
+ export interface SocketMessage {
20
+ /** The frame's name, unique within its socket: `state`, `error`, `action`. */
21
+ readonly name: string;
22
+ /** What the frame is, in one sentence. */
23
+ readonly summary: string;
24
+ /** The frame's JSON body. Absent for a plain-text frame. */
25
+ readonly schema?: z.ZodType;
26
+ /** A plain-text frame's exact text — `ping`, `pong`. The one exception to JSON frames, stated per frame. */
27
+ readonly text?: string;
28
+ }
29
+ /** One WebSocket a capability serves. */
30
+ export interface SocketChannel {
31
+ /** A stable name for the socket, unique in the Worker: `multiplayerSession`, `matchmakingPresence`. */
32
+ readonly name: string;
33
+ /** The upgrade path, as Hono registered it: `/multiplayer/sessions/:id/socket`. */
34
+ readonly path: string;
35
+ /** What the socket is for, in one sentence. */
36
+ readonly summary: string;
37
+ /** Anything a client author must know that the schemas do not say. */
38
+ readonly description?: string;
39
+ /** How the upgrade request is verified. */
40
+ readonly verification: VerificationStrategy | readonly VerificationStrategy[];
41
+ /** The path parameters' schema — the same one the upgrade route validates them with. */
42
+ readonly params?: z.ZodType;
43
+ /** The subprotocol versions this socket serves. */
44
+ readonly versions: SocketVersions;
45
+ /** Every frame the Worker sends. */
46
+ readonly send: readonly SocketMessage[];
47
+ /** Every frame the Worker accepts. Anything else is answered with a `validation/invalid_input` error frame. */
48
+ readonly receive: readonly SocketMessage[];
49
+ /** Further named schemas a client needs, such as each configured game's `action` and `view`. */
50
+ readonly schemas?: Readonly<Record<string, z.ZodType>>;
51
+ }
52
+ //# sourceMappingURL=socket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../../src/http/socket.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D;;;;;;;;;;;GAWG;AAEH,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IAC5B,4GAA4G;IAC5G,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,yCAAyC;AACzC,MAAM,WAAW,aAAa;IAC5B,uGAAuG;IACvG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,2CAA2C;IAC3C,QAAQ,CAAC,YAAY,EAAE,oBAAoB,GAAG,SAAS,oBAAoB,EAAE,CAAC;IAC9E,wFAAwF;IACxF,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IAC5B,mDAAmD;IACnD,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,CAAC;IACxC,+GAA+G;IAC/G,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;IAC3C,gGAAgG;IAChG,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;CACxD"}
@@ -0,0 +1,2 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
@@ -0,0 +1,29 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * **A socket's version is negotiated at the upgrade, and the Worker serves two.**
5
+ *
6
+ * An app store client lags its Worker by months — the same version boundary an admin response crosses
7
+ * (CLAUDE.md §HTTP). So a socket's frames change the way a response does: a field added to a frame is
8
+ * optional, and a client ignores a `type` and a key it does not know. A change no client could ignore is a
9
+ * new subprotocol, offered in `Sec-WebSocket-Protocol` (`pithy.multiplayer.v1`), and the Worker keeps
10
+ * serving the one before it, so a client released before the change keeps playing.
11
+ *
12
+ * The first versioned protocol's previous version is the unversioned one: a client that offers no
13
+ * subprotocol at all, which is every client written before versions existed. `previous: null` names it.
14
+ */
15
+ /** The header a client offers subprotocols in, and the server answers with the one it chose. */
16
+ export declare const SUBPROTOCOL_HEADER = "Sec-WebSocket-Protocol";
17
+ /** The versions one socket serves: the current one, and the one before it — `null` when that is unversioned. */
18
+ export interface SocketVersions {
19
+ readonly current: string;
20
+ readonly previous: string | null;
21
+ }
22
+ /**
23
+ * The version this upgrade speaks: the current one when the client offers it, else the previous one when
24
+ * the client offers that, else `null` for an unversioned client where the previous version is unversioned.
25
+ * A client that offers only versions this Worker does not serve is refused with a 400 naming the ones it
26
+ * does — a socket that opened anyway would speak frames the client cannot read.
27
+ */
28
+ export declare function negotiateSubprotocol(offered: string | null | undefined, versions: SocketVersions): string | null;
29
+ //# sourceMappingURL=subprotocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subprotocol.d.ts","sourceRoot":"","sources":["../../src/http/subprotocol.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;GAWG;AAEH,gGAAgG;AAChG,eAAO,MAAM,kBAAkB,2BAA2B,CAAC;AAE3D,gHAAgH;AAChH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAahH"}
@@ -0,0 +1,37 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { ValidationError } from "../error/pithyError.js";
4
+ //#region src/http/subprotocol.ts
5
+ /**
6
+ * **A socket's version is negotiated at the upgrade, and the Worker serves two.**
7
+ *
8
+ * An app store client lags its Worker by months — the same version boundary an admin response crosses
9
+ * (CLAUDE.md §HTTP). So a socket's frames change the way a response does: a field added to a frame is
10
+ * optional, and a client ignores a `type` and a key it does not know. A change no client could ignore is a
11
+ * new subprotocol, offered in `Sec-WebSocket-Protocol` (`pithy.multiplayer.v1`), and the Worker keeps
12
+ * serving the one before it, so a client released before the change keeps playing.
13
+ *
14
+ * The first versioned protocol's previous version is the unversioned one: a client that offers no
15
+ * subprotocol at all, which is every client written before versions existed. `previous: null` names it.
16
+ */
17
+ /** The header a client offers subprotocols in, and the server answers with the one it chose. */
18
+ const SUBPROTOCOL_HEADER = "Sec-WebSocket-Protocol";
19
+ /**
20
+ * The version this upgrade speaks: the current one when the client offers it, else the previous one when
21
+ * the client offers that, else `null` for an unversioned client where the previous version is unversioned.
22
+ * A client that offers only versions this Worker does not serve is refused with a 400 naming the ones it
23
+ * does — a socket that opened anyway would speak frames the client cannot read.
24
+ */
25
+ function negotiateSubprotocol(offered, versions) {
26
+ const asked = (offered ?? "").split(",").map((protocol) => protocol.trim()).filter((protocol) => protocol.length > 0);
27
+ if (asked.length === 0 && versions.previous === null) return null;
28
+ if (asked.includes(versions.current)) return versions.current;
29
+ if (versions.previous !== null && asked.includes(versions.previous)) return versions.previous;
30
+ const served = [versions.current, ...versions.previous === null ? [] : [versions.previous]].join(", ");
31
+ throw new ValidationError({
32
+ message: `This socket speaks ${served}${versions.previous === null ? ", or no subprotocol" : ""}. The client offered ${asked.length === 0 ? "none" : asked.join(", ")}.`,
33
+ action: `Offer ${versions.current} in ${SUBPROTOCOL_HEADER}.`
34
+ });
35
+ }
36
+ //#endregion
37
+ export { SUBPROTOCOL_HEADER, negotiateSubprotocol };
@@ -1,5 +1,6 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
+ import type { ValidationTargets } from "hono";
3
4
  import type { z } from "zod";
4
5
  /**
5
6
  * How a route declares what it accepts. Its sibling {@link ./verification.VerificationStrategy}
@@ -7,15 +8,21 @@ import type { z } from "zod";
7
8
  * mandatory, and both belong on the route line — reading a route tells you what it takes without
8
9
  * opening the handler.
9
10
  *
10
- * Routes use `zValidator(target, Schema, validationHook)` from `@hono/zod-validator`. The library
11
- * owns the middleware and the `c.req.valid(target)` typing; this file owns exactly one thing —
12
- * what happens when the parse fails. Without the hook, zod-validator answers with its own JSON
13
- * body, which is neither a `PithyError` payload nor routed through `pithyErrorHandler`. With it,
14
- * a malformed request is indistinguishable from every other failure the app can produce.
11
+ * Routes use `validate(target, Schema)`. `@hono/zod-validator` owns the middleware and the
12
+ * `c.req.valid(target)` typing; this file owns two things — what happens when the parse fails, and
13
+ * leaving the schema readable on the middleware afterwards.
15
14
  *
16
- * The hook is deliberately not a wrapper around `zValidator`. A wrapper would be a second name for
17
- * a library we do not re-export (CLAUDE.md §Toolchain), and it would hide the target from the route
18
- * line — the one thing the migration exists to make visible.
15
+ * The failure: without {@link validationHook}, zod-validator answers with its own JSON body, which is
16
+ * neither a `PithyError` payload nor routed through `pithyErrorHandler`. With it, a malformed request
17
+ * is indistinguishable from every other failure the app can produce.
18
+ *
19
+ * The schema: `zValidator` returns the same anonymous closure whatever it validates, so once a route is
20
+ * registered nothing can tell which schema it holds. That was tolerable while the only reader was the
21
+ * route line. The OpenAPI contract (`../openapi/document.ts`) is a second reader, and it reads the
22
+ * composed app, so the schema has to survive registration. `validate` is `zValidator` with the hook
23
+ * applied and the `{ target, schema }` pair attached under {@link REQUEST_CONTRACT}. It was once kept
24
+ * out of here on purpose, as a second name for a library; it earns the name now by carrying the one
25
+ * thing the library drops. The target stays the first argument, so the route line reads as before.
19
26
  */
20
27
  /**
21
28
  * The outcome `@hono/zod-validator` hands a hook. Declared structurally rather than by importing
@@ -36,4 +43,29 @@ export type ValidationOutcome = {
36
43
  * validation is declared, not what a caller sees when it fails.
37
44
  */
38
45
  export declare function validationHook(outcome: ValidationOutcome): void;
46
+ /** The key a {@link validate} middleware carries its contract under. Global, so two copies of core agree. */
47
+ export declare const REQUEST_CONTRACT: unique symbol;
48
+ /** What one validator declares: the part of the request it reads, and the schema that part must satisfy. */
49
+ export interface RequestContract {
50
+ /** The request part, as Hono names it: `json`, `query`, `param`, `header`, `cookie` or `form`. */
51
+ readonly target: keyof ValidationTargets;
52
+ /** The schema the part is parsed with. Its input side is what a caller sends. */
53
+ readonly schema: z.ZodType;
54
+ }
55
+ /**
56
+ * Declare what a route accepts in one part of the request, and fail a malformed one as a
57
+ * `validation/invalid_input` 400. The handler reads the parsed value with `c.req.valid(target)`.
58
+ *
59
+ * ```ts
60
+ * app.post(path, requireAuth(), validate("param", BoardParam), validate("json", SubmitScoreBody), handler);
61
+ * ```
62
+ */
63
+ export declare function validate<Target extends keyof ValidationTargets, Schema extends z.ZodType>(target: Target, schema: Schema): import("hono").MiddlewareHandler<import("hono").Env, string, {
64
+ in: (undefined extends (Schema extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<Schema> : Schema extends z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> ? z.core.input<Schema> : never) ? true : false) extends true ? { [K in Target]?: ([Schema extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<Schema> : Schema extends z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> ? z.core.input<Schema> : never] extends [ValidationTargets[K]] ? ValidationTargets[K] & (Schema extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<Schema> : Schema extends z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> ? z.core.input<Schema> : never) : import("hono/validator").InferInput<Schema extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<Schema> : Schema extends z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> ? z.core.input<Schema> : never, K, import("hono/types").ParsedFormValue>) | undefined; } : { [K in Target]: [Schema extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<Schema> : Schema extends z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> ? z.core.input<Schema> : never] extends [ValidationTargets[K]] ? ValidationTargets[K] & (Schema extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<Schema> : Schema extends z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> ? z.core.input<Schema> : never) : import("hono/validator").InferInput<Schema extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").input<Schema> : Schema extends z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> ? z.core.input<Schema> : never, K, import("hono/types").ParsedFormValue>; };
65
+ out: { [K in Target]: Schema extends import("zod/v3").ZodType<any, import("zod/v3").ZodTypeDef, any> ? import("zod/v3").output<Schema> : Schema extends z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> ? z.infer<Schema> : never; };
66
+ }, never> & {
67
+ [REQUEST_CONTRACT]: RequestContract;
68
+ };
69
+ /** The contract a {@link validate} middleware carries, or `undefined` for any other handler. */
70
+ export declare function requestContractOf(handler: unknown): RequestContract | undefined;
39
71
  //# sourceMappingURL=validation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/http/validation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B;;;;;;;;;;;;;;;GAeG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAA;CAAE,CAAC;AAEhG;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAG/D"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/http/validation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAA;CAAE,CAAC;AAEhG;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAG/D;AAED,6GAA6G;AAC7G,eAAO,MAAM,gBAAgB,eAAsC,CAAC;AAEpE,4GAA4G;AAC5G,MAAM,WAAW,eAAe;IAC9B,kGAAkG;IAClG,QAAQ,CAAC,MAAM,EAAE,MAAM,iBAAiB,CAAC;IACzC,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,SAAS,MAAM,iBAAiB,EAAE,MAAM,SAAS,CAAC,CAAC,OAAO,EACvF,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM;;;;;EAKf;AAED,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,GAAG,SAAS,CAG/E"}
@@ -1,6 +1,7 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { fromZodError } from "../error/pithyError.js";
4
+ import { zValidator } from "@hono/zod-validator";
4
5
  //#region src/http/validation.ts
5
6
  /**
6
7
  * The one validation-failure hook every provided route passes to `zValidator`. Maps the `ZodError`
@@ -12,5 +13,28 @@ function validationHook(outcome) {
12
13
  if (outcome.success) return;
13
14
  throw fromZodError(outcome.error);
14
15
  }
16
+ /** The key a {@link validate} middleware carries its contract under. Global, so two copies of core agree. */
17
+ const REQUEST_CONTRACT = Symbol.for("pithy.requestContract");
18
+ /**
19
+ * Declare what a route accepts in one part of the request, and fail a malformed one as a
20
+ * `validation/invalid_input` 400. The handler reads the parsed value with `c.req.valid(target)`.
21
+ *
22
+ * ```ts
23
+ * app.post(path, requireAuth(), validate("param", BoardParam), validate("json", SubmitScoreBody), handler);
24
+ * ```
25
+ */
26
+ function validate(target, schema) {
27
+ const middleware = zValidator(target, schema, validationHook);
28
+ const contract = {
29
+ target,
30
+ schema
31
+ };
32
+ return Object.assign(middleware, { [REQUEST_CONTRACT]: contract });
33
+ }
34
+ /** The contract a {@link validate} middleware carries, or `undefined` for any other handler. */
35
+ function requestContractOf(handler) {
36
+ if (typeof handler !== "function") return void 0;
37
+ return handler[REQUEST_CONTRACT];
38
+ }
15
39
  //#endregion
16
- export { validationHook };
40
+ export { REQUEST_CONTRACT, requestContractOf, validate, validationHook };
package/dist/index.js CHANGED
@@ -10,10 +10,10 @@ import { ControlPlaneAuditActions } from "./controlPlane/audit/actions.js";
10
10
  import { CONTROL_PLANE_HEADER, CONTROL_PLANE_VERSION_CREATED_HEADER, CONTROL_PLANE_VERSION_HEADER, workerBuildChanged } from "./controlPlane/wire.js";
11
11
  import { ControlPlaneContext } from "./controlPlane/context.js";
12
12
  import { requireControlPlane } from "./controlPlane/http/guard.js";
13
- import { CONTROL_PLANE_KV_BINDING } from "./controlPlane/replay/kvGuard.js";
14
- import { CONTROLPLANE_MIGRATION_ORDER, controlplane, isControlPlaneCapability } from "./controlPlane/capability.js";
15
13
  import { ManifestConfigKey, ManifestConfigValue, ManifestConfigValues, defineManifestConfig, namedConfigValues } from "./controlPlane/discovery/configuration.js";
16
14
  import { AdminRoute, CapabilityDeclaration, CapabilityDescriptor, ControlPlaneManifest } from "./controlPlane/discovery/adminRoute.js";
15
+ import { CONTROL_PLANE_KV_BINDING } from "./controlPlane/replay/kvGuard.js";
16
+ import { CONTROLPLANE_MIGRATION_ORDER, controlplane, isControlPlaneCapability } from "./controlPlane/capability.js";
17
17
  import { missingAdminRoutes } from "./controlPlane/discovery/drift.js";
18
18
  import { exportPublicJwk, mintControlPlaneToken } from "./controlPlane/token/mint.js";
19
19
  export { ANY_VERIFIED_CALLER, AdminRoute, CONTROLPLANE_MIGRATION_ORDER, CONTROL_PLANE_CONNECTIONS_TABLE, CONTROL_PLANE_HEADER, CONTROL_PLANE_KV_BINDING, CONTROL_PLANE_VERSION_CREATED_HEADER, CONTROL_PLANE_VERSION_HEADER, CapabilityDeclaration, CapabilityDescriptor, CapabilityHealthReport, ControlPlaneAuditActions, ControlPlaneConfig, ControlPlaneConnection, ControlPlaneContext, ControlPlaneManifest, ControlPlaneScope, Ed25519PublicJwk, HealthCountNominal, HealthNominal, HealthSummary, HealthSummaryKey, HealthSummaryValue, HealthValueCost, HealthValueKind, KEYS_ROTATE_SCOPE, MANIFEST_READ_SCOPE, ManifestConfigKey, ManifestConfigValue, ManifestConfigValues, RegisteredKey, SEAM_SCOPES, capabilityHealthSources, controlPlaneDatabase, controlplane, defineCapabilityHealth, defineManifestConfig, exportPublicJwk, healthAttention, healthReport, healthWire, isControlPlaneCapability, mintControlPlaneToken, missingAdminRoutes, namedConfigValues, namedHealthValues, readCapabilityHealth, requireControlPlane, standingOf, workerBuildChanged };
@@ -56,6 +56,29 @@ export declare const LogRecord: z.ZodObject<{
56
56
  message: z.ZodString;
57
57
  code: z.ZodString;
58
58
  }, z.core.$strip>>;
59
+ }, z.core.$strip>, z.ZodObject<{
60
+ message: z.ZodString;
61
+ params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
62
+ action: z.ZodOptional<z.ZodString>;
63
+ detail: z.ZodOptional<z.ZodString>;
64
+ code: z.ZodLiteral<"input/required">;
65
+ status: z.ZodLiteral<400>;
66
+ flag: z.ZodNullable<z.ZodString>;
67
+ choices: z.ZodOptional<z.ZodArray<z.ZodString>>;
68
+ }, z.core.$strip>, z.ZodObject<{
69
+ message: z.ZodString;
70
+ params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
71
+ action: z.ZodOptional<z.ZodString>;
72
+ detail: z.ZodOptional<z.ZodString>;
73
+ code: z.ZodLiteral<"core/precondition_failed">;
74
+ status: z.ZodLiteral<412>;
75
+ }, z.core.$strip>, z.ZodObject<{
76
+ message: z.ZodString;
77
+ params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
78
+ action: z.ZodOptional<z.ZodString>;
79
+ detail: z.ZodOptional<z.ZodString>;
80
+ code: z.ZodLiteral<"core/person_required">;
81
+ status: z.ZodLiteral<403>;
59
82
  }, z.core.$strip>, z.ZodObject<{
60
83
  message: z.ZodString;
61
84
  params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
@@ -1 +1 @@
1
- {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../src/logger/record.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;GAKG;AAEH,4GAA4G;AAC5G,eAAO,MAAM,QAAQ;;;;;EAE4F,CAAC;AAClH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEjD;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAgD,CAAC;AAEtG;;;;GAIG;AACH,eAAO,MAAM,SAAS,wCAEsF,CAAC;AAC7G,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAW+E,CAAC;AACtG,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC;AAmBnD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAMzD"}
1
+ {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../src/logger/record.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;GAKG;AAEH,4GAA4G;AAC5G,eAAO,MAAM,QAAQ;;;;;EAE4F,CAAC;AAClH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEjD;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAgD,CAAC;AAEtG;;;;GAIG;AACH,eAAO,MAAM,SAAS,wCAEsF,CAAC;AAC7G,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAW+E,CAAC;AACtG,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC;AAmBnD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAMzD"}
@@ -17,10 +17,31 @@ export declare const MIGRATION_LOCK_TABLE = "pithy_migrations_lock";
17
17
  * migration, so a rollback or a full reset must not remove it, and no capability has to ship it.
18
18
  */
19
19
  export declare const MIGRATION_OWNER_TABLE = "pithy_migrations_owner";
20
+ /**
21
+ * Which run applied each migration — one row per applied migration, written by the runner from the
22
+ * `MigrationResult[]` Kysely hands back (`./groups`). Beside the ledger for the same reason the owner
23
+ * stamp is: Kysely creates `pithy_migrations` and its insert sets `name` and `timestamp` only, so there
24
+ * is nowhere in it to put a group — and a group is not a migration, so a rollback or a full reset must
25
+ * not remove the table. A reversed group's own rows go; the table stays.
26
+ */
27
+ export declare const MIGRATION_GROUP_TABLE = "pithy_migrations_groups";
20
28
  /** The D1 dialect Pithy migrates through: stock kysely-d1, with the `sqlite_master`-only introspector. */
21
29
  export declare class D1MigrationDialect extends D1Dialect {
22
30
  createIntrospector(db: Kysely<unknown>): DatabaseIntrospector;
23
31
  }
24
32
  /** A Kysely over one D1 binding, with Pithy's dialect and `CamelCasePlugin` — the one way in. */
25
33
  export declare function migrationKysely<DB = unknown>(database: D1Database): Kysely<DB>;
34
+ /**
35
+ * The applied migrations, **in the order Kysely steps down from them** — oldest first, tip last.
36
+ *
37
+ * Kysely's `Migrator` sorts the ledger by `timestamp`, breaks a tie on the name, and reverses that list to
38
+ * decide what the next `migrateDown()` reverses. So anything that asks *is this the top of the chain* has
39
+ * to read the same order: concluding it from the order some other table was written in answers a different
40
+ * question, and the answer only differs in the cases that matter (`./groups`, #694).
41
+ *
42
+ * Empty when the ledger table does not exist — a database nothing has migrated. Existence is checked
43
+ * against `sqlite_master`, a plain select D1 permits, rather than by catching the read's error, so a
44
+ * genuine read failure surfaces instead of being read as "nothing applied".
45
+ */
46
+ export declare function appliedMigrationChain<DB>(db: Kysely<DB>): Promise<string[]>;
26
47
  //# sourceMappingURL=bookkeeping.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bookkeeping.d.ts","sourceRoot":"","sources":["../../src/migrations/bookkeeping.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAA0D,MAAM,QAAQ,CAAC;AAC3G,OAAO,EAAmB,MAAM,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAsBtC;;;GAGG;AACH,eAAO,MAAM,eAAe,qBAAqB,CAAC;AAElD,oGAAoG;AACpG,eAAO,MAAM,oBAAoB,0BAA0B,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,2BAA2B,CAAC;AAgC9D,0GAA0G;AAC1G,qBAAa,kBAAmB,SAAQ,SAAS;IACtC,kBAAkB,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,oBAAoB,CAErE;CACF;AAED,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,UAAU,GAAG,MAAM,CAAC,EAAE,CAAC,CAE9E"}
1
+ {"version":3,"file":"bookkeeping.d.ts","sourceRoot":"","sources":["../../src/migrations/bookkeeping.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAA0D,MAAM,QAAQ,CAAC;AAC3G,OAAO,EAAmB,MAAM,EAAO,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAsBtC;;;GAGG;AACH,eAAO,MAAM,eAAe,qBAAqB,CAAC;AAElD,oGAAoG;AACpG,eAAO,MAAM,oBAAoB,0BAA0B,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,2BAA2B,CAAC;AAE9D;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAgC/D,0GAA0G;AAC1G,qBAAa,kBAAmB,SAAQ,SAAS;IACtC,kBAAkB,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,oBAAoB,CAErE;CACF;AAED,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,UAAU,GAAG,MAAM,CAAC,EAAE,CAAC,CAE9E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAcjF"}
@@ -1,6 +1,6 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
- import { CamelCasePlugin, Kysely } from "kysely";
3
+ import { CamelCasePlugin, Kysely, sql } from "kysely";
4
4
  import { D1Dialect } from "kysely-d1";
5
5
  //#region src/migrations/bookkeeping.ts
6
6
  /**
@@ -16,11 +16,20 @@ const MIGRATION_LOCK_TABLE = "pithy_migrations_lock";
16
16
  * migration, so a rollback or a full reset must not remove it, and no capability has to ship it.
17
17
  */
18
18
  const MIGRATION_OWNER_TABLE = "pithy_migrations_owner";
19
+ /**
20
+ * Which run applied each migration — one row per applied migration, written by the runner from the
21
+ * `MigrationResult[]` Kysely hands back (`./groups`). Beside the ledger for the same reason the owner
22
+ * stamp is: Kysely creates `pithy_migrations` and its insert sets `name` and `timestamp` only, so there
23
+ * is nowhere in it to put a group — and a group is not a migration, so a rollback or a full reset must
24
+ * not remove the table. A reversed group's own rows go; the table stays.
25
+ */
26
+ const MIGRATION_GROUP_TABLE = "pithy_migrations_groups";
19
27
  /** Pithy's bookkeeping tables — hidden by the introspector the way Kysely hides its own defaults. */
20
28
  const INTERNAL_TABLES = [
21
29
  MIGRATION_TABLE,
22
30
  MIGRATION_LOCK_TABLE,
23
- MIGRATION_OWNER_TABLE
31
+ MIGRATION_OWNER_TABLE,
32
+ MIGRATION_GROUP_TABLE
24
33
  ];
25
34
  /** Table names from `sqlite_master` only — no pragma joins, no column metadata. */
26
35
  var D1Introspector = class {
@@ -55,5 +64,25 @@ function migrationKysely(database) {
55
64
  plugins: [new CamelCasePlugin()]
56
65
  });
57
66
  }
67
+ /**
68
+ * The applied migrations, **in the order Kysely steps down from them** — oldest first, tip last.
69
+ *
70
+ * Kysely's `Migrator` sorts the ledger by `timestamp`, breaks a tie on the name, and reverses that list to
71
+ * decide what the next `migrateDown()` reverses. So anything that asks *is this the top of the chain* has
72
+ * to read the same order: concluding it from the order some other table was written in answers a different
73
+ * question, and the answer only differs in the cases that matter (`./groups`, #694).
74
+ *
75
+ * Empty when the ledger table does not exist — a database nothing has migrated. Existence is checked
76
+ * against `sqlite_master`, a plain select D1 permits, rather than by catching the read's error, so a
77
+ * genuine read failure surfaces instead of being read as "nothing applied".
78
+ */
79
+ async function appliedMigrationChain(db) {
80
+ if ((await sql`select name from sqlite_master where type = 'table' and name = ${"pithy_migrations"}`.execute(db)).rows.length === 0) return [];
81
+ const { rows } = await sql`select name, timestamp from ${sql.table(MIGRATION_TABLE)}`.execute(db);
82
+ return [...rows].sort((left, right) => {
83
+ const elapsed = new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime();
84
+ return elapsed === 0 ? left.name.localeCompare(right.name) : elapsed;
85
+ }).map((row) => row.name);
86
+ }
58
87
  //#endregion
59
- export { D1MigrationDialect, MIGRATION_LOCK_TABLE, MIGRATION_OWNER_TABLE, MIGRATION_TABLE, migrationKysely };
88
+ export { D1MigrationDialect, MIGRATION_GROUP_TABLE, MIGRATION_LOCK_TABLE, MIGRATION_OWNER_TABLE, MIGRATION_TABLE, appliedMigrationChain, migrationKysely };