@pithy-sh/core 0.7.2 → 0.7.4

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 (47) hide show
  1. package/dist/capability/capability.d.ts +22 -0
  2. package/dist/capability/capability.d.ts.map +1 -1
  3. package/dist/controlPlane/capability.d.ts.map +1 -1
  4. package/dist/controlPlane/capability.js +3 -1
  5. package/dist/controlPlane/discovery/adminRoute.d.ts +10 -0
  6. package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -1
  7. package/dist/controlPlane/discovery/adminRoute.js +10 -3
  8. package/dist/controlPlane/http/handlers.d.ts +1 -0
  9. package/dist/controlPlane/http/handlers.d.ts.map +1 -1
  10. package/dist/naming/domains.d.ts +43 -3
  11. package/dist/naming/domains.d.ts.map +1 -1
  12. package/dist/naming/domains.js +62 -4
  13. package/dist/naming/feature.d.ts +126 -11
  14. package/dist/naming/feature.d.ts.map +1 -1
  15. package/dist/naming/feature.js +184 -30
  16. package/dist/naming/limits.d.ts +23 -7
  17. package/dist/naming/limits.d.ts.map +1 -1
  18. package/dist/naming/limits.js +22 -9
  19. package/dist/naming/provisionScope.d.ts +25 -11
  20. package/dist/naming/provisionScope.d.ts.map +1 -1
  21. package/dist/naming/provisionScope.js +28 -11
  22. package/dist/seed/seed.d.ts +11 -7
  23. package/dist/seed/seed.d.ts.map +1 -1
  24. package/dist/version.generated.d.ts +1 -1
  25. package/dist/version.generated.js +1 -1
  26. package/dist/worker/identity.d.ts +9 -0
  27. package/dist/worker/identity.d.ts.map +1 -1
  28. package/dist/worker/identity.js +10 -1
  29. package/dist/workflow/host.d.ts +7 -0
  30. package/dist/workflow/host.d.ts.map +1 -1
  31. package/dist/workflow/host.js +5 -3
  32. package/dist/workflow/naming.d.ts +8 -0
  33. package/dist/workflow/naming.d.ts.map +1 -1
  34. package/dist/workflow/naming.js +15 -0
  35. package/package.json +1 -1
  36. package/src/capability/capability.ts +22 -0
  37. package/src/controlPlane/capability.ts +11 -1
  38. package/src/controlPlane/discovery/adminRoute.ts +23 -3
  39. package/src/naming/domains.ts +75 -5
  40. package/src/naming/feature.ts +274 -38
  41. package/src/naming/limits.ts +25 -8
  42. package/src/naming/provisionScope.ts +39 -14
  43. package/src/seed/seed.ts +11 -7
  44. package/src/version.generated.ts +1 -1
  45. package/src/worker/identity.ts +10 -0
  46. package/src/workflow/host.ts +15 -3
  47. package/src/workflow/naming.ts +25 -0
@@ -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.7.2";
6
+ export declare const PACKAGE_VERSION = "0.7.4";
7
7
  //# sourceMappingURL=version.generated.d.ts.map
@@ -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.7.2";
7
+ const PACKAGE_VERSION = "0.7.4";
8
8
  //#endregion
9
9
  export { PACKAGE_NAME, PACKAGE_VERSION };
@@ -24,6 +24,15 @@
24
24
  export declare const PROJECT_VAR = "PROJECT";
25
25
  /** The var naming the environment this deployment serves — `dev`, `staging`, or `prod`. */
26
26
  export declare const ENVIRONMENT_VAR = "ENVIRONMENT";
27
+ /**
28
+ * The var naming where a deployed Worker answers — `https://<host>`, no path, no trailing slash.
29
+ *
30
+ * Generated, never hand-set: `pithy worker sync` writes it from a declared domain, and `pithy provision
31
+ * --feature` writes a feature's from its `workers.dev` address (#643). It is the one statement of a feature's
32
+ * origin a running Worker can read, because the account's `workers.dev` subdomain is something only the
33
+ * Cloudflare API knows, and a Worker has no business calling it at module scope.
34
+ */
35
+ export declare const BASE_URL_VAR = "BASE_URL";
27
36
  /**
28
37
  * The var carrying a Worker's **own** dev origin, from `pithy dev` to whatever launches that Worker.
29
38
  *
@@ -1 +1 @@
1
- {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/worker/identity.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,qFAAqF;AACrF,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC,2FAA2F;AAC3F,eAAO,MAAM,eAAe,gBAAgB,CAAC;AAE7C;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,iBAAiB,wBAAwB,CAAC;AAEvD;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,WAAW,CAAC;AAEnC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB,wBAAwB,CAAC;AAE9D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,SAAS,CAAC;AAEnC,qGAAqG;AACrG,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wEAAwE;IACxE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,2EAA2E;IAC3E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;;;;;;;;OAQG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,qBAAqB;IACpC,yGAAyG;IACzG,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAW3D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,qBAAqB,CAazE;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAEzD"}
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/worker/identity.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,qFAAqF;AACrF,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC,2FAA2F;AAC3F,eAAO,MAAM,eAAe,gBAAgB,CAAC;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,aAAa,CAAC;AAEvC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,iBAAiB,wBAAwB,CAAC;AAEvD;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,WAAW,CAAC;AAEnC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB,wBAAwB,CAAC;AAE9D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,SAAS,CAAC;AAEnC,qGAAqG;AACrG,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wEAAwE;IACxE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,2EAA2E;IAC3E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;;;;;;;;OAQG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,qBAAqB;IACpC,yGAAyG;IACzG,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAW3D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,qBAAqB,CAazE;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAEzD"}
@@ -26,6 +26,15 @@ const PROJECT_VAR = "PROJECT";
26
26
  /** The var naming the environment this deployment serves — `dev`, `staging`, or `prod`. */
27
27
  const ENVIRONMENT_VAR = "ENVIRONMENT";
28
28
  /**
29
+ * The var naming where a deployed Worker answers — `https://<host>`, no path, no trailing slash.
30
+ *
31
+ * Generated, never hand-set: `pithy worker sync` writes it from a declared domain, and `pithy provision
32
+ * --feature` writes a feature's from its `workers.dev` address (#643). It is the one statement of a feature's
33
+ * origin a running Worker can read, because the account's `workers.dev` subdomain is something only the
34
+ * Cloudflare API knows, and a Worker has no business calling it at module scope.
35
+ */
36
+ const BASE_URL_VAR = "BASE_URL";
37
+ /**
29
38
  * The var carrying a Worker's **own** dev origin, from `pithy dev` to whatever launches that Worker.
30
39
  *
31
40
  * Not stamped into `wrangler.jsonc` like the three above, and that is the whole point of it. A dev port
@@ -175,4 +184,4 @@ function workerVersion(env) {
175
184
  return workerVersionMetadata(env).id;
176
185
  }
177
186
  //#endregion
178
- export { ENVIRONMENT_VAR, PROJECT_VAR, SELF_BINDING, VERSION_METADATA_BINDING, WORKER_ORIGIN_VAR, WORKER_VAR, workerIdentity, workerVersion, workerVersionMetadata };
187
+ export { BASE_URL_VAR, ENVIRONMENT_VAR, PROJECT_VAR, SELF_BINDING, VERSION_METADATA_BINDING, WORKER_ORIGIN_VAR, WORKER_VAR, workerIdentity, workerVersion, workerVersionMetadata };
@@ -1,5 +1,6 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
+ import type { FeatureIdentity } from "../naming/feature";
3
4
  import { type WorkflowHostNameParts } from "./naming";
4
5
  import type { WorkflowRegistry } from "./spec";
5
6
  /**
@@ -106,6 +107,12 @@ export interface WorkflowHostParams {
106
107
  capability: string;
107
108
  /** The target environment (`dev` | `staging` | `production`). */
108
109
  env: string;
110
+ /**
111
+ * The feature this host serves, when it serves one (#643). Its name then takes the feature's head —
112
+ * `<project>-f<issue>-<slug>-<capability>` — as the Workflows derived with the same parts do; `env` still
113
+ * says `feature`, and that is what the host's `ENVIRONMENT` var is stamped with.
114
+ */
115
+ feature?: FeatureIdentity;
109
116
  /** D1 binding name → database id. An unlisted binding keeps the template's id. */
110
117
  databaseIds?: Record<string, string>;
111
118
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/workflow/host.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/workflow/host.ts"],"names":[],"mappings":"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"}
@@ -31,7 +31,8 @@ function resolveWorkflowHost(template, params) {
31
31
  resolved.name = workflowHostName({
32
32
  project: params.project,
33
33
  capability: params.capability,
34
- env: params.env
34
+ env: params.env,
35
+ ...params.feature ? { feature: params.feature } : {}
35
36
  });
36
37
  if (resolved.d1_databases) resolved.d1_databases = resolved.d1_databases.map((entry) => ({
37
38
  ...entry,
@@ -91,7 +92,7 @@ function resolveWorkflowHost(template, params) {
91
92
  * `className` cannot be hosted: there would be no class for wrangler to instantiate.
92
93
  */
93
94
  function hostWorkflowsFor(registry, parts) {
94
- const { project, capability, env } = parts;
95
+ const { project, capability, env, feature } = parts;
95
96
  const owned = Object.values(registry).filter((entry) => entry.capability === capability);
96
97
  const workflows = [];
97
98
  const crons = [];
@@ -106,7 +107,8 @@ function hostWorkflowsFor(registry, parts) {
106
107
  project,
107
108
  capability,
108
109
  job: entry.job,
109
- env
110
+ env,
111
+ ...feature ? { feature } : {}
110
112
  }),
111
113
  class_name: entry.spec.className
112
114
  });
@@ -1,5 +1,6 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
+ import { type FeatureIdentity } from "../naming/feature";
3
4
  /**
4
5
  * Every name a durable job answers to, in one module. The CLI writes these into a host worker's
5
6
  * resolved `wrangler.jsonc`, the provisioner deploys under them, and the dispatcher resolves
@@ -43,6 +44,13 @@ export interface WorkflowHostNameParts {
43
44
  capability: string;
44
45
  /** The deployment environment (e.g. `staging`). */
45
46
  env: string;
47
+ /**
48
+ * **The feature this deployment is, when it is one (#643).** A feature's host Worker and Workflows take the
49
+ * feature's head — `<project>-f<issue>-<slug>-…` — not `<project>-feature-…`, which every open branch would
50
+ * share: Worker and Workflow names are account-wide, so a second branch's deploy would replace the first's.
51
+ * `env` stays `feature`, which is what the host's `ENVIRONMENT` var says; only the names change.
52
+ */
53
+ feature?: FeatureIdentity;
46
54
  }
47
55
  /** The identity of one durable job's Workflow in one environment. */
48
56
  export interface WorkflowScriptNameParts extends WorkflowHostNameParts {
@@ -1 +1 @@
1
- {"version":3,"file":"naming.d.ts","sourceRoot":"","sources":["../../src/workflow/naming.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,IAAsC,CAAC;AAE3E;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,IAAoC,CAAC;AAmEvE,oEAAoE;AACpE,MAAM,WAAW,qBAAqB;IACpC,gHAAgH;IAChH,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;CACb;AAED,qEAAqE;AACrE,MAAM,WAAW,uBAAwB,SAAQ,qBAAqB;IACpE,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAKzE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CAGrE;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAInE"}
1
+ {"version":3,"file":"naming.d.ts","sourceRoot":"","sources":["../../src/workflow/naming.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,eAAe,EAA0C,MAAM,mBAAmB,CAAC;AAKjG;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,IAAsC,CAAC;AAE3E;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,IAAoC,CAAC;AAkFvE,oEAAoE;AACpE,MAAM,WAAW,qBAAqB;IACpC,gHAAgH;IAChH,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED,qEAAqE;AACrE,MAAM,WAAW,uBAAwB,SAAQ,qBAAqB;IACpE,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAMzE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CAIrE;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAInE"}
@@ -4,6 +4,7 @@ import { InternalError } from "../error/pithyError.js";
4
4
  import { NAME_SEGMENT } from "../naming/segment.js";
5
5
  import { NAMESPACE_LIMITS } from "../naming/limits.js";
6
6
  import { assertValidProjectName, kebab } from "../naming/resource.js";
7
+ import { featureWorkerName, featureWorkflowName } from "../naming/feature.js";
7
8
  //#region src/workflow/naming.ts
8
9
  /**
9
10
  * Every name a durable job answers to, in one module. The CLI writes these into a host worker's
@@ -96,6 +97,18 @@ function head(project, env) {
96
97
  return `${kebab(project)}-${env}`;
97
98
  }
98
99
  /**
100
+ * The feature a name is composed for, held to the project the parts name — two projects in one set of parts is
101
+ * a name composed for somebody else's feature.
102
+ */
103
+ function featureOf(parts) {
104
+ const feature = parts.feature;
105
+ if (feature.project !== parts.project) throw new InternalError({
106
+ message: "A feature's Workflow name was asked for with two different projects.",
107
+ detail: `parts name project "${parts.project}" and a feature of project "${feature.project}".`
108
+ });
109
+ return feature;
110
+ }
111
+ /**
99
112
  * The deployed name of one job's Workflow in one environment — `<project>-<env>-<capability>-<job>`,
100
113
  * e.g. `acme-staging-media-image-to-text`. This is what the CLI writes as the `name` of a
101
114
  * `workflows` entry, and what `CloudflareWorkflowsClient` addresses when triggering from outside
@@ -109,6 +122,7 @@ function workflowScriptName(parts) {
109
122
  assertSegment(parts.capability, "capability");
110
123
  assertSegment(parts.job, "job");
111
124
  assertCapabilityJob(parts.capability, parts.job);
125
+ if (parts.feature) return featureWorkflowName(featureOf(parts), parts.capability, parts.job);
112
126
  return assertLength(`${head(parts.project, parts.env)}-${parts.capability}-${parts.job}`, MAX_WORKFLOW_NAME_BYTES);
113
127
  }
114
128
  /**
@@ -120,6 +134,7 @@ function workflowScriptName(parts) {
120
134
  */
121
135
  function workflowHostName(parts) {
122
136
  assertSegment(parts.capability, "capability");
137
+ if (parts.feature) return featureWorkerName(featureOf(parts), parts.capability);
123
138
  return assertLength(`${head(parts.project, parts.env)}-${parts.capability}`, MAX_WORKER_NAME_BYTES);
124
139
  }
125
140
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pithy-sh/core",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -483,6 +483,28 @@ export interface Capability<
483
483
  * question.
484
484
  */
485
485
  version?: string | null;
486
+ /**
487
+ * The npm **package** that supplies this capability, or `null` where there is none.
488
+ *
489
+ * The join key {@link Capability.version} is useless without. A version joins against a release feed
490
+ * by package name, and this manifest reported the version alone until #626 — so a client had to guess
491
+ * `@pithy-sh/${name}`, which is right for most capabilities and wrong for the one that matters:
492
+ * `controlplane` ships inside `@pithy-sh/core`, so the guess reaches a package that has never been
493
+ * published and the join comes back empty, which is indistinguishable from "up to date".
494
+ *
495
+ * **A capability name and a package name are different kinds of thing.** One is what a composition
496
+ * calls a concept; the other is how that concept is distributed. A package may ship more than one
497
+ * capability — `core` already ships `controlplane` — so no convention makes the second derivable from
498
+ * the first, and the relationship is a fact only the producing package knows. Which is why **the
499
+ * framework never fills this in from `name`**: deriving it here would re-create the bug inside the fix.
500
+ *
501
+ * Every `@pithy-sh/*` capability sets it from its generated `PACKAGE_NAME` constant
502
+ * (`scripts/stampVersions.ts`), beside the version off the same stamp. **Nullable for the same reason
503
+ * `version` is**, and null in the same breath: the adopter's own `app` capability has a name, no
504
+ * package and no version. Neither alone is a state worth having — a package with no version says
505
+ * nothing joinable, and a version with no package is the defect this field closes.
506
+ */
507
+ package?: string | null;
486
508
  /**
487
509
  * Optional startup hook, called once when {@link createBackend} assembles the backend, with every
488
510
  * composed capability. Runs after binding and `dependsOn` validation, before middleware and routes
@@ -7,7 +7,7 @@ import type { BindingSpecInput } from "../capability/bindings";
7
7
  import { type Capability, defineCapability, type PithyHonoEnv } from "../capability/capability";
8
8
  import type { DatabaseSpecMap } from "../data/databases";
9
9
  import type { KvNamespaceSpecMap, KvRegistry } from "../kv/namespaces";
10
- import { PACKAGE_VERSION } from "../version.generated";
10
+ import { PACKAGE_NAME, PACKAGE_VERSION } from "../version.generated";
11
11
  import { ENVIRONMENT_VAR } from "../worker/identity";
12
12
  import { ControlPlaneConfig, type ControlPlaneConfigInput } from "./config/config";
13
13
  import { ControlPlaneConnection } from "./data/connection";
@@ -138,6 +138,12 @@ export function controlplane(options: ControlPlaneOptions = {}): ControlPlaneCap
138
138
  // package.json. The seam reports itself in the manifest beside every other composed capability, so a
139
139
  // client can see which core an adopter is on without a separate question.
140
140
  version: PACKAGE_VERSION,
141
+ // And the package that version belongs to — **the case no naming convention survives** (#626). This
142
+ // capability is called `controlplane` and ships inside `@pithy-sh/core`, so a client deriving
143
+ // `@pithy-sh/${name}` asks a release feed about a package that has never been published and is told
144
+ // nothing, which reads as up to date. Stamped from this package's own `package.json`, like the
145
+ // version above, because only the producing package knows which one it is.
146
+ package: PACKAGE_NAME,
141
147
  // No `dependsOn`. The seam must work in a Worker composing neither auth nor audit nor secrets: it
142
148
  // holds no secret, mints no session, and emits through a seam that is a no-op when absent.
143
149
  requiredBindings,
@@ -166,6 +172,10 @@ export function controlplane(options: ControlPlaneOptions = {}): ControlPlaneCap
166
172
  // `?? null`, never undefined: the adopter's own `app` capability has a name and no package
167
173
  // version, and that must render as an explicit null rather than vanish from the JSON.
168
174
  version: cap.version ?? null,
175
+ // The join key that version is worth nothing without (#626), **read off the capability and never
176
+ // derived from its name here**. `@pithy-sh/${cap.name}` would be right for most of them and
177
+ // wrong for the seam writing this line — which is the whole defect, rebuilt inside its fix.
178
+ package: cap.package ?? null,
169
179
  adminRoutes: [...(cap.adminRoutes ?? [])],
170
180
  // The vocabulary, always — a capability declaring a number this connection cannot see must be
171
181
  // visible as a withheld number rather than as silence.
@@ -71,7 +71,17 @@ export const CapabilityDeclaration = z
71
71
  .string()
72
72
  .nullable()
73
73
  .describe(
74
- "The npm version of the package supplying this capability, or null where there is none — the adopter's own `app` capability has a name and no package. Reported per capability and never aggregated: the package name is the join key against a release feed, and a project composes some capabilities and not others, so only the intersection of what it composes and what changed is worth reporting.",
74
+ "The npm version of the package supplying this capability, or null where there is none — the adopter's own `app` capability has a name and no package. Reported per capability and never aggregated: a project composes some capabilities and not others, so only the intersection of what it composes and what changed is worth reporting. Joined against a release feed on the `package` beside it, never on `name` — the two are different kinds of thing.",
75
+ ),
76
+ // **Defaulted, like every field added after the first manifest shipped**, and for the reason written
77
+ // out below against `healthKeys`: a Worker deployed before #626 sends no package, and a required
78
+ // field would cost that client the whole manifest rather than the one key it did not get.
79
+ package: z
80
+ .string()
81
+ .nullable()
82
+ .default(null)
83
+ .describe(
84
+ "The npm package supplying this capability — the join key against a release feed — or null where there is none, which is the adopter's own `app` capability and a Worker that predates this field. **Never derivable from `name`:** `controlplane` ships inside `@pithy-sh/core`, so a name-derived `@pithy-sh/<name>` reaches a package that has never been published and the join comes back empty, which reads exactly like up to date. Null together with `version`: a package with no version says nothing joinable, and a version with no package is the reason this field exists.",
75
85
  ),
76
86
  adminRoutes: z
77
87
  .array(AdminRoute)
@@ -103,7 +113,7 @@ export const CapabilityDeclaration = z
103
113
  ),
104
114
  })
105
115
  .describe(
106
- "One capability this Worker composes, the version it is at, the admin surface it exposes, the summary it may report, and the configured facts a client must respect to call any of it.",
116
+ "One capability this Worker composes, the package and version it is at, the admin surface it exposes, the summary it may report, and the configured facts a client must respect to call any of it.",
107
117
  );
108
118
  export type CapabilityDeclaration = z.infer<typeof CapabilityDeclaration>;
109
119
 
@@ -163,8 +173,13 @@ export const CapabilityDescriptor = z
163
173
  // Both fields, from the one value, in the one place. A handler cannot write the numbers and forget
164
174
  // the flag, which is what makes `encode(decode(entry))` the entry again — the property the seam's
165
175
  // response contract test rests on.
166
- encode: ({ health, healthKeys, configKeys, config, ...declaration }) => ({
176
+ encode: ({ health, healthKeys, configKeys, config, package: suppliedBy, ...declaration }) => ({
167
177
  ...declaration,
178
+ // The package, explicitly, for the same reason the two below are explicit: it is defaulted, so it
179
+ // is optional on the way in and required on the way out, and absence and null mean the one thing —
180
+ // this capability comes from no npm package, which is the adopter's own app capability. Renamed on
181
+ // the way past because `package` is a reserved word and cannot be a binding name.
182
+ package: suppliedBy ?? null,
168
183
  // Every defaulted field is optional on the way back out, and empty is the meaning absence already
169
184
  // had: a capability that declares no summary, and one that states no configured fact.
170
185
  //
@@ -198,6 +213,11 @@ export type CapabilityDescriptor = z.infer<typeof CapabilityDescriptor>;
198
213
  * now — so a capability that changed its paths or its scopes reports the change directly, and a schema
199
214
  * version would be a second source of truth to keep in sync with the first. That reasoning is unchanged.
200
215
  *
216
+ * **And `capabilities[].version` travels with the package it belongs to** (#626). A version is only
217
+ * actionable joined against a release feed, a feed is keyed by package name, and a capability name is
218
+ * not one — `controlplane` ships inside `@pithy-sh/core`. Reporting the version alone left every client
219
+ * guessing the key, and the guess failed silently on the most frequently released package there is.
220
+ *
201
221
  * What the manifest does carry is **identity**, not schema, and it carries two of them because they
202
222
  * answer questions neither can answer alone. `version` is Cloudflare's opaque per-deploy id: it says
203
223
  * *exactly which build* is running, which is what forensics needs, what reproduces a report, and what
@@ -2,7 +2,9 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
4
  import { z } from "zod";
5
- import { ENVIRONMENTS } from "./environment";
5
+ import { type AmbientEnv, ambientEnv } from "../env/ambient";
6
+ import { BASE_URL_VAR } from "../worker/identity";
7
+ import { ENVIRONMENTS, FEATURE_ENVIRONMENT } from "./environment";
6
8
 
7
9
  /**
8
10
  * Where a Worker answers, declared once per Worker per environment.
@@ -39,6 +41,18 @@ import { ENVIRONMENTS } from "./environment";
39
41
  /** A hostname a Worker answers on. No scheme, no path, no port — wrangler's `routes` pattern is a host. */
40
42
  const HOSTNAME_PATTERN = /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/;
41
43
 
44
+ /**
45
+ * Whether a string is a public hostname: dot-separated DNS labels, at least two, lowercase. No scheme, no
46
+ * port, no trailing dot, and nothing a URL parser decoded into one — `%2e%2e` is `..` once parsed, which is an
47
+ * empty label rather than a host (#643). `localhost` is not one: it has a single label, and it is this machine.
48
+ *
49
+ * The one test for "is this a host" that an origin read from a stamp and an origin typed on a command line
50
+ * both pass, so the two cannot accept different things.
51
+ */
52
+ export function isPublicHostname(hostname: string): boolean {
53
+ return hostname.length <= 253 && HOSTNAME_PATTERN.test(hostname);
54
+ }
55
+
42
56
  /** The environments a domain may be declared for — every managed one, never `dev`. */
43
57
  export const DOMAIN_ENVIRONMENTS = ENVIRONMENTS.filter((environment) => environment !== "dev");
44
58
 
@@ -135,10 +149,46 @@ export interface ResolvedOrigin {
135
149
  origin: string;
136
150
  /** The hostname alone: a route pattern, and what Turnstile binds a widget to. */
137
151
  hostname: string;
138
- /** Whether `domains` declared it. `false` means {@link LOCAL_ORIGIN} was substituted. */
152
+ /**
153
+ * Whether `domains` declared it. `false` means {@link LOCAL_ORIGIN} was substituted, or — for a feature
154
+ * environment only — that the address is the `workers.dev` one provisioning stamped ({@link featureOrigin}).
155
+ */
139
156
  declared: boolean;
140
157
  }
141
158
 
159
+ /**
160
+ * **A feature environment's origin: the `workers.dev` address provisioning stamped, or null (#643).**
161
+ *
162
+ * A feature Worker answers on `https://<script>.<account subdomain>.workers.dev`. Nothing can declare that —
163
+ * `domains` has no feature key, because a branch's hostname is composed from the branch — and the Worker cannot
164
+ * derive it either, because the subdomain is the account's and only the Cloudflare API knows it. So `pithy
165
+ * provision --feature` derives it, through the CLI's one address resolver, and stamps it as the feature
166
+ * stanza's {@link BASE_URL_VAR}. This is the one reader of that stamp.
167
+ *
168
+ * **Only a `workers.dev` https origin is accepted, and that is the guard rather than a tidiness check.** A
169
+ * feature stanza is generated from the Worker's top level, so a `BASE_URL` somebody set by hand up there
170
+ * arrives in the feature too — and it is another environment's origin, most likely production's. Honoring it
171
+ * would send a branch's magic links into production, which is the one thing {@link resolveOrigin} exists to
172
+ * make impossible. A feature's address is `workers.dev` by construction, so anything else is not one and
173
+ * resolves to nothing. No path, no port, no credentials: an origin, or null.
174
+ */
175
+ export function featureOrigin(stamped: string | undefined): { origin: string; hostname: string } | null {
176
+ if (!stamped) return null;
177
+ let url: URL;
178
+ try {
179
+ url = new URL(stamped);
180
+ } catch {
181
+ return null;
182
+ }
183
+ if (url.protocol !== "https:" || url.port !== "" || url.username !== "" || url.password !== "") return null;
184
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") return null;
185
+ // A script name and an account subdomain, at least: `workers.dev` alone, or `<sub>.workers.dev`, is not a Worker.
186
+ if (!url.hostname.endsWith(".workers.dev") || url.hostname.split(".").length < 4) return null;
187
+ // Every label a DNS label: the parser decodes `%2e%2e` into `..`, which ends in `.workers.dev` and is no host.
188
+ if (!isPublicHostname(url.hostname)) return null;
189
+ return { origin: url.origin, hostname: url.hostname };
190
+ }
191
+
142
192
  /**
143
193
  * **Where is this Worker publicly reachable, in this environment?** — asked once, answered once.
144
194
  *
@@ -164,10 +214,21 @@ export interface ResolvedOrigin {
164
214
  * checks that stored value, so a per-environment issuer would make a connection minted in staging
165
215
  * unverifiable in production. That may be the better isolation, but it is a decision about trust rather
166
216
  * than about reachability, and a helper whose job is "where am I reachable" must not sweep it up.
217
+ *
218
+ * **`stamped` is a feature environment's `BASE_URL`, and is read for nothing else** — see {@link featureOrigin}.
167
219
  */
168
- export function resolveOrigin(environment: string | undefined, domains: WorkerDomains | undefined): ResolvedOrigin {
220
+ export function resolveOrigin(
221
+ environment: string | undefined,
222
+ domains: WorkerDomains | undefined,
223
+ stamped?: string | undefined,
224
+ ): ResolvedOrigin {
169
225
  const domain = domainFor(domains, environment);
170
226
  if (domain) return { origin: baseUrlFor(domain), hostname: domain.pattern, declared: true };
227
+ // A feature environment, and only a feature environment, reads the address provisioning stamped. Every other
228
+ // environment ignores `stamped` outright: a declared one without a domain is unpublished, and `dev` resolves
229
+ // its address from the request.
230
+ const feature = environment === FEATURE_ENVIRONMENT ? featureOrigin(stamped) : null;
231
+ if (feature) return { ...feature, declared: false };
171
232
  return { origin: LOCAL_ORIGIN, hostname: "localhost", declared: false };
172
233
  }
173
234
 
@@ -188,7 +249,16 @@ export function resolveOrigin(environment: string | undefined, domains: WorkerDo
188
249
  * `AUTH_BASE_URL`, which is part of why `email` and `payments` were missed for days: the constant read
189
250
  * as auth's private business when it is the Worker's address, and every capability that needs an origin
190
251
  * needs this one.
252
+ *
253
+ * **A feature deployment is answered from its own vars (#643).** `env` is the ambient environment — inside a
254
+ * Worker, its own vars — and it is read for one thing: the `BASE_URL` a feature's provisioning stamped. So the
255
+ * same scaffolded line gives auth's base URL, email's links and payments' return URLs a feature's real
256
+ * `workers.dev` origin, rather than `http://localhost`, with nothing added to the adopter's config.
191
257
  */
192
- export function originFor(environment: string | undefined, domains: WorkerDomains | undefined): string {
193
- return resolveOrigin(environment, domains).origin;
258
+ export function originFor(
259
+ environment: string | undefined,
260
+ domains: WorkerDomains | undefined,
261
+ env: AmbientEnv = ambientEnv(),
262
+ ): string {
263
+ return resolveOrigin(environment, domains, env[BASE_URL_VAR]).origin;
194
264
  }