@pithy-sh/core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/dist/access/access.d.ts +172 -0
  2. package/dist/access/access.d.ts.map +1 -0
  3. package/dist/access/access.js +147 -0
  4. package/dist/access/acting.d.ts +49 -0
  5. package/dist/access/acting.d.ts.map +1 -0
  6. package/dist/access/acting.js +21 -0
  7. package/dist/access/policyCheck.d.ts +33 -0
  8. package/dist/access/policyCheck.d.ts.map +1 -0
  9. package/dist/access/policyCheck.js +48 -0
  10. package/dist/access/usage.d.ts +83 -0
  11. package/dist/access/usage.d.ts.map +1 -0
  12. package/dist/access/usage.js +104 -0
  13. package/dist/access/visibility.d.ts +66 -0
  14. package/dist/access/visibility.d.ts.map +1 -0
  15. package/dist/access/visibility.js +86 -0
  16. package/dist/asyncapi/document.d.ts +46 -0
  17. package/dist/asyncapi/document.d.ts.map +1 -0
  18. package/dist/asyncapi/document.js +115 -0
  19. package/dist/audit/auditEvent.d.ts +5 -0
  20. package/dist/audit/auditEvent.d.ts.map +1 -1
  21. package/dist/audit/auditEvent.js +2 -0
  22. package/dist/capability/bindings.d.ts +47 -2
  23. package/dist/capability/bindings.d.ts.map +1 -1
  24. package/dist/capability/bindings.js +117 -7
  25. package/dist/capability/capability.d.ts +105 -28
  26. package/dist/capability/capability.d.ts.map +1 -1
  27. package/dist/capability/composition.d.ts.map +1 -1
  28. package/dist/capability/composition.js +5 -1
  29. package/dist/capability/manifest.d.ts +2 -1
  30. package/dist/capability/manifest.d.ts.map +1 -1
  31. package/dist/capability/manifest.js +1 -1
  32. package/dist/capability/settings.d.ts +96 -4
  33. package/dist/capability/settings.d.ts.map +1 -1
  34. package/dist/capability/settings.js +85 -4
  35. package/dist/cli/command.d.ts +364 -0
  36. package/dist/cli/command.d.ts.map +1 -0
  37. package/dist/cli/command.js +418 -0
  38. package/dist/cli/commandCatalog.generated.d.ts +2437 -0
  39. package/dist/cli/commandCatalog.generated.d.ts.map +1 -0
  40. package/dist/cli/commandCatalog.generated.js +2564 -0
  41. package/dist/cli/commandFacts.d.ts +164 -0
  42. package/dist/cli/commandFacts.d.ts.map +1 -0
  43. package/dist/cli/commandFacts.js +216 -0
  44. package/dist/controlPlane/capability.d.ts.map +1 -1
  45. package/dist/controlPlane/capability.js +12 -2
  46. package/dist/controlPlane/context.d.ts +5 -0
  47. package/dist/controlPlane/context.d.ts.map +1 -1
  48. package/dist/controlPlane/context.js +3 -1
  49. package/dist/controlPlane/data/connection.d.ts +47 -0
  50. package/dist/controlPlane/data/connection.d.ts.map +1 -1
  51. package/dist/controlPlane/data/connection.js +55 -1
  52. package/dist/controlPlane/error/errors.d.ts.map +1 -1
  53. package/dist/controlPlane/error/errors.js +4 -3
  54. package/dist/controlPlane/http/cors.d.ts +4 -4
  55. package/dist/controlPlane/http/cors.js +4 -4
  56. package/dist/controlPlane/http/guard.d.ts +11 -0
  57. package/dist/controlPlane/http/guard.d.ts.map +1 -1
  58. package/dist/controlPlane/http/guard.js +28 -2
  59. package/dist/controlPlane/http/handlers.d.ts +60 -0
  60. package/dist/controlPlane/http/handlers.d.ts.map +1 -1
  61. package/dist/controlPlane/http/handlers.js +31 -2
  62. package/dist/controlPlane/http/readiness.d.ts +33 -0
  63. package/dist/controlPlane/http/readiness.d.ts.map +1 -0
  64. package/dist/controlPlane/http/readiness.js +39 -0
  65. package/dist/controlPlane/http/responses.d.ts +147 -0
  66. package/dist/controlPlane/http/responses.d.ts.map +1 -1
  67. package/dist/controlPlane/http/responses.js +61 -4
  68. package/dist/controlPlane/http/routes.d.ts +2 -1
  69. package/dist/controlPlane/http/routes.d.ts.map +1 -1
  70. package/dist/controlPlane/http/routes.js +50 -9
  71. package/dist/controlPlane/http/verify.d.ts.map +1 -1
  72. package/dist/controlPlane/http/verify.js +2 -1
  73. package/dist/controlPlane/migrationState/migrationState.d.ts +102 -0
  74. package/dist/controlPlane/migrationState/migrationState.d.ts.map +1 -0
  75. package/dist/controlPlane/migrationState/migrationState.js +142 -0
  76. package/dist/controlPlane/migrations/0003_connection_principal.d.ts +21 -0
  77. package/dist/controlPlane/migrations/0003_connection_principal.d.ts.map +1 -0
  78. package/dist/controlPlane/migrations/0003_connection_principal.js +33 -0
  79. package/dist/controlPlane/scope/scope.d.ts +13 -2
  80. package/dist/controlPlane/scope/scope.d.ts.map +1 -1
  81. package/dist/controlPlane/scope/scope.js +14 -3
  82. package/dist/createBackend.d.ts +7 -0
  83. package/dist/createBackend.d.ts.map +1 -1
  84. package/dist/createBackend.js +41 -6
  85. package/dist/createEntrypoint.d.ts.map +1 -1
  86. package/dist/createEntrypoint.js +5 -3
  87. package/dist/entitlement/allowance.d.ts +25 -0
  88. package/dist/entitlement/allowance.d.ts.map +1 -0
  89. package/dist/entitlement/allowance.js +80 -0
  90. package/dist/entitlement/entitlement.d.ts +41 -0
  91. package/dist/entitlement/entitlement.d.ts.map +1 -1
  92. package/dist/entitlement/meter.d.ts +57 -0
  93. package/dist/entitlement/meter.d.ts.map +1 -0
  94. package/dist/entitlement/meter.js +13 -0
  95. package/dist/entitlement/require.js +1 -1
  96. package/dist/error/cause.d.ts +16 -116
  97. package/dist/error/cause.d.ts.map +1 -1
  98. package/dist/error/cause.js +19 -153
  99. package/dist/error/http.d.ts +184 -0
  100. package/dist/error/http.d.ts.map +1 -1
  101. package/dist/error/payload.d.ts +368 -0
  102. package/dist/error/payload.d.ts.map +1 -1
  103. package/dist/error/payload.js +126 -4
  104. package/dist/error/pithyError.d.ts +44 -0
  105. package/dist/error/pithyError.d.ts.map +1 -1
  106. package/dist/error/pithyError.js +56 -1
  107. package/dist/error/unresolved.d.ts +265 -0
  108. package/dist/error/unresolved.d.ts.map +1 -0
  109. package/dist/error/unresolved.js +599 -0
  110. package/dist/error/wire.d.ts +20 -0
  111. package/dist/error/wire.d.ts.map +1 -0
  112. package/dist/error/wire.js +19 -0
  113. package/dist/http/authContext.d.ts +7 -0
  114. package/dist/http/authContext.d.ts.map +1 -1
  115. package/dist/http/authContext.js +8 -1
  116. package/dist/http/oidcWebhook.d.ts +3 -3
  117. package/dist/http/oidcWebhook.js +3 -3
  118. package/dist/http/routeContract.js +1 -1
  119. package/dist/http/routeSpec.d.ts +66 -0
  120. package/dist/http/routeSpec.d.ts.map +1 -0
  121. package/dist/http/routeSpec.js +40 -0
  122. package/dist/http/sameOrigin.d.ts +1 -1
  123. package/dist/http/sameOrigin.js +1 -1
  124. package/dist/http/scopeGates.d.ts +20 -0
  125. package/dist/http/scopeGates.d.ts.map +1 -0
  126. package/dist/http/scopeGates.js +36 -0
  127. package/dist/http/signedWebhook.d.ts +3 -3
  128. package/dist/http/signedWebhook.js +3 -3
  129. package/dist/http/socket.d.ts +52 -0
  130. package/dist/http/socket.d.ts.map +1 -0
  131. package/dist/http/socket.js +2 -0
  132. package/dist/http/subprotocol.d.ts +29 -0
  133. package/dist/http/subprotocol.d.ts.map +1 -0
  134. package/dist/http/subprotocol.js +37 -0
  135. package/dist/http/validation.d.ts +40 -8
  136. package/dist/http/validation.d.ts.map +1 -1
  137. package/dist/http/validation.js +25 -1
  138. package/dist/index.d.ts +1 -1
  139. package/dist/index.d.ts.map +1 -1
  140. package/dist/index.js +4 -4
  141. package/dist/logger/record.d.ts +107 -0
  142. package/dist/logger/record.d.ts.map +1 -1
  143. package/dist/migrations/bookkeeping.d.ts +21 -0
  144. package/dist/migrations/bookkeeping.d.ts.map +1 -1
  145. package/dist/migrations/bookkeeping.js +32 -3
  146. package/dist/migrations/capabilitySets.d.ts +23 -0
  147. package/dist/migrations/capabilitySets.d.ts.map +1 -0
  148. package/dist/migrations/capabilitySets.js +74 -0
  149. package/dist/migrations/groups.d.ts +140 -0
  150. package/dist/migrations/groups.d.ts.map +1 -0
  151. package/dist/migrations/groups.js +191 -0
  152. package/dist/migrations/migrateStep.d.ts +13 -0
  153. package/dist/migrations/migrateStep.d.ts.map +1 -0
  154. package/dist/migrations/migrateStep.js +20 -0
  155. package/dist/migrations/owner.d.ts +7 -0
  156. package/dist/migrations/owner.d.ts.map +1 -1
  157. package/dist/migrations/owner.js +11 -7
  158. package/dist/migrations/registry.d.ts +26 -0
  159. package/dist/migrations/registry.d.ts.map +1 -1
  160. package/dist/migrations/registry.js +38 -7
  161. package/dist/migrations/retained.js +2 -2
  162. package/dist/migrations/runner.d.ts +76 -5
  163. package/dist/migrations/runner.d.ts.map +1 -1
  164. package/dist/migrations/runner.js +130 -19
  165. package/dist/migrations/state.d.ts +60 -0
  166. package/dist/migrations/state.d.ts.map +1 -0
  167. package/dist/migrations/state.js +46 -0
  168. package/dist/naming/domains.d.ts +17 -0
  169. package/dist/naming/domains.d.ts.map +1 -1
  170. package/dist/naming/domains.js +62 -4
  171. package/dist/naming/environment.d.ts.map +1 -1
  172. package/dist/naming/environment.js +2 -1
  173. package/dist/naming/feature.d.ts +27 -5
  174. package/dist/naming/feature.d.ts.map +1 -1
  175. package/dist/naming/feature.js +36 -6
  176. package/dist/naming/limits.d.ts +47 -3
  177. package/dist/naming/limits.d.ts.map +1 -1
  178. package/dist/naming/limits.js +47 -3
  179. package/dist/naming/packageName.d.ts +17 -0
  180. package/dist/naming/packageName.d.ts.map +1 -0
  181. package/dist/naming/packageName.js +22 -0
  182. package/dist/naming/provisionScope.d.ts +6 -0
  183. package/dist/naming/provisionScope.d.ts.map +1 -1
  184. package/dist/naming/provisionScope.js +10 -4
  185. package/dist/naming/resourceNames.d.ts +21 -2
  186. package/dist/naming/resourceNames.d.ts.map +1 -1
  187. package/dist/naming/resourceNames.js +21 -3
  188. package/dist/openapi/document.d.ts +106 -0
  189. package/dist/openapi/document.d.ts.map +1 -0
  190. package/dist/openapi/document.js +314 -0
  191. package/dist/openapi/routeGroups.d.ts +8 -0
  192. package/dist/openapi/routeGroups.d.ts.map +1 -0
  193. package/dist/openapi/routeGroups.js +23 -0
  194. package/dist/seed/writeD1.d.ts +7 -1
  195. package/dist/seed/writeD1.d.ts.map +1 -1
  196. package/dist/seed/writeD1.js +9 -3
  197. package/dist/seed/writeKv.d.ts +2 -0
  198. package/dist/seed/writeKv.d.ts.map +1 -1
  199. package/dist/seed/writeKv.js +6 -2
  200. package/dist/text/comments.js +66 -4
  201. package/dist/text/literals.d.ts +43 -0
  202. package/dist/text/literals.d.ts.map +1 -0
  203. package/dist/text/literals.js +293 -0
  204. package/dist/version.generated.d.ts +1 -1
  205. package/dist/version.generated.d.ts.map +1 -1
  206. package/dist/version.generated.js +1 -1
  207. package/dist/worker/serviceLoopback.d.ts +36 -0
  208. package/dist/worker/serviceLoopback.d.ts.map +1 -0
  209. package/dist/worker/serviceLoopback.js +74 -0
  210. package/dist/workflow/dispatchRoute.d.ts.map +1 -1
  211. package/dist/workflow/dispatchRoute.js +12 -6
  212. package/dist/workflow/faults.d.ts +12 -1
  213. package/dist/workflow/faults.d.ts.map +1 -1
  214. package/dist/workflow/faults.js +24 -18
  215. package/dist/workflow/hostEnv.d.ts +1 -0
  216. package/dist/workflow/hostEnv.d.ts.map +1 -1
  217. package/dist/workflow/hostEnv.js +4 -2
  218. package/dist/workflow/loopback.d.ts.map +1 -1
  219. package/dist/workflow/loopback.js +2 -1
  220. package/package.json +4 -4
  221. package/src/access/access.ts +298 -0
  222. package/src/access/acting.ts +59 -0
  223. package/src/access/policyCheck.ts +77 -0
  224. package/src/access/usage.ts +141 -0
  225. package/src/access/visibility.ts +109 -0
  226. package/src/asyncapi/document.ts +164 -0
  227. package/src/audit/auditEvent.ts +4 -0
  228. package/src/capability/bindings.ts +159 -13
  229. package/src/capability/capability.ts +106 -29
  230. package/src/capability/composition.ts +2 -1
  231. package/src/capability/settings.ts +165 -4
  232. package/src/cli/command.ts +865 -0
  233. package/src/cli/commandCatalog.generated.ts +2723 -0
  234. package/src/cli/commandFacts.ts +220 -0
  235. package/src/controlPlane/capability.ts +25 -1
  236. package/src/controlPlane/context.ts +4 -0
  237. package/src/controlPlane/data/connection.ts +90 -0
  238. package/src/controlPlane/error/errors.ts +10 -3
  239. package/src/controlPlane/http/cors.ts +4 -4
  240. package/src/controlPlane/http/guard.ts +35 -2
  241. package/src/controlPlane/http/handlers.ts +41 -0
  242. package/src/controlPlane/http/readiness.ts +50 -0
  243. package/src/controlPlane/http/responses.ts +140 -2
  244. package/src/controlPlane/http/routes.ts +82 -10
  245. package/src/controlPlane/http/verify.ts +2 -0
  246. package/src/controlPlane/migrationState/migrationState.ts +200 -0
  247. package/src/controlPlane/migrations/0003_connection_principal.ts +37 -0
  248. package/src/controlPlane/scope/scope.ts +14 -2
  249. package/src/createBackend.ts +59 -8
  250. package/src/createEntrypoint.ts +9 -3
  251. package/src/entitlement/allowance.ts +85 -0
  252. package/src/entitlement/entitlement.ts +38 -0
  253. package/src/entitlement/meter.ts +69 -0
  254. package/src/entitlement/require.ts +1 -1
  255. package/src/error/cause.ts +18 -181
  256. package/src/error/payload.ts +270 -4
  257. package/src/error/pithyError.ts +76 -0
  258. package/src/error/unresolved.ts +853 -0
  259. package/src/error/wire.ts +40 -0
  260. package/src/http/authContext.ts +13 -1
  261. package/src/http/oidcWebhook.ts +3 -3
  262. package/src/http/routeContract.ts +1 -1
  263. package/src/http/routeSpec.ts +81 -0
  264. package/src/http/sameOrigin.ts +1 -1
  265. package/src/http/scopeGates.ts +34 -0
  266. package/src/http/signedWebhook.ts +3 -3
  267. package/src/http/socket.ts +55 -0
  268. package/src/http/subprotocol.ts +47 -0
  269. package/src/http/validation.ts +50 -8
  270. package/src/index.ts +1 -0
  271. package/src/migrations/bookkeeping.ts +39 -2
  272. package/src/migrations/capabilitySets.ts +87 -0
  273. package/src/migrations/groups.ts +295 -0
  274. package/src/migrations/migrateStep.ts +19 -0
  275. package/src/migrations/owner.ts +10 -6
  276. package/src/migrations/registry.ts +46 -2
  277. package/src/migrations/retained.ts +2 -2
  278. package/src/migrations/runner.ts +214 -22
  279. package/src/migrations/state.ts +94 -0
  280. package/src/naming/domains.ts +75 -16
  281. package/src/naming/environment.ts +2 -1
  282. package/src/naming/feature.ts +46 -7
  283. package/src/naming/limits.ts +35 -3
  284. package/src/naming/packageName.ts +20 -0
  285. package/src/naming/provisionScope.ts +21 -2
  286. package/src/naming/resourceNames.ts +52 -5
  287. package/src/openapi/document.ts +412 -0
  288. package/src/openapi/routeGroups.ts +25 -0
  289. package/src/seed/writeD1.ts +13 -4
  290. package/src/seed/writeKv.ts +6 -2
  291. package/src/text/comments.ts +56 -4
  292. package/src/text/literals.ts +294 -0
  293. package/src/version.generated.ts +1 -1
  294. package/src/worker/serviceLoopback.ts +105 -0
  295. package/src/workflow/dispatchRoute.ts +13 -6
  296. package/src/workflow/faults.ts +56 -27
  297. package/src/workflow/hostEnv.ts +9 -1
  298. package/src/workflow/loopback.ts +2 -1
@@ -1,9 +1,11 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
- import { InternalError } from "../error/pithyError.js";
3
+ import { InternalError, ValidationError } from "../error/pithyError.js";
4
+ import { pithyCommand } from "../cli/command.js";
4
5
  import { causeMessage } from "../error/cause.js";
5
- import { MIGRATION_LOCK_TABLE, MIGRATION_TABLE, migrationKysely } from "./bookkeeping.js";
6
+ import { MIGRATION_LOCK_TABLE, MIGRATION_TABLE, appliedMigrationChain, migrationKysely } from "./bookkeeping.js";
6
7
  import { batchedProvider } from "./batch.js";
8
+ import { forgetMigrationGroups, generatedMigrationGroup, readGroupPosition, recordMigrationGroup } from "./groups.js";
7
9
  import { RetainedBudget, guardRetained, isDownRefusal } from "./retained.js";
8
10
  import { sql } from "kysely";
9
11
  import { Migrator, NO_MIGRATIONS } from "kysely/migration";
@@ -31,6 +33,12 @@ import { Migrator, NO_MIGRATIONS } from "kysely/migration";
31
33
  * are argued.** The short version: a migration is now all-or-nothing where it used to be able to
32
34
  * half-apply, and nothing across a migration boundary changed, because the ledger names migrations
33
35
  * and a partial chain has to stay representable in it.
36
+ *
37
+ * **Every run belongs to a group, and a reversal names one — see `./groups` (#694).** A forward run
38
+ * records the group it applied each migration under, in a table beside the ledger;
39
+ * {@link reverseMigrationGroup} reverses one group's portion of one database, in reverse chain order and
40
+ * nothing outside it. {@link rollbackMigration} is still the single step underneath, for a caller holding
41
+ * one migration rather than a release — it is not what `pithy migrate --rollback` runs any more.
34
42
  */
35
43
  /**
36
44
  * **`allowUnorderedMigrations` is on, and it is not a loosening.** A composed key leads with its
@@ -63,9 +71,30 @@ function guardOptions(target, consent) {
63
71
  budget: consent?.budget ?? new RetainedBudget(void 0)
64
72
  };
65
73
  }
66
- /** Run every pending migration to latest. An empty provider resolves to `[]`. */
67
- async function runMigrations(database, provider, target) {
74
+ /**
75
+ * Run every pending migration to latest, recording the run's group against each one. An empty provider
76
+ * resolves to `[]` and records nothing.
77
+ *
78
+ * **The group is written from what Kysely hands back, and before the failure is raised.** A run that
79
+ * half-applies is retried with the key it already has, so the migrations that stuck have to be in the
80
+ * group — otherwise the retry's group would sit on top of an ungrouped migration, and reversing the
81
+ * retry would leave half a release applied with nothing recording it.
82
+ */
83
+ async function runMigrations(database, provider, target, options) {
68
84
  const { error, results } = await migrator(database, provider, target, void 0).migrateToLatest();
85
+ const applied = (results ?? []).filter((result) => result.direction === "Up" && result.status === "Success").map((result) => result.migrationName);
86
+ try {
87
+ await recordMigrationGroup(database, {
88
+ group: options?.group ?? generatedMigrationGroup(),
89
+ migrations: applied
90
+ });
91
+ } catch (cause) {
92
+ if (error === void 0) throw new InternalError({
93
+ message: `Applied ${applied.length === 1 ? "a migration" : `${applied.length} migrations`}${on(target)}, then couldn't record the group.`,
94
+ detail: `${where(target)} ${reasonOf(cause)}`,
95
+ action: "Check the database is writable. A second run records nothing for them, so they cannot be reversed by group."
96
+ }, { cause });
97
+ }
69
98
  return settle("run", error, results, target);
70
99
  }
71
100
  /**
@@ -94,15 +123,79 @@ async function readMigrationLedger(database, provider) {
94
123
  };
95
124
  }
96
125
  /**
97
- * Step this database's latest applied migration back — one step, per database. `pithy migrate --rollback`
98
- * calls it once for **every** database in its fan-out. Refused while a retained table here holds rows the
99
- * caller has not counted in `consent` (#588).
126
+ * Step this database's latest applied migration back — **one** step, in **one** database.
127
+ *
128
+ * It is no longer what `pithy migrate --rollback` runs: a rollback names a group and reverses that
129
+ * group's portion of each database ({@link reverseMigrationGroup}, #694). What this is, is the single
130
+ * step underneath — the primitive every capability's "its `down` is the inverse of its `up`" suite
131
+ * reverses one migration with, and the honest answer for a caller holding one migration rather than a
132
+ * release.
133
+ *
134
+ * It forgets the reversed migration's group row, so the group bookkeeping follows the ledger whichever
135
+ * way a migration came down. Refused while a retained table here holds rows the caller has not counted in
136
+ * `consent` (#588).
100
137
  */
101
138
  async function rollbackMigration(database, provider, target, consent) {
102
139
  const { error, results } = await migrator(database, provider, target, consent).migrateDown();
140
+ await forgetReversed(database, results);
103
141
  return settle("rollback", error, results, target);
104
142
  }
105
143
  /**
144
+ * **Reverse one group's portion of one database — the operation behind `pithy migrate --rollback
145
+ * --group` (#694).**
146
+ *
147
+ * Kysely steps down from the tip, so this is `migrateDown()` once per migration in the group, in reverse
148
+ * chain order, and **nothing outside it**. Both halves of that are read from the ledger here, not taken on
149
+ * the caller's word: the group has to be the ledger's own tail before the first `down`, and what each step
150
+ * brings down has to be one of the migrations that read said the group holds — anything else is an
151
+ * internal fault and throws rather than carrying on. **And a migration with no `down` stops the group**:
152
+ * Kysely reports one as `NotExecuted` and leaves it applied, so counting it as reversed is how a group
153
+ * reported itself fully rolled back with half of it still in the database. The caller's pre-flight is what
154
+ * makes a group reverse completely or not at all across databases; this is the floor under it.
155
+ *
156
+ * **One migrator for the whole group**, so the retained guard clears once and the caller's budget is spent
157
+ * once across every `down` it covers (#588). Each migration's group row is forgotten as it comes down —
158
+ * before the step's failure is raised — so a group reversal that dies partway can be re-run under the
159
+ * same group and reverses what is left.
160
+ */
161
+ async function reverseMigrationGroup(database, provider, step, target, consent) {
162
+ if (step.migrations.length === 0) return [];
163
+ const position = await readGroupPosition(database, step.group);
164
+ if (position.state !== "top") throw new InternalError({
165
+ message: `Group "${step.group}" is not the top of ${target?.binding ?? "this database"}'s chain.`,
166
+ detail: `${where(target)} ${position.state === "absent" ? "It records nothing under that group." : `Applied over it: ${overIt(position.above, position.ungrouped)}.`}`,
167
+ action: `Run ${pithyCommand("doctor")} to see where each database stands.`
168
+ });
169
+ const remaining = new Set(position.migrations);
170
+ const runner = migrator(database, provider, target, consent);
171
+ const reversed = [];
172
+ while (remaining.size > 0) {
173
+ const { error, results } = await runner.migrateDown();
174
+ await forgetReversed(database, results);
175
+ const settled = settle("rollback", error, results, target);
176
+ if (settled.length === 0) break;
177
+ for (const result of settled) {
178
+ if (result.status !== "Success") throw new ValidationError({
179
+ message: `Migration "${result.migrationName}" has no down, so group "${step.group}" cannot be reversed${on(target)}.`,
180
+ action: `Give ${result.migrationName} a down, then reverse the group again.`,
181
+ detail: [where(target), reversed.length === 0 ? "Nothing was reversed. A group reverses completely or not at all." : `Reversed before the refusal: ${reversed.map((entry) => `"${entry.migrationName}"`).join(", ")}. The rest of the group is still applied.`].filter(Boolean).join(" ")
182
+ });
183
+ if (!remaining.delete(result.migrationName)) throw new InternalError({
184
+ message: `Reversing group "${step.group}"${on(target)} would have reversed "${result.migrationName}", which is not in it.`,
185
+ detail: `${where(target)} The group was not the top of this database's chain. Kysely steps down from the tip, so only the newest group can be reversed.`,
186
+ action: `Run ${pithyCommand("doctor")} to see where each database stands.`
187
+ });
188
+ reversed.push(result);
189
+ }
190
+ }
191
+ return reversed;
192
+ }
193
+ /** Forget the group rows of whatever actually came down, whether or not the step then failed. */
194
+ async function forgetReversed(database, results) {
195
+ const names = (results ?? []).filter((result) => result.direction === "Down" && result.status === "Success").map((result) => result.migrationName);
196
+ await forgetMigrationGroups(database, names);
197
+ }
198
+ /**
106
199
  * Fully reset one database's schema: every applied migration's `down` runs, in one pass, in reverse
107
200
  * chronological order — Kysely's `NO_MIGRATIONS` target, not just the latest — then every migration's
108
201
  * `up` reapplies from empty. The seam behind `pithy seed --redo`'s destructive rebuild: because the
@@ -110,6 +203,16 @@ async function rollbackMigration(database, provider, target, consent) {
110
203
  * skip-if-exists) simply work afterward — there is no per-row identity problem to solve. An empty
111
204
  * ledger rolls back nothing; an empty provider reapplies nothing. Refused, before the first `down`, while a
112
205
  * retained table here holds rows the caller has not counted in `consent` (#588).
206
+ *
207
+ * **It leaves the group bookkeeping exactly as it was (#694).** The table is not a migration, so the `down`
208
+ * pass cannot remove it, and rewriting the rows would restamp somebody's release as having happened during
209
+ * a `seed --redo`. Every group row still describes a migration this reset reapplied.
210
+ *
211
+ * **What it does not do is record a group for the rest.** A reset reapplies everything *declared*, which
212
+ * includes migrations that were pending when it started, and those come back applied under no group — this
213
+ * takes no group and invents none. Such a migration sits over the groups below it, so a `--rollback
214
+ * --group` of one of them is refused by name (`./groups`, `readGroupPosition`) rather than reversing a
215
+ * migration nobody recorded.
113
216
  */
114
217
  async function resetMigrations(database, provider, target, consent) {
115
218
  const runner = migrator(database, provider, target, consent);
@@ -120,14 +223,12 @@ async function resetMigrations(database, provider, target, consent) {
120
223
  return [...downResults, ...upResults];
121
224
  }
122
225
  /**
123
- * The applied migration names recorded in the ledger — empty when the ledger table doesn't exist yet.
124
- * Existence is checked against `sqlite_master` (a plain select D1 permits) rather than by catching the
125
- * read's error, so a genuine read failure surfaces instead of being silently treated as "none applied".
226
+ * The applied migration names recorded in the ledger — empty when the ledger table doesn't exist yet. The
227
+ * set half of `appliedMigrationChain`, which is the one read of the ledger: order matters to a group
228
+ * rollback and not at all to a membership test, and two reads of one table would drift.
126
229
  */
127
230
  async function appliedMigrationNames(db) {
128
- if ((await sql`select name from sqlite_master where type = 'table' and name = ${"pithy_migrations"}`.execute(db)).rows.length === 0) return /* @__PURE__ */ new Set();
129
- const { rows } = await sql`select name from ${sql.table(MIGRATION_TABLE)}`.execute(db);
130
- return new Set(rows.map((row) => row.name));
231
+ return new Set(await appliedMigrationChain(db));
131
232
  }
132
233
  /**
133
234
  * Surgically drop **one capability's** migrations: run each of `drop.reverse`'s `down` functions in
@@ -160,6 +261,7 @@ async function dropMigrations(database, drop, target, consent) {
160
261
  try {
161
262
  await down(db);
162
263
  await sql`delete from ${sql.table(MIGRATION_TABLE)} where name = ${name}`.execute(db);
264
+ await forgetMigrationGroups(database, [name]);
163
265
  results.push({
164
266
  migrationName: name,
165
267
  direction: "Down",
@@ -171,7 +273,7 @@ async function dropMigrations(database, drop, target, consent) {
171
273
  throw new InternalError({
172
274
  message: `Couldn't drop "${name}"${on(target)}. ${reasonOf(error)}`,
173
275
  detail: `${where(target)}${dropped.length ? ` Dropped before the failure: ${dropped.join(", ")}.` : ""}`,
174
- action: "Fix the migration's down, or drop its table by hand. Run pithy remove --drop again."
276
+ action: "Fix the migration's down, or drop its table by hand. Then run this again."
175
277
  }, { cause: error });
176
278
  }
177
279
  }
@@ -182,24 +284,33 @@ const VOICE = {
182
284
  run: {
183
285
  failed: (key) => `Couldn't apply "${key}"`,
184
286
  fallback: "The migration run failed",
185
- action: "Fix the migration. Run pithy migrate again."
287
+ action: "Fix the migration, then run this again."
186
288
  },
187
289
  rollback: {
188
290
  failed: (key) => `Couldn't roll back "${key}"`,
189
291
  fallback: "The rollback failed",
190
- action: "Fix the migration's down. Run pithy doctor to see where each database stands."
292
+ action: `Fix the migration's down. Run ${pithyCommand("doctor")} to see where each database stands.`
191
293
  },
192
294
  resetDown: {
193
295
  failed: (key) => `Couldn't roll back "${key}" during reset`,
194
296
  fallback: "The schema reset failed while rolling back",
195
- action: "Fix the migration. Run pithy seed --redo again."
297
+ action: "Fix the migration, then run this again."
196
298
  },
197
299
  resetUp: {
198
300
  failed: (key) => `Couldn't reapply "${key}" during reset`,
199
301
  fallback: "The schema reset failed while reapplying",
200
- action: "Fix the migration. Run pithy seed --redo again."
302
+ action: "Fix the migration, then run this again."
201
303
  }
202
304
  };
305
+ /**
306
+ * What sits over a buried group, groups and ungrouped migrations alike, newest first.
307
+ *
308
+ * An ungrouped migration is named with its state, because the two are different problems: another group is
309
+ * something to reverse first, while a migration no group claims cannot be reversed by group at all.
310
+ */
311
+ function overIt(above, ungrouped) {
312
+ return [...above, ...ungrouped.map((name) => `${name} (ungrouped)`)].join(", ");
313
+ }
203
314
  /** ` on DB`, or nothing at all — every problem line here ends with this and then a period. */
204
315
  function on(target) {
205
316
  return target ? ` on ${target.binding}` : "";
@@ -243,4 +354,4 @@ function settle(verb, error, results, target) {
243
354
  return results ?? [];
244
355
  }
245
356
  //#endregion
246
- export { dropMigrations, readMigrationLedger, resetMigrations, rollbackMigration, runMigrations };
357
+ export { dropMigrations, readMigrationLedger, resetMigrations, reverseMigrationGroup, rollbackMigration, runMigrations };
@@ -0,0 +1,60 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import type { D1Database } from "@cloudflare/workers-types";
4
+ import type { MigrationProvider } from "kysely/migration";
5
+ /**
6
+ * **Where one database stands, read in one place (#729).**
7
+ *
8
+ * The control-plane seam answers `GET {basePath}/migrations` from this: what is applied, what this build
9
+ * declares and the database lacks, what the database holds and this build no longer declares, and which
10
+ * run put it there. Each half already had a reader — {@link readMigrationLedger} for both directions,
11
+ * {@link readMigrationGroups} and {@link readGroupPosition} for the group (#694) — and this composes them
12
+ * rather than reading either table a second way.
13
+ *
14
+ * Read-only. It applies nothing, creates nothing, and reverses nothing: a screen somebody opened to *ask*
15
+ * must not be the thing that changes the schema.
16
+ */
17
+ /** Whether the last group can still be taken back by name — {@link readGroupPosition}, minus `absent`. */
18
+ export type MigrationGroupStanding = {
19
+ /** The group's migrations are the contiguous tip of this ledger. The CLI's rollback has refusals of its own. */
20
+ state: "top";
21
+ } | {
22
+ /** Something sits over the group or inside it, so reversing it by name is refused. */
23
+ state: "buried";
24
+ /** The groups in the way, newest first. */
25
+ above: string[];
26
+ /** The migrations in the way that no group claims, newest first. */
27
+ ungrouped: string[];
28
+ };
29
+ /** The newest group with anything still applied here, and whether it is still reversible. */
30
+ export interface MigrationLastGroup {
31
+ /** The group's value — what `--rollback --group` takes. */
32
+ group: string;
33
+ /** When it last applied something here. */
34
+ appliedAt: Date;
35
+ /** What it applied here and is still applied, in application order. */
36
+ migrations: string[];
37
+ /** Whether it is the tip of the chain, or what is in its way. */
38
+ position: MigrationGroupStanding;
39
+ }
40
+ /** One database's migration state. */
41
+ export interface MigrationState {
42
+ /** Every applied migration, in the order the ledger applied them. */
43
+ applied: string[];
44
+ /** Declared and not yet applied, in the order they would run. */
45
+ pending: string[];
46
+ /** Applied and not declared by this build, sorted. Kysely refuses the whole chain on any one of these. */
47
+ undeclared: string[];
48
+ /** The newest group with anything applied here, or null when nothing applied here was ever grouped. */
49
+ lastGroup: MigrationLastGroup | null;
50
+ }
51
+ /**
52
+ * Read one database's migration state against the provider this build composes for it.
53
+ *
54
+ * **The last group is the newest one with something still applied.** Group rows are forgotten as their
55
+ * migrations come down, so this is ordinarily the newest group recorded. A group whose rows outlived their
56
+ * ledger rows reads as `absent` and is passed over: naming it as where the environment stands would name a
57
+ * release that is no longer in the database.
58
+ */
59
+ export declare function readMigrationState(database: D1Database, provider: MigrationProvider): Promise<MigrationState>;
60
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/migrations/state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAK1D;;;;;;;;;;;GAWG;AAEH,0GAA0G;AAC1G,MAAM,MAAM,sBAAsB,GAC9B;IACE,gHAAgH;IAChH,KAAK,EAAE,KAAK,CAAC;CACd,GACD;IACE,sFAAsF;IACtF,KAAK,EAAE,QAAQ,CAAC;IAChB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,oEAAoE;IACpE,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEN,6FAA6F;AAC7F,MAAM,WAAW,kBAAkB;IACjC,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,SAAS,EAAE,IAAI,CAAC;IAChB,uEAAuE;IACvE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,iEAAiE;IACjE,QAAQ,EAAE,sBAAsB,CAAC;CAClC;AAED,sCAAsC;AACtC,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,iEAAiE;IACjE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0GAA0G;IAC1G,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,uGAAuG;IACvG,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACtC;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC,CAInH"}
@@ -0,0 +1,46 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { appliedMigrationChain, migrationKysely } from "./bookkeeping.js";
4
+ import { readGroupPosition, readMigrationGroups } from "./groups.js";
5
+ import { readMigrationLedger } from "./runner.js";
6
+ //#region src/migrations/state.ts
7
+ /**
8
+ * Read one database's migration state against the provider this build composes for it.
9
+ *
10
+ * **The last group is the newest one with something still applied.** Group rows are forgotten as their
11
+ * migrations come down, so this is ordinarily the newest group recorded. A group whose rows outlived their
12
+ * ledger rows reads as `absent` and is passed over: naming it as where the environment stands would name a
13
+ * release that is no longer in the database.
14
+ */
15
+ async function readMigrationState(database, provider) {
16
+ const { pending, undeclared } = await readMigrationLedger(database, provider);
17
+ const applied = await appliedMigrationChain(migrationKysely(database));
18
+ return {
19
+ applied,
20
+ pending,
21
+ undeclared,
22
+ lastGroup: await lastGroupOf(database, applied)
23
+ };
24
+ }
25
+ /** Walk the groups newest first and stop at the first one this database still holds any of. */
26
+ async function lastGroupOf(database, applied) {
27
+ const held = new Set(applied);
28
+ const groups = await readMigrationGroups(database);
29
+ for (const group of [...groups].reverse()) {
30
+ const position = await readGroupPosition(database, group.group);
31
+ if (position.state === "absent") continue;
32
+ return {
33
+ group: group.group,
34
+ appliedAt: group.appliedAt,
35
+ migrations: group.migrations.filter((name) => held.has(name)),
36
+ position: position.state === "top" ? { state: "top" } : {
37
+ state: "buried",
38
+ above: position.above,
39
+ ungrouped: position.ungrouped
40
+ }
41
+ };
42
+ }
43
+ return null;
44
+ }
45
+ //#endregion
46
+ export { readMigrationState };
@@ -24,6 +24,23 @@ import { type AmbientEnv } from "../env/ambient";
24
24
  * them rather than a side effect of a parser version.
25
25
  */
26
26
  export declare function isPublicHostname(hostname: string): boolean;
27
+ /**
28
+ * Why a string is not a hostname a Worker can answer on — the reason, not a boolean.
29
+ *
30
+ * **It exists so a refusal can say which rule it broke.** {@link isPublicHostname} is the answer when the
31
+ * caller only needs yes or no; `WorkerDomain` needs the reason, because the three are not interchangeable
32
+ * to the adopter reading the message. A name over the limit is a typo. A name with a scheme on it is a
33
+ * misunderstanding. An internationalized name is neither — it is a thing this kit cannot do, for a reason
34
+ * that is not this kit's, and a message about bare hostnames would send that adopter looking for a typo
35
+ * they do not have (#665).
36
+ *
37
+ * Ordered by how specific the answer is. The length check is first because a name that long is over the
38
+ * limit whatever else is wrong with it, and `punycode` last because it is only reachable for a string that
39
+ * is otherwise a well-formed hostname.
40
+ */
41
+ export declare function publicHostnameProblem(hostname: string): HostnameProblem | null;
42
+ /** The three ways a string fails {@link publicHostnameProblem}, each with its own sentence to say. */
43
+ export type HostnameProblem = "too-long" | "shape" | "punycode";
27
44
  /** The environments a domain may be declared for — every managed one, never `dev`. */
28
45
  export declare const DOMAIN_ENVIRONMENTS: ("prod" | "staging")[];
29
46
  /** One environment's public address for one Worker. */
@@ -1 +1 @@
1
- {"version":3,"file":"domains.d.ts","sourceRoot":"","sources":["../../src/naming/domains.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,KAAK,UAAU,EAAc,MAAM,gBAAgB,CAAC;AAgD7D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,wBAA8D,CAAC;AAE/F,uDAAuD;AACvD,eAAO,MAAM,YAAY;;;iBA+BrB,CAAC;AACL,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,aAAa;;;;;;;;;iBAOvB,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,IAAI,CAKlH;AAED,gHAAgH;AAChH,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAEvD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,qBAAqB,CAAC;AAE/C,yFAAyF;AACzF,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAetG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,GAC3B,cAAc,CAShB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,GAAG,GAAE,UAAyB,GAC7B,MAAM,CAER"}
1
+ {"version":3,"file":"domains.d.ts","sourceRoot":"","sources":["../../src/naming/domains.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,KAAK,UAAU,EAAc,MAAM,gBAAgB,CAAC;AAgD7D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAK9E;AAED,sGAAsG;AACtG,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,CAAC;AAyChE,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,wBAA8D,CAAC;AAE/F,uDAAuD;AACvD,eAAO,MAAM,YAAY;;;iBAwBrB,CAAC;AACL,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,aAAa;;;;;;;;;iBAOvB,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,IAAI,CAKlH;AAED,gHAAgH;AAChH,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAEvD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,qBAAqB,CAAC;AAE/C,yFAAyF;AACzF,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAetG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,GAC3B,cAAc,CAShB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,GAAG,GAAE,UAAyB,GAC7B,MAAM,CAER"}
@@ -68,14 +68,72 @@ const PUNYCODE_LABEL = /(^|\.)xn--/;
68
68
  * them rather than a side effect of a parser version.
69
69
  */
70
70
  function isPublicHostname(hostname) {
71
- return hostname.length <= 253 && HOSTNAME_PATTERN.test(hostname) && !PUNYCODE_LABEL.test(hostname);
71
+ return publicHostnameProblem(hostname) === null;
72
+ }
73
+ /** The DNS limit on a whole name, and the only length any caller measures. */
74
+ const MAX_HOSTNAME_LENGTH = 253;
75
+ /**
76
+ * Why a string is not a hostname a Worker can answer on — the reason, not a boolean.
77
+ *
78
+ * **It exists so a refusal can say which rule it broke.** {@link isPublicHostname} is the answer when the
79
+ * caller only needs yes or no; `WorkerDomain` needs the reason, because the three are not interchangeable
80
+ * to the adopter reading the message. A name over the limit is a typo. A name with a scheme on it is a
81
+ * misunderstanding. An internationalized name is neither — it is a thing this kit cannot do, for a reason
82
+ * that is not this kit's, and a message about bare hostnames would send that adopter looking for a typo
83
+ * they do not have (#665).
84
+ *
85
+ * Ordered by how specific the answer is. The length check is first because a name that long is over the
86
+ * limit whatever else is wrong with it, and `punycode` last because it is only reachable for a string that
87
+ * is otherwise a well-formed hostname.
88
+ */
89
+ function publicHostnameProblem(hostname) {
90
+ if (hostname.length > MAX_HOSTNAME_LENGTH) return "too-long";
91
+ if (!HOSTNAME_PATTERN.test(hostname)) return "shape";
92
+ if (PUNYCODE_LABEL.test(hostname)) return "punycode";
93
+ return null;
94
+ }
95
+ /**
96
+ * What an adopter is told, per reason, for a hostname this kit refuses.
97
+ *
98
+ * `shape` is the caller's to say, because what a bare hostname is differs between a `pattern` and a
99
+ * `zone` and each already had a sentence worth keeping. The other two are the same wherever they are hit.
100
+ *
101
+ * **The internationalized-domain sentence names Cloudflare**, and that is deliberate: the limit is not
102
+ * ours. `pithy-sh/pithy#691` tested it against a real zone — adding `tëst.pithy-sh.com` as a Worker
103
+ * Custom Domain returns *Internationalized Domain Names not supported* — so the kit refuses what the
104
+ * platform will not route rather than inventing a rule of its own. `docs/ACCEPTED-LIMITS.md` carries the
105
+ * evidence and the condition that would lift it.
106
+ */
107
+ const HOSTNAME_PROBLEM_MESSAGES = {
108
+ "too-long": `A hostname is at most ${MAX_HOSTNAME_LENGTH} characters — the DNS limit on a whole name.`,
109
+ punycode: "Internationalized domains are not supported: Cloudflare Workers will not route one, so a Worker could never answer on it. Use an ASCII hostname."
110
+ };
111
+ /**
112
+ * One `pattern` or `zone` field, held to the one hostname rule.
113
+ *
114
+ * **Through {@link publicHostnameProblem}, never `HOSTNAME_PATTERN` directly.** A configured domain was
115
+ * the third caller of that rule and the only one that did not go through the function — it handed the
116
+ * pattern to `.regex()`, so it took a punycode A-label and a 267-character name that `pithy seed --host`
117
+ * refused for the same string. Two answers to one question, forty lines apart (#665). A rule added to the
118
+ * function now reaches a configured domain without anyone remembering to.
119
+ */
120
+ function hostnameField(shapeMessage) {
121
+ return z.string().check((ctx) => {
122
+ const problem = publicHostnameProblem(ctx.value);
123
+ if (problem === null) return;
124
+ ctx.issues.push({
125
+ code: "custom",
126
+ input: ctx.value,
127
+ message: problem === "shape" ? shapeMessage : HOSTNAME_PROBLEM_MESSAGES[problem]
128
+ });
129
+ });
72
130
  }
73
131
  /** The environments a domain may be declared for — every managed one, never `dev`. */
74
132
  const DOMAIN_ENVIRONMENTS = ENVIRONMENTS.filter((environment) => environment !== "dev");
75
133
  /** One environment's public address for one Worker. */
76
134
  const WorkerDomain = z.object({
77
- pattern: z.string().regex(HOSTNAME_PATTERN, "A domain is a bare hostname — no scheme, no path, no port (e.g. `api.example.com`, not `https://api.example.com/`).").describe("The hostname this Worker answers on in this environment, e.g. `api.example.com`. Written into `wrangler.jsonc` as a `routes` entry with `custom_domain: true`, and into `vars.BASE_URL` as `https://<pattern>`. A bare hostname rather than a URL, because that is what wrangler's route matcher takes — a scheme here would be silently wrong."),
78
- zone: z.string().regex(HOSTNAME_PATTERN, "A zone is the registrable domain on your Cloudflare account, e.g. `example.com`.").describe("The Cloudflare zone `pattern` sits under — the registrable domain as it appears on your account, e.g. `example.com` for `api.example.com`. Cloudflare needs it to attach a custom domain, and it is not always derivable from the hostname: a zone can be a subdomain, and a public-suffix guess would be wrong for exactly the adopters who are hardest to debug.")
135
+ pattern: hostnameField("A domain is a bare hostname — no scheme, no path, no port (e.g. `api.example.com`, not `https://api.example.com/`).").describe("The hostname this Worker answers on in this environment, e.g. `api.example.com`. Written into `wrangler.jsonc` as a `routes` entry with `custom_domain: true`, and into `vars.BASE_URL` as `https://<pattern>`. A bare hostname rather than a URL, because that is what wrangler's route matcher takes — a scheme here would be silently wrong."),
136
+ zone: hostnameField("A zone is the registrable domain on your Cloudflare account, e.g. `example.com`.").describe("The Cloudflare zone `pattern` sits under — the registrable domain as it appears on your account, e.g. `example.com` for `api.example.com`. Cloudflare needs it to attach a custom domain, and it is not always derivable from the hostname: a zone can be a subdomain, and a public-suffix guess would be wrong for exactly the adopters who are hardest to debug.")
79
137
  }).describe("Where one Worker answers in one environment: the hostname, and the Cloudflare zone it sits under.").check((ctx) => {
80
138
  const { pattern, zone } = ctx.value;
81
139
  if (pattern !== zone && !pattern.endsWith(`.${zone}`)) ctx.issues.push({
@@ -234,4 +292,4 @@ function originFor(environment, domains, env = ambientEnv()) {
234
292
  return resolveOrigin(environment, domains, env[BASE_URL_VAR]).origin;
235
293
  }
236
294
  //#endregion
237
- export { DOMAIN_ENVIRONMENTS, LOCAL_ORIGIN, WorkerDomain, WorkerDomains, baseUrlFor, domainFor, featureOrigin, isPublicHostname, originFor, resolveOrigin };
295
+ export { DOMAIN_ENVIRONMENTS, LOCAL_ORIGIN, WorkerDomain, WorkerDomains, baseUrlFor, domainFor, featureOrigin, isPublicHostname, originFor, publicHostnameProblem, resolveOrigin };
@@ -1 +1 @@
1
- {"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/naming/environment.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,YAAY,YAAI,KAAK,EAAE,SAAS,EAAE,MAAM,CAAU,CAAC;AAEhE,wGAAwG;AACxG,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,WAAW,CAAC;AAErC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD;AAKD,kHAAkH;AAClH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAExD;AAgBD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,QAAqE,CAAC;AAEvG;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAIxD;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAmCzD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,YAAI,SAAS,EAAE,MAAM,CAAU,CAAC;AAEjE;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,oBAAoB,yBA2D9B,CAAC;AAEJ,sGAAsG;AACtG,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
1
+ {"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/naming/environment.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;;;;GAQG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,YAAY,YAAI,KAAK,EAAE,SAAS,EAAE,MAAM,CAAU,CAAC;AAEhE,wGAAwG;AACxG,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,WAAW,CAAC;AAErC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD;AAKD,kHAAkH;AAClH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAExD;AAgBD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,QAAqE,CAAC;AAEvG;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAIxD;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAmCzD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,YAAI,SAAS,EAAE,MAAM,CAAU,CAAC;AAEjE;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,oBAAoB,yBA2D9B,CAAC;AAEJ,sGAAsG;AACtG,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
@@ -1,6 +1,7 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { PithyError, ValidationError } from "../error/pithyError.js";
4
+ import { commandName } from "../cli/command.js";
4
5
  import { NAME_SEGMENT } from "./segment.js";
5
6
  import { z } from "zod";
6
7
  //#region src/naming/environment.ts
@@ -129,7 +130,7 @@ function assertValidEnvironment(name) {
129
130
  if (startsWithFeatureMarker(name)) throw new ValidationError({
130
131
  message: `"${name}" starts the way a feature's name does.`,
131
132
  action: "Start the environment with something other than f and a number.",
132
- detail: `A feature takes the environment's slot with f<issue>, so <project>-${name}-<worker> can be a feature's Worker name exactly, and \`pithy feature destroy\` deletes that Worker.`
133
+ detail: `A feature takes the environment's slot with f<issue>, so <project>-${name}-<worker> can be a feature's Worker name exactly, and \`${commandName("feature destroy")}\` deletes that Worker.`
133
134
  });
134
135
  if (name.length > MAX_ENVIRONMENT_NAME && NAME_SEGMENT.test(name)) throw new ValidationError({
135
136
  message: `"${name}" is ${name.length} characters. An environment stops at ${MAX_ENVIRONMENT_NAME}.`,
@@ -27,10 +27,19 @@
27
27
  * It lives in `core`, beside the rest of the naming rule, so the CLI and the facade compose one
28
28
  * implementation rather than two that drift.
29
29
  */
30
- /** A Cloudflare resource kind a feature can provision. All two characters — `MAX_FEATURE_KIND` in `./limits`. */
31
- export type FeatureResourceKind = "d1" | "kv" | "r2";
30
+ /** A Cloudflare resource kind Pithy names and creates — in a feature, and in every declared environment. */
31
+ export type FeatureResourceKind = "d1" | "kv" | "r2" | "queue";
32
32
  /** Every feature resource kind, for tests and for callers that enumerate. */
33
33
  export declare const FEATURE_RESOURCE_KINDS: readonly FeatureResourceKind[];
34
+ /**
35
+ * **The segment each kind ends a feature resource's name with — never more than `MAX_FEATURE_KIND` (#662).**
36
+ *
37
+ * `d1`, `kv` and `r2` spell themselves. A queue is `q`, and that is load-bearing: `MAX_FEATURE_KIND` is a term in
38
+ * `FEATURE_DERIVED_PROJECT_NAME`, so spelling it `queue` would take the project budget from 26 to 23 — and a
39
+ * project name is stable forever once anything is provisioned, so a smaller budget orphans every resource of a
40
+ * project already named against the old one. `feature.test.ts` fails on a segment longer than the budget.
41
+ */
42
+ export declare const FEATURE_KIND_SEGMENT: Readonly<Record<FeatureResourceKind, string>>;
34
43
  /** The identity of a feature — everything a feature name is derived from. */
35
44
  export interface FeatureIdentity {
36
45
  /** The project name, from the root `pithy.config.ts` (e.g. `acme`). */
@@ -126,10 +135,11 @@ export declare function maxFeatureSlug(parts: FeatureHead, shapes: readonly Feat
126
135
  */
127
136
  export declare function assertFeatureSlugFits(identity: FeatureIdentity, shapes: readonly FeatureNameShape[]): void;
128
137
  /**
129
- * The full Cloudflare resource name for a feature's binding — `<project>-f<issue>-<slug>--<binding>-<kind>`.
138
+ * The full Cloudflare resource name for a feature's binding — `<project>-f<issue>-<slug>--<binding>-<kind>`, the
139
+ * kind spelled by {@link FEATURE_KIND_SEGMENT}.
130
140
  *
131
- * Held to **R2's 63**, the strictest of the three kinds a feature provisions, so one shape is legal for
132
- * all of them: lowercase, hyphenated, alphanumeric at both ends. A D1 or KV name could be longer, but a
141
+ * Held to **R2's 63**, the strictest of the kinds a feature provisions and a queue's number too, so one shape is
142
+ * legal for all of them: lowercase, hyphenated, alphanumeric at both ends. A D1 or KV name could be longer, but a
133
143
  * feature that provisions a bucket and a database wants them recognizably the same name.
134
144
  */
135
145
  export declare function featureResourceName(identity: FeatureIdentity, binding: string, kind: FeatureResourceKind): string;
@@ -168,6 +178,18 @@ export declare function featureSecretEntryName(identity: FeatureIdentity, secret
168
178
  * and picks.
169
179
  */
170
180
  export declare function featureWorkerName(identity: FeatureIdentity, app: string): string;
181
+ /** The `<thing>` a feature's Artifacts namespace ends with, after {@link FEATURE_TAIL_SEPARATOR} (#673). */
182
+ export declare const FEATURE_ARTIFACTS_THING = "artifacts";
183
+ /**
184
+ * A feature's **Artifacts namespace** — `<project>-f<issue>-<slug>--artifacts` (#673).
185
+ *
186
+ * A declared environment's namespace is `<project>-<env>` and has no `<thing>`. A feature's needs one, because the
187
+ * separator and what follows it are what make a name a feature name: {@link isFeatureOwnedName} reads this one back
188
+ * as this feature's, so `pithy feature destroy` reaps the repos of exactly this branch and of no sibling. Held to the
189
+ * namespace's 63, the same number the rest of a feature's names are derived against, and `artifacts` is shorter than
190
+ * the shortest binding tail, so it moves no slug budget.
191
+ */
192
+ export declare function featureArtifactsNamespace(identity: FeatureIdentity): string;
171
193
  /**
172
194
  * The deployed name of one of a feature's **Workflows** — `<project>-f<issue>-<slug>--<capability>-<job>` (#643).
173
195
  *
@@ -1 +1 @@
1
- {"version":3,"file":"feature.d.ts","sourceRoot":"","sources":["../../src/naming/feature.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"feature.d.ts","sourceRoot":"","sources":["../../src/naming/feature.ts"],"names":[],"mappings":"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"}