@pithy-sh/core 0.1.4 → 0.2.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 (270) hide show
  1. package/dist/address/address.d.ts +2 -0
  2. package/dist/address/address.js +2 -0
  3. package/dist/audit/auditEvent.d.ts +2 -0
  4. package/dist/audit/auditEvent.js +2 -0
  5. package/dist/audit/recorder.d.ts +2 -0
  6. package/dist/audit/recorder.js +2 -0
  7. package/dist/capability/bindings.d.ts +2 -0
  8. package/dist/capability/bindings.js +2 -0
  9. package/dist/capability/capability.d.ts +20 -0
  10. package/dist/capability/capability.d.ts.map +1 -1
  11. package/dist/capability/capability.js +2 -0
  12. package/dist/capability/client.d.ts +2 -0
  13. package/dist/capability/client.js +2 -0
  14. package/dist/capability/compose.d.ts +2 -0
  15. package/dist/capability/compose.js +2 -0
  16. package/dist/capability/composition.d.ts +2 -0
  17. package/dist/capability/composition.js +2 -0
  18. package/dist/capability/config.d.ts +2 -0
  19. package/dist/capability/config.js +2 -0
  20. package/dist/capability/devSecret.d.ts +2 -0
  21. package/dist/capability/devSecret.js +2 -0
  22. package/dist/capability/manifest.d.ts +63 -0
  23. package/dist/capability/manifest.d.ts.map +1 -1
  24. package/dist/capability/manifest.js +63 -1
  25. package/dist/capability/secretOrigin.d.ts +2 -0
  26. package/dist/capability/secretOrigin.js +2 -0
  27. package/dist/capability/settings.d.ts +2 -0
  28. package/dist/capability/settings.js +2 -0
  29. package/dist/capability/validateBindings.d.ts +2 -0
  30. package/dist/capability/validateBindings.js +2 -0
  31. package/dist/capability/vanishingKey.d.ts +2 -0
  32. package/dist/capability/vanishingKey.js +2 -0
  33. package/dist/controlPlane/audit/actions.d.ts +2 -0
  34. package/dist/controlPlane/audit/actions.js +2 -0
  35. package/dist/controlPlane/capability.d.ts +2 -0
  36. package/dist/controlPlane/capability.js +2 -0
  37. package/dist/controlPlane/config/config.d.ts +2 -0
  38. package/dist/controlPlane/config/config.js +2 -0
  39. package/dist/controlPlane/context.d.ts +2 -0
  40. package/dist/controlPlane/context.js +2 -0
  41. package/dist/controlPlane/data/connection.d.ts +2 -0
  42. package/dist/controlPlane/data/connection.js +2 -0
  43. package/dist/controlPlane/data/keyLifecycle.d.ts +2 -0
  44. package/dist/controlPlane/data/keyLifecycle.js +2 -0
  45. package/dist/controlPlane/data/replay.d.ts +2 -0
  46. package/dist/controlPlane/data/replay.js +2 -0
  47. package/dist/controlPlane/data/tables.d.ts +2 -0
  48. package/dist/controlPlane/data/tables.js +2 -0
  49. package/dist/controlPlane/discovery/adminRoute.d.ts +2 -0
  50. package/dist/controlPlane/discovery/adminRoute.js +2 -0
  51. package/dist/controlPlane/discovery/configuration.d.ts +2 -0
  52. package/dist/controlPlane/discovery/configuration.js +2 -0
  53. package/dist/controlPlane/discovery/drift.d.ts +2 -0
  54. package/dist/controlPlane/discovery/drift.js +2 -0
  55. package/dist/controlPlane/discovery/health.d.ts +2 -0
  56. package/dist/controlPlane/discovery/health.js +2 -0
  57. package/dist/controlPlane/discovery/healthSummary.d.ts +2 -0
  58. package/dist/controlPlane/discovery/healthSummary.js +2 -0
  59. package/dist/controlPlane/error/errors.d.ts +2 -0
  60. package/dist/controlPlane/error/errors.js +2 -0
  61. package/dist/controlPlane/http/cors.d.ts +2 -0
  62. package/dist/controlPlane/http/cors.js +2 -0
  63. package/dist/controlPlane/http/guard.d.ts +2 -0
  64. package/dist/controlPlane/http/guard.js +2 -0
  65. package/dist/controlPlane/http/handlers.d.ts +2 -0
  66. package/dist/controlPlane/http/handlers.js +2 -0
  67. package/dist/controlPlane/http/responses.d.ts +2 -0
  68. package/dist/controlPlane/http/responses.js +2 -0
  69. package/dist/controlPlane/http/routes.d.ts +2 -0
  70. package/dist/controlPlane/http/routes.js +2 -0
  71. package/dist/controlPlane/http/schemas.d.ts +2 -0
  72. package/dist/controlPlane/http/schemas.js +2 -0
  73. package/dist/controlPlane/http/verify.d.ts +2 -0
  74. package/dist/controlPlane/http/verify.js +2 -0
  75. package/dist/controlPlane/migrations/0001_init.d.ts +2 -0
  76. package/dist/controlPlane/migrations/0001_init.js +2 -0
  77. package/dist/controlPlane/replay/d1Guard.d.ts +2 -0
  78. package/dist/controlPlane/replay/d1Guard.js +2 -0
  79. package/dist/controlPlane/replay/guard.d.ts +2 -0
  80. package/dist/controlPlane/replay/guard.js +2 -0
  81. package/dist/controlPlane/replay/kvGuard.d.ts +2 -0
  82. package/dist/controlPlane/replay/kvGuard.js +2 -0
  83. package/dist/controlPlane/scope/scope.d.ts +2 -0
  84. package/dist/controlPlane/scope/scope.js +2 -0
  85. package/dist/controlPlane/token/base64url.d.ts +2 -0
  86. package/dist/controlPlane/token/base64url.js +2 -0
  87. package/dist/controlPlane/token/claims.d.ts +2 -0
  88. package/dist/controlPlane/token/claims.js +2 -0
  89. package/dist/controlPlane/token/digest.d.ts +2 -0
  90. package/dist/controlPlane/token/digest.js +2 -0
  91. package/dist/controlPlane/token/jws.d.ts +2 -0
  92. package/dist/controlPlane/token/jws.js +2 -0
  93. package/dist/controlPlane/token/mint.d.ts +2 -0
  94. package/dist/controlPlane/token/mint.js +2 -0
  95. package/dist/controlPlane/wire.d.ts +2 -0
  96. package/dist/controlPlane/wire.js +2 -0
  97. package/dist/createBackend.d.ts +2 -0
  98. package/dist/createBackend.js +2 -0
  99. package/dist/createEntrypoint.d.ts +2 -0
  100. package/dist/createEntrypoint.d.ts.map +1 -1
  101. package/dist/createEntrypoint.js +3 -0
  102. package/dist/data/boundParameters.d.ts +2 -0
  103. package/dist/data/boundParameters.js +2 -0
  104. package/dist/data/codecs.d.ts +2 -0
  105. package/dist/data/codecs.js +2 -0
  106. package/dist/data/cursor.d.ts +2 -0
  107. package/dist/data/cursor.js +2 -0
  108. package/dist/data/databases.d.ts +2 -0
  109. package/dist/data/databases.js +2 -0
  110. package/dist/data/db.d.ts +2 -0
  111. package/dist/data/db.js +2 -0
  112. package/dist/data/withD1Retry.d.ts +2 -0
  113. package/dist/data/withD1Retry.js +2 -0
  114. package/dist/entitlement/entitlement.d.ts +2 -0
  115. package/dist/entitlement/entitlement.js +2 -0
  116. package/dist/entitlement/gateScan.d.ts +2 -0
  117. package/dist/entitlement/gateScan.js +2 -0
  118. package/dist/entitlement/require.d.ts +2 -0
  119. package/dist/entitlement/require.js +2 -0
  120. package/dist/env/ambient.d.ts +2 -0
  121. package/dist/env/ambient.js +2 -0
  122. package/dist/env/ci.d.ts +2 -0
  123. package/dist/env/ci.js +2 -0
  124. package/dist/env/stem.d.ts +2 -0
  125. package/dist/env/stem.js +2 -0
  126. package/dist/error/cause.d.ts +68 -0
  127. package/dist/error/cause.d.ts.map +1 -1
  128. package/dist/error/cause.js +94 -1
  129. package/dist/error/client.d.ts +2 -0
  130. package/dist/error/client.js +2 -0
  131. package/dist/error/extend.d.ts +2 -0
  132. package/dist/error/extend.js +2 -0
  133. package/dist/error/http.d.ts +2 -0
  134. package/dist/error/http.js +2 -0
  135. package/dist/error/payload.d.ts +2 -0
  136. package/dist/error/payload.js +2 -0
  137. package/dist/error/pithyError.d.ts +2 -0
  138. package/dist/error/pithyError.js +2 -0
  139. package/dist/error/terminal.d.ts +2 -0
  140. package/dist/error/terminal.js +2 -0
  141. package/dist/http/authContext.d.ts +2 -0
  142. package/dist/http/authContext.js +2 -0
  143. package/dist/http/routeContract.d.ts +2 -0
  144. package/dist/http/routeContract.js +2 -0
  145. package/dist/http/sameOrigin.d.ts +2 -0
  146. package/dist/http/sameOrigin.js +2 -0
  147. package/dist/http/signedWebhook.d.ts +2 -0
  148. package/dist/http/signedWebhook.js +2 -0
  149. package/dist/http/validation.d.ts +2 -0
  150. package/dist/http/validation.js +2 -0
  151. package/dist/http/verification.d.ts +2 -0
  152. package/dist/http/verification.js +2 -0
  153. package/dist/i18n/acceptLanguage.d.ts +2 -0
  154. package/dist/i18n/acceptLanguage.js +2 -0
  155. package/dist/i18n/catalog.d.ts +2 -0
  156. package/dist/i18n/catalog.js +2 -0
  157. package/dist/i18n/locale.d.ts +2 -0
  158. package/dist/i18n/locale.js +2 -0
  159. package/dist/i18n/localeMarker.d.ts +2 -0
  160. package/dist/i18n/localeMarker.js +2 -0
  161. package/dist/i18n/match.d.ts +2 -0
  162. package/dist/i18n/match.js +2 -0
  163. package/dist/i18n/registry.d.ts +2 -0
  164. package/dist/i18n/registry.js +2 -0
  165. package/dist/i18n/translator.d.ts +2 -0
  166. package/dist/i18n/translator.js +2 -0
  167. package/dist/index.d.ts +2 -0
  168. package/dist/index.js +2 -0
  169. package/dist/kv/kv.d.ts +2 -0
  170. package/dist/kv/kv.js +2 -0
  171. package/dist/kv/namespaces.d.ts +2 -0
  172. package/dist/kv/namespaces.js +2 -0
  173. package/dist/logger/local.d.ts +2 -0
  174. package/dist/logger/local.js +2 -0
  175. package/dist/logger/logger.d.ts +2 -0
  176. package/dist/logger/logger.js +2 -0
  177. package/dist/logger/record.d.ts +2 -0
  178. package/dist/logger/record.js +2 -0
  179. package/dist/logger/worker.d.ts +2 -0
  180. package/dist/logger/worker.js +2 -0
  181. package/dist/migrations/batch.d.ts +2 -0
  182. package/dist/migrations/batch.js +2 -0
  183. package/dist/migrations/bookkeeping.d.ts +2 -0
  184. package/dist/migrations/bookkeeping.js +2 -0
  185. package/dist/migrations/owner.d.ts +2 -0
  186. package/dist/migrations/owner.js +2 -0
  187. package/dist/migrations/registry.d.ts +2 -0
  188. package/dist/migrations/registry.js +2 -0
  189. package/dist/migrations/runner.d.ts +2 -0
  190. package/dist/migrations/runner.js +2 -0
  191. package/dist/naming/domains.d.ts +2 -0
  192. package/dist/naming/domains.js +2 -0
  193. package/dist/naming/environment.d.ts +2 -0
  194. package/dist/naming/environment.js +2 -0
  195. package/dist/naming/feature.d.ts +2 -0
  196. package/dist/naming/feature.js +2 -0
  197. package/dist/naming/limits.d.ts +2 -0
  198. package/dist/naming/limits.js +2 -0
  199. package/dist/naming/provisionScope.d.ts +2 -0
  200. package/dist/naming/provisionScope.js +2 -0
  201. package/dist/naming/resource.d.ts +2 -0
  202. package/dist/naming/resource.js +2 -0
  203. package/dist/naming/resourceNames.d.ts +2 -0
  204. package/dist/naming/resourceNames.js +2 -0
  205. package/dist/naming/segment.d.ts +2 -0
  206. package/dist/naming/segment.js +2 -0
  207. package/dist/projection/asRead.d.ts +2 -0
  208. package/dist/projection/asRead.js +2 -0
  209. package/dist/projection/published.d.ts +2 -0
  210. package/dist/projection/published.js +2 -0
  211. package/dist/schema/describedness.d.ts +2 -0
  212. package/dist/schema/describedness.js +2 -0
  213. package/dist/seed/compose.d.ts +2 -0
  214. package/dist/seed/compose.js +2 -0
  215. package/dist/seed/devLogin.d.ts +2 -0
  216. package/dist/seed/devLogin.js +2 -0
  217. package/dist/seed/exampleIdentities.d.ts +2 -0
  218. package/dist/seed/exampleIdentities.js +2 -0
  219. package/dist/seed/metadata.d.ts +2 -0
  220. package/dist/seed/metadata.js +2 -0
  221. package/dist/seed/seed.d.ts +2 -0
  222. package/dist/seed/seed.js +2 -0
  223. package/dist/seed/seededRows.d.ts +2 -0
  224. package/dist/seed/seededRows.js +2 -0
  225. package/dist/seed/writeD1.d.ts +2 -0
  226. package/dist/seed/writeD1.js +2 -0
  227. package/dist/seed/writeKv.d.ts +2 -0
  228. package/dist/seed/writeKv.js +2 -0
  229. package/dist/semver/semver.d.ts +2 -0
  230. package/dist/semver/semver.js +2 -0
  231. package/dist/text/comments.d.ts +2 -0
  232. package/dist/text/comments.js +2 -0
  233. package/dist/version.generated.d.ts +3 -1
  234. package/dist/version.generated.js +3 -1
  235. package/dist/worker/health.d.ts +2 -0
  236. package/dist/worker/health.js +2 -0
  237. package/dist/worker/identity.d.ts +2 -0
  238. package/dist/worker/identity.js +2 -0
  239. package/dist/workflow/bindings.d.ts +2 -0
  240. package/dist/workflow/bindings.js +2 -0
  241. package/dist/workflow/dispatch.d.ts +2 -0
  242. package/dist/workflow/dispatch.js +2 -0
  243. package/dist/workflow/dispatchRoute.d.ts +2 -0
  244. package/dist/workflow/dispatchRoute.js +2 -0
  245. package/dist/workflow/faults.d.ts +2 -0
  246. package/dist/workflow/faults.js +2 -0
  247. package/dist/workflow/host.d.ts +2 -0
  248. package/dist/workflow/host.js +2 -0
  249. package/dist/workflow/hostEntry.d.ts +2 -0
  250. package/dist/workflow/hostEntry.js +2 -0
  251. package/dist/workflow/hostEnv.d.ts +2 -0
  252. package/dist/workflow/hostEnv.js +2 -0
  253. package/dist/workflow/loopback.d.ts +2 -0
  254. package/dist/workflow/loopback.js +2 -0
  255. package/dist/workflow/naming.d.ts +2 -0
  256. package/dist/workflow/naming.js +2 -0
  257. package/dist/workflow/register.d.ts +2 -0
  258. package/dist/workflow/register.js +2 -0
  259. package/dist/workflow/schemas.d.ts +2 -0
  260. package/dist/workflow/schemas.js +2 -0
  261. package/dist/workflow/spec.d.ts +2 -0
  262. package/dist/workflow/spec.js +2 -0
  263. package/dist/workflow/stepMessage.d.ts +2 -0
  264. package/dist/workflow/stepMessage.js +2 -0
  265. package/package.json +2 -2
  266. package/src/capability/capability.ts +18 -0
  267. package/src/capability/manifest.ts +94 -0
  268. package/src/createEntrypoint.ts +5 -0
  269. package/src/error/cause.ts +116 -0
  270. package/src/version.generated.ts +1 -1
@@ -281,6 +281,25 @@ export function renderConfigOptionComment(describe: string, indent: string): str
281
281
  return `${indent}// ${describe}`;
282
282
  }
283
283
 
284
+ /**
285
+ * The line a writer puts in `pithy.config.ts` for a **seam** — the option a capability takes that is a
286
+ * function, so no renderer can ever state its value.
287
+ *
288
+ * Shorthand (`resolveSubject,`) rather than `key: value`, because the two are the same identifier: the
289
+ * scaffolded module exports the name the factory takes, and a config that spelled it twice would invite
290
+ * an adopter to rename one half of it.
291
+ *
292
+ * **It takes the seam's key and nothing else.** That is the same closed-set argument
293
+ * {@link CONFIG_CONSTANTS} makes one function up, and it has to be made again here for a stronger
294
+ * reason: a constant is one identifier in an expression position, and a seam is an identifier *plus* an
295
+ * import statement plus a file written into the adopter's repository. A manifest states which seam, by a
296
+ * key this package enumerates; every character that reaches the adopter's TypeScript comes from
297
+ * {@link CONFIG_SEAMS}.
298
+ */
299
+ export function renderConfigSeamLine(seam: ConfigSeam, indent: string): string {
300
+ return `${indent}${CONFIG_SEAMS[seam].binding},`;
301
+ }
302
+
284
303
  /**
285
304
  * A capability's own name, as generated source carries it.
286
305
  *
@@ -439,6 +458,53 @@ export const ConfigConstant = z
439
458
  .describe("A constant the scaffolded pithy.config.ts defines, named from a closed set rather than spelled out.");
440
459
  export type ConfigConstant = z.infer<typeof ConfigConstant>;
441
460
 
461
+ /** One seam a capability takes: what `pithy add` writes for it, and what the config then references. */
462
+ export interface ConfigSeamSpec {
463
+ /** The identifier the scaffolded module exports, and the key the capability's factory takes for it. */
464
+ readonly binding: string;
465
+ /** The module `pithy add` writes, relative to the Worker's own directory. */
466
+ readonly module: string;
467
+ /** How that module is imported from the Worker's `pithy.config.ts`. */
468
+ readonly specifier: string;
469
+ /** The comment written above the seam's line, saying what the adopter still owes it. */
470
+ readonly describe: string;
471
+ }
472
+
473
+ /**
474
+ * The seams `pithy add` knows how to scaffold — a capability option whose value is **behavior**, named
475
+ * from a closed set exactly as {@link CONFIG_CONSTANTS} names an identifier.
476
+ *
477
+ * A missing *value* has always been scaffoldable: `pithy add secrets` writes an empty registry and the
478
+ * project still loads, because an empty registry is a valid empty state. A missing *behavior* was not,
479
+ * so `payments`' `billingSubject: "organization"` was refused outright — and its remedy routed through
480
+ * `--set billingSubject=user`, a value that is untrue for every project that wanted the other one (#500).
481
+ *
482
+ * What makes the behavior scaffoldable is that the scaffold is **not a stub that answers**. A resolver
483
+ * returning nothing composes cleanly and then denies every gate, which is indistinguishable from a
484
+ * customer who has not paid. The module written here is branded as unimplemented by the capability that
485
+ * owns it, and that capability refuses the Worker's entrypoint while the brand is still on it — so the
486
+ * adopter says what they mean, gets the file and the signature, and the project still refuses to boot
487
+ * until they have written the one thing only they can write.
488
+ *
489
+ * One entry today. Every string in it is this package's, because all four of them reach the adopter's
490
+ * repository unquoted: an identifier, a path, an import specifier, a comment.
491
+ */
492
+ export const CONFIG_SEAMS = {
493
+ paymentsSubject: {
494
+ binding: "resolveSubject",
495
+ module: "src/billing/subject.ts",
496
+ specifier: "./src/billing/subject",
497
+ describe:
498
+ "Which organization this caller is acting for. Scaffolded unimplemented — this Worker refuses to boot until src/billing/subject.ts answers it.",
499
+ },
500
+ } as const satisfies Record<string, ConfigSeamSpec>;
501
+
502
+ /** Which scaffolded seam a choice needs, by key. */
503
+ export const ConfigSeam = z
504
+ .enum(Object.keys(CONFIG_SEAMS) as [keyof typeof CONFIG_SEAMS])
505
+ .describe("A seam pithy add knows how to scaffold, named from a closed set rather than described.");
506
+ export type ConfigSeam = z.infer<typeof ConfigSeam>;
507
+
442
508
  export const ConfigOption = z
443
509
  .object({
444
510
  key: z
@@ -467,6 +533,11 @@ export const ConfigOption = z
467
533
  .describe(
468
534
  "Choices `pithy add` must refuse, each mapped to the sentence telling the adopter what to do instead — a value whose composition needs a seam only they can write. `pithy add` renders JSON into `pithy.config.ts`; it cannot render a function, so a choice whose validity depends on one is a choice it can write and the kit will then refuse to load. `payments`' `billingSubject: \"organization\"` is the one: it requires a `resolveSubject` saying which organization a caller acts for, deliberately has no default because a capability that guessed would key a company's plan to whoever signed in first, and so a config carrying the choice without the seam is refused at assembly — bricking every later command in the project, since they all begin by loading the config (#483). Refusing the choice up front costs the adopter one hand-edit; writing it costs them the project.",
469
535
  ),
536
+ choicesNeedingSeam: manifestRecord(z.record(PrintableString, ConfigSeam))
537
+ .optional()
538
+ .describe(
539
+ "Choices `pithy add` writes **together with the code they need** — the other half of `choicesNeedingCode`, for a choice whose missing half the kit can scaffold. Each names a seam from `CONFIG_SEAMS`; `pithy add` writes that module into the Worker, imports it, and passes it to the capability's factory beside the choice. `payments`' `billingSubject: \"organization\"` is the one: reaching it used to mean passing `--set billingSubject=user`, a value that is wrong for the project, and then hand-editing both halves (#500). The scaffolded module is deliberately **not** a working stub — it is branded unimplemented, and the capability refuses the Worker's entrypoint while the brand is on it, because a resolver that answers nothing denies every gate and reads exactly like a customer who has not paid. A choice states this or `choicesNeedingCode`, never both: one is a refusal and the other is a scaffold.",
540
+ ),
470
541
  constant: ConfigConstant.optional().describe(
471
542
  "A constant the scaffolded pithy.config.ts defines, rendered *unquoted* in place of `default` when the target config declares it. The one that exists is `publicOrigin` — `PUBLIC_ORIGIN`, this Worker's address for the environment it composes in. An option whose value is an origin must name it rather than state a URL, because a URL written down is production's URL written into staging: it is what mailed staging's testers magic links into production and unsubscribed them there (#256). `default` is still required *for such an option* and is what a config with no such constant gets, so an older project is never handed an identifier it does not define — the one place the absent-default rule does not reach, and it is checked rather than trusted.",
472
543
  ),
@@ -506,6 +577,29 @@ export const ConfigOption = z
506
577
  });
507
578
  }
508
579
  }
580
+ // The same rule the line above holds `choicesNeedingCode` to, for the same reason: a seam keyed on a
581
+ // value this option does not offer never fires, and the choice is written with no seam beside it.
582
+ for (const choice of Object.keys(option.choicesNeedingSeam ?? {})) {
583
+ if (!option.choices?.includes(choice)) {
584
+ ctx.issues.push({
585
+ code: "custom",
586
+ input: choice,
587
+ path: ["choicesNeedingSeam", choice],
588
+ message: `A choice needing a seam must be one this option offers, and ${JSON.stringify(choice)} is not — ${option.key} offers ${JSON.stringify(option.choices ?? [])}.`,
589
+ });
590
+ }
591
+ // Refused *and* scaffolded is not a state with an answer. `pithy add` reads one map to decide
592
+ // whether to stop and the other to decide what to write, so a choice in both would be refused by
593
+ // the first reader and the second would never run — a scaffold declared and silently never written.
594
+ if (option.choicesNeedingCode?.[choice] !== undefined) {
595
+ ctx.issues.push({
596
+ code: "custom",
597
+ input: choice,
598
+ path: ["choicesNeedingSeam", choice],
599
+ message: `A choice is refused or scaffolded, never both, and ${option.key} states ${JSON.stringify(choice)} in choicesNeedingCode and choicesNeedingSeam alike.`,
600
+ });
601
+ }
602
+ }
509
603
  if (option.constant !== undefined && option.default === undefined) {
510
604
  ctx.issues.push({
511
605
  code: "custom",
@@ -80,6 +80,11 @@ export function createEntrypoint<
80
80
  const all: Capability[] = options.app ? [...options.capabilities, options.app] : [...options.capabilities];
81
81
  const handlers: CapabilityEmailHandler[] = all.flatMap((cap) => (cap.email ? [cap.email] : []));
82
82
 
83
+ // The last gate before this Worker can take a request, and the first one the CLI never reaches: only a
84
+ // Worker's own entry calls `createEntrypoint`, while `createBackend` is assembled by tooling too. A
85
+ // capability that must load for the adopter's toolchain and must not serve refuses here (#500).
86
+ for (const cap of all) cap.boot?.({ capabilities: all });
87
+
83
88
  const registry = composeWorkflows(all);
84
89
  const scheduled = scheduledWorkflows(registry);
85
90
 
@@ -191,6 +191,63 @@ export function failurePosition(cause: unknown): FailurePosition | undefined {
191
191
  return { line: Number(match[1]), column: Number(match[2]) };
192
192
  }
193
193
 
194
+ /** What kind of thing failed to resolve. Three causes that wore one sentence until #489. */
195
+ export type UnresolvedKind =
196
+ /** A bare specifier for a package that is not installed. Installing dependencies is the remedy. */
197
+ | "package"
198
+ /** An installed package asked for a subpath it does not provide. Installing again changes nothing. */
199
+ | "package-subpath"
200
+ /** A relative import of a file in the project's own tree. Installing again changes nothing. */
201
+ | "local-file";
202
+
203
+ /** Whether a path sits inside an installed package. */
204
+ function insideNodeModules(path: string): boolean {
205
+ return path.split(/[\\/]/).includes("node_modules");
206
+ }
207
+
208
+ /**
209
+ * Which of the three unresolved-import causes this was, or `undefined` when it cannot be told.
210
+ *
211
+ * **The discriminator was already in the message and was being thrown away.** `unresolvedSpecifier`
212
+ * matches `Cannot find (?:package|module)` — and `package` versus `module` is exactly the fact that
213
+ * separates a missing dependency from the other two. It sat in a non-capturing group, so every cause
214
+ * arrived wearing one sentence, and for two of the three that sentence advised an install which is
215
+ * *guaranteed* to change nothing. That is #489's class exactly: guidance a caller cannot follow, worst
216
+ * for the caller it is written for, which has no human to shrug and try something else.
217
+ *
218
+ * Measured across Node 24.13.0 and Bun 1.3.14, because the two runtimes hand back different halves and
219
+ * each hands back exactly one usable one:
220
+ *
221
+ * | cause | Node says | Bun's `specifier` |
222
+ * |---|---|---|
223
+ * | package absent | `Cannot find package 'pkg'`, no `url` | `pkg` |
224
+ * | subpath missing | `Cannot find module '/abs/node_modules/pkg/src/nope.js'`, `url` under node_modules | `pkg/src/nope` |
225
+ * | adopter's file | `Cannot find module '/abs/apps/board/src/x'`, `url` outside node_modules | `./src/x` |
226
+ *
227
+ * Bun gives the **specifier as written**, so a leading `.` settles it. Node gives the **already-resolved
228
+ * path**, where being under `node_modules` settles it. Neither read works on both runtimes; the pair
229
+ * does, and it is the same predicate — *is this inside node_modules* — applied to whichever fact the
230
+ * runtime supplied.
231
+ */
232
+ export function unresolvedKind(cause: unknown): UnresolvedKind | undefined {
233
+ if (/Cannot find package /.test(causeMessage(cause) ?? "")) return "package";
234
+
235
+ const specifier = unresolvedSpecifier(cause);
236
+ if (specifier === undefined) return undefined;
237
+
238
+ // Bun's field is the specifier as written: relative means the adopter's own file.
239
+ if (/^(?:\.|\/|[A-Za-z]:[\\/])/.test(specifier)) {
240
+ return insideNodeModules(specifier) ? "package-subpath" : "local-file";
241
+ }
242
+
243
+ // Node states an already-resolved absolute path; a bare specifier reaching here is a package subpath.
244
+ const url = prop(cause, "url");
245
+ if (typeof url === "string" && url.length > 0) {
246
+ return insideNodeModules(url) ? "package-subpath" : "local-file";
247
+ }
248
+ return "package-subpath";
249
+ }
250
+
194
251
  /**
195
252
  * The specifier that did not resolve.
196
253
  *
@@ -206,3 +263,62 @@ export function unresolvedSpecifier(cause: unknown): string | undefined {
206
263
  const match = /Cannot find (?:package|module) ['"]([^'"]+)['"]/.exec(causeMessage(cause) ?? "");
207
264
  return match?.[1];
208
265
  }
266
+
267
+ /** A package name as npm allows one: an optional `@scope/`, and never a store's dot-directory. */
268
+ const PACKAGE_NAME = /^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i;
269
+
270
+ /**
271
+ * The file the failing import was written in, as the runtime named it. **Module-private on purpose**: it
272
+ * is an absolute path, and {@link importingPackage} exists to make sure only its package half leaves here.
273
+ */
274
+ function importerPath(cause: unknown): string | undefined {
275
+ const field = prop(cause, "referrer");
276
+ if (typeof field === "string" && field.length > 0) return field;
277
+ const message = causeMessage(cause) ?? "";
278
+ // Node ends its sentence with the path, so it is taken to the end of the line — a directory may
279
+ // contain a space. Bun quotes it when it states one in prose, and always carries the field as well.
280
+ return (/ imported from (.+)$/.exec(message) ?? /\bfrom ['"]([^'"]+)['"]/.exec(message))?.[1];
281
+ }
282
+
283
+ /**
284
+ * The package whose own source contained the import that did not resolve — **never a path**.
285
+ *
286
+ * {@link unresolvedSpecifier} answers *what* did not resolve. This answers *where the import lives*, and
287
+ * the two are different facts whenever the failing import is not the adopter's own. #480 is that case
288
+ * verbatim: `pithy add email` on a fresh project refused with
289
+ *
290
+ * > Nothing resolves "@pithy-sh/core/src/capability/capability".
291
+ *
292
+ * while `@pithy-sh/core` resolved from the Worker at every step of the run, before and after. The
293
+ * specifier was honest — the runtime really could not resolve it — but the import belonged to
294
+ * `@pithy-sh/email`, which had just been installed and was the package genuinely not linked. So the
295
+ * refusal named the one dependency that was definitely fine, and an adopter following it ran `bun
296
+ * install` against a tree that already had core in it. The importer is the half that would have pointed
297
+ * at `email`, and both runtimes hand it over: **Bun's `ResolveMessage` carries `referrer` as a field**
298
+ * (measured on 1.3.14) and Node states it in prose as `imported from <path>`.
299
+ *
300
+ * **A package name, or nothing.** The referrer is an absolute path — our frame, and the reason
301
+ * {@link unresolvedSpecifier} drops the prose around the specifier — so what travels is the one part of
302
+ * it that is not a path: the package directory it sits under. A referrer outside `node_modules` is the
303
+ * adopter's own file, and there is nothing to say about it that the refusal does not already say by
304
+ * naming the config, so it answers `undefined` rather than a path. The path itself is never returned
305
+ * from this module, which is what makes leaking it impossible rather than merely discouraged.
306
+ *
307
+ * The **last** `node_modules` in the path, because every store layout nests: pnpm's
308
+ * `node_modules/.pnpm/<id>/node_modules/<pkg>` and Bun's isolated linker's
309
+ * `node_modules/.bun/<pkg>@<version>/node_modules/<pkg>` (both measured) put the real owner last, and so
310
+ * does an ordinary nested install. The name is then checked against what npm allows, so a store's own
311
+ * bookkeeping directory (`.bun`, `.pnpm`, `hono@4.6.3`) can only ever answer nothing.
312
+ *
313
+ * Whether a surface names it, and what it advises when it does, is that surface's policy and stays there.
314
+ */
315
+ export function importingPackage(cause: unknown): string | undefined {
316
+ const referrer = importerPath(cause);
317
+ if (referrer === undefined) return undefined;
318
+ const segments = referrer.split(/[\\/]/);
319
+ const at = segments.lastIndexOf("node_modules");
320
+ const head = at === -1 ? undefined : segments[at + 1];
321
+ if (head === undefined) return undefined;
322
+ const name = head.startsWith("@") ? `${head}/${segments[at + 2] ?? ""}` : head;
323
+ return PACKAGE_NAME.test(name) ? name : undefined;
324
+ }
@@ -13,4 +13,4 @@
13
13
  export const PACKAGE_NAME = "@pithy-sh/core";
14
14
 
15
15
  /** This package's version, stamped from its own package.json at generation time. */
16
- export const PACKAGE_VERSION = "0.1.4";
16
+ export const PACKAGE_VERSION = "0.2.0";