@pithy-sh/core 0.7.3 → 0.8.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 (39) hide show
  1. package/dist/naming/domains.d.ts +43 -3
  2. package/dist/naming/domains.d.ts.map +1 -1
  3. package/dist/naming/domains.js +62 -4
  4. package/dist/naming/feature.d.ts +126 -11
  5. package/dist/naming/feature.d.ts.map +1 -1
  6. package/dist/naming/feature.js +184 -30
  7. package/dist/naming/limits.d.ts +23 -7
  8. package/dist/naming/limits.d.ts.map +1 -1
  9. package/dist/naming/limits.js +22 -9
  10. package/dist/naming/provisionScope.d.ts +25 -11
  11. package/dist/naming/provisionScope.d.ts.map +1 -1
  12. package/dist/naming/provisionScope.js +28 -11
  13. package/dist/seed/seed.d.ts +36 -7
  14. package/dist/seed/seed.d.ts.map +1 -1
  15. package/dist/version.generated.d.ts +1 -1
  16. package/dist/version.generated.js +1 -1
  17. package/dist/worker/identity.d.ts +9 -0
  18. package/dist/worker/identity.d.ts.map +1 -1
  19. package/dist/worker/identity.js +10 -1
  20. package/dist/workflow/faults.d.ts +34 -2
  21. package/dist/workflow/faults.d.ts.map +1 -1
  22. package/dist/workflow/faults.js +42 -18
  23. package/dist/workflow/host.d.ts +7 -0
  24. package/dist/workflow/host.d.ts.map +1 -1
  25. package/dist/workflow/host.js +5 -3
  26. package/dist/workflow/naming.d.ts +8 -0
  27. package/dist/workflow/naming.d.ts.map +1 -1
  28. package/dist/workflow/naming.js +15 -0
  29. package/package.json +1 -1
  30. package/src/naming/domains.ts +75 -5
  31. package/src/naming/feature.ts +274 -38
  32. package/src/naming/limits.ts +25 -8
  33. package/src/naming/provisionScope.ts +39 -14
  34. package/src/seed/seed.ts +36 -7
  35. package/src/version.generated.ts +1 -1
  36. package/src/worker/identity.ts +10 -0
  37. package/src/workflow/faults.ts +61 -4
  38. package/src/workflow/host.ts +15 -3
  39. package/src/workflow/naming.ts +25 -0
@@ -1,6 +1,16 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { z } from "zod";
4
+ import { type AmbientEnv } from "../env/ambient";
5
+ /**
6
+ * Whether a string is a public hostname: dot-separated DNS labels, at least two, lowercase. No scheme, no
7
+ * port, no trailing dot, and nothing a URL parser decoded into one — `%2e%2e` is `..` once parsed, which is an
8
+ * empty label rather than a host (#643). `localhost` is not one: it has a single label, and it is this machine.
9
+ *
10
+ * The one test for "is this a host" that an origin read from a stamp and an origin typed on a command line
11
+ * both pass, so the two cannot accept different things.
12
+ */
13
+ export declare function isPublicHostname(hostname: string): boolean;
4
14
  /** The environments a domain may be declared for — every managed one, never `dev`. */
5
15
  export declare const DOMAIN_ENVIRONMENTS: ("prod" | "staging")[];
6
16
  /** One environment's public address for one Worker. */
@@ -58,9 +68,32 @@ export interface ResolvedOrigin {
58
68
  origin: string;
59
69
  /** The hostname alone: a route pattern, and what Turnstile binds a widget to. */
60
70
  hostname: string;
61
- /** Whether `domains` declared it. `false` means {@link LOCAL_ORIGIN} was substituted. */
71
+ /**
72
+ * Whether `domains` declared it. `false` means {@link LOCAL_ORIGIN} was substituted, or — for a feature
73
+ * environment only — that the address is the `workers.dev` one provisioning stamped ({@link featureOrigin}).
74
+ */
62
75
  declared: boolean;
63
76
  }
77
+ /**
78
+ * **A feature environment's origin: the `workers.dev` address provisioning stamped, or null (#643).**
79
+ *
80
+ * A feature Worker answers on `https://<script>.<account subdomain>.workers.dev`. Nothing can declare that —
81
+ * `domains` has no feature key, because a branch's hostname is composed from the branch — and the Worker cannot
82
+ * derive it either, because the subdomain is the account's and only the Cloudflare API knows it. So `pithy
83
+ * provision --feature` derives it, through the CLI's one address resolver, and stamps it as the feature
84
+ * stanza's {@link BASE_URL_VAR}. This is the one reader of that stamp.
85
+ *
86
+ * **Only a `workers.dev` https origin is accepted, and that is the guard rather than a tidiness check.** A
87
+ * feature stanza is generated from the Worker's top level, so a `BASE_URL` somebody set by hand up there
88
+ * arrives in the feature too — and it is another environment's origin, most likely production's. Honoring it
89
+ * would send a branch's magic links into production, which is the one thing {@link resolveOrigin} exists to
90
+ * make impossible. A feature's address is `workers.dev` by construction, so anything else is not one and
91
+ * resolves to nothing. No path, no port, no credentials: an origin, or null.
92
+ */
93
+ export declare function featureOrigin(stamped: string | undefined): {
94
+ origin: string;
95
+ hostname: string;
96
+ } | null;
64
97
  /**
65
98
  * **Where is this Worker publicly reachable, in this environment?** — asked once, answered once.
66
99
  *
@@ -86,8 +119,10 @@ export interface ResolvedOrigin {
86
119
  * checks that stored value, so a per-environment issuer would make a connection minted in staging
87
120
  * unverifiable in production. That may be the better isolation, but it is a decision about trust rather
88
121
  * than about reachability, and a helper whose job is "where am I reachable" must not sweep it up.
122
+ *
123
+ * **`stamped` is a feature environment's `BASE_URL`, and is read for nothing else** — see {@link featureOrigin}.
89
124
  */
90
- export declare function resolveOrigin(environment: string | undefined, domains: WorkerDomains | undefined): ResolvedOrigin;
125
+ export declare function resolveOrigin(environment: string | undefined, domains: WorkerDomains | undefined, stamped?: string | undefined): ResolvedOrigin;
91
126
  /**
92
127
  * The one line an adopter writes: this Worker's public origin for the environment it is running in.
93
128
  *
@@ -105,6 +140,11 @@ export declare function resolveOrigin(environment: string | undefined, domains:
105
140
  * `AUTH_BASE_URL`, which is part of why `email` and `payments` were missed for days: the constant read
106
141
  * as auth's private business when it is the Worker's address, and every capability that needs an origin
107
142
  * needs this one.
143
+ *
144
+ * **A feature deployment is answered from its own vars (#643).** `env` is the ambient environment — inside a
145
+ * Worker, its own vars — and it is read for one thing: the `BASE_URL` a feature's provisioning stamped. So the
146
+ * same scaffolded line gives auth's base URL, email's links and payments' return URLs a feature's real
147
+ * `workers.dev` origin, rather than `http://localhost`, with nothing added to the adopter's config.
108
148
  */
109
- export declare function originFor(environment: string | undefined, domains: WorkerDomains | undefined): string;
149
+ export declare function originFor(environment: string | undefined, domains: WorkerDomains | undefined, env?: AmbientEnv): string;
110
150
  //# sourceMappingURL=domains.d.ts.map
@@ -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;AAsCxB,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,yFAAyF;IACzF,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,aAAa,GAAG,SAAS,GAAG,cAAc,CAIjH;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,CAErG"}
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;AAuC7D;;;;;;;GAOG;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,6 +1,8 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { ENVIRONMENTS } from "./environment.js";
4
+ import { BASE_URL_VAR } from "../worker/identity.js";
5
+ import { ambientEnv } from "../env/ambient.js";
4
6
  import { z } from "zod";
5
7
  //#region src/naming/domains.ts
6
8
  /**
@@ -36,6 +38,17 @@ import { z } from "zod";
36
38
  */
37
39
  /** A hostname a Worker answers on. No scheme, no path, no port — wrangler's `routes` pattern is a host. */
38
40
  const HOSTNAME_PATTERN = /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/;
41
+ /**
42
+ * Whether a string is a public hostname: dot-separated DNS labels, at least two, lowercase. No scheme, no
43
+ * port, no trailing dot, and nothing a URL parser decoded into one — `%2e%2e` is `..` once parsed, which is an
44
+ * empty label rather than a host (#643). `localhost` is not one: it has a single label, and it is this machine.
45
+ *
46
+ * The one test for "is this a host" that an origin read from a stamp and an origin typed on a command line
47
+ * both pass, so the two cannot accept different things.
48
+ */
49
+ function isPublicHostname(hostname) {
50
+ return hostname.length <= 253 && HOSTNAME_PATTERN.test(hostname);
51
+ }
39
52
  /** The environments a domain may be declared for — every managed one, never `dev`. */
40
53
  const DOMAIN_ENVIRONMENTS = ENVIRONMENTS.filter((environment) => environment !== "dev");
41
54
  /** One environment's public address for one Worker. */
@@ -95,6 +108,39 @@ function baseUrlFor(domain) {
95
108
  */
96
109
  const LOCAL_ORIGIN = "http://localhost";
97
110
  /**
111
+ * **A feature environment's origin: the `workers.dev` address provisioning stamped, or null (#643).**
112
+ *
113
+ * A feature Worker answers on `https://<script>.<account subdomain>.workers.dev`. Nothing can declare that —
114
+ * `domains` has no feature key, because a branch's hostname is composed from the branch — and the Worker cannot
115
+ * derive it either, because the subdomain is the account's and only the Cloudflare API knows it. So `pithy
116
+ * provision --feature` derives it, through the CLI's one address resolver, and stamps it as the feature
117
+ * stanza's {@link BASE_URL_VAR}. This is the one reader of that stamp.
118
+ *
119
+ * **Only a `workers.dev` https origin is accepted, and that is the guard rather than a tidiness check.** A
120
+ * feature stanza is generated from the Worker's top level, so a `BASE_URL` somebody set by hand up there
121
+ * arrives in the feature too — and it is another environment's origin, most likely production's. Honoring it
122
+ * would send a branch's magic links into production, which is the one thing {@link resolveOrigin} exists to
123
+ * make impossible. A feature's address is `workers.dev` by construction, so anything else is not one and
124
+ * resolves to nothing. No path, no port, no credentials: an origin, or null.
125
+ */
126
+ function featureOrigin(stamped) {
127
+ if (!stamped) return null;
128
+ let url;
129
+ try {
130
+ url = new URL(stamped);
131
+ } catch {
132
+ return null;
133
+ }
134
+ if (url.protocol !== "https:" || url.port !== "" || url.username !== "" || url.password !== "") return null;
135
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") return null;
136
+ if (!url.hostname.endsWith(".workers.dev") || url.hostname.split(".").length < 4) return null;
137
+ if (!isPublicHostname(url.hostname)) return null;
138
+ return {
139
+ origin: url.origin,
140
+ hostname: url.hostname
141
+ };
142
+ }
143
+ /**
98
144
  * **Where is this Worker publicly reachable, in this environment?** — asked once, answered once.
99
145
  *
100
146
  * The two halves already existed ({@link domainFor} and {@link baseUrlFor}) and nothing composed them,
@@ -119,14 +165,21 @@ const LOCAL_ORIGIN = "http://localhost";
119
165
  * checks that stored value, so a per-environment issuer would make a connection minted in staging
120
166
  * unverifiable in production. That may be the better isolation, but it is a decision about trust rather
121
167
  * than about reachability, and a helper whose job is "where am I reachable" must not sweep it up.
168
+ *
169
+ * **`stamped` is a feature environment's `BASE_URL`, and is read for nothing else** — see {@link featureOrigin}.
122
170
  */
123
- function resolveOrigin(environment, domains) {
171
+ function resolveOrigin(environment, domains, stamped) {
124
172
  const domain = domainFor(domains, environment);
125
173
  if (domain) return {
126
174
  origin: baseUrlFor(domain),
127
175
  hostname: domain.pattern,
128
176
  declared: true
129
177
  };
178
+ const feature = environment === "feature" ? featureOrigin(stamped) : null;
179
+ if (feature) return {
180
+ ...feature,
181
+ declared: false
182
+ };
130
183
  return {
131
184
  origin: LOCAL_ORIGIN,
132
185
  hostname: "localhost",
@@ -150,9 +203,14 @@ function resolveOrigin(environment, domains) {
150
203
  * `AUTH_BASE_URL`, which is part of why `email` and `payments` were missed for days: the constant read
151
204
  * as auth's private business when it is the Worker's address, and every capability that needs an origin
152
205
  * needs this one.
206
+ *
207
+ * **A feature deployment is answered from its own vars (#643).** `env` is the ambient environment — inside a
208
+ * Worker, its own vars — and it is read for one thing: the `BASE_URL` a feature's provisioning stamped. So the
209
+ * same scaffolded line gives auth's base URL, email's links and payments' return URLs a feature's real
210
+ * `workers.dev` origin, rather than `http://localhost`, with nothing added to the adopter's config.
153
211
  */
154
- function originFor(environment, domains) {
155
- return resolveOrigin(environment, domains).origin;
212
+ function originFor(environment, domains, env = ambientEnv()) {
213
+ return resolveOrigin(environment, domains, env[BASE_URL_VAR]).origin;
156
214
  }
157
215
  //#endregion
158
- export { DOMAIN_ENVIRONMENTS, LOCAL_ORIGIN, WorkerDomain, WorkerDomains, baseUrlFor, domainFor, originFor, resolveOrigin };
216
+ export { DOMAIN_ENVIRONMENTS, LOCAL_ORIGIN, WorkerDomain, WorkerDomains, baseUrlFor, domainFor, featureOrigin, isPublicHostname, originFor, resolveOrigin };
@@ -1,8 +1,15 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
3
  /**
4
- * The names a feature environment provisions under: `<project>-f<issue>-<slug>-<binding>-<kind>` for a
5
- * resource, `<project>-f<issue>-<slug>-<worker>` for a Worker script.
4
+ * The names a feature environment provisions under: `<project>-f<issue>-<slug>--<binding>-<kind>` for a
5
+ * resource, `<project>-f<issue>-<slug>--<worker>` for a Worker script.
6
+ *
7
+ * **Two hyphens after the slug, and a project with no `f<digits>` segment (#643).** Together they make every
8
+ * feature name parse back to exactly one (project, issue, slug): the double hyphen is something no other name
9
+ * Pithy composes can hold ({@link FEATURE_TAIL_SEPARATOR}), and the first `f<digits>` segment is the issue's,
10
+ * because the project may carry none ({@link assertFeatureProject}). Before them, project `acme-f12-x`'s `prod`
11
+ * and project `acme`'s branch `feature/12-x-prod` composed every name between them, and a branch whose slug
12
+ * was a hyphen-prefix of a sibling's could compose the sibling's database. See {@link parseFeatureName}.
6
13
  *
7
14
  * **No environment segment, and no Worker segment.** A feature *is* an environment, and two Workers that
8
15
  * declare the same binding are meant to share one resource — so the binding name, not the topology, is
@@ -10,9 +17,12 @@
10
17
  *
11
18
  * Every name here is **derived and recomputed**, never stored: `provision` and `destroy` both compute the
12
19
  * same string from `(identity, binding, kind)`, which is what lets teardown reconcile exactly rather than
13
- * scan a prefix a hyphenated sibling slug could ambiguously match. That is also why these names
14
- * **truncate** where a capability's Workflow refuses: a feature name addresses nothing that outlives the
15
- * feature, and failing a CI run because a branch slug was long would be the worse failure.
20
+ * scan a prefix a hyphenated sibling slug could ambiguously match.
21
+ *
22
+ * **Never truncated (#643).** These names used to fit a long slug with a hash, on the reasoning that a feature
23
+ * name addresses nothing that outlives the feature. But a short hash is a slug some sibling branch can have
24
+ * whole, so a fitted name was a name two features could compose. A slug that does not fit is refused instead,
25
+ * naming the maximum ({@link assertFeatureSlugFits}), and every feature name is exactly its feature's.
16
26
  *
17
27
  * It lives in `core`, beside the rest of the naming rule, so the CLI and the facade compose one
18
28
  * implementation rather than two that drift.
@@ -31,7 +41,92 @@ export interface FeatureIdentity {
31
41
  slug: string;
32
42
  }
33
43
  /**
34
- * The full Cloudflare resource name for a feature's binding — `<project>-f<issue>-<slug>-<binding>-<kind>`.
44
+ * **The issue as a name carries it: its digits, without leading zeros (#643).** `feature/0643-foo` and
45
+ * `feature/643-foo` are one issue, so they are one feature and compose one set of names. Read as strings they
46
+ * were two features, and their rate-limit namespaces collided every time. `0` stays `0`.
47
+ */
48
+ export declare function canonicalIssue(issue: string): string;
49
+ /**
50
+ * The `f<digits>` segment a project name carries, or `null` — the shape a feature's issue takes (#643).
51
+ *
52
+ * Read after kebabbing, because the kebab is what reaches an account.
53
+ */
54
+ export declare function featureMarkerInProjectName(project: string): string | null;
55
+ /**
56
+ * **Refuse features for a project whose name carries an `f<digits>` segment (#643).**
57
+ *
58
+ * A feature name is read left to right: the project, then the first `f<digits>` segment, which is the issue.
59
+ * A project named `acme-f12-x` puts that segment inside itself, and then its feature 3 on `y` and project `acme`'s
60
+ * feature 12 on `x-f3-y` are one string. Environment names already obey this rule ({@link isFeatureMarker}), so
61
+ * the project obeys it too, where the two shapes meet.
62
+ *
63
+ * **Refused here, not at `requireProjectName`.** Such a project's staging and prod are untouched: no name they
64
+ * compose holds {@link FEATURE_TAIL_SEPARATOR}, so no feature anywhere can compose one of theirs. Refusing the
65
+ * name everywhere would break every command of a project that is otherwise sound. `pithy init` refuses a new one,
66
+ * and `pithy doctor` names the segment an existing one carries. Nothing is renamed.
67
+ */
68
+ export declare function assertFeatureProject(project: string): void;
69
+ /** A feature name, read back: which project, issue and slug it belongs to, and what it names. */
70
+ export interface ParsedFeatureName {
71
+ /** The project segment, kebabbed. */
72
+ project: string;
73
+ /** The issue, canonical. */
74
+ issue: string;
75
+ /** The slug segment: the whole slug, since no feature name is truncated. */
76
+ slug: string;
77
+ /** Everything after {@link FEATURE_TAIL_SEPARATOR}: the binding and kind, Worker, Workflow or entry. */
78
+ thing: string;
79
+ }
80
+ /**
81
+ * **Read a feature name back into its owner, or `null` when it is not one (#643).**
82
+ *
83
+ * Exact, because the shape leaves one reading: a feature name holds {@link FEATURE_TAIL_SEPARATOR} exactly once,
84
+ * everything before it is `<project>-f<issue>-<slug>`, and the first `f<digits>` segment there is the issue,
85
+ * since a project may carry none. Nothing else Pithy composes holds the separator at all.
86
+ */
87
+ export declare function parseFeatureName(name: string): ParsedFeatureName | null;
88
+ /**
89
+ * **Is this name one this feature composes?** The ownership check the isolation gates ask (#643).
90
+ *
91
+ * Parsed, never prefix-matched, and exact: the name's project, issue and slug are this feature's, character for
92
+ * character. `<project>-f<issue>-` is shared by every branch of one issue, so a prefix match reached siblings; and
93
+ * a fitted form of the slug, which this used to accept, is a short hash some sibling can have as its whole slug
94
+ * (`feature/12-login` owned `feature/12-c`). No feature name is fitted any more ({@link composeFeatureName}), so
95
+ * there is nothing but the slug itself to accept.
96
+ */
97
+ export declare function isFeatureOwnedName(identity: FeatureIdentity, name: string): boolean;
98
+ /** A feature's project and issue: everything its names are derived from but the slug. */
99
+ export type FeatureHead = Pick<FeatureIdentity, "project" | "issue">;
100
+ /**
101
+ * **The longest slug one feature name leaves room for** — `limit` less `<project>-f<issue>-`, the
102
+ * {@link FEATURE_TAIL_SEPARATOR}, and `thing`, which is everything after it. Zero or less when `thing` leaves none.
103
+ */
104
+ export declare function featureSlugRoom(parts: FeatureHead, thing: string, limit: number): number;
105
+ /** One kind of name a feature composes, as far as its slug budget reads it. */
106
+ export interface FeatureNameShape {
107
+ /** How an error names the namespace, e.g. `a Workflow name`. */
108
+ label: string;
109
+ /** The longest name the namespace takes. */
110
+ limit: number;
111
+ /** Everything after {@link FEATURE_TAIL_SEPARATOR}, e.g. `email-suppressions-d1`. */
112
+ thing: string;
113
+ }
114
+ /** The longest slug every one of `shapes` leaves room for, for this project and issue, and the shape that sets it. */
115
+ export declare function tightestFeatureShape(parts: FeatureHead, shapes: readonly FeatureNameShape[]): {
116
+ max: number;
117
+ shape: FeatureNameShape | null;
118
+ };
119
+ /** The longest slug a feature of this project and issue may have, across every name it composes. */
120
+ export declare function maxFeatureSlug(parts: FeatureHead, shapes: readonly FeatureNameShape[]): number;
121
+ /**
122
+ * **Refuse a branch whose slug cannot fit every name its feature composes (#643).** The one refusal
123
+ * `pithy feature create` and `pithy provision --feature` make, before a worktree or a resource exists, with the
124
+ * project's maximum in the message. {@link composeFeatureName} refuses the same slug one name at a time; this says
125
+ * it once, for all of them.
126
+ */
127
+ export declare function assertFeatureSlugFits(identity: FeatureIdentity, shapes: readonly FeatureNameShape[]): void;
128
+ /**
129
+ * The full Cloudflare resource name for a feature's binding — `<project>-f<issue>-<slug>--<binding>-<kind>`.
35
130
  *
36
131
  * Held to **R2's 63**, the strictest of the three kinds a feature provisions, so one shape is legal for
37
132
  * all of them: lowercase, hyphenated, alphanumeric at both ends. A D1 or KV name could be longer, but a
@@ -40,7 +135,7 @@ export interface FeatureIdentity {
40
135
  export declare function featureResourceName(identity: FeatureIdentity, binding: string, kind: FeatureResourceKind): string;
41
136
  /**
42
137
  * The CF Secrets Store entry name holding one of a feature's **environment-scoped** secrets —
43
- * `<project>-f<issue>-<slug>-<secret>`.
138
+ * `<project>-f<issue>-<slug>--<secret>`.
44
139
  *
45
140
  * A Cloudflare account has one Secrets Store, flat and unpartitionable, so the entry name is the only
46
141
  * partition there is. A feature therefore needs its own names for the same reason it needs its own
@@ -50,12 +145,12 @@ export declare function featureResourceName(identity: FeatureIdentity, binding:
50
145
  * no documented Cloudflare cap, and holding it to the strictest kind's would hash
51
146
  * `secrets-encryption-keys` down to nothing for no reason.
52
147
  *
53
- * A `global` secret is not named here at all: it is one account-level value every environment binds,
54
- * so a feature binds the project's `<project>-global-<secret>` rather than minting a second copy.
148
+ * A `global` secret is named here too since #643: a feature shares nothing with any other environment, so
149
+ * its entry for a global secret is its own rather than the project's `<project>-global-<secret>`.
55
150
  */
56
151
  export declare function featureSecretEntryName(identity: FeatureIdentity, secret: string): string;
57
152
  /**
58
- * The Worker **script name** for one of a feature's workers — `<project>-f<issue>-<slug>-<worker>`.
153
+ * The Worker **script name** for one of a feature's workers — `<project>-f<issue>-<slug>--<worker>`.
59
154
  *
60
155
  * This is the name the feature's Workers deploy under, and therefore the name a sibling's `service`
61
156
  * binding must target, so RPC inside a feature environment reaches that feature's deployment rather than
@@ -67,10 +162,30 @@ export declare function featureSecretEntryName(identity: FeatureIdentity, secret
67
162
  * has a charset rule and no length rule, so nothing upstream was going to stop it either.
68
163
  *
69
164
  * **`app` is the `apps/<app>` directory, never the deploy name.** A scaffolded Worker deploys as
70
- * `<project>-<app>`, and handing that here composed `<project>-f<issue>-<slug>-<project>-<app>` — the
165
+ * `<project>-<app>`, and handing that here composed `<project>-f<issue>-<slug>--<project>-<app>` — the
71
166
  * project twice, spent out of the budget above (#587). This function takes a string and cannot tell the
72
167
  * two apart, so the provisioning path reaches it only through `featureScope`, which is handed both names
73
168
  * and picks.
74
169
  */
75
170
  export declare function featureWorkerName(identity: FeatureIdentity, app: string): string;
171
+ /**
172
+ * The deployed name of one of a feature's **Workflows** — `<project>-f<issue>-<slug>--<capability>-<job>` (#643).
173
+ *
174
+ * A Workflow name is account-wide, like a Worker's, so a feature's email host cannot run
175
+ * `<project>-feature-email-send`: every open branch would deploy the same Workflow over every other's. It takes
176
+ * the feature's head instead, exactly as its host Worker does ({@link featureWorkerName}), so the Worker and the
177
+ * Workflows it hosts are recognizably one feature's and teardown recomputes both from the identity.
178
+ *
179
+ * Held to the **Workflow** limit ({@link NAMESPACE_LIMITS.workflow}), and fitted the way every feature name is:
180
+ * the slug gives way first, then the `<capability>-<job>` tail, deterministically.
181
+ */
182
+ export declare function featureWorkflowName(identity: FeatureIdentity, capability: string, job: string): string;
183
+ /**
184
+ * The name of one of a feature's **Vectorize indexes** — `<project>-f<issue>-<slug>--<thing>` (#643).
185
+ *
186
+ * An index name is account-wide, so a feature's vector host cannot bind `<project>-feature-vector-<index>`: every
187
+ * open branch would read and write one index. Held to the Vectorize limit and fitted the way every feature name
188
+ * is, so teardown recomputes it from the identity.
189
+ */
190
+ export declare function featureVectorizeIndexName(identity: FeatureIdentity, thing: string): string;
76
191
  //# sourceMappingURL=feature.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"feature.d.ts","sourceRoot":"","sources":["../../src/naming/feature.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;GAgBG;AAEH,iHAAiH;AACjH,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAErD,6EAA6E;AAC7E,eAAO,MAAM,sBAAsB,EAAE,SAAS,mBAAmB,EAAuB,CAAC;AAEzF,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;IAChB,6FAA6F;IAC7F,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;CACd;AAkED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAQjH;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAQxF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEhF"}
1
+ {"version":3,"file":"feature.d.ts","sourceRoot":"","sources":["../../src/naming/feature.ts"],"names":[],"mappings":"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"}