@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,293 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ //#region src/text/literals.ts
4
+ /**
5
+ * **Every string a source file writes, as it reads at run time near enough to scan (#711)** — each quoted or
6
+ * template literal's text, quotes off, escapes resolved to the character they stand for, and every `${…}` blanked
7
+ * to `<x>`.
8
+ *
9
+ * Gates that read what the code *says* — `packages/cli/src/ci/runnableCommands.test.ts` reads every `pithy …` a
10
+ * line names — used one regular expression for "a string or template literal", and it has a measured false
11
+ * reading: a template whose interpolation holds a template of its own, `` `${path}${detail ? ` — ${detail}` :
12
+ * ""}` ``, closed at the inner backtick, and everything after it was read as a literal that opened there. The
13
+ * scan then reported text the file does not contain, and missed the text it does. So this walks the source and
14
+ * keeps a stack: a `${` opens an expression, whose own literals are read as literals in their turn, and its
15
+ * matching `}` resumes the template it interrupted.
16
+ *
17
+ * Comments must be blanked first (`./comments`), as every caller here already does. A regex literal is stepped
18
+ * over by the same rule `./comments` uses — a `/` where a value may begin opens one — so a quote inside a pattern
19
+ * does not open a string.
20
+ */
21
+ function literalTexts(source, options = {}) {
22
+ return literalSpans(source, options).map((span) => span.text);
23
+ }
24
+ /**
25
+ * {@link literalTexts}, with where each one sits — so a gate can read what is around a literal: a `+` beside it, or
26
+ * the array it is an element of being joined.
27
+ */
28
+ function literalSpans(source, options = {}) {
29
+ const found = [];
30
+ walk(source, 0, found, false, options.jsx === true);
31
+ return found;
32
+ }
33
+ /** After one of these, a `/` opens a regex literal; after anything else it divides. */
34
+ const VALUE_POSITION = /* @__PURE__ */ new Set([
35
+ "",
36
+ "(",
37
+ ",",
38
+ "=",
39
+ ":",
40
+ "[",
41
+ "!",
42
+ "&",
43
+ "|",
44
+ "?",
45
+ "{",
46
+ "}",
47
+ ";",
48
+ "+",
49
+ "-",
50
+ "*",
51
+ "%",
52
+ "^",
53
+ "~",
54
+ "<",
55
+ ">"
56
+ ]);
57
+ /**
58
+ * Walk code from `start`, pushing every literal it meets. Inside an interpolation (`nested`), an unmatched `}`
59
+ * closes it: the walk returns the index just past that brace.
60
+ */
61
+ function walk(source, start, found, nested, jsx) {
62
+ let depth = 0;
63
+ let previous = "";
64
+ let index = start;
65
+ while (index < source.length) {
66
+ const char = source[index];
67
+ if (char === "\"" || char === "'") {
68
+ index = quoted(source, index, char, found);
69
+ previous = char;
70
+ continue;
71
+ }
72
+ if (char === "`") {
73
+ index = template(source, index, found, jsx);
74
+ previous = char;
75
+ continue;
76
+ }
77
+ if (jsx && char === "<" && opensElement(source, index, previous)) {
78
+ const mark = found.length;
79
+ const end = element(source, index, found);
80
+ if (end !== null) {
81
+ index = end;
82
+ previous = ")";
83
+ continue;
84
+ }
85
+ found.length = mark;
86
+ }
87
+ if (char === "/" && VALUE_POSITION.has(previous)) {
88
+ index = endOfRegex(source, index);
89
+ previous = "/";
90
+ continue;
91
+ }
92
+ if (char === "{") depth += 1;
93
+ if (char === "}") {
94
+ if (depth === 0 && nested) return index + 1;
95
+ depth -= 1;
96
+ }
97
+ if (!/\s/.test(char)) previous = char;
98
+ index += 1;
99
+ }
100
+ return index;
101
+ }
102
+ /** One quoted string: its text pushed, the index past its closing quote returned. A newline ends it unclosed. */
103
+ function quoted(source, start, quote, found) {
104
+ let text = "";
105
+ for (let index = start + 1; index < source.length; index += 1) {
106
+ const char = source[index];
107
+ if (char === "\\") {
108
+ text += source[index + 1] ?? "";
109
+ index += 1;
110
+ continue;
111
+ }
112
+ if (char === quote || char === "\n") {
113
+ const end = char === quote ? index + 1 : index;
114
+ found.push({
115
+ text,
116
+ start,
117
+ end
118
+ });
119
+ return end;
120
+ }
121
+ text += char;
122
+ }
123
+ found.push({
124
+ text,
125
+ start,
126
+ end: source.length
127
+ });
128
+ return source.length;
129
+ }
130
+ /**
131
+ * One template: its text pushed with each `${…}` as `<x>`, and every literal inside an interpolation pushed in
132
+ * its own right. Returns the index past the closing backtick.
133
+ */
134
+ function template(source, start, found, jsx) {
135
+ let text = "";
136
+ const holes = [];
137
+ const slot = found.length;
138
+ found.push({
139
+ text: "",
140
+ start,
141
+ end: start
142
+ });
143
+ const close = (end) => ({
144
+ text,
145
+ start,
146
+ end,
147
+ ...holes.length > 0 ? { holes } : {}
148
+ });
149
+ let index = start + 1;
150
+ while (index < source.length) {
151
+ const char = source[index];
152
+ if (char === "\\") {
153
+ text += source[index + 1] ?? "";
154
+ index += 2;
155
+ continue;
156
+ }
157
+ if (char === "`") {
158
+ found[slot] = close(index + 1);
159
+ return index + 1;
160
+ }
161
+ if (char === "$" && source[index + 1] === "{") {
162
+ text += "<x>";
163
+ const from = index + 2;
164
+ index = walk(source, from, found, true, jsx);
165
+ holes.push([from, source[index - 1] === "}" ? index - 1 : index]);
166
+ continue;
167
+ }
168
+ text += char;
169
+ index += 1;
170
+ }
171
+ found[slot] = close(source.length);
172
+ return source.length;
173
+ }
174
+ /** After one of these words, as after a {@link VALUE_POSITION} character, a value begins. */
175
+ const VALUE_WORDS = /* @__PURE__ */ new Set([
176
+ "return",
177
+ "yield",
178
+ "await",
179
+ "default",
180
+ "case",
181
+ "else",
182
+ "do"
183
+ ]);
184
+ /**
185
+ * Whether the `<` at `index` opens a JSX element: where a value may begin — after an operator, a bracket, or a
186
+ * word such as `return` — and followed by a tag name or, for a fragment, `>`. After an identifier or a closing
187
+ * bracket it compares, or opens a type's arguments.
188
+ */
189
+ function opensElement(source, index, previous) {
190
+ if (!/[A-Za-z_$>]/.test(source[index + 1] ?? "")) return false;
191
+ if (VALUE_POSITION.has(previous)) return true;
192
+ const before = /([A-Za-z_$][\w$]*)\s*$/.exec(source.slice(Math.max(0, index - 32), index));
193
+ return before !== null && VALUE_WORDS.has(before[1]);
194
+ }
195
+ /** A JSX text run as React renders it: whitespace collapsed, and nothing pushed for a run that is only space. */
196
+ function pushText(text, start, end, found) {
197
+ const collapsed = text.replace(/\s+/g, " ").trim();
198
+ if (collapsed.length > 0) found.push({
199
+ text: collapsed,
200
+ start,
201
+ end
202
+ });
203
+ }
204
+ /**
205
+ * One JSX element from its `<`: each attribute string and each text run pushed, every `{…}` walked as code. Returns
206
+ * the index past the element, or `null` when what follows the `<` is not an element — a generic arrow `<T,>(…)`,
207
+ * or a `<` that never closes — for the caller to read as code.
208
+ */
209
+ function element(source, start, found) {
210
+ let index = start + 1;
211
+ const name = /^[A-Za-z_$][\w$.:-]*/.exec(source.slice(index))?.[0] ?? "";
212
+ index += name.length;
213
+ for (;;) {
214
+ while (/\s/.test(source[index] ?? "")) index += 1;
215
+ const char = source[index];
216
+ if (char === void 0) return null;
217
+ if (char === "/" && source[index + 1] === ">") return index + 2;
218
+ if (char === ">") {
219
+ index += 1;
220
+ break;
221
+ }
222
+ if (char === "{") {
223
+ index = walk(source, index + 1, found, true, true);
224
+ continue;
225
+ }
226
+ const attribute = /^[A-Za-z_$][\w$:-]*/.exec(source.slice(index))?.[0];
227
+ if (attribute === void 0 || name === "") return null;
228
+ index += attribute.length;
229
+ while (/\s/.test(source[index] ?? "")) index += 1;
230
+ if (source[index] !== "=") continue;
231
+ index += 1;
232
+ while (/\s/.test(source[index] ?? "")) index += 1;
233
+ const value = source[index];
234
+ if (value === "{") {
235
+ index = walk(source, index + 1, found, true, true);
236
+ continue;
237
+ }
238
+ if (value !== "\"" && value !== "'") return null;
239
+ const close = source.indexOf(value, index + 1);
240
+ if (close === -1) return null;
241
+ found.push({
242
+ text: source.slice(index + 1, close),
243
+ start: index,
244
+ end: close + 1
245
+ });
246
+ index = close + 1;
247
+ }
248
+ let text = "";
249
+ let from = index;
250
+ while (index < source.length) {
251
+ const char = source[index];
252
+ if (char === "{") {
253
+ text += "<x>";
254
+ index = walk(source, index + 1, found, true, true);
255
+ continue;
256
+ }
257
+ if (char === "<") {
258
+ if (/^<\s*\//.exec(source.slice(index)) !== null) {
259
+ pushText(text, from, index, found);
260
+ const end = source.indexOf(">", index);
261
+ return end === -1 ? null : end + 1;
262
+ }
263
+ pushText(text, from, index, found);
264
+ const end = element(source, index, found);
265
+ if (end === null) return null;
266
+ index = end;
267
+ text = "";
268
+ from = index;
269
+ continue;
270
+ }
271
+ text += char;
272
+ index += 1;
273
+ }
274
+ return null;
275
+ }
276
+ /** Where a regex literal ends. A `/` inside a character class does not close it; a newline does. */
277
+ function endOfRegex(source, start) {
278
+ let inClass = false;
279
+ for (let index = start + 1; index < source.length; index += 1) {
280
+ const char = source[index];
281
+ if (char === "\\") {
282
+ index += 1;
283
+ continue;
284
+ }
285
+ if (char === "\n") return index;
286
+ if (char === "[") inClass = true;
287
+ else if (char === "]") inClass = false;
288
+ else if (char === "/" && !inClass) return index + 1;
289
+ }
290
+ return source.length;
291
+ }
292
+ //#endregion
293
+ export { literalSpans, literalTexts };
@@ -3,5 +3,5 @@
3
3
  /** This package's npm name — the join key against a release feed. */
4
4
  export declare const PACKAGE_NAME = "@pithy-sh/core";
5
5
  /** This package's version, stamped from its own package.json at generation time. */
6
- export declare const PACKAGE_VERSION = "0.9.0";
6
+ export declare const PACKAGE_VERSION = "0.10.0";
7
7
  //# sourceMappingURL=version.generated.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.generated.d.ts","sourceRoot":"","sources":["../src/version.generated.ts"],"names":[],"mappings":"AAWA,qEAAqE;AACrE,eAAO,MAAM,YAAY,mBAAmB,CAAC;AAE7C,oFAAoF;AACpF,eAAO,MAAM,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"version.generated.d.ts","sourceRoot":"","sources":["../src/version.generated.ts"],"names":[],"mappings":"AAWA,qEAAqE;AACrE,eAAO,MAAM,YAAY,mBAAmB,CAAC;AAE7C,oFAAoF;AACpF,eAAO,MAAM,eAAe,WAAW,CAAC"}
@@ -4,6 +4,6 @@
4
4
  /** This package's npm name — the join key against a release feed. */
5
5
  const PACKAGE_NAME = "@pithy-sh/core";
6
6
  /** This package's version, stamped from its own package.json at generation time. */
7
- const PACKAGE_VERSION = "0.9.0";
7
+ const PACKAGE_VERSION = "0.10.0";
8
8
  //#endregion
9
9
  export { PACKAGE_NAME, PACKAGE_VERSION };
@@ -1 +1 @@
1
- {"version":3,"file":"dispatchRoute.d.ts","sourceRoot":"","sources":["../../src/workflow/dispatchRoute.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAW,IAAI,EAAQ,MAAM,MAAM,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,KAAK,UAAU,EAAsC,MAAM,gBAAgB,CAAC;AAGrF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAQjE,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,EAAE,oBAA+B,CAAC;AAK7E,iGAAiG;AACjG,MAAM,WAAW,uBAAuB;IACtC,wGAAwG;IACxG,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,EAAE,gBAAgB,CAAC;IAC3B;;;OAGG;IACH,GAAG,CAAC,EAAE,UAAU,CAAC;CAClB;AAqBD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAkE7G"}
1
+ {"version":3,"file":"dispatchRoute.d.ts","sourceRoot":"","sources":["../../src/workflow/dispatchRoute.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAW,IAAI,EAAQ,MAAM,MAAM,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,EAAE,KAAK,UAAU,EAAsC,MAAM,gBAAgB,CAAC;AAIrF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAQjE,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,EAAE,oBAA+B,CAAC;AAK7E,iGAAiG;AACjG,MAAM,WAAW,uBAAuB;IACtC,wGAAwG;IACxG,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,EAAE,gBAAgB,CAAC;IAC3B;;;OAGG;IACH,GAAG,CAAC,EAAE,UAAU,CAAC;CAClB;AAqBD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAwE7G"}
@@ -1,11 +1,12 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { ForbiddenError, PithyError } from "../error/pithyError.js";
4
- import { validationHook } from "../http/validation.js";
5
- import { WORKFLOW_DISPATCH_ROUTE, WorkflowDispatchParams, WorkflowDispatchRequest } from "./schemas.js";
6
- import { workflowBindingFor } from "./dispatch.js";
4
+ import { commandName } from "../cli/command.js";
7
5
  import { ambientEnv, compositionEnvironment } from "../env/ambient.js";
8
- import { zValidator } from "@hono/zod-validator";
6
+ import { describeRoute } from "../http/routeSpec.js";
7
+ import { validate } from "../http/validation.js";
8
+ import { WORKFLOW_DISPATCH_ROUTE, WorkflowDispatchParams, WorkflowDispatchRequest, WorkflowDispatchResponse } from "./schemas.js";
9
+ import { workflowBindingFor } from "./dispatch.js";
9
10
  //#region src/workflow/dispatchRoute.ts
10
11
  /**
11
12
  * The host's dispatch route — the loopback door `pithy dev` starts an instance through.
@@ -73,7 +74,7 @@ function specForBinding(registry, binding) {
73
74
  function refuseOutsideDev(capability, environment) {
74
75
  return new ForbiddenError({
75
76
  message: `Workflow dispatch over HTTP is served only in the ${DISPATCH_ENVIRONMENT} environment.`,
76
- action: `Dispatch through the cross-script ${capability} Workflow binding, or run this host under pithy dev.`,
77
+ action: `Dispatch through the cross-script ${capability} Workflow binding, or run this host under ${commandName("dev")}.`,
77
78
  detail: `The composition's ENVIRONMENT is ${environment ?? "unstamped"}.`
78
79
  });
79
80
  }
@@ -91,7 +92,12 @@ function registerWorkflowDispatchRoute(app, options) {
91
92
  if (environment !== DISPATCH_ENVIRONMENT) throw refuseOutsideDev(capability, environment);
92
93
  await next();
93
94
  };
94
- app.post(WORKFLOW_DISPATCH_ROUTE, guard, zValidator("param", WorkflowDispatchParams, validationHook), zValidator("json", WorkflowDispatchRequest, validationHook), async (c) => {
95
+ app.post(WORKFLOW_DISPATCH_ROUTE, describeRoute({
96
+ summary: "Start a Workflow instance on this host, in dev only.",
97
+ description: "Refused outside a `dev` composition. Deployed Workers dispatch through a Workflow binding instead.",
98
+ verification: WORKFLOW_DISPATCH_VERIFICATION,
99
+ responses: { 202: WorkflowDispatchResponse }
100
+ }), guard, validate("param", WorkflowDispatchParams), validate("json", WorkflowDispatchRequest), async (c) => {
95
101
  const { binding } = c.req.valid("param");
96
102
  const { id, params } = c.req.valid("json");
97
103
  const entry = specForBinding(registry, binding);
@@ -1 +1 @@
1
- {"version":3,"file":"faults.d.ts","sourceRoot":"","sources":["../../src/workflow/faults.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,2FAA2F;AAC3F,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,UAAU,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,iHAAiH;IACjH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC7C;AAED,2FAA2F;AAC3F,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,WAAW,EAAE,wBAAwB,CAAC;IACtC,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB;AAsDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,GAAG,aAAa,CA0BhG;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+FAA+F;IAC/F,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACtD;;;OAGG;IACH,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,wDAAwD;IACxD,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,wBAAwB;IACvC,KAAK,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,mBAAmB,EAC3B,aAAa,EAAE,wBAAwB,GACtC,eAAe,CAmDjB"}
1
+ {"version":3,"file":"faults.d.ts","sourceRoot":"","sources":["../../src/workflow/faults.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,2FAA2F;AAC3F,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,UAAU,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,iHAAiH;IACjH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC7C;AAED,2FAA2F;AAC3F,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,WAAW,EAAE,wBAAwB,CAAC;IACtC,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB;AAsDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,GAAG,aAAa,CA0BhG;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+FAA+F;IAC/F,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACtD;;;OAGG;IACH,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,wDAAwD;IACxD,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,wBAAwB;IACvC,KAAK,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,mBAAmB,EAC3B,aAAa,EAAE,wBAAwB,GACtC,eAAe,CAqDjB"}
@@ -120,15 +120,14 @@ function classifiedSteps(step, policy, terminalError) {
120
120
  * answer that cannot change.
121
121
  */
122
122
  sleep(name, durationMs) {
123
- const pause = step.sleep;
124
- if (pause === void 0) return runner.do(`${name}/cannot-pause`, async () => {
123
+ if (step.sleep === void 0) return runner.do(`${name}/cannot-pause`, async () => {
125
124
  throw new InternalError({
126
125
  message: "This Workflow step runner cannot pause.",
127
126
  action: "Give the step runner a `sleep`; the platform's own `WorkflowStep` always has one.",
128
127
  detail: `step '${name}' asked to pause ${durationMs}ms through a runner that declares no sleep`
129
128
  });
130
129
  });
131
- return pause.call(step, name, durationMs);
130
+ return step.sleep(name, durationMs);
132
131
  }
133
132
  };
134
133
  return runner;
@@ -51,6 +51,7 @@ export declare const HostEnvProvider: z.ZodObject<{
51
51
  }>;
52
52
  name: z.ZodString;
53
53
  command: z.ZodOptional<z.ZodString>;
54
+ decision: z.ZodOptional<z.ZodString>;
54
55
  }, z.core.$strip>;
55
56
  export type HostEnvProvider = z.infer<typeof HostEnvProvider>;
56
57
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"hostEnv.d.ts","sourceRoot":"","sources":["../../src/workflow/hostEnv.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG/C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,yGAAyG;AACzG,eAAO,MAAM,mBAAmB;;;;;EAI7B,CAAC;AACJ,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;;;;;;;iBAaiF,CAAC;AAC9G,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB,CAAC,GAAG,SAAS,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS;IACvE,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,qGAAqG;IACrG,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,6FAA6F;IAC7F,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;CAC9D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,SAAS,CAAC,CAAC,SAAS,EAAE,WAAW,EAAE;IAClE,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,EAAE;QAAE,QAAQ,EAAE,KAAK,IAAI,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,MAAM,GAAG,eAAe;KAAE,CAAC;CAChF,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAW1B;AAED,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,wBAAwB;IACxB,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAED,qGAAqG;AACrG,MAAM,WAAW,aAAa,CAAC,KAAK;IAClC,2CAA2C;IAC3C,EAAE,EAAE,OAAO,CAAC;IACZ,yFAAyF;IACzF,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC;IACzB,kEAAkE;IAClE,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAOD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,SAAS,CAAC,CAAC,SAAS,EAClD,WAAW,EAAE,kBAAkB,CAAC,GAAG,CAAC,EACpC,GAAG,EAAE,OAAO,GACX,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAsB9B;AAED,6GAA6G;AAC7G,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CAUzE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CAMrG;AAWD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,GAAG,SAAS,CAAC,CAAC,SAAS,EACpD,WAAW,EAAE,kBAAkB,CAAC,GAAG,CAAC,EACpC,GAAG,EAAE,OAAO,EACZ,GAAG,GAAE,MAA6B,GACjC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAkBf;AAED,kGAAkG;AAClG,MAAM,WAAW,YAAY;IAC3B,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,wBAAwB;IACxB,QAAQ,EAAE,eAAe,CAAC;IAC1B,+FAA+F;IAC/F,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,kBAAkB,GAAG,YAAY,EAAE,CAS7E"}
1
+ {"version":3,"file":"hostEnv.d.ts","sourceRoot":"","sources":["../../src/workflow/hostEnv.ts"],"names":[],"mappings":"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"}
@@ -46,7 +46,8 @@ const HostEnvProviderKind = z.enum([
46
46
  const HostEnvProvider = z.object({
47
47
  kind: HostEnvProviderKind.describe("Which kind of thing fills this field."),
48
48
  name: z.string().min(1).describe("The binding name, var name, secret name, or config key — spelled exactly as it is authored."),
49
- command: z.string().min(1).optional().describe("The `pithy` command that fills it, where one does. Rendered as the report's action line.")
49
+ command: z.string().min(1).optional().describe("The `pithy` command that fills it, where one does. Rendered as the report's action line."),
50
+ decision: z.string().min(1).optional().describe("What fills it when only a person may run the command that does — one that touches production — said as their decision. Rendered in place of `command`.")
50
51
  }).describe("What fills one field of a host worker's env, so a failure names the thing an operator changes.");
51
52
  /**
52
53
  * Declare a host's env. The mapped `provided` type makes an unaccounted field a compile error; the
@@ -109,7 +110,8 @@ function checkHostEnv(declaration, env) {
109
110
  /** One sentence naming the thing an operator changes, plus the command that writes it where there is one. */
110
111
  function hostEnvProviderSentence(provider) {
111
112
  const where = provider.kind === "binding" ? `Binding ${provider.name} in the host's wrangler.jsonc.` : provider.kind === "var" ? `Var ${provider.name} in the host's wrangler.jsonc.` : provider.kind === "secret" ? `Secret ${provider.name}, read through @pithy-sh/secrets.` : `Config key ${provider.name}.`;
112
- return provider.command ? `${where} Run ${provider.command}.` : where;
113
+ if (provider.command) return `${where} Run ${provider.command}.`;
114
+ return provider.decision ? `${where} ${provider.decision}` : where;
113
115
  }
114
116
  /**
115
117
  * The block a host logs before it refuses. One headline, then one line per field: the field, why it
@@ -1 +1 @@
1
- {"version":3,"file":"loopback.d.ts","sourceRoot":"","sources":["../../src/workflow/loopback.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,iGAAiG;AACjG,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElF,mFAAmF;AACnF,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB,mGAAmG;IACnG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,qGAAqG;IACrG,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAuBD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,uBAAuB,GAAG,eAAe,CAoEzF"}
1
+ {"version":3,"file":"loopback.d.ts","sourceRoot":"","sources":["../../src/workflow/loopback.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,iGAAiG;AACjG,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElF,mFAAmF;AACnF,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB,mGAAmG;IACnG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,qGAAqG;IACrG,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAuBD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,uBAAuB,GAAG,eAAe,CAoEzF"}
@@ -1,6 +1,7 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { UpstreamError, UpstreamTimeoutError } from "../error/pithyError.js";
4
+ import { commandName } from "../cli/command.js";
4
5
  import { workflowDispatchPath } from "./schemas.js";
5
6
  //#region src/workflow/loopback.ts
6
7
  /** 10 seconds. A loopback dispatch either lands immediately or is not going to. */
@@ -32,7 +33,7 @@ function loopbackWorkflowBinding(options) {
32
33
  const owner = options.capability ?? options.binding;
33
34
  const send = options.fetch ?? ((target, init) => fetch(target, init));
34
35
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
35
- const running = `Check that the ${owner} host is running under pithy dev at ${origin}.`;
36
+ const running = `Check that the ${owner} host is running under ${commandName("dev")} at ${origin}.`;
36
37
  return { async create(instance) {
37
38
  const body = {
38
39
  id: instance?.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pithy-sh/core",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,7 +39,7 @@
39
39
  "kysely-d1": "^0.4.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@cloudflare/vitest-plugin": "^1.0.0",
42
+ "@cloudflare/vitest-plugin": "^1.4.2",
43
43
  "@pithy-sh/tsconfig": "workspace:*",
44
44
  "@vitest/coverage-v8": "^4.1.0",
45
45
  "hono": "^4.13.2",
@@ -47,7 +47,7 @@
47
47
  "tsdown": "^0.23.0",
48
48
  "typescript": "^7.0.2",
49
49
  "vitest": "^4.1.0",
50
- "wrangler": "^4.115.0",
50
+ "wrangler": "^4.151.0",
51
51
  "zod": "^4.4.0"
52
52
  },
53
53
  "peerDependencies": {
@@ -0,0 +1,164 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { z } from "zod";
5
+ import type { Capability } from "../capability/capability";
6
+ import { ErrorBody } from "../error/wire";
7
+ import type { SocketChannel, SocketMessage } from "../http/socket";
8
+ import type { VerificationStrategy } from "../http/verification";
9
+ import {
10
+ type ContractInfo,
11
+ type JsonSchema,
12
+ jsonSchema,
13
+ openApiPath,
14
+ type UndescribedField,
15
+ undescribedFieldsIn,
16
+ } from "../openapi/document";
17
+
18
+ /**
19
+ * **The AsyncAPI contract: every socket a Worker serves, and every frame on it.**
20
+ *
21
+ * The OpenAPI document stops at the upgrade. This picks up after it, from the `sockets` each composed
22
+ * capability declares (`../http/socket.ts`): one channel per socket, one message per frame, and one operation
23
+ * per direction — `send` for what the Worker sends, `receive` for what it accepts, as AsyncAPI names them from
24
+ * the application's side. Every schema is the one the socket parses with, converted by `z.toJSONSchema` with
25
+ * its descriptions intact, so an engine with no generator still has typed messages to write against.
26
+ *
27
+ * AsyncAPI 3.0, whose schemas are JSON Schema — the dialect `z.toJSONSchema` writes.
28
+ */
29
+
30
+ /** The AsyncAPI version this module writes. */
31
+ export const ASYNCAPI_VERSION = "3.0.0";
32
+
33
+ /** One capability's sockets. Becomes the channels tagged with its name. */
34
+ export interface SocketGroup {
35
+ /** The capability's name. */
36
+ readonly tag: string;
37
+ /** The sockets it declared. */
38
+ readonly sockets: readonly SocketChannel[];
39
+ }
40
+
41
+ /** The sockets of every capability that declares one, grouped by capability, in composition order. */
42
+ export function socketGroupsOf(capabilities: readonly Capability[]): SocketGroup[] {
43
+ return capabilities
44
+ .filter((capability) => (capability.sockets ?? []).length > 0)
45
+ .map((capability) => ({ tag: capability.name, sockets: capability.sockets ?? [] }));
46
+ }
47
+
48
+ /** The subset of an AsyncAPI 3.0 document this module writes. */
49
+ export interface AsyncApiDocument {
50
+ asyncapi: string;
51
+ info: ContractInfo;
52
+ channels: Record<string, Record<string, unknown>>;
53
+ operations: Record<string, Record<string, unknown>>;
54
+ components: {
55
+ messages: Record<string, Record<string, unknown>>;
56
+ schemas: Record<string, JsonSchema>;
57
+ };
58
+ }
59
+
60
+ /** A frame's id in the document: its socket's name and its own, so two sockets' `error` frames stay apart. */
61
+ function messageId(socket: SocketChannel, message: SocketMessage, direction: "send" | "receive"): string {
62
+ return `${socket.name}.${direction}.${message.name}`;
63
+ }
64
+
65
+ /** One `components.messages` entry: a JSON frame by its schema, a text frame by its exact text. */
66
+ function messageOf(message: SocketMessage): Record<string, unknown> {
67
+ if (message.schema === undefined) {
68
+ return {
69
+ name: message.name,
70
+ summary: message.summary,
71
+ contentType: "text/plain",
72
+ payload: { type: "string", const: message.text ?? "", description: message.summary },
73
+ };
74
+ }
75
+ return {
76
+ name: message.name,
77
+ summary: message.summary,
78
+ contentType: "application/json",
79
+ payload: jsonSchema(message.schema, "input"),
80
+ };
81
+ }
82
+
83
+ /** The path parameters, from the same schema the upgrade route validates them with. */
84
+ function parametersOf(params: z.ZodType | undefined): Record<string, Record<string, unknown>> {
85
+ if (params === undefined) return {};
86
+ const properties = (jsonSchema(params, "input").properties ?? {}) as Record<string, JsonSchema>;
87
+ return Object.fromEntries(
88
+ Object.entries(properties).map(([name, property]) => [
89
+ name,
90
+ typeof property.description === "string" ? { description: property.description } : {},
91
+ ]),
92
+ );
93
+ }
94
+
95
+ function strategiesOf(socket: SocketChannel): readonly VerificationStrategy[] {
96
+ return typeof socket.verification === "string" ? [socket.verification] : socket.verification;
97
+ }
98
+
99
+ /** The AsyncAPI document for these capabilities' sockets. */
100
+ export function asyncApiDocument(info: ContractInfo, groups: readonly SocketGroup[]): AsyncApiDocument {
101
+ const document: AsyncApiDocument = {
102
+ asyncapi: ASYNCAPI_VERSION,
103
+ info,
104
+ channels: {},
105
+ operations: {},
106
+ components: { messages: {}, schemas: { Error: jsonSchema(ErrorBody, "input") } },
107
+ };
108
+
109
+ for (const group of groups) {
110
+ for (const socket of group.sockets) {
111
+ const channelMessages: Record<string, { $ref: string }> = {};
112
+ for (const direction of ["send", "receive"] as const) {
113
+ const refs: { $ref: string }[] = [];
114
+ for (const message of socket[direction]) {
115
+ const id = messageId(socket, message, direction);
116
+ document.components.messages[id] = messageOf(message);
117
+ channelMessages[id] = { $ref: `#/components/messages/${id}` };
118
+ refs.push({ $ref: `#/channels/${socket.name}/messages/${id}` });
119
+ }
120
+ if (refs.length > 0) {
121
+ document.operations[`${socket.name}.${direction}`] = {
122
+ action: direction,
123
+ channel: { $ref: `#/channels/${socket.name}` },
124
+ summary: direction === "send" ? "Frames the Worker sends." : "Frames the Worker accepts.",
125
+ messages: refs,
126
+ };
127
+ }
128
+ }
129
+ const parameters = parametersOf(socket.params);
130
+ document.channels[socket.name] = {
131
+ address: openApiPath(socket.path),
132
+ summary: socket.summary,
133
+ ...(socket.description === undefined ? {} : { description: socket.description }),
134
+ ...(Object.keys(parameters).length === 0 ? {} : { parameters }),
135
+ messages: channelMessages,
136
+ tags: [{ name: group.tag }],
137
+ bindings: { ws: { method: "GET" } },
138
+ "x-pithy-verification": strategiesOf(socket),
139
+ "x-pithy-subprotocols": socket.versions,
140
+ };
141
+ for (const [name, schema] of Object.entries(socket.schemas ?? {})) {
142
+ document.components.schemas[`${socket.name}.${name}`] = jsonSchema(schema, "input");
143
+ }
144
+ }
145
+ }
146
+ return document;
147
+ }
148
+
149
+ /**
150
+ * Every message property, schema property and channel parameter in the document without a description —
151
+ * the same rule as the OpenAPI document's, over the parts a client generates from.
152
+ */
153
+ export function undescribedAsyncFields(document: AsyncApiDocument): UndescribedField[] {
154
+ const parameters = Object.entries(document.channels).flatMap(([channel, item]) =>
155
+ Object.entries((item.parameters ?? {}) as Record<string, Record<string, unknown>>)
156
+ .filter(([, parameter]) => typeof parameter.description !== "string")
157
+ .map(([name]) => ({ pointer: `/channels/${channel}/parameters/${name}` })),
158
+ );
159
+ return [
160
+ ...parameters,
161
+ ...undescribedFieldsIn(document.components.messages, "/components/messages"),
162
+ ...undescribedFieldsIn(document.components.schemas, "/components/schemas"),
163
+ ];
164
+ }
@@ -14,6 +14,7 @@ import type { DatabaseSpecMap } from "../data/databases";
14
14
  import type { EntitlementResolver } from "../entitlement/entitlement";
15
15
  import type { AuthContext } from "../http/authContext";
16
16
  import type { SameOriginGate } from "../http/sameOrigin";
17
+ import type { SocketChannel } from "../http/socket";
17
18
  import type { LocaleCatalogs } from "../i18n/catalog";
18
19
  import type { LocaleContext } from "../i18n/locale";
19
20
  import type { Translator } from "../i18n/translator";
@@ -532,6 +533,12 @@ export interface Capability<
532
533
  boot?: (context: CapabilityComposeContext) => void;
533
534
  /** Mounts a Hono sub-router. */
534
535
  routes?: (app: Hono<PithyHonoEnv>) => void;
536
+ /**
537
+ * The WebSockets this capability serves, described for the AsyncAPI contract (`../http/socket.ts`). Built
538
+ * from the resolved config, like `adminRoutes`: the paths it mounted, and the schemas its frames are
539
+ * parsed with. A capability with no socket leaves it unset.
540
+ */
541
+ sockets?: readonly SocketChannel[];
535
542
  /** Composable middleware (e.g. turnstile(), requireAuth()). */
536
543
  middleware?: PithyMiddleware[];
537
544
  /**