pm-ops 2026.8.31 → 2026.9.2
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/CHANGELOG.md +48 -0
- package/dist/attestation.d.ts +177 -0
- package/dist/attestation.d.ts.map +1 -0
- package/dist/attestation.js +593 -0
- package/dist/attestation.js.map +1 -0
- package/dist/index.d.ts +101 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/shell-scan.d.ts +438 -0
- package/dist/shell-scan.d.ts.map +1 -0
- package/dist/shell-scan.js +1516 -0
- package/dist/shell-scan.js.map +1 -0
- package/manifest.json +1 -1
- package/package.json +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Repaired the ops-5jp6 history drift that had blocked two stacked release pull requests ([ops-yxej](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-yxej.toon))
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
- Four more fail modes in the canonical gate: an arm pattern read as an opener, an escaped run key read as data, a sequence marker missing from the indent, and composite actions unscanned ([ops-cxrx](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-cxrx.toon))
|
|
12
|
+
- Attributing a case arm label by presence rather than position refused an attested publish, and a quoted key hid a phantom one ([ops-setc](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-setc.toon))
|
|
13
|
+
- An arm that opens a nested case skipped the sibling reset, so a publish could borrow a flag from a mutually exclusive arm ([ops-rsbn](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-rsbn.toon))
|
|
14
|
+
|
|
15
|
+
## 2026.9.2 - 2026-09-02
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Publish the canonical attestation scanner as pm-ops exports ([ops-4b34](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/features/ops-4b34.toon))
|
|
20
|
+
- A release-completeness gate that requires every release tag to have a matching npm version and GitHub Release ([ops-i8yp](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/features/ops-i8yp.toon))
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- A case opener sharing its first arm cleared the outer scope and refused a genuinely attested publish ([ops-cout](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-cout.toon))
|
|
25
|
+
- Three shell-scope and quoting fail-opens attest unproven npm publishes ([ops-p9lb](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-p9lb.toon))
|
|
26
|
+
- A malformed npm reply crashed the release audit, and the docstring already promised it would not ([ops-f5m7](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-f5m7.toon))
|
|
27
|
+
- The revert-proof test read its vulnerable fixture from origin/main, so merging it would have turned it red ([ops-9ktn](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-9ktn.toon))
|
|
28
|
+
- The paginated release listing was posted rather than fetched, because gh api switches to POST as soon as a parameter is present ([ops-eugo](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-eugo.toon))
|
|
29
|
+
- P2: GitHub Release history truncated by gh release list -L 500 (a capped read misses older releases) ([ops-ud1m](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-ud1m.toon))
|
|
30
|
+
- P2: exit 0 inside the retry loop instead of returning status to loop-level termination ([ops-emii](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-emii.toon))
|
|
31
|
+
- P1: Completeness gate makes a partial release permanent (release:check blocks repair) ([ops-94me](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-94me.toon))
|
|
32
|
+
- The bun install verification retried a cached negative manifest, so published versions were never observed ([ops-i3ie](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-i3ie.toon))
|
|
33
|
+
|
|
34
|
+
### Security
|
|
35
|
+
|
|
36
|
+
- Skipping the sibling-arm reset for any depth-increasing segment let a later arm borrow the previous arm's binding ([ops-narm](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-narm.toon))
|
|
37
|
+
- A tracked executable whose name merely ends in package.json has its publish hidden from the audit ([ops-fnbd](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-fnbd.toon))
|
|
38
|
+
- A case opener sharing a segment with its first arm leaks that arm's binding past esac ([ops-csop](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-csop.toon))
|
|
39
|
+
- Branch-local and compound-line publish bindings let an unattested npm publish pass the attestation gate ([ops-brsc](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-brsc.toon))
|
|
40
|
+
- Security: repository-host check matches too loosely (notgithub.com accepted as GitHub) ([ops-042o](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-042o.toon))
|
|
41
|
+
|
|
42
|
+
### Other
|
|
43
|
+
|
|
44
|
+
- Converge the pinned pm-cli on 2026.8.31 and repair history hashes ([ops-uulf](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/chores/ops-uulf.toon))
|
|
45
|
+
|
|
3
46
|
## 2026.8.31 - 2026-08-31
|
|
4
47
|
|
|
5
48
|
### Fixed
|
|
6
49
|
|
|
50
|
+
- Finish shell-accurate heredoc and unset handling in publish attestation ([ops-62d1](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-62d1.toon))
|
|
7
51
|
- The release changelog remained Unreleased after the release tag was created ([ops-3ete](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-3ete.toon))
|
|
8
52
|
|
|
53
|
+
### Security
|
|
54
|
+
|
|
55
|
+
- A heredoc body and a discarded binding both let an unattested publish pass the attestation gate ([ops-5jp6](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-5jp6.toon))
|
|
56
|
+
|
|
9
57
|
## 2026.8.29 - 2026-08-29
|
|
10
58
|
|
|
11
59
|
### Fixed
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { type ShellCommand, type SourceFile, type VerifierResult } from "./shell-scan.ts";
|
|
2
|
+
/** The flag that attaches a build attestation to the published tarball. */
|
|
3
|
+
export declare const ATTESTATION_FLAG = "--provenance";
|
|
4
|
+
/** Whether the auditor recognized any publish command at all. */
|
|
5
|
+
export type PublishRecognition = {
|
|
6
|
+
readonly kind: "none";
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: "recognized";
|
|
9
|
+
readonly count: number;
|
|
10
|
+
};
|
|
11
|
+
/** Publish-attestation outcome with non-vacuity preserved as structured data. */
|
|
12
|
+
export interface PublishAttestationResult extends VerifierResult {
|
|
13
|
+
/** Recognition cannot be inferred from an empty failure list. */
|
|
14
|
+
readonly recognition: PublishRecognition;
|
|
15
|
+
}
|
|
16
|
+
/** One publish invocation found in a tracked file. */
|
|
17
|
+
export interface PublishInvocation {
|
|
18
|
+
/** File the invocation was found in. */
|
|
19
|
+
file: string;
|
|
20
|
+
/** The program the invocation runs, reduced to its basename. */
|
|
21
|
+
program: string;
|
|
22
|
+
/** The invocation's tokens, quoting resolved. */
|
|
23
|
+
command: ShellCommand;
|
|
24
|
+
}
|
|
25
|
+
/** Publishers other than npm, which this repository has no attested path for. */
|
|
26
|
+
export declare const FOREIGN_PUBLISHERS: Set<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Join manifest script bodies for legacy text consumers without continuations.
|
|
29
|
+
*
|
|
30
|
+
* The auditor itself analyzes each returned body in a fresh scope; this text
|
|
31
|
+
* form remains for callers that need the earlier newline-joined contract.
|
|
32
|
+
*
|
|
33
|
+
* @param text - The manifest's contents.
|
|
34
|
+
* @returns One sanitized line per script body, newline joined.
|
|
35
|
+
*/
|
|
36
|
+
export declare function manifestCommandLines(text: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Decide whether one command is a direct `npm publish`.
|
|
39
|
+
*
|
|
40
|
+
* `publish` does not have to follow `npm` immediately: npm accepts its
|
|
41
|
+
* configuration flags anywhere on the line, so `npm --access public publish` is
|
|
42
|
+
* a real publish that an adjacency test discards silently, leaving an attested
|
|
43
|
+
* sibling elsewhere in the file to carry the audit to a pass.
|
|
44
|
+
*
|
|
45
|
+
* Reading the first non-flag word as the subcommand does not work either,
|
|
46
|
+
* because npm has flags that take a separate value (`--access public`) and
|
|
47
|
+
* flags that do not (`--ignore-scripts`), and telling them apart needs npm's
|
|
48
|
+
* own option table. So the word is looked for anywhere in the arguments, and
|
|
49
|
+
* only a preceding runner subcommand rules it out -- `npm run publish` runs a
|
|
50
|
+
* package script whose body is scanned from the manifest, and requiring the
|
|
51
|
+
* flag on the runner would report a defect that is not there.
|
|
52
|
+
*
|
|
53
|
+
* The residual imprecision is `npm --tag publish ...`, a dist-tag named after
|
|
54
|
+
* the subcommand, which this reads as a publish. That direction is deliberate:
|
|
55
|
+
* a false positive is a report line to argue with, a false negative is an
|
|
56
|
+
* unattested artifact on the registry.
|
|
57
|
+
*
|
|
58
|
+
* The program is checked in command position by the caller, so `echo npm
|
|
59
|
+
* publish` and `notnpm publish` never reach here.
|
|
60
|
+
*
|
|
61
|
+
* @param command - One simple command's tokens.
|
|
62
|
+
* @returns True when the command publishes.
|
|
63
|
+
*/
|
|
64
|
+
export declare function isPublishCommand(command: ShellCommand): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Decide whether one publish command actually enables the attestation.
|
|
67
|
+
*
|
|
68
|
+
* A substring test is not enough. `--provenance=false`, `--provenance false` and
|
|
69
|
+
* `--no-provenance` all contain the flag's spelling and all turn the
|
|
70
|
+
* attestation off, so a containment check accepts precisely the regression this
|
|
71
|
+
* gate exists to catch -- while reporting the file as attested. `--provenance-file`
|
|
72
|
+
* is a different flag entirely and must not be read as this one.
|
|
73
|
+
*
|
|
74
|
+
* Tokens are judged in order and the last one wins, which is how npm resolves a
|
|
75
|
+
* flag given more than once: `--provenance --no-provenance` publishes without an
|
|
76
|
+
* attestation, so this must answer false for it.
|
|
77
|
+
*
|
|
78
|
+
* Quoting is irrelevant to the shell and so is irrelevant here: `npm publish
|
|
79
|
+
* "--provenance"` is attested, and the scan this replaces read it as bare.
|
|
80
|
+
*
|
|
81
|
+
* @param command - One simple command's tokens.
|
|
82
|
+
* @returns True when the command publishes with an attestation.
|
|
83
|
+
*/
|
|
84
|
+
export declare function attestationEnabled(command: ShellCommand): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Find every publish invocation in one source, isolating package scripts.
|
|
87
|
+
*
|
|
88
|
+
* npm launches every manifest script in a fresh shell, so scalar evidence from
|
|
89
|
+
* one body can never attest a publish in another body. A workflow file's `run:`
|
|
90
|
+
* blocks are dedented first — before {@link joinContinuations} and heredoc
|
|
91
|
+
* detection — because that is what happens to them before bash sees the text:
|
|
92
|
+
* YAML strips the block indentation when it delivers the value. The order
|
|
93
|
+
* matters: `dedentRunBlocks` reads the raw YAML line structure to find block
|
|
94
|
+
* headers and strip their indentation, and `joinContinuations` would merge a
|
|
95
|
+
* `run:` header with the next line when a backslash continuation sits between
|
|
96
|
+
* them, destroying the header the dedent needs to see. Heredoc detection runs
|
|
97
|
+
* after both, because a heredoc terminator must match at the start of the line
|
|
98
|
+
* bash receives — which is the dedented, continuation-joined text. The one
|
|
99
|
+
* scanner rule that is whitespace-sensitive is the heredoc terminator, so
|
|
100
|
+
* dedenting before that rule runs is what makes a `run: |2` block's heredoc
|
|
101
|
+
* terminator match instead of swallowing the rest of the file.
|
|
102
|
+
*
|
|
103
|
+
* @param source - The file's path and contents.
|
|
104
|
+
* @returns The publish invocations found, in file and script order.
|
|
105
|
+
*/
|
|
106
|
+
export declare function publishInvocationsIn(source: SourceFile): PublishInvocation[];
|
|
107
|
+
/**
|
|
108
|
+
* Render an invocation back to a readable command for a report line.
|
|
109
|
+
*
|
|
110
|
+
* @param command - The invocation's tokens.
|
|
111
|
+
* @returns The command as a single space-separated string.
|
|
112
|
+
*/
|
|
113
|
+
export declare function renderCommand(command: ShellCommand): string;
|
|
114
|
+
/**
|
|
115
|
+
* Audit every publish invocation across the given files.
|
|
116
|
+
*
|
|
117
|
+
* An absent invocation is a failure rather than a pass: a scan that finds
|
|
118
|
+
* nothing has either been pointed at the wrong files or outlived the workflow
|
|
119
|
+
* it guards, and both look identical to a clean result unless said out loud.
|
|
120
|
+
*
|
|
121
|
+
* A publisher other than npm fails outright rather than being checked for a
|
|
122
|
+
* flag. This repository's attested path is npm's `--provenance`; no equivalent
|
|
123
|
+
* is configured for yarn, pnpm or bun, so such an invocation is an unattested
|
|
124
|
+
* publish path regardless of the flags it carries, and guessing at another
|
|
125
|
+
* tool's spelling would be a gate that only looked strict.
|
|
126
|
+
*
|
|
127
|
+
* @param sources - The tracked files to scan.
|
|
128
|
+
* @returns Failures and per-file notes.
|
|
129
|
+
*/
|
|
130
|
+
export declare function auditPublishAttestation(sources: SourceFile[]): PublishAttestationResult;
|
|
131
|
+
/**
|
|
132
|
+
* Decide whether a tracked path can run a command.
|
|
133
|
+
*
|
|
134
|
+
* The previous enumeration named two paths -- `.github/workflows` and
|
|
135
|
+
* `package.json` -- which meant a publish added to any tracked script was never
|
|
136
|
+
* audited, and because the workflow's own attested publish satisfied the
|
|
137
|
+
* non-vacuity check the gate still reported that every invocation was attested.
|
|
138
|
+
* Auditing every shape that can execute closes that, and a shebang is honoured
|
|
139
|
+
* so an extensionless tracked script is not a blind spot either.
|
|
140
|
+
*
|
|
141
|
+
* Build output is excluded. `dist/` is generated from sources this scan already
|
|
142
|
+
* reads, it is regenerated and compared byte-for-byte on the release path, and
|
|
143
|
+
* including it would audit a bundled copy of a command rather than the command.
|
|
144
|
+
*
|
|
145
|
+
* @param path - Repository-relative path.
|
|
146
|
+
* @param firstLine - The file's first line, for shebang detection.
|
|
147
|
+
* @returns True when the file should be scanned.
|
|
148
|
+
*/
|
|
149
|
+
export declare function isExecutableSource(path: string, firstLine: string): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* List the tracked files that can run a publish.
|
|
152
|
+
*
|
|
153
|
+
* Git is asked rather than the filesystem walked, so an untracked scratch copy
|
|
154
|
+
* of a workflow cannot satisfy or fail the gate. `-z` is used because a tracked
|
|
155
|
+
* path may legally contain a newline, and splitting such a listing on newlines
|
|
156
|
+
* invents two paths that do not exist and drops the one that does.
|
|
157
|
+
*
|
|
158
|
+
* @param root - Repository root.
|
|
159
|
+
* @returns Repository-relative paths of every tracked file that can execute.
|
|
160
|
+
*/
|
|
161
|
+
export declare function trackedPublishSources(root: string): string[];
|
|
162
|
+
/**
|
|
163
|
+
* Read the tracked sources and audit them.
|
|
164
|
+
*
|
|
165
|
+
* @param root - Repository root to verify.
|
|
166
|
+
* @returns Failures and notes for the whole repository.
|
|
167
|
+
*/
|
|
168
|
+
export declare function verify(root: string): PublishAttestationResult;
|
|
169
|
+
/**
|
|
170
|
+
* Print a result and set a failing exit code when it failed.
|
|
171
|
+
*
|
|
172
|
+
* @param result - The audit outcome.
|
|
173
|
+
* @param write - Sink for the report lines.
|
|
174
|
+
* @param exit - Called with the process exit code when there were failures.
|
|
175
|
+
*/
|
|
176
|
+
export declare function report(result: VerifierResult, write: (line: string) => void, exit: (code: number) => void): void;
|
|
177
|
+
//# sourceMappingURL=attestation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../attestation.ts"],"names":[],"mappings":"AAuBA,OAAO,EAeL,KAAK,YAAY,EACjB,KAAK,UAAU,EAIf,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAEzB,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,iBAAiB,CAAC;AAE/C,iEAAiE;AACjE,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D,iFAAiF;AACjF,MAAM,WAAW,wBAAyB,SAAQ,cAAc;IAC9D,iEAAiE;IACjE,QAAQ,CAAC,WAAW,EAAE,kBAAkB,CAAC;CAC1C;AAED,sDAAsD;AACtD,MAAM,WAAW,iBAAiB;IAChC,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,OAAO,EAAE,YAAY,CAAC;CACvB;AAED,iFAAiF;AACjF,eAAO,MAAM,kBAAkB,aAAmC,CAAC;AA2EnE;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMzD;AAuBD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAe/D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAwBjE;AAsKD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,GAAG,iBAAiB,EAAE,CAO5E;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAE3D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,wBAAwB,CAqCvF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAI3E;AAiCD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAU5D;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,wBAAwB,CAM7D;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CACpB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAC7B,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC3B,IAAI,CASN"}
|