@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.
- package/dist/asyncapi/document.d.ts +46 -0
- package/dist/asyncapi/document.d.ts.map +1 -0
- package/dist/asyncapi/document.js +115 -0
- package/dist/capability/capability.d.ts +7 -0
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/composition.d.ts.map +1 -1
- package/dist/capability/composition.js +5 -1
- package/dist/capability/settings.d.ts +10 -2
- package/dist/capability/settings.d.ts.map +1 -1
- package/dist/capability/settings.js +5 -3
- package/dist/cli/command.d.ts +364 -0
- package/dist/cli/command.d.ts.map +1 -0
- package/dist/cli/command.js +418 -0
- package/dist/cli/commandCatalog.generated.d.ts +2275 -0
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -0
- package/dist/cli/commandCatalog.generated.js +2390 -0
- package/dist/cli/commandFacts.d.ts +164 -0
- package/dist/cli/commandFacts.d.ts.map +1 -0
- package/dist/cli/commandFacts.js +215 -0
- package/dist/controlPlane/error/errors.d.ts.map +1 -1
- package/dist/controlPlane/error/errors.js +4 -3
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +11 -1
- package/dist/controlPlane/http/routes.d.ts +1 -1
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +34 -7
- package/dist/createBackend.d.ts +7 -0
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +34 -4
- package/dist/entitlement/require.js +1 -1
- package/dist/error/cause.d.ts +16 -116
- package/dist/error/cause.d.ts.map +1 -1
- package/dist/error/cause.js +19 -153
- package/dist/error/http.d.ts +40 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +80 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +26 -0
- package/dist/error/pithyError.d.ts +44 -0
- package/dist/error/pithyError.d.ts.map +1 -1
- package/dist/error/pithyError.js +56 -1
- package/dist/error/unresolved.d.ts +265 -0
- package/dist/error/unresolved.d.ts.map +1 -0
- package/dist/error/unresolved.js +599 -0
- package/dist/error/wire.d.ts +20 -0
- package/dist/error/wire.d.ts.map +1 -0
- package/dist/error/wire.js +19 -0
- package/dist/http/oidcWebhook.d.ts +3 -3
- package/dist/http/oidcWebhook.js +3 -3
- package/dist/http/routeContract.js +1 -1
- package/dist/http/routeSpec.d.ts +66 -0
- package/dist/http/routeSpec.d.ts.map +1 -0
- package/dist/http/routeSpec.js +40 -0
- package/dist/http/sameOrigin.d.ts +1 -1
- package/dist/http/sameOrigin.js +1 -1
- package/dist/http/signedWebhook.d.ts +3 -3
- package/dist/http/signedWebhook.js +3 -3
- package/dist/http/socket.d.ts +52 -0
- package/dist/http/socket.d.ts.map +1 -0
- package/dist/http/socket.js +2 -0
- package/dist/http/subprotocol.d.ts +29 -0
- package/dist/http/subprotocol.d.ts.map +1 -0
- package/dist/http/subprotocol.js +37 -0
- package/dist/http/validation.d.ts +40 -8
- package/dist/http/validation.d.ts.map +1 -1
- package/dist/http/validation.js +25 -1
- package/dist/index.js +2 -2
- package/dist/logger/record.d.ts +23 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/migrations/bookkeeping.d.ts +21 -0
- package/dist/migrations/bookkeeping.d.ts.map +1 -1
- package/dist/migrations/bookkeeping.js +32 -3
- package/dist/migrations/groups.d.ts +140 -0
- package/dist/migrations/groups.d.ts.map +1 -0
- package/dist/migrations/groups.js +191 -0
- package/dist/migrations/migrateStep.d.ts +13 -0
- package/dist/migrations/migrateStep.d.ts.map +1 -0
- package/dist/migrations/migrateStep.js +20 -0
- package/dist/migrations/owner.d.ts +7 -0
- package/dist/migrations/owner.d.ts.map +1 -1
- package/dist/migrations/owner.js +11 -7
- package/dist/migrations/retained.js +2 -2
- package/dist/migrations/runner.d.ts +76 -5
- package/dist/migrations/runner.d.ts.map +1 -1
- package/dist/migrations/runner.js +130 -19
- package/dist/naming/domains.d.ts +30 -0
- package/dist/naming/domains.d.ts.map +1 -1
- package/dist/naming/domains.js +83 -4
- package/dist/naming/environment.d.ts.map +1 -1
- package/dist/naming/environment.js +2 -1
- package/dist/naming/packageName.d.ts +17 -0
- package/dist/naming/packageName.d.ts.map +1 -0
- package/dist/naming/packageName.js +22 -0
- package/dist/naming/provisionScope.d.ts.map +1 -1
- package/dist/naming/provisionScope.js +2 -1
- package/dist/openapi/document.d.ts +106 -0
- package/dist/openapi/document.d.ts.map +1 -0
- package/dist/openapi/document.js +314 -0
- package/dist/openapi/routeGroups.d.ts +8 -0
- package/dist/openapi/routeGroups.d.ts.map +1 -0
- package/dist/openapi/routeGroups.js +23 -0
- package/dist/seed/devLogin.d.ts +33 -0
- package/dist/seed/devLogin.d.ts.map +1 -1
- package/dist/seed/devLogin.js +28 -1
- package/dist/text/comments.js +66 -4
- package/dist/text/literals.d.ts +43 -0
- package/dist/text/literals.d.ts.map +1 -0
- package/dist/text/literals.js +293 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.d.ts.map +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workflow/dispatchRoute.d.ts.map +1 -1
- package/dist/workflow/dispatchRoute.js +12 -6
- package/dist/workflow/faults.d.ts.map +1 -1
- package/dist/workflow/faults.js +2 -3
- package/dist/workflow/hostEnv.d.ts +1 -0
- package/dist/workflow/hostEnv.d.ts.map +1 -1
- package/dist/workflow/hostEnv.js +4 -2
- package/dist/workflow/loopback.d.ts.map +1 -1
- package/dist/workflow/loopback.js +2 -1
- package/package.json +3 -3
- package/src/asyncapi/document.ts +164 -0
- package/src/capability/capability.ts +7 -0
- package/src/capability/composition.ts +2 -1
- package/src/capability/settings.ts +19 -2
- package/src/cli/command.ts +865 -0
- package/src/cli/commandCatalog.generated.ts +2533 -0
- package/src/cli/commandFacts.ts +219 -0
- package/src/controlPlane/error/errors.ts +10 -3
- package/src/controlPlane/http/guard.ts +14 -2
- package/src/controlPlane/http/routes.ts +57 -9
- package/src/createBackend.ts +35 -2
- package/src/entitlement/require.ts +1 -1
- package/src/error/cause.ts +18 -181
- package/src/error/payload.ts +59 -0
- package/src/error/pithyError.ts +76 -0
- package/src/error/unresolved.ts +853 -0
- package/src/error/wire.ts +40 -0
- package/src/http/oidcWebhook.ts +3 -3
- package/src/http/routeContract.ts +1 -1
- package/src/http/routeSpec.ts +81 -0
- package/src/http/sameOrigin.ts +1 -1
- package/src/http/signedWebhook.ts +3 -3
- package/src/http/socket.ts +55 -0
- package/src/http/subprotocol.ts +47 -0
- package/src/http/validation.ts +50 -8
- package/src/migrations/bookkeeping.ts +39 -2
- package/src/migrations/groups.ts +295 -0
- package/src/migrations/migrateStep.ts +19 -0
- package/src/migrations/owner.ts +10 -6
- package/src/migrations/retained.ts +2 -2
- package/src/migrations/runner.ts +214 -22
- package/src/naming/domains.ts +97 -16
- package/src/naming/environment.ts +2 -1
- package/src/naming/packageName.ts +20 -0
- package/src/naming/provisionScope.ts +2 -1
- package/src/openapi/document.ts +412 -0
- package/src/openapi/routeGroups.ts +25 -0
- package/src/seed/devLogin.ts +31 -0
- package/src/text/comments.ts +56 -4
- package/src/text/literals.ts +294 -0
- package/src/version.generated.ts +1 -1
- package/src/workflow/dispatchRoute.ts +13 -6
- package/src/workflow/faults.ts +5 -3
- package/src/workflow/hostEnv.ts +9 -1
- package/src/workflow/loopback.ts +2 -1
package/dist/http/oidcWebhook.js
CHANGED
|
@@ -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({…}),
|
|
238
|
-
*
|
|
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 `
|
|
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 `
|
|
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(),
|
|
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.
|
package/dist/http/sameOrigin.js
CHANGED
|
@@ -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(),
|
|
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({…}),
|
|
143
|
-
* Body
|
|
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 `
|
|
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({…}),
|
|
210
|
-
* Body
|
|
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 `
|
|
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,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 `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
17
|
-
* a
|
|
18
|
-
*
|
|
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":"
|
|
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"}
|
package/dist/http/validation.js
CHANGED
|
@@ -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 };
|
package/dist/logger/record.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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 };
|