Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/constructive-pnpm-policy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pnpm add -D pnpm-policy @constructive-io/pnpm-policy
`pnpm-policy.yaml` at the workspace root:

```yaml
minimumReleaseAge: 14d
minimumReleaseAge: 2d
blockExoticSubdeps: true
maintainers:
- pyramation
Expand Down
6 changes: 3 additions & 3 deletions packages/constructive-pnpm-policy/pnpm-policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
# ships it plus the generated inventory so our repos can point at one pinned
# version instead of each maintaining its own exemption list.

# Third-party releases wait two weeks. Most compromised releases are found and
# yanked well inside that window.
minimumReleaseAge: 14d
# Third-party releases wait two days. A compromised release is normally reported
# and yanked within hours, so the short wait catches it without stalling upgrades.
minimumReleaseAge: 2d

# Transitive dependencies must resolve from the registry, not from git or a URL.
blockExoticSubdeps: true
Expand Down
12 changes: 6 additions & 6 deletions packages/pnpm-policy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@

`minimumReleaseAge` is the single most effective supply-chain control pnpm ships: a package must have existed for N days before it can be installed, and most compromised releases are caught and yanked well inside that window. It is also the one control a package maintainer cannot turn on.

You publish `@acme/parser` at 2pm and consume it in three workspaces at 2:05pm. A 14-day quarantine means your own release is unusable for two weeks, in every repo you own. So the cooldown gets set to `0`, and the protection that would have stopped a compromised transitive dependency is gone — not because you decided the risk was acceptable, but because the tool could not tell your packages from everyone else's.
You publish `@acme/parser` at 2pm and consume it in three workspaces at 2:05pm. Even a two-day quarantine means your own release is unusable until Thursday, in every repo you own. So the cooldown gets set to `0`, and the protection that would have stopped a compromised transitive dependency is gone — not because you decided the risk was acceptable, but because the tool could not tell your packages from everyone else's.

`pnpm-policy` makes that distinction. It asks npm what your maintainer accounts publish, and writes the answer into `pnpm-workspace.yaml` as an exemption list:

```yaml
minimumReleaseAge: 20160 # 14 days, for everything third-party
minimumReleaseAge: 2880 # 2 days, for everything third-party
minimumReleaseAgeExclude:
- "@acme/*" # a scope you own
- my-unscoped-package # a package you publish
Expand Down Expand Up @@ -60,7 +60,7 @@ Commit `pnpm-policy.yaml`, `pnpm-policy.inventory.json`, and the generated `pnpm
```yaml
# How old a third-party release must be before it may be installed.
# Accepts 14d / 2w / 36h / 90m, or a bare number of minutes (what pnpm stores).
minimumReleaseAge: 14d
minimumReleaseAge: 2d

# Transitive dependencies must resolve from the registry, not from git or a URL.
blockExoticSubdeps: true
Expand Down Expand Up @@ -97,7 +97,7 @@ settings:

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `minimumReleaseAge` | duration | `14d` | Quarantine applied to everything not exempted. |
| `minimumReleaseAge` | duration | `2d` | Quarantine applied to everything not exempted. |
| `blockExoticSubdeps` | boolean | `false` | Refuse transitive deps from git/URL sources. |
| `maintainers` | string[] | `[]` | **Your own** npm accounts. See the warning below. |
| `scopes` | string[] | `[]` | Scopes you own, emitted as globs. |
Expand Down Expand Up @@ -196,9 +196,9 @@ packages:

# Managed by pnpm-policy — run `pnpm-policy generate` after editing pnpm-policy.yaml.

# A third-party release must be 2w old before it can be installed.
# A third-party release must be 2d old before it can be installed.
# Most malicious releases are found and yanked well inside that window.
minimumReleaseAge: 20160
minimumReleaseAge: 2880
# Exempt from the wait: 1 scope glob(s), 2 first-party package(s).
# First-party membership comes from what your-npm-username publishes on npm — waiting on your own release protects nothing.
minimumReleaseAgeExclude:
Expand Down
6 changes: 3 additions & 3 deletions packages/pnpm-policy/__tests__/policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ const resolve = (config: PolicyConfig, resolved?: string[]) =>
});

describe('resolvePolicy', () => {
it('defaults to a two-week wait', () => {
expect(resolve({}).settings.minimumReleaseAge).toBe(20160);
it('defaults to a two-day wait', () => {
expect(resolve({}).settings.minimumReleaseAge).toBe(2880);
});

it('converts a human duration to the minutes pnpm expects', () => {
Expand Down Expand Up @@ -124,7 +124,7 @@ describe('resolvePolicy', () => {

it('explains the wait and where the exemptions came from', () => {
const { comments } = resolve({ maintainers: ['pyramation'] });
expect(commentAt(comments.before, ['minimumReleaseAge'])).toContain('2w');
expect(commentAt(comments.before, ['minimumReleaseAge'])).toContain('2d');
expect(commentAt(comments.before, ['minimumReleaseAgeExclude'])).toContain('pyramation');
});

Expand Down
4 changes: 2 additions & 2 deletions packages/pnpm-policy/__tests__/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('applyPolicy', () => {
it('creates the policy block in an empty file', () => {
const out = applyPolicy('', policyFor());
expect(parseYaml(out)).toEqual({
minimumReleaseAge: 20160,
minimumReleaseAge: 2880,
minimumReleaseAgeExclude: ['@constructive-io/*', 'yanse'],
blockExoticSubdeps: false
});
Expand Down Expand Up @@ -51,7 +51,7 @@ describe('applyPolicy', () => {
it('replaces a hand-edited value rather than appending a second key', () => {
const out = applyPolicy('minimumReleaseAge: 0\n', policyFor());
expect(out.match(/minimumReleaseAge:/g)).toHaveLength(1);
expect(parseYaml(out)).toMatchObject({ minimumReleaseAge: 20160 });
expect(parseYaml(out)).toMatchObject({ minimumReleaseAge: 2880 });
});

it('removes a managed key the policy no longer sets', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/pnpm-policy/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const STARTER = `# pnpm-policy — https://github.com/constructive-io/dev-utils
# Run \`pnpm-policy generate\` to patch these settings into pnpm-workspace.yaml.

# How long a third-party release must exist before it may be installed.
minimumReleaseAge: 14d
minimumReleaseAge: 2d

# Transitive dependencies must come from the registry, not from git or a URL.
blockExoticSubdeps: true
Expand Down
7 changes: 5 additions & 2 deletions packages/pnpm-policy/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import type { AllowedBuild, PolicyConfig, PolicyException, ResolvedConfig } from
/** Filenames searched for, in order, when no explicit path is given. */
export const CONFIG_FILENAMES = ['pnpm-policy.yaml', 'pnpm-policy.yml', 'pnpm-policy.json'];

/** Two weeks: long enough that a malicious release is usually yanked first. */
export const DEFAULT_MINIMUM_RELEASE_AGE = '14d';
/**
* Two days: a compromised release is normally reported and yanked within hours,
* so this catches the attack without holding legitimate upgrades for a fortnight.
*/
export const DEFAULT_MINIMUM_RELEASE_AGE = '2d';

/** Find the config file for a directory, or undefined if there is none. */
export function findConfig(dir: string): string | undefined {
Expand Down
4 changes: 2 additions & 2 deletions packages/pnpm-policy/src/duration.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* Durations, in the unit pnpm speaks.
*
* `minimumReleaseAge` is minutes, which nobody wants to write for a two-week
* cooldown, so the config takes `14d` and this converts.
* `minimumReleaseAge` is minutes, which nobody wants to write for a multi-day
* cooldown, so the config takes `2d` and this converts.
*/

import { PolicyError } from './errors';
Expand Down
Loading