@ploaness/governance 1.0.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 (199) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -0
  3. package/dist/access-boundary.d.ts +41 -0
  4. package/dist/access-boundary.d.ts.map +1 -0
  5. package/dist/access-boundary.js +26 -0
  6. package/dist/access-boundary.js.map +1 -0
  7. package/dist/agent-references.d.ts +12 -0
  8. package/dist/agent-references.d.ts.map +1 -0
  9. package/dist/agent-references.js +53 -0
  10. package/dist/agent-references.js.map +1 -0
  11. package/dist/architecture-layers.d.ts +11 -0
  12. package/dist/architecture-layers.d.ts.map +1 -0
  13. package/dist/architecture-layers.js +47 -0
  14. package/dist/architecture-layers.js.map +1 -0
  15. package/dist/asset-policy.d.ts +138 -0
  16. package/dist/asset-policy.d.ts.map +1 -0
  17. package/dist/asset-policy.js +312 -0
  18. package/dist/asset-policy.js.map +1 -0
  19. package/dist/banned-typography.d.ts +13 -0
  20. package/dist/banned-typography.d.ts.map +1 -0
  21. package/dist/banned-typography.js +78 -0
  22. package/dist/banned-typography.js.map +1 -0
  23. package/dist/bundle-budget.d.ts +16 -0
  24. package/dist/bundle-budget.d.ts.map +1 -0
  25. package/dist/bundle-budget.js +37 -0
  26. package/dist/bundle-budget.js.map +1 -0
  27. package/dist/commit-message.d.ts +37 -0
  28. package/dist/commit-message.d.ts.map +1 -0
  29. package/dist/commit-message.js +144 -0
  30. package/dist/commit-message.js.map +1 -0
  31. package/dist/config-references.d.ts +17 -0
  32. package/dist/config-references.d.ts.map +1 -0
  33. package/dist/config-references.js +54 -0
  34. package/dist/config-references.js.map +1 -0
  35. package/dist/container-failure.d.ts +40 -0
  36. package/dist/container-failure.d.ts.map +1 -0
  37. package/dist/container-failure.js +154 -0
  38. package/dist/container-failure.js.map +1 -0
  39. package/dist/container-files.d.ts +36 -0
  40. package/dist/container-files.d.ts.map +1 -0
  41. package/dist/container-files.js +74 -0
  42. package/dist/container-files.js.map +1 -0
  43. package/dist/container-freshness.d.ts +102 -0
  44. package/dist/container-freshness.d.ts.map +1 -0
  45. package/dist/container-freshness.js +129 -0
  46. package/dist/container-freshness.js.map +1 -0
  47. package/dist/dependency-freshness.d.ts +129 -0
  48. package/dist/dependency-freshness.d.ts.map +1 -0
  49. package/dist/dependency-freshness.js +161 -0
  50. package/dist/dependency-freshness.js.map +1 -0
  51. package/dist/doc-blocks.d.ts +13 -0
  52. package/dist/doc-blocks.d.ts.map +1 -0
  53. package/dist/doc-blocks.js +75 -0
  54. package/dist/doc-blocks.js.map +1 -0
  55. package/dist/document-references.d.ts +44 -0
  56. package/dist/document-references.d.ts.map +1 -0
  57. package/dist/document-references.js +94 -0
  58. package/dist/document-references.js.map +1 -0
  59. package/dist/editorconfig-policy.d.ts +24 -0
  60. package/dist/editorconfig-policy.d.ts.map +1 -0
  61. package/dist/editorconfig-policy.js +106 -0
  62. package/dist/editorconfig-policy.js.map +1 -0
  63. package/dist/environment-files.d.ts +42 -0
  64. package/dist/environment-files.d.ts.map +1 -0
  65. package/dist/environment-files.js +76 -0
  66. package/dist/environment-files.js.map +1 -0
  67. package/dist/file-roles.d.ts +39 -0
  68. package/dist/file-roles.d.ts.map +1 -0
  69. package/dist/file-roles.js +82 -0
  70. package/dist/file-roles.js.map +1 -0
  71. package/dist/generated-denial.d.ts +42 -0
  72. package/dist/generated-denial.d.ts.map +1 -0
  73. package/dist/generated-denial.js +87 -0
  74. package/dist/generated-denial.js.map +1 -0
  75. package/dist/history-policy.d.ts +32 -0
  76. package/dist/history-policy.d.ts.map +1 -0
  77. package/dist/history-policy.js +50 -0
  78. package/dist/history-policy.js.map +1 -0
  79. package/dist/image-assets.d.ts +14 -0
  80. package/dist/image-assets.d.ts.map +1 -0
  81. package/dist/image-assets.js +205 -0
  82. package/dist/image-assets.js.map +1 -0
  83. package/dist/index.d.ts +49 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +53 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/install-policy.d.ts +35 -0
  88. package/dist/install-policy.d.ts.map +1 -0
  89. package/dist/install-policy.js +61 -0
  90. package/dist/install-policy.js.map +1 -0
  91. package/dist/json-shapes.d.ts +86 -0
  92. package/dist/json-shapes.d.ts.map +1 -0
  93. package/dist/json-shapes.js +124 -0
  94. package/dist/json-shapes.js.map +1 -0
  95. package/dist/license-allowlist.d.ts +15 -0
  96. package/dist/license-allowlist.d.ts.map +1 -0
  97. package/dist/license-allowlist.js +157 -0
  98. package/dist/license-allowlist.js.map +1 -0
  99. package/dist/network-policy.d.ts +69 -0
  100. package/dist/network-policy.d.ts.map +1 -0
  101. package/dist/network-policy.js +173 -0
  102. package/dist/network-policy.js.map +1 -0
  103. package/dist/paired-assets.d.ts +34 -0
  104. package/dist/paired-assets.d.ts.map +1 -0
  105. package/dist/paired-assets.js +63 -0
  106. package/dist/paired-assets.js.map +1 -0
  107. package/dist/payload-access.d.ts +9 -0
  108. package/dist/payload-access.d.ts.map +1 -0
  109. package/dist/payload-access.js +168 -0
  110. package/dist/payload-access.js.map +1 -0
  111. package/dist/payload-policy.d.ts +18 -0
  112. package/dist/payload-policy.d.ts.map +1 -0
  113. package/dist/payload-policy.js +124 -0
  114. package/dist/payload-policy.js.map +1 -0
  115. package/dist/payload-source.d.ts +29 -0
  116. package/dist/payload-source.d.ts.map +1 -0
  117. package/dist/payload-source.js +84 -0
  118. package/dist/payload-source.js.map +1 -0
  119. package/dist/release-age.d.ts +23 -0
  120. package/dist/release-age.d.ts.map +1 -0
  121. package/dist/release-age.js +38 -0
  122. package/dist/release-age.js.map +1 -0
  123. package/dist/request-limits.d.ts +39 -0
  124. package/dist/request-limits.d.ts.map +1 -0
  125. package/dist/request-limits.js +57 -0
  126. package/dist/request-limits.js.map +1 -0
  127. package/dist/run-plan.d.ts +32 -0
  128. package/dist/run-plan.d.ts.map +1 -0
  129. package/dist/run-plan.js +43 -0
  130. package/dist/run-plan.js.map +1 -0
  131. package/dist/run-policy.d.ts +32 -0
  132. package/dist/run-policy.d.ts.map +1 -0
  133. package/dist/run-policy.js +37 -0
  134. package/dist/run-policy.js.map +1 -0
  135. package/dist/secret-policy.d.ts +13 -0
  136. package/dist/secret-policy.d.ts.map +1 -0
  137. package/dist/secret-policy.js +69 -0
  138. package/dist/secret-policy.js.map +1 -0
  139. package/dist/settings-layering.d.ts +45 -0
  140. package/dist/settings-layering.d.ts.map +1 -0
  141. package/dist/settings-layering.js +134 -0
  142. package/dist/settings-layering.js.map +1 -0
  143. package/dist/settings.d.ts +238 -0
  144. package/dist/settings.d.ts.map +1 -0
  145. package/dist/settings.js +378 -0
  146. package/dist/settings.js.map +1 -0
  147. package/dist/skill-manifest.d.ts +18 -0
  148. package/dist/skill-manifest.d.ts.map +1 -0
  149. package/dist/skill-manifest.js +125 -0
  150. package/dist/skill-manifest.js.map +1 -0
  151. package/dist/source-text.d.ts +83 -0
  152. package/dist/source-text.d.ts.map +1 -0
  153. package/dist/source-text.js +310 -0
  154. package/dist/source-text.js.map +1 -0
  155. package/dist/suppression-ceiling.d.ts +48 -0
  156. package/dist/suppression-ceiling.d.ts.map +1 -0
  157. package/dist/suppression-ceiling.js +115 -0
  158. package/dist/suppression-ceiling.js.map +1 -0
  159. package/dist/tailwind-tokens.d.ts +16 -0
  160. package/dist/tailwind-tokens.d.ts.map +1 -0
  161. package/dist/tailwind-tokens.js +70 -0
  162. package/dist/tailwind-tokens.js.map +1 -0
  163. package/dist/text-escapes.d.ts +7 -0
  164. package/dist/text-escapes.d.ts.map +1 -0
  165. package/dist/text-escapes.js +14 -0
  166. package/dist/text-escapes.js.map +1 -0
  167. package/dist/toolchain-pins.d.ts +42 -0
  168. package/dist/toolchain-pins.d.ts.map +1 -0
  169. package/dist/toolchain-pins.js +99 -0
  170. package/dist/toolchain-pins.js.map +1 -0
  171. package/dist/url-safety.d.ts +12 -0
  172. package/dist/url-safety.d.ts.map +1 -0
  173. package/dist/url-safety.js +58 -0
  174. package/dist/url-safety.js.map +1 -0
  175. package/dist/version-policy.d.ts +99 -0
  176. package/dist/version-policy.d.ts.map +1 -0
  177. package/dist/version-policy.js +306 -0
  178. package/dist/version-policy.js.map +1 -0
  179. package/dist/vulnerability-policy.d.ts +66 -0
  180. package/dist/vulnerability-policy.d.ts.map +1 -0
  181. package/dist/vulnerability-policy.js +106 -0
  182. package/dist/vulnerability-policy.js.map +1 -0
  183. package/dist/wiring-policy.d.ts +226 -0
  184. package/dist/wiring-policy.d.ts.map +1 -0
  185. package/dist/wiring-policy.js +458 -0
  186. package/dist/wiring-policy.js.map +1 -0
  187. package/dist/wiring-violation.d.ts +6 -0
  188. package/dist/wiring-violation.d.ts.map +1 -0
  189. package/dist/wiring-violation.js +8 -0
  190. package/dist/wiring-violation.js.map +1 -0
  191. package/dist/workspace-policy.d.ts +177 -0
  192. package/dist/workspace-policy.d.ts.map +1 -0
  193. package/dist/workspace-policy.js +228 -0
  194. package/dist/workspace-policy.js.map +1 -0
  195. package/dist/yaml-blocks.d.ts +43 -0
  196. package/dist/yaml-blocks.d.ts.map +1 -0
  197. package/dist/yaml-blocks.js +87 -0
  198. package/dist/yaml-blocks.js.map +1 -0
  199. package/package.json +41 -0
@@ -0,0 +1,42 @@
1
+ /** An analyzer ploaness runs in a container rather than installing from the registry. */
2
+ export type ContainerTool = 'gitleaks' | 'hadolint' | 'actionlint' | 'shellcheck';
3
+ /** The exact image each containerised analyzer runs, by digest. */
4
+ export declare const CONTAINER_IMAGES: Readonly<Record<ContainerTool, string>>;
5
+ /** Matches a reference that names image bytes AND the release they are, and only such a reference. */
6
+ export declare const DIGEST_PINNED: RegExp;
7
+ /**
8
+ * Report every containerised analyzer whose image is not pinned to an exact digest.
9
+ * @param images the image reference declared for each tool.
10
+ * @returns one message per unpinned tool; empty means every verdict is reproducible.
11
+ */
12
+ export declare const findUnpinnedImages: (images: Readonly<Record<string, string>>) => readonly string[];
13
+ /**
14
+ * The lowest Node major an engines range admits.
15
+ * @param enginesNode the declared range, such as `>=26`.
16
+ * @returns the major version, or undefined when the range names none.
17
+ */
18
+ export declare const minimumNodeMajor: (enginesNode: string | undefined) => number | undefined;
19
+ /**
20
+ * The exact pnpm version a `packageManager` specifier names.
21
+ *
22
+ * The version has one source - the `packageManager` pin - and everything else that states it is
23
+ * derived from here. `engines.pnpm` was pinned separately at `>=11` while `packageManager` said
24
+ * `pnpm@11.9.0`, which is the range ban applied to every dependency except the tool that resolves
25
+ * them: it told a reader that any pnpm 11 would build the same tree.
26
+ * @param packageManager the declared specifier, such as `pnpm@11.9.0`.
27
+ * @returns the version, or undefined when the specifier names another manager or no version.
28
+ */
29
+ export declare const pinnedPnpmVersion: (packageManager: string | undefined) => string | undefined;
30
+ /**
31
+ * Report the pnpm actually running this command when it is not the pinned one.
32
+ *
33
+ * `packageManager` is a declaration, and Corepack obeys it only where Corepack is enabled. Without
34
+ * this the pin was a comment: a project could declare `pnpm@11.9.0`, pass the wiring gate, and resolve
35
+ * its whole tree with another pnpm - which is the one difference that changes what every other pin
36
+ * means, because the lockfile it produces is the input to every gate that follows.
37
+ * @param userAgent the `npm_config_user_agent` of the running process.
38
+ * @param required the pinned version, from {@link pinnedPnpmVersion}.
39
+ * @returns one finding when the running pnpm disagrees; empty when it agrees or cannot be observed.
40
+ */
41
+ export declare const findPnpmRuntimeViolations: (userAgent: string | undefined, required: string | undefined) => readonly string[];
42
+ //# sourceMappingURL=toolchain-pins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolchain-pins.d.ts","sourceRoot":"","sources":["../src/toolchain-pins.ts"],"names":[],"mappings":"AAoBA,yFAAyF;AACzF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,YAAY,GAAG,YAAY,CAAA;AAEjF,mEAAmE;AACnE,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAWpE,CAAA;AAED,sGAAsG;AACtG,eAAO,MAAM,aAAa,EAAE,MACsC,CAAA;AAElE;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAG,SAAS,MAAM,EAOxF,CAAA;AAOL;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SAG3E,CAAA;AAOD;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,gBAAgB,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SAG/E,CAAA;AAMD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,GACpC,WAAW,MAAM,GAAG,SAAS,EAC7B,UAAU,MAAM,GAAG,SAAS,KAC3B,SAAS,MAAM,EAajB,CAAA"}
@@ -0,0 +1,99 @@
1
+ // The container images the gates run their analyzers in.
2
+ //
3
+ // Pinned by digest rather than by tag. A tag - `latest` most obviously, but a version tag too - is a
4
+ // mutable reference: the registry can repoint it at new bytes, and the same repository would then reach
5
+ // a different verdict on a different day. A digest names the bytes, so it is the only form that makes
6
+ // "an upstream release cannot change a verdict while the repository does not change" literally true.
7
+ //
8
+ // The tag is written too, and it is not decoration. A digest alone says nothing about which release it
9
+ // is, so the version used to live in a comment beside it - unreadable to anything but a person, which
10
+ // is why nothing reported when a newer image existed. `<repo>:<tag>@sha256:<digest>` states both: docker
11
+ // resolves the digest and ignores the tag, and the freshness check reads the tag to ask the registry
12
+ // what is newer. A bare tag is still rejected; the spec requires both halves.
13
+ //
14
+ // To move a pin, read the new digest with `docker buildx imagetools inspect <repo>:<tag>`.
15
+ //
16
+ // Two of the four tags below are not the versions their comments used to claim. The hadolint pin was
17
+ // commented `2.14.0` and is the bytes of `v2.15.1`; the actionlint pin was commented `1.7.7` and is
18
+ // `1.7.12`. Nothing could have caught that while the version was prose, which is the whole argument for
19
+ // writing it where a check can read it.
20
+ /** The exact image each containerised analyzer runs, by digest. */
21
+ export const CONTAINER_IMAGES = {
22
+ gitleaks: 'zricethezav/gitleaks:v8.30.1@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f',
23
+ hadolint: 'hadolint/hadolint:v2.15.1@sha256:32dac94127fd60b7b7e3fbfc65e1383b9b5e25c9bfd7b8536de7a539fe68a12d',
24
+ actionlint: 'rhysd/actionlint:1.7.12@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667',
25
+ // The standard makes a check a repository implements itself into its source code, and the ploaness
26
+ // verification command is a shell script that no analyzer read.
27
+ shellcheck: 'koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d',
28
+ };
29
+ /** Matches a reference that names image bytes AND the release they are, and only such a reference. */
30
+ export const DIGEST_PINNED = /^[a-z0-9][a-z0-9._/-]*:[A-Za-z0-9][\w.-]*@sha256:[0-9a-f]{64}$/;
31
+ /**
32
+ * Report every containerised analyzer whose image is not pinned to an exact digest.
33
+ * @param images the image reference declared for each tool.
34
+ * @returns one message per unpinned tool; empty means every verdict is reproducible.
35
+ */
36
+ export const findUnpinnedImages = (images) => Object.entries(images)
37
+ .filter(([, reference]) => !DIGEST_PINNED.test(reference))
38
+ .map(([tool, reference]) => `${tool} runs "${reference}", which does not name both the release and its bytes; ` +
39
+ 'pin it as <repo>:<tag>@sha256:<digest>');
40
+ // The major version an `engines.node` range admits at its lowest. The CLI carried its own
41
+ // `MINIMUM_NODE_MAJOR = 26` and decided the verdict with it, which put a rule in the I/O layer and made
42
+ // a fourth copy of a number `pins.json` already states - and the copy that decided was the loose one.
43
+ const LEADING_MAJOR = /(\d+)/;
44
+ /**
45
+ * The lowest Node major an engines range admits.
46
+ * @param enginesNode the declared range, such as `>=26`.
47
+ * @returns the major version, or undefined when the range names none.
48
+ */
49
+ export const minimumNodeMajor = (enginesNode) => {
50
+ const found = LEADING_MAJOR.exec(enginesNode ?? '');
51
+ return found === null ? undefined : Number(found[1]);
52
+ };
53
+ // Corepack permits an integrity suffix - `pnpm@11.9.0+sha512.<hash>` - and that specifier names the
54
+ // same version as the bare one. Reading up to the `+` keeps a project that pasted Corepack's own
55
+ // output from failing a rule about a number it got right.
56
+ const PNPM_SPECIFIER = /^pnpm@([^+\s]+)(?:\+\S+)?$/;
57
+ /**
58
+ * The exact pnpm version a `packageManager` specifier names.
59
+ *
60
+ * The version has one source - the `packageManager` pin - and everything else that states it is
61
+ * derived from here. `engines.pnpm` was pinned separately at `>=11` while `packageManager` said
62
+ * `pnpm@11.9.0`, which is the range ban applied to every dependency except the tool that resolves
63
+ * them: it told a reader that any pnpm 11 would build the same tree.
64
+ * @param packageManager the declared specifier, such as `pnpm@11.9.0`.
65
+ * @returns the version, or undefined when the specifier names another manager or no version.
66
+ */
67
+ export const pinnedPnpmVersion = (packageManager) => {
68
+ const found = PNPM_SPECIFIER.exec(packageManager ?? '');
69
+ return found?.[1];
70
+ };
71
+ // pnpm identifies itself to every script it runs: `pnpm/11.9.0 npm/? node/v26.2.0 darwin arm64`. The
72
+ // prefix is anchored at a word boundary because `@pnpm/exe/11.9.0` would otherwise match as `pnpm`.
73
+ const PNPM_USER_AGENT = /(?:^|\s)pnpm\/(\S+)/;
74
+ /**
75
+ * Report the pnpm actually running this command when it is not the pinned one.
76
+ *
77
+ * `packageManager` is a declaration, and Corepack obeys it only where Corepack is enabled. Without
78
+ * this the pin was a comment: a project could declare `pnpm@11.9.0`, pass the wiring gate, and resolve
79
+ * its whole tree with another pnpm - which is the one difference that changes what every other pin
80
+ * means, because the lockfile it produces is the input to every gate that follows.
81
+ * @param userAgent the `npm_config_user_agent` of the running process.
82
+ * @param required the pinned version, from {@link pinnedPnpmVersion}.
83
+ * @returns one finding when the running pnpm disagrees; empty when it agrees or cannot be observed.
84
+ */
85
+ export const findPnpmRuntimeViolations = (userAgent, required) => {
86
+ const found = PNPM_USER_AGENT.exec(userAgent ?? '');
87
+ const running = found?.[1];
88
+ // An absent or non-pnpm user agent means this command was not started by pnpm at all - `npx`, a
89
+ // direct `node` invocation, a CI step calling the binary. There is no version to disagree with, and
90
+ // inventing a failure there would report a project for how its operator launched one command.
91
+ if (required === undefined || running === undefined || running === required) {
92
+ return [];
93
+ }
94
+ return [
95
+ `pnpm ${running} is running this command but ploaness pins pnpm@${required}; ` +
96
+ 'enable Corepack, because the package manager resolves the tree every other pin describes',
97
+ ];
98
+ };
99
+ //# sourceMappingURL=toolchain-pins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolchain-pins.js","sourceRoot":"","sources":["../src/toolchain-pins.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,EAAE;AACF,qGAAqG;AACrG,wGAAwG;AACxG,sGAAsG;AACtG,qGAAqG;AACrG,EAAE;AACF,uGAAuG;AACvG,sGAAsG;AACtG,yGAAyG;AACzG,qGAAqG;AACrG,8EAA8E;AAC9E,EAAE;AACF,2FAA2F;AAC3F,EAAE;AACF,qGAAqG;AACrG,oGAAoG;AACpG,wGAAwG;AACxG,wCAAwC;AAKxC,mEAAmE;AACnE,MAAM,CAAC,MAAM,gBAAgB,GAA4C;IACvE,QAAQ,EACN,sGAAsG;IACxG,QAAQ,EACN,mGAAmG;IACrG,UAAU,EACR,iGAAiG;IACnG,mGAAmG;IACnG,gEAAgE;IAChE,UAAU,EACR,qGAAqG;CACxG,CAAA;AAED,sGAAsG;AACtG,MAAM,CAAC,MAAM,aAAa,GACxB,gEAAgE,CAAA;AAElE;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,MAAwC,EAAqB,EAAE,CAChG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;KACnB,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,CAA4B,EAAW,EAAE,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;KAC7F,GAAG,CACF,CAAC,CAAC,IAAI,EAAE,SAAS,CAA4B,EAAU,EAAE,CACvD,GAAG,IAAI,UAAU,SAAS,yDAAyD;IACnF,wCAAwC,CAC3C,CAAA;AAEL,0FAA0F;AAC1F,wGAAwG;AACxG,sGAAsG;AACtG,MAAM,aAAa,GAAW,OAAO,CAAA;AAErC;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,WAA+B,EAAsB,EAAE;IACtF,MAAM,KAAK,GAA2B,aAAa,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAA;IAC3E,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;AACtD,CAAC,CAAA;AAED,oGAAoG;AACpG,iGAAiG;AACjG,0DAA0D;AAC1D,MAAM,cAAc,GAAW,4BAA4B,CAAA;AAE3D;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,cAAkC,EAAsB,EAAE;IAC1F,MAAM,KAAK,GAA2B,cAAc,CAAC,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC,CAAA;IAC/E,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,CAAA;AACnB,CAAC,CAAA;AAED,qGAAqG;AACrG,oGAAoG;AACpG,MAAM,eAAe,GAAW,qBAAqB,CAAA;AAErD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,SAA6B,EAC7B,QAA4B,EACT,EAAE;IACrB,MAAM,KAAK,GAA2B,eAAe,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAA;IAC3E,MAAM,OAAO,GAAuB,KAAK,EAAE,CAAC,CAAC,CAAC,CAAA;IAC9C,gGAAgG;IAChG,oGAAoG;IACpG,8FAA8F;IAC9F,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5E,OAAO,EAAE,CAAA;IACX,CAAC;IACD,OAAO;QACL,QAAQ,OAAO,mDAAmD,QAAQ,IAAI;YAC5E,0FAA0F;KAC7F,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Neutralise an untrusted href so a stored `javascript:`, `data:`, or `vbscript:` URL cannot execute
3
+ * when it is rendered into an anchor.
4
+ *
5
+ * Returning {@link NEUTRAL} is this function doing its job rather than falling back silently: the
6
+ * caller asked for an href safe to render, and a link that goes nowhere is that answer for an unsafe
7
+ * input. A caller that must know the input was rejected compares the result against what it passed in.
8
+ * @param href the raw href, typically authored in a CMS field.
9
+ * @returns the href unchanged when its scheme is safe or absent, and `#` when it is neither.
10
+ */
11
+ export declare const safeHref: (href: string) => string;
12
+ //# sourceMappingURL=url-safety.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-safety.d.ts","sourceRoot":"","sources":["../src/url-safety.ts"],"names":[],"mappings":"AAqCA;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,KAAG,MAevC,CAAA"}
@@ -0,0 +1,58 @@
1
+ // The first inhabitant of this package that a consumer's application calls rather than a rule the
2
+ // harness applies, and the placement is deliberate rather than convenient. It is a pure function over a
3
+ // string, which is what this package holds and what its coverage floor measures; a package of its own
4
+ // for one function would be a versioning surface bought for nothing. A reader who expects only rules
5
+ // here should take this comment as the reason rather than as an accident.
6
+ //
7
+ // It exists because React does not cover this and every content-managed site needs it. React escapes
8
+ // text content, so an editor cannot inject markup through a field - but an href is an attribute, and
9
+ // `javascript:alert(1)` in a link field executes as the site the moment a visitor clicks it. Nothing in
10
+ // the framework, the type system, or a linter reports that: the value is a string and it is rendered as
11
+ // a string. Every project mapping a CMS field into an anchor has this problem, so the harness owns the
12
+ // answer rather than leaving each project to rediscover it.
13
+ /** The schemes safe to render into an anchor. A URL carrying no scheme at all is relative, and safe. */
14
+ const SAFE_SCHEMES = new Set(['http', 'https', 'mailto', 'tel']);
15
+ // A well-formed scheme, anchored at both ends so a relative path that merely contains a colon later -
16
+ // `/schedule:today` - is not read as declaring one.
17
+ const SCHEME_SHAPE = /^[a-z][a-z\d+.-]*$/;
18
+ // Whitespace and control characters are removed before the scheme is read, because a browser removes
19
+ // them too: a `javascript:` URL with a tab inside the scheme is still a javascript URL by the time it
20
+ // is navigated, and reading the raw string would classify it as relative and pass it straight through.
21
+ // Stripping more than a browser does can only move an input from relative to scheme-bearing, which is
22
+ // the direction that neutralises rather than admits, so the wider strip is the conservative choice.
23
+ //
24
+ // `\p{Cc}` rather than the code points themselves: a class naming them is a control character inside a
25
+ // regular expression, which the lint pass rejects and is right to, since the literal bytes would then
26
+ // sit in this file invisibly. The property escape names the category in the open, and it leaves every
27
+ // ordinary character - a hyphen in a host, for one - untouched.
28
+ const IGNORED_BY_BROWSERS = /[\s\p{Cc}]+/gu;
29
+ /** What an unsafe href is replaced with: a link that goes nowhere rather than one that runs. */
30
+ const NEUTRAL = '#';
31
+ const NO_COLON = -1;
32
+ /**
33
+ * Neutralise an untrusted href so a stored `javascript:`, `data:`, or `vbscript:` URL cannot execute
34
+ * when it is rendered into an anchor.
35
+ *
36
+ * Returning {@link NEUTRAL} is this function doing its job rather than falling back silently: the
37
+ * caller asked for an href safe to render, and a link that goes nowhere is that answer for an unsafe
38
+ * input. A caller that must know the input was rejected compares the result against what it passed in.
39
+ * @param href the raw href, typically authored in a CMS field.
40
+ * @returns the href unchanged when its scheme is safe or absent, and `#` when it is neither.
41
+ */
42
+ export const safeHref = (href) => {
43
+ const trimmed = href.trim();
44
+ if (trimmed.length === 0) {
45
+ return NEUTRAL;
46
+ }
47
+ const readable = trimmed.replaceAll(IGNORED_BY_BROWSERS, '').toLowerCase();
48
+ const colon = readable.indexOf(':');
49
+ if (colon === NO_COLON) {
50
+ return trimmed;
51
+ }
52
+ const scheme = readable.slice(0, colon);
53
+ if (!SCHEME_SHAPE.test(scheme)) {
54
+ return trimmed;
55
+ }
56
+ return SAFE_SCHEMES.has(scheme) ? trimmed : NEUTRAL;
57
+ };
58
+ //# sourceMappingURL=url-safety.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-safety.js","sourceRoot":"","sources":["../src/url-safety.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,wGAAwG;AACxG,sGAAsG;AACtG,qGAAqG;AACrG,0EAA0E;AAC1E,EAAE;AACF,qGAAqG;AACrG,qGAAqG;AACrG,wGAAwG;AACxG,wGAAwG;AACxG,uGAAuG;AACvG,4DAA4D;AAE5D,wGAAwG;AACxG,MAAM,YAAY,GAAwB,IAAI,GAAG,CAAS,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAA;AAE7F,sGAAsG;AACtG,oDAAoD;AACpD,MAAM,YAAY,GAAW,oBAAoB,CAAA;AAEjD,qGAAqG;AACrG,sGAAsG;AACtG,uGAAuG;AACvG,sGAAsG;AACtG,oGAAoG;AACpG,EAAE;AACF,uGAAuG;AACvG,sGAAsG;AACtG,sGAAsG;AACtG,gEAAgE;AAChE,MAAM,mBAAmB,GAAW,eAAe,CAAA;AAEnD,gGAAgG;AAChG,MAAM,OAAO,GAAW,GAAG,CAAA;AAE3B,MAAM,QAAQ,GAAW,CAAC,CAAC,CAAA;AAE3B;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAU,EAAE;IAC/C,MAAM,OAAO,GAAW,IAAI,CAAC,IAAI,EAAE,CAAA;IACnC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,MAAM,QAAQ,GAAW,OAAO,CAAC,UAAU,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;IAClF,MAAM,KAAK,GAAW,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAC3C,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvB,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,MAAM,MAAM,GAAW,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;IAC/C,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/B,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,OAAO,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAA;AACrD,CAAC,CAAA"}
@@ -0,0 +1,99 @@
1
+ import type { WiringViolation } from './wiring-violation.js';
2
+ /**
3
+ * How a finding names what it actually found.
4
+ *
5
+ * Exported because `wiring-policy.ts` reported the same thing in the same words with its own copy of
6
+ * this function, and the two modules already share this direction of import.
7
+ * @param found the declared value, or undefined when the key is absent.
8
+ * @returns `missing`, or the value in quotes.
9
+ */
10
+ export declare const describeFound: (found: string | undefined) => string;
11
+ /** The meta package a governed project declares, and the root of everything it inherits from ploaness. */
12
+ export declare const HARNESS_PACKAGE: string;
13
+ /**
14
+ * Whether a package name is one of ploaness's own.
15
+ *
16
+ * Exported because the `deps` gate asks the same question when it walks the manifests a project
17
+ * inherits, and it had no way to ask it but a second copy of the two literals above - which is the
18
+ * shape of drift this repository refuses everywhere else.
19
+ * @param packageName the declared name.
20
+ * @returns true for `ploaness` and for anything under its scope.
21
+ */
22
+ export declare const isHarnessPackage: (packageName: string) => boolean;
23
+ /**
24
+ * The package whose presence makes a project a Payload application.
25
+ *
26
+ * Exported because `preflight` and `workspace-policy` both ask the question and both wrote the literal
27
+ * themselves. The name decides which scope's gates a member receives, so it is not a string to keep
28
+ * three copies of.
29
+ */
30
+ export declare const PAYLOAD_PACKAGE: string;
31
+ /**
32
+ * The package whose presence makes a member a Next application.
33
+ *
34
+ * Read for the same reason as {@link PAYLOAD_PACKAGE}: it decides which shipped configuration a member
35
+ * receives. A Next application that is not a Payload one still has a build, a client bundle and a
36
+ * browser suite to govern.
37
+ */
38
+ export declare const NEXT_PACKAGE: string;
39
+ /** What the repository as a whole must declare, and the invariants those declarations imply. */
40
+ export interface RepositoryVersionInputs {
41
+ /** Every pinned package, mapped to the exact version ploaness owns for it. */
42
+ readonly expected: Readonly<Record<string, string>>;
43
+ /** The exact `packageManager` field, or undefined when ploaness pins none. */
44
+ readonly requiredPackageManager: string | undefined;
45
+ /** The `engines` block the repository must declare. */
46
+ readonly requiredEngines: Readonly<Record<string, string>>;
47
+ /** The contents of pnpm-workspace.yaml, read at the repository root and nowhere else. */
48
+ readonly workspaceFile: string;
49
+ /** Every dependency any member declares, because a root override shadows all of them. */
50
+ readonly declaredAcrossMembers: Readonly<Record<string, string>>;
51
+ }
52
+ /** What one member must declare, judged against that member's own manifest. */
53
+ export interface PackageVersionInputs {
54
+ readonly expected: Readonly<Record<string, string>>;
55
+ /** The subset this member must declare rather than merely match when present. */
56
+ readonly required: ReadonlySet<string>;
57
+ /** The version every `@payloadcms/*` package must carry, which is the pinned `payload` version. */
58
+ readonly payloadVersion: string | undefined;
59
+ }
60
+ /**
61
+ * Return every way the repository has changed, or could change, a version ploaness owns.
62
+ *
63
+ * These are the declarations pnpm honours at the workspace root and nowhere else. Read from a member's
64
+ * directory they described a file that was not there: the overrides check found none and reported no
65
+ * violation, vouching for pins that the root had already replaced.
66
+ * @param packageJson the repository root's parsed package.json.
67
+ * @param inputs the pinned versions and the invariants they imply.
68
+ * @returns one violation per defect, in a stable order.
69
+ */
70
+ export declare const findRepositoryVersionViolations: (packageJson: Record<string, unknown>, inputs: RepositoryVersionInputs) => readonly WiringViolation[];
71
+ /**
72
+ * Return every way one member has changed, or could change, a version ploaness owns.
73
+ * @param packageJson the member's parsed package.json.
74
+ * @param inputs the pinned versions this member is held to.
75
+ * @returns one violation per defect, in a stable order.
76
+ */
77
+ export declare const findPackageVersionViolations: (packageJson: Record<string, unknown>, inputs: PackageVersionInputs) => readonly WiringViolation[];
78
+ /** What a single-package project must declare, which is both scopes over one manifest. */
79
+ export interface VersionInputs {
80
+ readonly expected: Readonly<Record<string, string>>;
81
+ readonly required: ReadonlySet<string>;
82
+ readonly payloadVersion: string | undefined;
83
+ readonly requiredPackageManager: string | undefined;
84
+ readonly requiredEngines: Readonly<Record<string, string>>;
85
+ readonly workspaceFile: string;
86
+ }
87
+ /**
88
+ * Every version violation for a repository holding exactly one package.
89
+ *
90
+ * Composes the two halves the way that case actually is: the sole member's manifest is also the
91
+ * repository's, so an override at the root and a declaration in the package are the same file. Kept as
92
+ * an entry point because the spec that pins this behaviour predates the split, and its passing
93
+ * unchanged is what shows nothing was lost in it.
94
+ * @param packageJson the project's parsed package.json.
95
+ * @param inputs the pinned versions and the invariants they imply.
96
+ * @returns one violation per defect, in a stable order.
97
+ */
98
+ export declare const findVersionViolations: (packageJson: Record<string, unknown>, inputs: VersionInputs) => readonly WiringViolation[];
99
+ //# sourceMappingURL=version-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-policy.d.ts","sourceRoot":"","sources":["../src/version-policy.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAc5D;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,GAAG,SAAS,KAAG,MACV,CAAA;AAWhD,0GAA0G;AAC1G,eAAO,MAAM,eAAe,EAAE,MAAmB,CAAA;AAEjD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,KAAG,OACmB,CAAA;AA+M1E;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,MAAkB,CAAA;AAEhD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,MAAe,CAAA;AA8D1C,gGAAgG;AAChG,MAAM,WAAW,uBAAuB;IACtC,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,8EAA8E;IAC9E,QAAQ,CAAC,sBAAsB,EAAE,MAAM,GAAG,SAAS,CAAA;IACnD,uDAAuD;IACvD,QAAQ,CAAC,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC1D,yFAAyF;IACzF,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,yFAAyF;IACzF,QAAQ,CAAC,qBAAqB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACjE;AAED,+EAA+E;AAC/E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IACtC,mGAAmG;IACnG,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5C;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,+BAA+B,GAC1C,aAAa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,QAAQ,uBAAuB,KAC9B,SAAS,eAAe,EAc1B,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,GACvC,aAAa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,QAAQ,oBAAoB,KAC3B,SAAS,eAAe,EAK1B,CAAA;AAED,0FAA0F;AAC1F,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IACtC,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3C,QAAQ,CAAC,sBAAsB,EAAE,MAAM,GAAG,SAAS,CAAA;IACnD,QAAQ,CAAC,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC1D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAC/B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,GAChC,aAAa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,QAAQ,aAAa,KACpB,SAAS,eAAe,EAa1B,CAAA"}
@@ -0,0 +1,306 @@
1
+ // The versions a governed project declares, and every way it could change one without saying so.
2
+ //
3
+ // This is the wiring module's other half. `wiring-policy.ts` judges the files a project points at
4
+ // ploaness with; this judges the numbers beside its dependencies. The two were one file until it passed
5
+ // the size cap, which is the cap doing its job: they answer different questions and share only the
6
+ // helpers below.
7
+ import { findOverrides, OVERRIDE_KEYS } from './install-policy.js';
8
+ import { asOptionalText, asRecord, asStringRecord, declaredDependencies } from './json-shapes.js';
9
+ import { pinnedPnpmVersion } from './toolchain-pins.js';
10
+ // Which block a package is actually declared in. A finding that named `devDependencies` for a package
11
+ // sitting in `dependencies` sent the reader to the wrong half of the file - and the framework pins,
12
+ // which are runtime dependencies, are exactly the ones that would be misreported.
13
+ const blockOf = (packageJson, name) => {
14
+ if (Object.hasOwn(asStringRecord(packageJson['dependencies']), name)) {
15
+ return 'dependencies';
16
+ }
17
+ return Object.hasOwn(asStringRecord(packageJson['devDependencies']), name)
18
+ ? 'devDependencies'
19
+ : undefined;
20
+ };
21
+ /**
22
+ * How a finding names what it actually found.
23
+ *
24
+ * Exported because `wiring-policy.ts` reported the same thing in the same words with its own copy of
25
+ * this function, and the two modules already share this direction of import.
26
+ * @param found the declared value, or undefined when the key is absent.
27
+ * @returns `missing`, or the value in quotes.
28
+ */
29
+ export const describeFound = (found) => found === undefined ? 'missing' : `"${found}"`;
30
+ // The harness's own packages, pointed at a local artefact. A consumer verifying ploaness before it is
31
+ // published resolves them from a tarball, and that is the arrangement `it/verify.sh` exists to exercise
32
+ // - so the rule has to permit it deliberately. It permitted it by accident until now: the override
33
+ // reader split the line at its LAST colon, so `ploaness: "file:../ploaness-1.0.0.tgz"` parsed as a
34
+ // package called `ploaness: "file` and matched nothing. Repairing the reader turned an accident into a
35
+ // finding, which is the right time to say what was actually intended.
36
+ const HARNESS_SCOPE = '@ploaness/';
37
+ const LOCAL_ARTEFACT = /^(?:file|link|workspace):/;
38
+ /** The meta package a governed project declares, and the root of everything it inherits from ploaness. */
39
+ export const HARNESS_PACKAGE = 'ploaness';
40
+ /**
41
+ * Whether a package name is one of ploaness's own.
42
+ *
43
+ * Exported because the `deps` gate asks the same question when it walks the manifests a project
44
+ * inherits, and it had no way to ask it but a second copy of the two literals above - which is the
45
+ * shape of drift this repository refuses everywhere else.
46
+ * @param packageName the declared name.
47
+ * @returns true for `ploaness` and for anything under its scope.
48
+ */
49
+ export const isHarnessPackage = (packageName) => packageName === HARNESS_PACKAGE || packageName.startsWith(HARNESS_SCOPE);
50
+ const isHarnessTarball = (entry) => isHarnessPackage(entry.packageName) && LOCAL_ARTEFACT.test(entry.specifier);
51
+ const overrideViolation = (entry, expected, declared) => {
52
+ if (isHarnessTarball(entry)) {
53
+ return [];
54
+ }
55
+ const location = `pnpm-workspace.yaml ${entry.key}.${entry.packageName}`;
56
+ if (Object.hasOwn(expected, entry.packageName)) {
57
+ return [
58
+ {
59
+ location,
60
+ reason: 'redefines a version ploaness pins; remove it, because the pin decides what the ' +
61
+ 'gates run against',
62
+ },
63
+ ];
64
+ }
65
+ const own = declared[entry.packageName];
66
+ return own === undefined
67
+ ? []
68
+ : [
69
+ {
70
+ location,
71
+ reason: `overrides a package the project declares at "${own}"; change the declaration ` +
72
+ 'instead, or the installed version is not the declared one',
73
+ },
74
+ ];
75
+ };
76
+ // A project may override what it does not declare, and may not override what it does. The permitted
77
+ // case is a transitive package carrying an advisory with no upgrade path above it, which the project
78
+ // can reach no other way and which the standard says to resolve by upgrading rather than excusing. The
79
+ // forbidden case is a package whose version the project already wrote down: two declarations of one
80
+ // version will not stay equal, and the one that loses is the one every reader believes. The ploaness
81
+ // packages are neither, so a pre-publication consumer may still point them at a local tarball.
82
+ // The declared set is the union across members rather than the root's own, because a root override
83
+ // shadows a version ANY member wrote down - and in a workspace the package that declared it is usually
84
+ // not the root at all.
85
+ const checkPinnedOverridesAcross = (workspaceFile, expected, declaredAcrossMembers) => findOverrides(workspaceFile).flatMap((entry) => overrideViolation(entry, expected, { ...declaredAcrossMembers }));
86
+ // Corepack reads `packageManager` and runs exactly that version, so it decides how every other pinned
87
+ // version is resolved. A project on a different pnpm can produce a different tree from the same
88
+ // lockfile, which makes every pin above it a statement about a graph nobody built.
89
+ const checkPackageManager = (packageJson, required) => {
90
+ if (required === undefined) {
91
+ return [];
92
+ }
93
+ const declared = packageJson['packageManager'];
94
+ return declared === required
95
+ ? []
96
+ : [
97
+ {
98
+ location: 'package.json packageManager',
99
+ reason: `is ${describeFound(asOptionalText(declared))} but ploaness requires "${required}"`,
100
+ },
101
+ ];
102
+ };
103
+ // The `engines.pnpm` entry is DERIVED from the `packageManager` pin rather than pinned beside it.
104
+ // Corepack reads one field and an installer reads the other, so two literals would be two statements
105
+ // about a single version - and this was the pair that had already drifted apart in meaning: an exact
106
+ // `pnpm@11.9.0` beside a `>=11` floor told a reader that any pnpm 11 would resolve the same tree.
107
+ const enginesRequiredBy = (inputs) => {
108
+ const pnpm = pinnedPnpmVersion(inputs.requiredPackageManager);
109
+ return pnpm === undefined ? inputs.requiredEngines : { ...inputs.requiredEngines, pnpm };
110
+ };
111
+ // `preflight` checks the Node that is actually running, which is the version a gate is executed by.
112
+ // The `engines` block is a different statement: it is what the project tells an installer, a CI image
113
+ // and a reader to use. Leaving it unchecked let a project declare a runtime ploaness refuses.
114
+ const checkEngines = (packageJson, required) => {
115
+ const declared = asStringRecord(packageJson['engines']);
116
+ return Object.entries(required).flatMap(([name, range]) => declared[name] === range
117
+ ? []
118
+ : [
119
+ {
120
+ location: `package.json engines.${name}`,
121
+ reason: `is ${describeFound(declared[name])} but ploaness requires "${range}"`,
122
+ },
123
+ ]);
124
+ };
125
+ // The same four keys `install-policy.ts` reads out of the workspace file, read here out of package.json
126
+ // instead: an override or resolution declared locally; a patch, which swaps a package's code while its
127
+ // version says otherwise; and a package extension, which rewrites a manifest the resolver then obeys.
128
+ // None is visible in the dependency block a reader checks, which is what makes them worth naming - and
129
+ // the list is imported rather than restated, because two copies of it in one gate will not stay equal.
130
+ // A patch is keyed by `name@version`, and a scoped name begins with the same character, so the split is
131
+ // on the last `@` rather than the first.
132
+ const packageNameOf = (key) => {
133
+ const at = key.lastIndexOf('@');
134
+ return at > 0 ? key.slice(0, at) : key;
135
+ };
136
+ const escapeEntries = (packageJson) => OVERRIDE_KEYS.flatMap((key) => [packageJson, asRecord(packageJson['pnpm'])].flatMap((holder) => Object.keys(asRecord(holder[key])).map((entry) => [key, entry])));
137
+ const checkVersionEscapes = (packageJson, expected) => escapeEntries(packageJson)
138
+ .filter(([, entry]) => Object.hasOwn(expected, packageNameOf(entry ?? '')))
139
+ .map(([key, entry]) => ({
140
+ location: `package.json ${key ?? ''}.${entry ?? ''}`,
141
+ reason: 'changes what a version ploaness pins installs; the pin decides what the gates run ' +
142
+ 'against, and a project cannot answer for a version it did not choose',
143
+ }));
144
+ // A specifier that is not a registry version at all: a local tarball, a workspace link, a git or npm
145
+ // alias. None of them is a range, and a pre-publication consumer resolves the ploaness packages this
146
+ // way, so the range ban must read past them rather than through them.
147
+ const NON_REGISTRY = /^(?:file|link|workspace|catalog|git\+[a-z]+|git|github|npm|https?):/;
148
+ // Anything that resolves to "whichever version is newest at install time". The standard pins the
149
+ // toolchain so an upstream release cannot change a verdict while the project stays unchanged, and a
150
+ // range on an application dependency is the same hole one layer down: the build, the suite and the
151
+ // end-to-end run all execute against something nobody wrote down. `*` and an empty specifier are the
152
+ // widest ranges of all rather than exceptions to the rule.
153
+ const RANGE_OPERATOR = /^\s*(?:[\^~><=*]|$)/;
154
+ // A union, a hyphen range, or an `x` standing in for a whole version component. Each `x` form is
155
+ // anchored at a component boundary: written unanchored, `.x` also matched a legitimate exact
156
+ // prerelease such as `1.0.0-canary.x1` and reported it as a range.
157
+ const RANGE_UNION = /\|\||\s-\s|(?:^|[.\s])x(?:[.\s]|$)/;
158
+ // An exact version begins with a digit. Everything else that is neither a range operator nor a
159
+ // non-registry specifier is a DIST TAG - `latest`, `next`, `beta` - which is the widest range of all:
160
+ // it resolves to whatever the publisher moved the tag to since the project last installed. The rule
161
+ // used to test only for range syntax, so every dist tag passed as though it were a pinned version.
162
+ const EXACT_VERSION = /^\d/;
163
+ // An `npm:` alias names a registry package like any other declaration - `npm:preact@^10` carries a
164
+ // range, and reading past the whole prefix let it through. Only the version half is judged, by the same
165
+ // rule; the alias itself is a naming decision the project is entitled to make.
166
+ const NPM_ALIAS = 'npm:';
167
+ const isExactVersion = (specifier) => {
168
+ if (specifier.startsWith(NPM_ALIAS)) {
169
+ const aliased = specifier.slice(NPM_ALIAS.length);
170
+ const at = aliased.lastIndexOf('@');
171
+ return at > 0 && isExactVersion(aliased.slice(at + 1));
172
+ }
173
+ if (NON_REGISTRY.test(specifier)) {
174
+ return true;
175
+ }
176
+ if (RANGE_OPERATOR.test(specifier) || RANGE_UNION.test(specifier)) {
177
+ return false;
178
+ }
179
+ return EXACT_VERSION.test(specifier);
180
+ };
181
+ const checkExactVersions = (packageJson) => Object.entries(declaredDependencies(packageJson))
182
+ .filter(([, specifier]) => !isExactVersion(specifier))
183
+ .map(([name, specifier]) => ({
184
+ location: `package.json ${name}`,
185
+ reason: `is "${specifier}", which is a range; declare one exact version, because a range ` +
186
+ 'lets an upstream release change what the gates run against',
187
+ }));
188
+ // Payload refuses to boot, or fails in ways that read as application defects, when its own packages
189
+ // disagree about their version. The rule is derived from the pinned `payload` rather than written as a
190
+ // list, so a project adding `@payloadcms/plugin-form-builder` is covered by the rule that already
191
+ // exists instead of by an entry somebody has to remember.
192
+ const PAYLOAD_SCOPE = '@payloadcms/';
193
+ /**
194
+ * The package whose presence makes a project a Payload application.
195
+ *
196
+ * Exported because `preflight` and `workspace-policy` both ask the question and both wrote the literal
197
+ * themselves. The name decides which scope's gates a member receives, so it is not a string to keep
198
+ * three copies of.
199
+ */
200
+ export const PAYLOAD_PACKAGE = 'payload';
201
+ /**
202
+ * The package whose presence makes a member a Next application.
203
+ *
204
+ * Read for the same reason as {@link PAYLOAD_PACKAGE}: it decides which shipped configuration a member
205
+ * receives. A Next application that is not a Payload one still has a build, a client bundle and a
206
+ * browser suite to govern.
207
+ */
208
+ export const NEXT_PACKAGE = 'next';
209
+ const checkPayloadFamily = (packageJson, payloadVersion) => {
210
+ if (payloadVersion === undefined) {
211
+ return [];
212
+ }
213
+ return Object.entries(declaredDependencies(packageJson))
214
+ .filter(([name, version]) => name.startsWith(PAYLOAD_SCOPE) && version !== payloadVersion)
215
+ .map(([name, version]) => ({
216
+ location: `package.json ${name}`,
217
+ reason: `is "${version}" but payload is pinned at "${payloadVersion}"; ` +
218
+ 'Payload fails at runtime when its own packages disagree',
219
+ }));
220
+ };
221
+ // Two obligations, not one. A project must DECLARE the few packages every project uses, because under
222
+ // the strict pnpm layout its own specs could not resolve them otherwise. Every other pinned package
223
+ // must MATCH when the project declares it, but is not forced on a project that has no use for it -
224
+ // requiring a declaration there would manufacture a dependency the dead-code gate then reports as
225
+ // unused. Either way no pinned version can float, which is the point.
226
+ const checkTestLibraries = (packageJson, expected, required) => {
227
+ const declared = declaredDependencies(packageJson);
228
+ return Object.entries(expected).flatMap(([name, version]) => {
229
+ const found = declared[name];
230
+ if (found === undefined) {
231
+ return required.has(name)
232
+ ? [
233
+ {
234
+ location: `package.json ${name}`,
235
+ reason: `missing; ploaness pins it, so the project must declare it at ${version}`,
236
+ },
237
+ ]
238
+ : [];
239
+ }
240
+ return found === version
241
+ ? []
242
+ : [
243
+ {
244
+ location: `package.json ${blockOf(packageJson, name) ?? ''}.${name}`,
245
+ reason: `is "${found}" but ploaness pins it at "${version}"; ` +
246
+ 'a range lets an upstream release change a verdict',
247
+ },
248
+ ];
249
+ });
250
+ };
251
+ /**
252
+ * Return every way the repository has changed, or could change, a version ploaness owns.
253
+ *
254
+ * These are the declarations pnpm honours at the workspace root and nowhere else. Read from a member's
255
+ * directory they described a file that was not there: the overrides check found none and reported no
256
+ * violation, vouching for pins that the root had already replaced.
257
+ * @param packageJson the repository root's parsed package.json.
258
+ * @param inputs the pinned versions and the invariants they imply.
259
+ * @returns one violation per defect, in a stable order.
260
+ */
261
+ export const findRepositoryVersionViolations = (packageJson, inputs) => [
262
+ ...checkPackageManager(packageJson, inputs.requiredPackageManager),
263
+ ...checkEngines(packageJson, enginesRequiredBy({
264
+ requiredEngines: inputs.requiredEngines,
265
+ requiredPackageManager: inputs.requiredPackageManager,
266
+ })),
267
+ ...checkPinnedOverridesAcross(inputs.workspaceFile, inputs.expected, inputs.declaredAcrossMembers),
268
+ ];
269
+ /**
270
+ * Return every way one member has changed, or could change, a version ploaness owns.
271
+ * @param packageJson the member's parsed package.json.
272
+ * @param inputs the pinned versions this member is held to.
273
+ * @returns one violation per defect, in a stable order.
274
+ */
275
+ export const findPackageVersionViolations = (packageJson, inputs) => [
276
+ ...checkTestLibraries(packageJson, inputs.expected, inputs.required),
277
+ ...checkExactVersions(packageJson),
278
+ ...checkPayloadFamily(packageJson, inputs.payloadVersion),
279
+ ...checkVersionEscapes(packageJson, inputs.expected),
280
+ ];
281
+ /**
282
+ * Every version violation for a repository holding exactly one package.
283
+ *
284
+ * Composes the two halves the way that case actually is: the sole member's manifest is also the
285
+ * repository's, so an override at the root and a declaration in the package are the same file. Kept as
286
+ * an entry point because the spec that pins this behaviour predates the split, and its passing
287
+ * unchanged is what shows nothing was lost in it.
288
+ * @param packageJson the project's parsed package.json.
289
+ * @param inputs the pinned versions and the invariants they imply.
290
+ * @returns one violation per defect, in a stable order.
291
+ */
292
+ export const findVersionViolations = (packageJson, inputs) => [
293
+ ...findPackageVersionViolations(packageJson, {
294
+ expected: inputs.expected,
295
+ required: inputs.required,
296
+ payloadVersion: inputs.payloadVersion,
297
+ }),
298
+ ...findRepositoryVersionViolations(packageJson, {
299
+ expected: inputs.expected,
300
+ requiredPackageManager: inputs.requiredPackageManager,
301
+ requiredEngines: inputs.requiredEngines,
302
+ workspaceFile: inputs.workspaceFile,
303
+ declaredAcrossMembers: declaredDependencies(packageJson),
304
+ }),
305
+ ];
306
+ //# sourceMappingURL=version-policy.js.map