@pithy-sh/core 0.9.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 (162) 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 +17 -0
  87. package/dist/naming/domains.d.ts.map +1 -1
  88. package/dist/naming/domains.js +62 -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/text/comments.js +66 -4
  103. package/dist/text/literals.d.ts +43 -0
  104. package/dist/text/literals.d.ts.map +1 -0
  105. package/dist/text/literals.js +293 -0
  106. package/dist/version.generated.d.ts +1 -1
  107. package/dist/version.generated.d.ts.map +1 -1
  108. package/dist/version.generated.js +1 -1
  109. package/dist/workflow/dispatchRoute.d.ts.map +1 -1
  110. package/dist/workflow/dispatchRoute.js +12 -6
  111. package/dist/workflow/faults.d.ts.map +1 -1
  112. package/dist/workflow/faults.js +2 -3
  113. package/dist/workflow/hostEnv.d.ts +1 -0
  114. package/dist/workflow/hostEnv.d.ts.map +1 -1
  115. package/dist/workflow/hostEnv.js +4 -2
  116. package/dist/workflow/loopback.d.ts.map +1 -1
  117. package/dist/workflow/loopback.js +2 -1
  118. package/package.json +3 -3
  119. package/src/asyncapi/document.ts +164 -0
  120. package/src/capability/capability.ts +7 -0
  121. package/src/capability/composition.ts +2 -1
  122. package/src/capability/settings.ts +19 -2
  123. package/src/cli/command.ts +865 -0
  124. package/src/cli/commandCatalog.generated.ts +2533 -0
  125. package/src/cli/commandFacts.ts +219 -0
  126. package/src/controlPlane/error/errors.ts +10 -3
  127. package/src/controlPlane/http/guard.ts +14 -2
  128. package/src/controlPlane/http/routes.ts +57 -9
  129. package/src/createBackend.ts +35 -2
  130. package/src/entitlement/require.ts +1 -1
  131. package/src/error/cause.ts +18 -181
  132. package/src/error/payload.ts +59 -0
  133. package/src/error/pithyError.ts +76 -0
  134. package/src/error/unresolved.ts +853 -0
  135. package/src/error/wire.ts +40 -0
  136. package/src/http/oidcWebhook.ts +3 -3
  137. package/src/http/routeContract.ts +1 -1
  138. package/src/http/routeSpec.ts +81 -0
  139. package/src/http/sameOrigin.ts +1 -1
  140. package/src/http/signedWebhook.ts +3 -3
  141. package/src/http/socket.ts +55 -0
  142. package/src/http/subprotocol.ts +47 -0
  143. package/src/http/validation.ts +50 -8
  144. package/src/migrations/bookkeeping.ts +39 -2
  145. package/src/migrations/groups.ts +295 -0
  146. package/src/migrations/migrateStep.ts +19 -0
  147. package/src/migrations/owner.ts +10 -6
  148. package/src/migrations/retained.ts +2 -2
  149. package/src/migrations/runner.ts +214 -22
  150. package/src/naming/domains.ts +75 -16
  151. package/src/naming/environment.ts +2 -1
  152. package/src/naming/packageName.ts +20 -0
  153. package/src/naming/provisionScope.ts +2 -1
  154. package/src/openapi/document.ts +412 -0
  155. package/src/openapi/routeGroups.ts +25 -0
  156. package/src/text/comments.ts +56 -4
  157. package/src/text/literals.ts +294 -0
  158. package/src/version.generated.ts +1 -1
  159. package/src/workflow/dispatchRoute.ts +13 -6
  160. package/src/workflow/faults.ts +5 -3
  161. package/src/workflow/hostEnv.ts +9 -1
  162. package/src/workflow/loopback.ts +2 -1
@@ -0,0 +1,314 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { CONTROL_PLANE_HEADER } from "../controlPlane/wire.js";
4
+ import { routeSpecOf } from "../http/routeSpec.js";
5
+ import { requestContractOf } from "../http/validation.js";
6
+ import { ErrorBody } from "../error/wire.js";
7
+ import { pathParams } from "../http/routeContract.js";
8
+ import { z } from "zod";
9
+ //#region src/openapi/document.ts
10
+ /**
11
+ * **The OpenAPI contract, read off a composed app.**
12
+ *
13
+ * Nothing here is written by hand. A route's request schemas come from its `validate` middleware, its
14
+ * summary, verification and responses from its `describeRoute`, and both are found by walking
15
+ * `app.routes` — the same table Hono dispatches from. So the document describes exactly what the Worker
16
+ * serves, the adopter's own routes included, and a schema edit reaches it with no second step.
17
+ *
18
+ * Zod is already the source of truth for every request and response, and every field carries a
19
+ * `.describe()` (CLAUDE.md §Zod). `z.toJSONSchema` keeps those descriptions, which is what lets a client
20
+ * generated in Swift or Kotlin carry the same documentation as the TypeScript one.
21
+ *
22
+ * OpenAPI 3.1, whose schema dialect is JSON Schema 2020-12 — the dialect `z.toJSONSchema` writes — so
23
+ * every schema crosses unchanged.
24
+ */
25
+ /** The OpenAPI version this module writes. */
26
+ const OPENAPI_VERSION = "3.1.0";
27
+ /** How each verification strategy is presented on the wire. `signed-webhook` and `public` have no scheme. */
28
+ const SECURITY_SCHEMES = {
29
+ bearer: {
30
+ type: "http",
31
+ scheme: "bearer",
32
+ description: "A short-lived access token from `@pithy-sh/auth`, sent as `Authorization: Bearer <token>`."
33
+ },
34
+ session: {
35
+ type: "apiKey",
36
+ in: "cookie",
37
+ name: "better-auth.session_token",
38
+ description: "A browser session cookie from `@pithy-sh/auth`, named `__Secure-better-auth.session_token` over HTTPS. A state-changing request also needs its `Origin` to be one the Worker trusts."
39
+ },
40
+ controlPlane: {
41
+ type: "apiKey",
42
+ in: "header",
43
+ name: CONTROL_PLANE_HEADER,
44
+ description: "A customer-issued, scoped control-plane credential. Default-denied."
45
+ }
46
+ };
47
+ /** The `security` requirement for each strategy that has one. */
48
+ const REQUIREMENT = {
49
+ bearer: { bearer: [] },
50
+ session: { session: [] },
51
+ "control-plane": { controlPlane: [] },
52
+ public: {}
53
+ };
54
+ const ERROR_RESPONSE = "Error";
55
+ /** Hono's `/a/:id{[0-9]+}/*` as OpenAPI's `/a/{id}/{path}`. */
56
+ function openApiPath(path) {
57
+ return path.replace(/:([A-Za-z0-9_]+)(\{[^}]*\})?\??/g, "{$1}").replace(/\/\*$/, "/{path}").replace(/\*/g, "{path}");
58
+ }
59
+ /**
60
+ * `get` + `/leaderboard/{board}/top` → `getLeaderboardByBoardTop`. A parameter reads as `By<Name>`, so
61
+ * `/items/{id}` and `/items/id` do not meet. Stable, but not unique on its own — `/a-b` and `/a/b` still
62
+ * share one — so {@link openApiDocument} numbers a repeat rather than writing an invalid document.
63
+ */
64
+ function operationId(method, path) {
65
+ const words = path.split("/").flatMap((segment) => {
66
+ const param = /^\{(.+)\}$/.exec(segment);
67
+ return param ? ["by", ...(param[1] ?? "").split(/[^A-Za-z0-9]+/)] : segment.split(/[^A-Za-z0-9]+/);
68
+ }).filter((word) => word.length > 0).map((word) => `${word[0]?.toUpperCase()}${word.slice(1)}`);
69
+ return `${method.toLowerCase()}${words.join("")}`;
70
+ }
71
+ /** A Zod schema as JSON Schema, without the `$schema` key OpenAPI does not want inside a document. */
72
+ function jsonSchema(schema, io) {
73
+ const { $schema: _dialect, ...rest } = z.toJSONSchema(schema, {
74
+ io,
75
+ unrepresentable: "any"
76
+ });
77
+ return rest;
78
+ }
79
+ function operationsOf(routes) {
80
+ const byKey = /* @__PURE__ */ new Map();
81
+ for (const route of routes) {
82
+ const key = `${route.method} ${route.path}`;
83
+ const entry = byKey.get(key) ?? {
84
+ method: route.method,
85
+ path: route.path,
86
+ handlers: []
87
+ };
88
+ entry.handlers.push(route.handler);
89
+ byKey.set(key, entry);
90
+ }
91
+ return [...byKey.values()];
92
+ }
93
+ /**
94
+ * An `ALL` entry with no spec is `app.use` — middleware, not a route — because Hono records both as
95
+ * `ALL`. An `app.all` route that forgot its `describeRoute` is therefore invisible to the coverage
96
+ * check, which is the one blind spot here; the kit has one such route and it is described.
97
+ */
98
+ function isMiddlewareOnly(operation) {
99
+ return operation.method === "ALL" && !operation.handlers.some((handler) => routeSpecOf(handler));
100
+ }
101
+ /** Every route in these groups that registered no `describeRoute`. Empty is the contract being met. */
102
+ function undescribedRoutes(groups) {
103
+ return groups.flatMap((group) => operationsOf(group.routes).filter((operation) => !isMiddlewareOnly(operation)).filter((operation) => !operation.handlers.some((handler) => routeSpecOf(handler))).map(({ method, path }) => ({
104
+ method,
105
+ path
106
+ })));
107
+ }
108
+ /** The parameters one validator contributes: one per property of its object schema. */
109
+ function parametersOf(contract) {
110
+ const location = {
111
+ param: "path",
112
+ query: "query",
113
+ header: "header",
114
+ cookie: "cookie"
115
+ }[contract.target];
116
+ if (location === void 0) return [];
117
+ const schema = jsonSchema(contract.schema, "input");
118
+ const properties = schema.properties ?? {};
119
+ const required = new Set(schema.required ?? []);
120
+ return Object.entries(properties).map(([name, property]) => {
121
+ const { description, ...rest } = property;
122
+ return {
123
+ name,
124
+ in: location,
125
+ required: location === "path" || required.has(name),
126
+ ...typeof description === "string" ? { description } : {},
127
+ schema: rest
128
+ };
129
+ });
130
+ }
131
+ /** The request body one validator contributes, if it reads the body. */
132
+ function requestBodyOf(contract) {
133
+ const media = {
134
+ json: "application/json",
135
+ form: "multipart/form-data"
136
+ }[contract.target];
137
+ if (media === void 0) return void 0;
138
+ const schema = jsonSchema(contract.schema, "input");
139
+ return {
140
+ required: !contract.schema.safeParse(void 0).success,
141
+ ...typeof schema.description === "string" ? { description: schema.description } : {},
142
+ content: { [media]: { schema } }
143
+ };
144
+ }
145
+ function isZodSchema(value) {
146
+ return value instanceof z.ZodType;
147
+ }
148
+ /** One `responses` entry. A bare schema is a JSON body; a {@link RouteResponse} says what it is. */
149
+ function responseOf(declared) {
150
+ if (isZodSchema(declared)) {
151
+ const schema = jsonSchema(declared, "input");
152
+ return {
153
+ description: typeof schema.description === "string" ? schema.description : "Success.",
154
+ content: { "application/json": { schema } }
155
+ };
156
+ }
157
+ const media = declared.contentType ?? (declared.schema ? "application/json" : void 0);
158
+ return {
159
+ description: declared.description,
160
+ ...media === void 0 ? {} : { content: { [media]: { schema: declared.schema ? jsonSchema(declared.schema, "input") : {} } } }
161
+ };
162
+ }
163
+ function strategiesOf(spec) {
164
+ return typeof spec.verification === "string" ? [spec.verification] : spec.verification;
165
+ }
166
+ /** One OpenAPI operation for a described route. */
167
+ function operationOf(tag, method, operation, spec) {
168
+ const contracts = operation.handlers.flatMap((handler) => {
169
+ const contract = requestContractOf(handler);
170
+ return contract ? [contract] : [];
171
+ });
172
+ const parameters = contracts.flatMap(parametersOf);
173
+ for (const name of pathParams(operation.path)) if (!parameters.some((parameter) => parameter.in === "path" && parameter.name === name)) parameters.push({
174
+ name,
175
+ in: "path",
176
+ required: true,
177
+ schema: { type: "string" }
178
+ });
179
+ if (/\*/.test(operation.path)) parameters.push({
180
+ name: "path",
181
+ in: "path",
182
+ required: true,
183
+ description: "The rest of the path, passed through to the handler.",
184
+ schema: { type: "string" }
185
+ });
186
+ const body = contracts.map(requestBodyOf).find((candidate) => candidate !== void 0);
187
+ const strategies = strategiesOf(spec);
188
+ const security = strategies.flatMap((strategy) => {
189
+ const requirement = REQUIREMENT[strategy];
190
+ return requirement === void 0 ? [] : [requirement];
191
+ });
192
+ const responses = {};
193
+ for (const [status, declared] of Object.entries(spec.responses)) responses[status] = responseOf(declared);
194
+ responses.default = { $ref: `#/components/responses/${ERROR_RESPONSE}` };
195
+ return {
196
+ tags: [tag],
197
+ operationId: operationId(method, openApiPath(operation.path)),
198
+ summary: spec.summary,
199
+ ...spec.description === void 0 ? {} : { description: spec.description },
200
+ ...parameters.length === 0 ? {} : { parameters },
201
+ ...body === void 0 ? {} : { requestBody: body },
202
+ responses,
203
+ ...strategies.length === 0 ? {} : {
204
+ security,
205
+ "x-pithy-verification": strategies
206
+ },
207
+ ...spec.scopes === void 0 || spec.scopes.length === 0 ? {} : { "x-pithy-scopes": spec.scopes }
208
+ };
209
+ }
210
+ /** The methods an operation is documented under: its own, or for `app.all` the ones its spec names. */
211
+ function methodsOf(operation, spec) {
212
+ if (operation.method !== "ALL") return [operation.method.toLowerCase()];
213
+ return spec.methods ?? ["get", "post"];
214
+ }
215
+ /** The spec an undescribed route is written with under `undescribed: "include"`. It claims nothing. */
216
+ const UNDESCRIBED = {
217
+ summary: "Not described. Add describeRoute to this route.",
218
+ verification: [],
219
+ responses: {}
220
+ };
221
+ /**
222
+ * The OpenAPI document for these route groups. Undescribed routes are left out rather than guessed at,
223
+ * unless {@link DocumentOptions.undescribed} says otherwise; {@link undescribedRoutes} is the check that
224
+ * keeps that set empty.
225
+ */
226
+ function openApiDocument(info, groups, options = {}) {
227
+ const paths = {};
228
+ const tags = [];
229
+ const ids = /* @__PURE__ */ new Map();
230
+ /** An operation id no earlier operation took: the derived one, or it with the next free number. */
231
+ const unique = (id) => {
232
+ const seen = ids.get(id) ?? 0;
233
+ ids.set(id, seen + 1);
234
+ return seen === 0 ? id : `${id}${seen + 1}`;
235
+ };
236
+ for (const group of groups) {
237
+ let documented = false;
238
+ for (const operation of operationsOf(group.routes)) {
239
+ if (isMiddlewareOnly(operation)) continue;
240
+ const declared = operation.handlers.map(routeSpecOf).find((candidate) => candidate !== void 0);
241
+ if (declared === void 0 && options.undescribed !== "include") continue;
242
+ const spec = declared ?? UNDESCRIBED;
243
+ documented = true;
244
+ const path = openApiPath(operation.path);
245
+ const item = paths[path] ?? {};
246
+ for (const method of methodsOf(operation, spec)) {
247
+ const derived = operationOf(group.tag, method, operation, spec);
248
+ const written = {
249
+ ...derived,
250
+ operationId: unique(String(derived.operationId))
251
+ };
252
+ item[method] = declared === void 0 ? {
253
+ ...written,
254
+ "x-pithy-undescribed": true
255
+ } : written;
256
+ }
257
+ paths[path] = item;
258
+ }
259
+ if (documented) tags.push({
260
+ name: group.tag,
261
+ ...group.description ? { description: group.description } : {}
262
+ });
263
+ }
264
+ return {
265
+ openapi: OPENAPI_VERSION,
266
+ info,
267
+ tags,
268
+ paths,
269
+ components: {
270
+ schemas: { [ERROR_RESPONSE]: jsonSchema(ErrorBody, "input") },
271
+ responses: { [ERROR_RESPONSE]: {
272
+ description: "Any failure. The status is the error's own; the body is always this shape.",
273
+ content: { "application/json": { schema: { $ref: `#/components/schemas/${ERROR_RESPONSE}` } } }
274
+ } },
275
+ securitySchemes: SECURITY_SCHEMES
276
+ }
277
+ };
278
+ }
279
+ const escapePointer = (segment) => segment.replaceAll("~", "~0").replaceAll("/", "~1");
280
+ /**
281
+ * Every object property and every parameter in the document without a description. The kit holds every
282
+ * Zod field to `.describe()`, and this is how that rule is checked on the wire shape a client actually
283
+ * generates from, rather than on the source it came from.
284
+ */
285
+ function undescribedFields(document) {
286
+ return undescribedFieldsIn(document.paths, "/paths");
287
+ }
288
+ /**
289
+ * The same sweep over any part of a contract document, from a JSON pointer: every object property and every
290
+ * OpenAPI parameter below `node` without a description. The AsyncAPI document runs it over its messages.
291
+ */
292
+ function undescribedFieldsIn(node, at) {
293
+ const found = [];
294
+ const visit = (node, pointer) => {
295
+ if (Array.isArray(node)) {
296
+ node.forEach((child, index) => {
297
+ visit(child, `${pointer}/${index}`);
298
+ });
299
+ return;
300
+ }
301
+ if (node === null || typeof node !== "object") return;
302
+ const record = node;
303
+ if (typeof record.in === "string" && typeof record.name === "string" && typeof record.description !== "string") found.push({ pointer });
304
+ const properties = record.properties;
305
+ if (properties !== null && typeof properties === "object" && !Array.isArray(properties)) {
306
+ for (const [name, property] of Object.entries(properties)) if (!(property !== null && typeof property === "object" && typeof property.description === "string")) found.push({ pointer: `${pointer}/properties/${escapePointer(name)}` });
307
+ }
308
+ for (const [key, child] of Object.entries(record)) visit(child, `${pointer}/${escapePointer(key)}`);
309
+ };
310
+ visit(node, at);
311
+ return found;
312
+ }
313
+ //#endregion
314
+ export { OPENAPI_VERSION, jsonSchema, openApiDocument, openApiPath, operationId, undescribedFields, undescribedFieldsIn, undescribedRoutes };
@@ -0,0 +1,8 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { RouteGroup } from "./document";
4
+ /** Note the route groups of an assembled app. Called by `createBackend` and nothing else. */
5
+ export declare function recordRouteGroups(app: object, groups: readonly RouteGroup[]): void;
6
+ /** The route groups `createBackend` recorded for this app, or `undefined` for an app it did not assemble. */
7
+ export declare function routeGroupsOf(app: object): readonly RouteGroup[] | undefined;
8
+ //# sourceMappingURL=routeGroups.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routeGroups.d.ts","sourceRoot":"","sources":["../../src/openapi/routeGroups.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAa7C,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI,CAElF;AAED,6GAA6G;AAC7G,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,UAAU,EAAE,GAAG,SAAS,CAE5E"}
@@ -0,0 +1,23 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ //#region src/openapi/routeGroups.ts
4
+ /**
5
+ * Which capability registered which routes, recorded by `createBackend` as it mounts them.
6
+ *
7
+ * Every capability registers onto the one shared app with absolute paths, so the finished route table
8
+ * says nothing about who owns a route. `createBackend` is the one place that knows — it calls each
9
+ * `routes` hook in turn — so it notes the slice of `app.routes` each hook added, and the contract tags
10
+ * each operation with the capability that owns it. Held beside the app rather than on it, so the app a
11
+ * Worker exports is the plain Hono instance it always was.
12
+ */
13
+ const recorded = /* @__PURE__ */ new WeakMap();
14
+ /** Note the route groups of an assembled app. Called by `createBackend` and nothing else. */
15
+ function recordRouteGroups(app, groups) {
16
+ recorded.set(app, groups);
17
+ }
18
+ /** The route groups `createBackend` recorded for this app, or `undefined` for an app it did not assemble. */
19
+ function routeGroupsOf(app) {
20
+ return recorded.get(app);
21
+ }
22
+ //#endregion
23
+ export { recordRouteGroups, routeGroupsOf };
@@ -82,9 +82,18 @@ function blankCommentsAndStrings(source) {
82
82
  }
83
83
  /** The one walk behind both: comments always blanked, a quoted run kept or blanked inside its delimiters. */
84
84
  function blank(source, strings) {
85
+ return walkCode(source, 0, strings, false).out;
86
+ }
87
+ /**
88
+ * Walk code from `start`. Inside an interpolation (`nested`), the `}` that closes it ends the walk, and is
89
+ * included. A template is walked by {@link walkTemplate}, which walks each of its interpolations as code in turn
90
+ * — so a comment inside `${…}` is blanked like any other, and a template inside one closes nothing (#711).
91
+ */
92
+ function walkCode(source, start, strings, nested) {
85
93
  let out = "";
86
94
  let previous = "";
87
- let index = 0;
95
+ let depth = 0;
96
+ let index = start;
88
97
  while (index < source.length) {
89
98
  const char = source[index];
90
99
  const pair = source.slice(index, index + 2);
@@ -94,8 +103,15 @@ function blank(source, strings) {
94
103
  index = stop;
95
104
  continue;
96
105
  }
97
- if (char === "\"" || char === "'" || char === "`") {
98
- const stop = endOfQuoted(source, index, char, char !== "`");
106
+ if (char === "`") {
107
+ const walked = walkTemplate(source, index, strings);
108
+ out += walked.out;
109
+ index = walked.end;
110
+ previous = char;
111
+ continue;
112
+ }
113
+ if (char === "\"" || char === "'") {
114
+ const stop = endOfQuoted(source, index, char, true);
99
115
  const run = source.slice(index, stop);
100
116
  out += strings ? blankInside(run, char) : run;
101
117
  index = stop;
@@ -109,11 +125,57 @@ function blank(source, strings) {
109
125
  previous = "/";
110
126
  continue;
111
127
  }
128
+ if (char === "{") depth += 1;
129
+ if (char === "}") {
130
+ if (nested && depth === 0) return {
131
+ out: `${out}}`,
132
+ end: index + 1
133
+ };
134
+ depth -= 1;
135
+ }
112
136
  out += char;
113
137
  if (!/\s/.test(char)) previous = char;
114
138
  index += 1;
115
139
  }
116
- return out;
140
+ return {
141
+ out,
142
+ end: index
143
+ };
144
+ }
145
+ /**
146
+ * Walk one template literal from its opening backtick. Its text is kept, or blanked whole — `${}` included — when
147
+ * `strings` asks; each interpolation is walked as code, so its comments are blanked and its own templates close
148
+ * nothing. Returns the text and the index past the closing backtick.
149
+ */
150
+ function walkTemplate(source, start, strings) {
151
+ let out = "`";
152
+ let index = start + 1;
153
+ while (index < source.length) {
154
+ const char = source[index];
155
+ if (char === "\\") {
156
+ const escaped = source.slice(index, index + 2);
157
+ out += strings ? escaped.replace(/[^\n]/g, " ") : escaped;
158
+ index += 2;
159
+ continue;
160
+ }
161
+ if (char === "`") return {
162
+ out: `${out}\``,
163
+ end: index + 1
164
+ };
165
+ if (char === "$" && source[index + 1] === "{") {
166
+ const walked = walkCode(source, index + 2, strings, true);
167
+ const text = `\${${walked.out}`;
168
+ out += strings ? text.replace(/[^\n]/g, " ") : text;
169
+ index = walked.end;
170
+ continue;
171
+ }
172
+ out += strings && char !== "\n" ? " " : char;
173
+ index += 1;
174
+ }
175
+ return {
176
+ out,
177
+ end: index
178
+ };
117
179
  }
118
180
  /** A quoted run with everything between its delimiters blanked, newlines kept. An unterminated run has no closer. */
119
181
  function blankInside(run, quote) {
@@ -0,0 +1,43 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * **Every string a source file writes, as it reads at run time near enough to scan (#711)** — each quoted or
5
+ * template literal's text, quotes off, escapes resolved to the character they stand for, and every `${…}` blanked
6
+ * to `<x>`.
7
+ *
8
+ * Gates that read what the code *says* — `packages/cli/src/ci/runnableCommands.test.ts` reads every `pithy …` a
9
+ * line names — used one regular expression for "a string or template literal", and it has a measured false
10
+ * reading: a template whose interpolation holds a template of its own, `` `${path}${detail ? ` — ${detail}` :
11
+ * ""}` ``, closed at the inner backtick, and everything after it was read as a literal that opened there. The
12
+ * scan then reported text the file does not contain, and missed the text it does. So this walks the source and
13
+ * keeps a stack: a `${` opens an expression, whose own literals are read as literals in their turn, and its
14
+ * matching `}` resumes the template it interrupted.
15
+ *
16
+ * Comments must be blanked first (`./comments`), as every caller here already does. A regex literal is stepped
17
+ * over by the same rule `./comments` uses — a `/` where a value may begin opens one — so a quote inside a pattern
18
+ * does not open a string.
19
+ */
20
+ export declare function literalTexts(source: string, options?: LiteralOptions): string[];
21
+ /** How a source is read. */
22
+ export interface LiteralOptions {
23
+ /**
24
+ * The source is a `.tsx` file: an element's text and its attribute strings are read as literals too, and a
25
+ * `<` where a value may begin opens an element rather than comparing. A JSX text run is read with its
26
+ * whitespace collapsed, as React renders it; a `{…}` child is `<x>`.
27
+ */
28
+ readonly jsx?: boolean;
29
+ }
30
+ /** One literal: its text as {@link literalTexts} reads it, and the source range it spans, delimiters included. */
31
+ export interface LiteralSpan {
32
+ readonly text: string;
33
+ readonly start: number;
34
+ readonly end: number;
35
+ /** A template's interpolations, each the range of the code between `${` and `}`. Absent for any other literal. */
36
+ readonly holes?: readonly (readonly [number, number])[];
37
+ }
38
+ /**
39
+ * {@link literalTexts}, with where each one sits — so a gate can read what is around a literal: a `+` beside it, or
40
+ * the array it is an element of being joined.
41
+ */
42
+ export declare function literalSpans(source: string, options?: LiteralOptions): LiteralSpan[];
43
+ //# sourceMappingURL=literals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"literals.d.ts","sourceRoot":"","sources":["../../src/text/literals.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,MAAM,EAAE,CAEnF;AAED,4BAA4B;AAC5B,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,kHAAkH;AAClH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,kHAAkH;IAClH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;CACzD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,WAAW,EAAE,CAIxF"}