@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.
- package/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/access-boundary.d.ts +41 -0
- package/dist/access-boundary.d.ts.map +1 -0
- package/dist/access-boundary.js +26 -0
- package/dist/access-boundary.js.map +1 -0
- package/dist/agent-references.d.ts +12 -0
- package/dist/agent-references.d.ts.map +1 -0
- package/dist/agent-references.js +53 -0
- package/dist/agent-references.js.map +1 -0
- package/dist/architecture-layers.d.ts +11 -0
- package/dist/architecture-layers.d.ts.map +1 -0
- package/dist/architecture-layers.js +47 -0
- package/dist/architecture-layers.js.map +1 -0
- package/dist/asset-policy.d.ts +138 -0
- package/dist/asset-policy.d.ts.map +1 -0
- package/dist/asset-policy.js +312 -0
- package/dist/asset-policy.js.map +1 -0
- package/dist/banned-typography.d.ts +13 -0
- package/dist/banned-typography.d.ts.map +1 -0
- package/dist/banned-typography.js +78 -0
- package/dist/banned-typography.js.map +1 -0
- package/dist/bundle-budget.d.ts +16 -0
- package/dist/bundle-budget.d.ts.map +1 -0
- package/dist/bundle-budget.js +37 -0
- package/dist/bundle-budget.js.map +1 -0
- package/dist/commit-message.d.ts +37 -0
- package/dist/commit-message.d.ts.map +1 -0
- package/dist/commit-message.js +144 -0
- package/dist/commit-message.js.map +1 -0
- package/dist/config-references.d.ts +17 -0
- package/dist/config-references.d.ts.map +1 -0
- package/dist/config-references.js +54 -0
- package/dist/config-references.js.map +1 -0
- package/dist/container-failure.d.ts +40 -0
- package/dist/container-failure.d.ts.map +1 -0
- package/dist/container-failure.js +154 -0
- package/dist/container-failure.js.map +1 -0
- package/dist/container-files.d.ts +36 -0
- package/dist/container-files.d.ts.map +1 -0
- package/dist/container-files.js +74 -0
- package/dist/container-files.js.map +1 -0
- package/dist/container-freshness.d.ts +102 -0
- package/dist/container-freshness.d.ts.map +1 -0
- package/dist/container-freshness.js +129 -0
- package/dist/container-freshness.js.map +1 -0
- package/dist/dependency-freshness.d.ts +129 -0
- package/dist/dependency-freshness.d.ts.map +1 -0
- package/dist/dependency-freshness.js +161 -0
- package/dist/dependency-freshness.js.map +1 -0
- package/dist/doc-blocks.d.ts +13 -0
- package/dist/doc-blocks.d.ts.map +1 -0
- package/dist/doc-blocks.js +75 -0
- package/dist/doc-blocks.js.map +1 -0
- package/dist/document-references.d.ts +44 -0
- package/dist/document-references.d.ts.map +1 -0
- package/dist/document-references.js +94 -0
- package/dist/document-references.js.map +1 -0
- package/dist/editorconfig-policy.d.ts +24 -0
- package/dist/editorconfig-policy.d.ts.map +1 -0
- package/dist/editorconfig-policy.js +106 -0
- package/dist/editorconfig-policy.js.map +1 -0
- package/dist/environment-files.d.ts +42 -0
- package/dist/environment-files.d.ts.map +1 -0
- package/dist/environment-files.js +76 -0
- package/dist/environment-files.js.map +1 -0
- package/dist/file-roles.d.ts +39 -0
- package/dist/file-roles.d.ts.map +1 -0
- package/dist/file-roles.js +82 -0
- package/dist/file-roles.js.map +1 -0
- package/dist/generated-denial.d.ts +42 -0
- package/dist/generated-denial.d.ts.map +1 -0
- package/dist/generated-denial.js +87 -0
- package/dist/generated-denial.js.map +1 -0
- package/dist/history-policy.d.ts +32 -0
- package/dist/history-policy.d.ts.map +1 -0
- package/dist/history-policy.js +50 -0
- package/dist/history-policy.js.map +1 -0
- package/dist/image-assets.d.ts +14 -0
- package/dist/image-assets.d.ts.map +1 -0
- package/dist/image-assets.js +205 -0
- package/dist/image-assets.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/install-policy.d.ts +35 -0
- package/dist/install-policy.d.ts.map +1 -0
- package/dist/install-policy.js +61 -0
- package/dist/install-policy.js.map +1 -0
- package/dist/json-shapes.d.ts +86 -0
- package/dist/json-shapes.d.ts.map +1 -0
- package/dist/json-shapes.js +124 -0
- package/dist/json-shapes.js.map +1 -0
- package/dist/license-allowlist.d.ts +15 -0
- package/dist/license-allowlist.d.ts.map +1 -0
- package/dist/license-allowlist.js +157 -0
- package/dist/license-allowlist.js.map +1 -0
- package/dist/network-policy.d.ts +69 -0
- package/dist/network-policy.d.ts.map +1 -0
- package/dist/network-policy.js +173 -0
- package/dist/network-policy.js.map +1 -0
- package/dist/paired-assets.d.ts +34 -0
- package/dist/paired-assets.d.ts.map +1 -0
- package/dist/paired-assets.js +63 -0
- package/dist/paired-assets.js.map +1 -0
- package/dist/payload-access.d.ts +9 -0
- package/dist/payload-access.d.ts.map +1 -0
- package/dist/payload-access.js +168 -0
- package/dist/payload-access.js.map +1 -0
- package/dist/payload-policy.d.ts +18 -0
- package/dist/payload-policy.d.ts.map +1 -0
- package/dist/payload-policy.js +124 -0
- package/dist/payload-policy.js.map +1 -0
- package/dist/payload-source.d.ts +29 -0
- package/dist/payload-source.d.ts.map +1 -0
- package/dist/payload-source.js +84 -0
- package/dist/payload-source.js.map +1 -0
- package/dist/release-age.d.ts +23 -0
- package/dist/release-age.d.ts.map +1 -0
- package/dist/release-age.js +38 -0
- package/dist/release-age.js.map +1 -0
- package/dist/request-limits.d.ts +39 -0
- package/dist/request-limits.d.ts.map +1 -0
- package/dist/request-limits.js +57 -0
- package/dist/request-limits.js.map +1 -0
- package/dist/run-plan.d.ts +32 -0
- package/dist/run-plan.d.ts.map +1 -0
- package/dist/run-plan.js +43 -0
- package/dist/run-plan.js.map +1 -0
- package/dist/run-policy.d.ts +32 -0
- package/dist/run-policy.d.ts.map +1 -0
- package/dist/run-policy.js +37 -0
- package/dist/run-policy.js.map +1 -0
- package/dist/secret-policy.d.ts +13 -0
- package/dist/secret-policy.d.ts.map +1 -0
- package/dist/secret-policy.js +69 -0
- package/dist/secret-policy.js.map +1 -0
- package/dist/settings-layering.d.ts +45 -0
- package/dist/settings-layering.d.ts.map +1 -0
- package/dist/settings-layering.js +134 -0
- package/dist/settings-layering.js.map +1 -0
- package/dist/settings.d.ts +238 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +378 -0
- package/dist/settings.js.map +1 -0
- package/dist/skill-manifest.d.ts +18 -0
- package/dist/skill-manifest.d.ts.map +1 -0
- package/dist/skill-manifest.js +125 -0
- package/dist/skill-manifest.js.map +1 -0
- package/dist/source-text.d.ts +83 -0
- package/dist/source-text.d.ts.map +1 -0
- package/dist/source-text.js +310 -0
- package/dist/source-text.js.map +1 -0
- package/dist/suppression-ceiling.d.ts +48 -0
- package/dist/suppression-ceiling.d.ts.map +1 -0
- package/dist/suppression-ceiling.js +115 -0
- package/dist/suppression-ceiling.js.map +1 -0
- package/dist/tailwind-tokens.d.ts +16 -0
- package/dist/tailwind-tokens.d.ts.map +1 -0
- package/dist/tailwind-tokens.js +70 -0
- package/dist/tailwind-tokens.js.map +1 -0
- package/dist/text-escapes.d.ts +7 -0
- package/dist/text-escapes.d.ts.map +1 -0
- package/dist/text-escapes.js +14 -0
- package/dist/text-escapes.js.map +1 -0
- package/dist/toolchain-pins.d.ts +42 -0
- package/dist/toolchain-pins.d.ts.map +1 -0
- package/dist/toolchain-pins.js +99 -0
- package/dist/toolchain-pins.js.map +1 -0
- package/dist/url-safety.d.ts +12 -0
- package/dist/url-safety.d.ts.map +1 -0
- package/dist/url-safety.js +58 -0
- package/dist/url-safety.js.map +1 -0
- package/dist/version-policy.d.ts +99 -0
- package/dist/version-policy.d.ts.map +1 -0
- package/dist/version-policy.js +306 -0
- package/dist/version-policy.js.map +1 -0
- package/dist/vulnerability-policy.d.ts +66 -0
- package/dist/vulnerability-policy.d.ts.map +1 -0
- package/dist/vulnerability-policy.js +106 -0
- package/dist/vulnerability-policy.js.map +1 -0
- package/dist/wiring-policy.d.ts +226 -0
- package/dist/wiring-policy.d.ts.map +1 -0
- package/dist/wiring-policy.js +458 -0
- package/dist/wiring-policy.js.map +1 -0
- package/dist/wiring-violation.d.ts +6 -0
- package/dist/wiring-violation.d.ts.map +1 -0
- package/dist/wiring-violation.js +8 -0
- package/dist/wiring-violation.js.map +1 -0
- package/dist/workspace-policy.d.ts +177 -0
- package/dist/workspace-policy.d.ts.map +1 -0
- package/dist/workspace-policy.js +228 -0
- package/dist/workspace-policy.js.map +1 -0
- package/dist/yaml-blocks.d.ts +43 -0
- package/dist/yaml-blocks.d.ts.map +1 -0
- package/dist/yaml-blocks.js +87 -0
- package/dist/yaml-blocks.js.map +1 -0
- 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
|