@pithy-sh/core 0.6.1 → 0.7.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 (56) hide show
  1. package/dist/capability/capability.d.ts.map +1 -1
  2. package/dist/capability/capability.js +2 -0
  3. package/dist/capability/settings.d.ts +15 -2
  4. package/dist/capability/settings.d.ts.map +1 -1
  5. package/dist/data/databases.d.ts +11 -0
  6. package/dist/data/databases.d.ts.map +1 -1
  7. package/dist/migrations/registry.d.ts +6 -0
  8. package/dist/migrations/registry.d.ts.map +1 -1
  9. package/dist/migrations/registry.js +2 -0
  10. package/dist/migrations/retained.d.ts +101 -0
  11. package/dist/migrations/retained.d.ts.map +1 -0
  12. package/dist/migrations/retained.js +243 -0
  13. package/dist/migrations/retainedDeclaration.d.ts +15 -0
  14. package/dist/migrations/retainedDeclaration.d.ts.map +1 -0
  15. package/dist/migrations/retainedDeclaration.js +49 -0
  16. package/dist/migrations/runner.d.ts +44 -7
  17. package/dist/migrations/runner.d.ts.map +1 -1
  18. package/dist/migrations/runner.js +41 -15
  19. package/dist/naming/environment.d.ts +12 -0
  20. package/dist/naming/environment.d.ts.map +1 -1
  21. package/dist/naming/environment.js +38 -1
  22. package/dist/naming/feature.d.ts +7 -1
  23. package/dist/naming/feature.d.ts.map +1 -1
  24. package/dist/naming/feature.js +10 -3
  25. package/dist/naming/provisionScope.d.ts +97 -9
  26. package/dist/naming/provisionScope.d.ts.map +1 -1
  27. package/dist/naming/provisionScope.js +87 -12
  28. package/dist/naming/resourceNames.d.ts +5 -2
  29. package/dist/naming/resourceNames.d.ts.map +1 -1
  30. package/dist/naming/resourceNames.js +1 -1
  31. package/dist/text/comments.d.ts +14 -0
  32. package/dist/text/comments.d.ts.map +1 -1
  33. package/dist/text/comments.js +28 -2
  34. package/dist/version.generated.d.ts +1 -1
  35. package/dist/version.generated.js +1 -1
  36. package/dist/workflow/host.d.ts +8 -0
  37. package/dist/workflow/host.d.ts.map +1 -1
  38. package/dist/workflow/host.js +5 -2
  39. package/dist/workflow/hostEnv.d.ts +1 -1
  40. package/dist/workflow/hostEnv.js +1 -1
  41. package/package.json +1 -1
  42. package/src/capability/capability.ts +6 -0
  43. package/src/capability/settings.ts +15 -2
  44. package/src/data/databases.ts +11 -0
  45. package/src/migrations/registry.ts +10 -0
  46. package/src/migrations/retained.ts +319 -0
  47. package/src/migrations/retainedDeclaration.ts +56 -0
  48. package/src/migrations/runner.ts +89 -15
  49. package/src/naming/environment.ts +43 -0
  50. package/src/naming/feature.ts +10 -3
  51. package/src/naming/provisionScope.ts +130 -13
  52. package/src/naming/resourceNames.ts +6 -3
  53. package/src/text/comments.ts +31 -1
  54. package/src/version.generated.ts +1 -1
  55. package/src/workflow/host.ts +14 -4
  56. package/src/workflow/hostEnv.ts +1 -1
@@ -1,7 +1,8 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
- import { InternalError } from "../error/pithyError.js";
4
- import { FEATURE_ENVIRONMENT, assertValidEnvironment } from "./environment.js";
3
+ import { InternalError, ValidationError } from "../error/pithyError.js";
4
+ import { FEATURE_ENVIRONMENT, assertValidEnvironment, isFeatureMarker } from "./environment.js";
5
+ import { kebab } from "./resource.js";
5
6
  import { featureResourceName, featureSecretEntryName, featureWorkerName } from "./feature.js";
6
7
  import { resourceNames } from "./resourceNames.js";
7
8
  //#region src/naming/provisionScope.ts
@@ -32,19 +33,52 @@ function bindingResourceName(project, binding, kind, naming, scoped) {
32
33
  return scoped(thing);
33
34
  }
34
35
  /**
36
+ * **Is this name inside a feature's namespace** — `<project>-f<issue>-…`, the project and then a feature's
37
+ * marker, with something after it?
38
+ *
39
+ * The namespace, not one feature's names, because that is what has to stay disjoint: `pithy feature destroy`
40
+ * deletes by exact name, and a feature Worker's name has no kind suffix, so a declared environment's name
41
+ * that lands anywhere in here is one some branch's teardown can delete (#587). Every name {@link featureScope}
42
+ * composes is inside; {@link environmentScope} refuses to name a Worker inside, and `isValidEnvironment`
43
+ * refuses every environment whose composed names could be.
44
+ *
45
+ * **What it does not see.** It is stated for one project. A second project on the same account whose name is
46
+ * this one's plus `-f<issue>` — `acme-f1` beside `acme` — composes names inside `acme`'s namespace from an
47
+ * ordinary environment, and nothing here can tell, because a project name carries no boundary a later hyphen
48
+ * cannot fake. That is the hyphen ambiguity every composed name carries, one segment further left.
49
+ */
50
+ function isFeatureName(project, name) {
51
+ const head = `${kebab(project)}-`;
52
+ if (!name.startsWith(head)) return false;
53
+ const [marker, ...rest] = name.slice(head.length).split("-");
54
+ return isFeatureMarker(marker ?? "") && rest.length > 0;
55
+ }
56
+ /**
35
57
  * A declared environment's scope — `staging`, `prod`, or whatever the root `pithy.config.ts` lists.
36
58
  *
37
59
  * The environment is validated here, once, before a single name exists: it is the middle segment of
38
60
  * every name below, so an illegal one must fail before anything is created rather than at the fourth
39
61
  * getter that happened to use it.
40
62
  *
41
- * **A Worker's script name is wrangler's own, `<script>-<env>`, deliberately.** It is the one name here
42
- * that is not `<project>-<env>-<thing>`, because `wrangler deploy --env staging` has always appended the
43
- * environment to the top-level `name` — so every project that ever deployed is already at that address.
44
- * A Worker script is a `refuse`-policy namespace precisely because renaming one orphans the deployment
45
- * and every `service` binding pointing at it; composing a "more correct" name here would do exactly
46
- * that, silently, on the next deploy. The name is written out rather than left implicit so a service
47
- * binding and a deploy agree on one string that is in the file.
63
+ * **A declared name wins; the fallback is wrangler's own, `<script>-<env>`.**
64
+ *
65
+ * This reverses the rule that stood here until #580, and the reversal is worth reading rather than
66
+ * assuming. The old rule *recomputed* `<script>-<env>` on every provision and wrote it over whatever the
67
+ * stanza said, on the argument that wrangler has always appended the environment to the top-level `name`,
68
+ * so every project that ever deployed is already at that address.
69
+ *
70
+ * That argument is still true about the **fallback**, and the fallback is unchanged: a stanza that names
71
+ * nothing gets `<script>-<env>` and every existing adopter deploys exactly where they always did. What was
72
+ * wrong was recomputing over a name somebody wrote down. Since #580 the starter template writes one —
73
+ * `<project>-<env>-<worker>`, the shape every other resource here takes — so recompute would have made the
74
+ * template's stamp pointless, and worse: it would have silently renamed the Worker of every project that
75
+ * had taken the new shape, on their next `pithy provision`. A Worker script is a `refuse`-policy namespace
76
+ * precisely because renaming one orphans the deployment, its routes, and every `service` binding pointing
77
+ * at it. Doing that as a side effect of provisioning is the failure the old rule was written to prevent,
78
+ * arriving from the other direction.
79
+ *
80
+ * So the name is read, not composed, when the file already holds one — and written out when it does not,
81
+ * so a service binding and a deploy still agree on one string that is in the file.
48
82
  */
49
83
  function environmentScope(project, environment) {
50
84
  assertValidEnvironment(environment);
@@ -54,7 +88,15 @@ function environmentScope(project, environment) {
54
88
  source: true,
55
89
  honorsGlobal: true,
56
90
  resource: (binding, kind, naming) => bindingResourceName(project, binding, kind, naming, (thing) => names[KIND_NAMER[kind]](thing)),
57
- worker: (worker) => `${worker}-${environment}`,
91
+ worker: ({ script }, declared) => {
92
+ const name = declared ?? `${script}-${environment}`;
93
+ if (isFeatureName(project, name)) throw new ValidationError({
94
+ message: `The ${environment} Worker "${name}" is named the way a feature's Worker is.`,
95
+ action: `Rename it in env.${environment}.name, or the Worker's top-level name, so it does not follow ${kebab(project)}- with f and a number.`,
96
+ detail: `${declared === void 0 ? "Composed by wrangler from the deploy name" : `Declared in env.${environment}.name`}. A feature's Worker is <project>-f<issue>-<slug>-<app>, and \`pithy feature destroy\` deletes it by that exact name.`
97
+ });
98
+ return name;
99
+ },
58
100
  secretEntry: (secret, secretScope) => secretEntryName(project, secret, secretScope, () => names.secretEntry(secret))
59
101
  };
60
102
  }
@@ -77,6 +119,12 @@ function environmentScope(project, environment) {
77
119
  * `resource` is ignored for the same reason it is moot: a feature's `<thing>` segment is the binding plus
78
120
  * the kind, and the whole name is recomputed on teardown rather than looked up, so nothing outside the
79
121
  * feature reads the string.
122
+ *
123
+ * **And a declared Worker name is ignored here too — the other half of the #580 asymmetry.** A declared
124
+ * environment's script name is the adopter's, written in a tracked file, and provisioning reads it. A
125
+ * feature's is the kit's: it is composed from the branch, written into a generated config nobody commits,
126
+ * and **recomputed on teardown**. Honoring a declared one would leave `pithy feature destroy` computing a
127
+ * name the deployment does not answer to, which is a Worker nothing deletes.
80
128
  */
81
129
  function featureScope(identity) {
82
130
  return {
@@ -84,9 +132,36 @@ function featureScope(identity) {
84
132
  source: false,
85
133
  honorsGlobal: false,
86
134
  resource: (binding, kind) => featureResourceName(identity, binding, kind),
87
- worker: (worker) => featureWorkerName(identity, worker),
135
+ worker: ({ app }) => featureWorkerName(identity, app),
88
136
  secretEntry: (secret, secretScope) => secretEntryName(identity.project, secret, secretScope, () => featureSecretEntryName(identity, secret))
89
137
  };
90
138
  }
139
+ /**
140
+ * **Every script name one Worker can be deployed under in a feature — what teardown looks for (#592).**
141
+ *
142
+ * The first is the name {@link featureScope} composes today, taken from the scope rather than composed a
143
+ * second time, so provisioning and teardown cannot disagree about it. The second is the one it composed
144
+ * until #587, from the deploy name: `<project>-f<issue>-<slug>-<project>-<app>`. A feature provisioned and
145
+ * deployed before that upgrade is still running under it, and after its next deploy it runs under both —
146
+ * so teardown looks for both. One entry when the two coincide, as they do for a Worker whose deploy name
147
+ * is its directory.
148
+ *
149
+ * **Exact names, never a prefix.** A prefix scan of `<project>-f<issue>-<slug>-` reaches a sibling whose
150
+ * slug extends this one's. The exact names can still meet a sibling in one shape: the doubled name for
151
+ * slug `demo` and deploy name `acme-api` is the current name for slug `demo-acme` and directory `api`, on
152
+ * the same issue number. That is the same hyphen ambiguity every feature name here carries.
153
+ *
154
+ * **They cannot meet a declared environment's Worker**, which has no suffix either and is `<project>-<env>-<app>`
155
+ * since #580: every name either shape yields is inside {@link isFeatureName}'s namespace, no environment may
156
+ * start with a feature's marker, and {@link environmentScope} refuses a declared or fallback Worker name inside
157
+ * it. What that does not reach is a name nothing in the kit chose — a stanza written by hand and deployed
158
+ * without `pithy provision --env` ever reading it, or another project on the account, as {@link isFeatureName}
159
+ * says.
160
+ */
161
+ function featureWorkerScriptNames(identity, worker) {
162
+ const current = featureScope(identity).worker(worker);
163
+ const beforeSingleProject = featureWorkerName(identity, worker.script);
164
+ return current === beforeSingleProject ? [current] : [current, beforeSingleProject];
165
+ }
91
166
  //#endregion
92
- export { bindingResourceName, environmentScope, featureScope };
167
+ export { bindingResourceName, environmentScope, featureScope, featureWorkerScriptNames, isFeatureName };
@@ -60,8 +60,11 @@ export interface ScopedNames {
60
60
  export interface FeatureNames {
61
61
  /** A feature's D1, KV, or R2 resource — `<project>-f<issue>-<slug>-<binding>-<kind>`. */
62
62
  resource(binding: string, kind: FeatureResourceKind): string;
63
- /** A feature's Worker script — `<project>-f<issue>-<slug>-<worker>`. */
64
- worker(worker: string): string;
63
+ /**
64
+ * A feature's Worker script — `<project>-f<issue>-<slug>-<app>`, from the `apps/<app>` directory and
65
+ * never the deploy name, which already leads with the project (#587).
66
+ */
67
+ worker(app: string): string;
65
68
  }
66
69
  /** Everything a project can be asked to name. Created once, from the project, by {@link resourceNames}. */
67
70
  export interface ProjectNames {
@@ -1 +1 @@
1
- {"version":3,"file":"resourceNames.d.ts","sourceRoot":"","sources":["../../src/naming/resourceNames.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,mBAAmB,EAA0C,MAAM,WAAW,CAAC;AAInH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,8FAA8F;AAC9F,MAAM,WAAW,WAAW;IAC1B,sGAAsG;IACtG,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,0GAA0G;IAC1G,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,uFAAuF;IACvF,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,kFAAkF;IAClF,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,4FAA4F;IAC5F,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,kGAAkG;IAClG,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,iEAAiE;IACjE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CACnC;AAED,wFAAwF;AACxF,MAAM,WAAW,YAAY;IAC3B,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAAC;IAC7D,wEAAwE;IACxE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CAChC;AAED,2GAA2G;AAC3G,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8FAA8F;IAC9F,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,CAAC;IACtC,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,wDAAwD;IACxD,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,CAAC,GAAG,YAAY,CAAC;CACnE;AA8DD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAmB3D"}
1
+ {"version":3,"file":"resourceNames.d.ts","sourceRoot":"","sources":["../../src/naming/resourceNames.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,mBAAmB,EAA0C,MAAM,WAAW,CAAC;AAInH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,8FAA8F;AAC9F,MAAM,WAAW,WAAW;IAC1B,sGAAsG;IACtG,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,0GAA0G;IAC1G,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,uFAAuF;IACvF,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,kFAAkF;IAClF,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,4FAA4F;IAC5F,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,kGAAkG;IAClG,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,iEAAiE;IACjE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CACnC;AAED,wFAAwF;AACxF,MAAM,WAAW,YAAY;IAC3B,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAAC;IAC7D;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B;AAED,2GAA2G;AAC3G,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8FAA8F;IAC9F,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,CAAC;IACtC,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,wDAAwD;IACxD,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,CAAC,GAAG,YAAY,CAAC;CACnE;AA8DD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAmB3D"}
@@ -95,7 +95,7 @@ function resourceNames(project) {
95
95
  };
96
96
  return {
97
97
  resource: (binding, kind) => featureResourceName(full, binding, kind),
98
- worker: (worker) => featureWorkerName(full, worker)
98
+ worker: (app) => featureWorkerName(full, app)
99
99
  };
100
100
  }
101
101
  };
@@ -61,4 +61,18 @@
61
61
  * `worker-safety.test.ts` exists to enforce, so the gate now polices the stripper it reads with.
62
62
  */
63
63
  export declare function blankComments(source: string): string;
64
+ /**
65
+ * `source` with every comment **and every string's contents** blanked — for a gate asking whether a name is
66
+ * *called*, where {@link blankComments}' own rule points the wrong way.
67
+ *
68
+ * `blankComments` keeps strings because its callers look for a forbidden read, and a string that merely contains
69
+ * one is a false positive, the safe direction. A gate looking for a *required* call inverts that: a validator named
70
+ * only inside an error message — `"requireEnvironment(args.env)"` — is a call that never runs, and counting it
71
+ * passes the file. So this blanks the contents too, delimiters kept, character for character.
72
+ *
73
+ * **A template literal goes whole, `${}` included.** A call written only inside an interpolation is not seen. For
74
+ * a presence check that is the safe direction again — the file is flagged, never passed — and a scan for a read
75
+ * belongs on {@link blankComments}, where the interpolation stays.
76
+ */
77
+ export declare function blankCommentsAndStrings(source: string): string;
64
78
  //# sourceMappingURL=comments.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../../src/text/comments.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAgCpD"}
1
+ {"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../../src/text/comments.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9D"}
@@ -62,6 +62,26 @@
62
62
  * `worker-safety.test.ts` exists to enforce, so the gate now polices the stripper it reads with.
63
63
  */
64
64
  function blankComments(source) {
65
+ return blank(source, false);
66
+ }
67
+ /**
68
+ * `source` with every comment **and every string's contents** blanked — for a gate asking whether a name is
69
+ * *called*, where {@link blankComments}' own rule points the wrong way.
70
+ *
71
+ * `blankComments` keeps strings because its callers look for a forbidden read, and a string that merely contains
72
+ * one is a false positive, the safe direction. A gate looking for a *required* call inverts that: a validator named
73
+ * only inside an error message — `"requireEnvironment(args.env)"` — is a call that never runs, and counting it
74
+ * passes the file. So this blanks the contents too, delimiters kept, character for character.
75
+ *
76
+ * **A template literal goes whole, `${}` included.** A call written only inside an interpolation is not seen. For
77
+ * a presence check that is the safe direction again — the file is flagged, never passed — and a scan for a read
78
+ * belongs on {@link blankComments}, where the interpolation stays.
79
+ */
80
+ function blankCommentsAndStrings(source) {
81
+ return blank(source, true);
82
+ }
83
+ /** The one walk behind both: comments always blanked, a quoted run kept or blanked inside its delimiters. */
84
+ function blank(source, strings) {
65
85
  let out = "";
66
86
  let previous = "";
67
87
  let index = 0;
@@ -76,7 +96,8 @@ function blankComments(source) {
76
96
  }
77
97
  if (char === "\"" || char === "'" || char === "`") {
78
98
  const stop = endOfQuoted(source, index, char, char !== "`");
79
- out += source.slice(index, stop);
99
+ const run = source.slice(index, stop);
100
+ out += strings ? blankInside(run, char) : run;
80
101
  index = stop;
81
102
  previous = char;
82
103
  continue;
@@ -94,6 +115,11 @@ function blankComments(source) {
94
115
  }
95
116
  return out;
96
117
  }
118
+ /** A quoted run with everything between its delimiters blanked, newlines kept. An unterminated run has no closer. */
119
+ function blankInside(run, quote) {
120
+ const closed = run.length > 1 && run.endsWith(quote);
121
+ return `${quote}${run.slice(1, closed ? -1 : void 0).replace(/[^\n]/g, " ")}${closed ? quote : ""}`;
122
+ }
97
123
  /**
98
124
  * After one of these, a `/` opens a regex literal; after anything else it divides. The empty string is
99
125
  * the start of the file. A wrong guess is bounded — see {@link endOfRegex}.
@@ -160,4 +186,4 @@ function endOfRegex(source, start) {
160
186
  return source.length;
161
187
  }
162
188
  //#endregion
163
- export { blankComments };
189
+ export { blankComments, blankCommentsAndStrings };
@@ -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.6.1";
6
+ export declare const PACKAGE_VERSION = "0.7.0";
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.6.1";
7
+ const PACKAGE_VERSION = "0.7.0";
8
8
  //#endregion
9
9
  export { PACKAGE_NAME, PACKAGE_VERSION };
@@ -127,6 +127,14 @@ export interface WorkflowHostParams {
127
127
  * not depend on `@pithy-sh/secrets`, which owns that naming.
128
128
  */
129
129
  masterKeySecretName?: string;
130
+ /**
131
+ * Secrets Store binding name → the entry it resolves to in this environment, for every store secret the
132
+ * host reads besides the master key. Filled **only** for the bindings listed, like
133
+ * {@link databaseNames}; the names come from the capability, which composes them with the same namer
134
+ * provisioning created the entries with. Core composes none of them itself, for the reason
135
+ * {@link masterKeySecretName} gives.
136
+ */
137
+ secretNames?: Record<string, string>;
130
138
  /** Vars merged over the template's, e.g. the capability's config serialized as one JSON blob. */
131
139
  vars?: Record<string, string>;
132
140
  /**
@@ -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":"AAKA,OAAO,EAAE,KAAK,qBAAqB,EAAwC,MAAM,UAAU,CAAC;AAC5F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,uGAAuG;AACvG,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,iDAAiD;AACjD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,8GAA8G;AAC9G,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,4GAA4G;AAC5G,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,kGAAkG;AAClG,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,sGAAsG;AACtG,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,aAAa,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,aAAa,EAAE,CAAC;IAChC,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACnC,EAAE,CAAC,EAAE,aAAa,CAAC;IACnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACpC,qBAAqB,CAAC,EAAE,uBAAuB,EAAE,CAAC;IAClD,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAClC,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,yGAAyG;AACzG,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,UAAU,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,sCAAsC;IACtC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,qCAAqC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,2CAA2C;IAC3C,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,iGAAiG;IACjG,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B;;;OAGG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;OAGG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;CAC5C;AAOD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,oBAAoB,EAAE,MAAM,EAAE,kBAAkB,GAAG,oBAAoB,CA2FpH;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,qBAAqB,GAC3B;IAAE,SAAS,EAAE,mBAAmB,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAsBvD"}
@@ -59,11 +59,14 @@ function resolveWorkflowHost(template, params) {
59
59
  if (resolved.send_email) resolved.send_email = resolved.send_email.map((entry) => withRemote(entry, entry.name, remote));
60
60
  if (resolved.secrets_store_secrets && params.secretsStoreId !== void 0) {
61
61
  const storeId = params.secretsStoreId;
62
- const masterKey = params.masterKeySecretName;
62
+ const names = {
63
+ ...params.masterKeySecretName !== void 0 ? { SECRETS_ENCRYPTION_KEYS: params.masterKeySecretName } : {},
64
+ ...params.secretNames
65
+ };
63
66
  resolved.secrets_store_secrets = resolved.secrets_store_secrets.map((entry) => ({
64
67
  ...entry,
65
68
  store_id: storeId,
66
- secret_name: masterKey !== void 0 && entry.binding === "SECRETS_ENCRYPTION_KEYS" ? masterKey : entry.secret_name
69
+ secret_name: Object.hasOwn(names, entry.binding) ? names[entry.binding] : entry.secret_name
67
70
  }));
68
71
  }
69
72
  if (resolved.workflows) {
@@ -40,7 +40,7 @@ export type HostEnvProviderKind = z.infer<typeof HostEnvProviderKind>;
40
40
  /**
41
41
  * What provides one field. The whole point of the declaration: a report that says `BASE_URL is
42
42
  * missing` sends a developer looking, and a report that says `Var BASE_URL in the host's
43
- * wrangler.jsonc. Run pithy email provision --env dev.` ends the search.
43
+ * wrangler.jsonc. Run pithy email provision.` ends the search.
44
44
  */
45
45
  export declare const HostEnvProvider: z.ZodObject<{
46
46
  kind: z.ZodEnum<{
@@ -41,7 +41,7 @@ const HostEnvProviderKind = z.enum([
41
41
  /**
42
42
  * What provides one field. The whole point of the declaration: a report that says `BASE_URL is
43
43
  * missing` sends a developer looking, and a report that says `Var BASE_URL in the host's
44
- * wrangler.jsonc. Run pithy email provision --env dev.` ends the search.
44
+ * wrangler.jsonc. Run pithy email provision.` ends the search.
45
45
  */
46
46
  const HostEnvProvider = z.object({
47
47
  kind: HostEnvProviderKind.describe("Which kind of thing fills this field."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pithy-sh/core",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,6 +19,7 @@ import type { LocaleContext } from "../i18n/locale";
19
19
  import type { Translator } from "../i18n/translator";
20
20
  import type { KvNamespaceSpecMap } from "../kv/namespaces";
21
21
  import type { Logger } from "../logger/logger";
22
+ import { declareRetained } from "../migrations/retainedDeclaration";
22
23
  import type { SeedSet } from "../seed/seed";
23
24
  import type { WorkflowSpecMap } from "../workflow/spec";
24
25
  import { BindingSpec, type BindingSpecInput } from "./bindings";
@@ -595,6 +596,11 @@ export function defineCapability<
595
596
  const Name extends string = string,
596
597
  const Workflows extends WorkflowSpecMap = Record<never, never>,
597
598
  >(input: CapabilityInput<Databases, Namespaces, Name, Workflows>): Capability<Databases, Namespaces, Name, Workflows> {
599
+ // A retained table is declared on the `down`s that could drop it the moment the capability exists, so a
600
+ // migration the runner is handed carries it however its provider was built (#588).
601
+ for (const spec of Object.values(input.databases ?? {})) {
602
+ if (spec.migrations && spec.retained) declareRetained(spec.migrations, spec.retained);
603
+ }
598
604
  return {
599
605
  ...input,
600
606
  requiredBindings: input.requiredBindings.map((binding) => BindingSpec.parse(binding)),
@@ -112,8 +112,21 @@ export interface SettingsAccountReader {
112
112
  d1Databases(): Promise<readonly string[]>;
113
113
  /** Whether this account holds a Cloudflare zone covering the hostname — the prerequisite for onboarding it. */
114
114
  zone(hostname: string): Promise<boolean>;
115
- /** Whether a declared secret has a value in that environment. The name is the registry's, not a binding's. */
116
- secret(request: { name: string; environment: string }): Promise<boolean>;
115
+ /**
116
+ * Whether that environment's **D1 vault** holds a value under this registry name — asked of its manager
117
+ * Worker, the only thing that can open it.
118
+ *
119
+ * Named for the store it asks, because it used to be `secret()` — "does this secret have a value" — and
120
+ * that question has two stores behind it (#596). A check asking it about a `cf-secrets-store` secret got a
121
+ * confident `false` from a vault the secret never lives in. Ask {@link storeEntry} about one of those.
122
+ */
123
+ vaultSecret(request: { name: string; environment: string }): Promise<boolean>;
124
+ /**
125
+ * Whether the account's **Secrets Store** holds an entry of exactly this name. The name is the entry's, not
126
+ * the registry's: compose it with `environmentScope(project, env).secretEntry(...)`, the namer provisioning
127
+ * created it with, so the check looks where the value went.
128
+ */
129
+ storeEntry(name: string): Promise<boolean>;
117
130
  }
118
131
 
119
132
  /** What the account tier is told: everything the local tier gets, plus the account it may ask. */
@@ -29,6 +29,17 @@ export interface DatabaseSpec<Tables extends SchemaMap = SchemaMap> {
29
29
  migrations?: Record<string, Migration>;
30
30
  /** Sort order within this database relative to other capabilities (core low, app high). Required with `migrations`. */
31
31
  migrationOrder?: number;
32
+ /**
33
+ * The tables in this slice whose rows **exist nowhere else** — a vault's sealed credentials, a
34
+ * suppression list of people who asked not to be mailed. Keys of `tables`, declared beside the
35
+ * `migrations` that create them (required with them).
36
+ *
37
+ * A schema `down` is right to drop a table; for these it destroys the only copy. So while one of them
38
+ * holds rows, no `down` runs against this database — a rollback, a `seed --redo` reset, a
39
+ * `remove --drop` — unless the operator passes the exact row count (`--destroy-retained <n>`). The
40
+ * refusal lives in the migration runner, so a new command that reverses migrations inherits it (#588).
41
+ */
42
+ retained?: readonly (keyof Tables & string)[];
32
43
  }
33
44
 
34
45
  /** A capability's databases: database name → {@link DatabaseSpec}. */
@@ -3,6 +3,7 @@
3
3
 
4
4
  import type { Migration, MigrationProvider } from "kysely/migration";
5
5
  import { InternalError } from "../error/pithyError";
6
+ import { declareRetained } from "./retainedDeclaration";
6
7
 
7
8
  /** Width of the zero-padded `order` prefix in a composed key — the stable anchor. */
8
9
  const ORDER_DIGITS = 4;
@@ -29,6 +30,12 @@ export interface NamespacedMigrations {
29
30
  order: number;
30
31
  /** Stable, per-namespace keys, each `^\d{4}_[a-z0-9_]+$` (e.g. "0001_init"). */
31
32
  migrations: Record<string, Migration>;
33
+ /**
34
+ * The table keys (camelCase, as the capability's `tables` declares them) this set's capability retains
35
+ * in this database — rows that exist nowhere else. Recorded on this set's `down`s, and the runner refuses
36
+ * any `down` against the database while one of them holds rows. See `./retained`.
37
+ */
38
+ retained?: readonly string[];
32
39
  }
33
40
 
34
41
  // These guard build-time registry invariants — a capability author wired its migrations wrong.
@@ -85,6 +92,9 @@ function composeDatabase(database: string, sets: NamespacedMigrations[]): Migrat
85
92
  }
86
93
  }
87
94
 
95
+ // Recorded on each migration's own `down`, so the runner enforces it on this provider and on anything
96
+ // later built from these same migrations — no caller has to remember to pass it along (#588).
97
+ for (const set of sets) declareRetained(set.migrations, set.retained ?? []);
88
98
  return {
89
99
  getMigrations: async (): Promise<Record<string, Migration>> => composed,
90
100
  };