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
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5
21 changes: 17 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,25 @@ on:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
validate:
runs-on: ubuntu-latest
test:
name: ${{ matrix.os }} / Node ${{ matrix.node-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
node-version: [22, 24]
include:
- os: ubuntu-latest
node-version: 22
- os: windows-latest
node-version: 22
- os: macos-latest
node-version: 22
- os: ubuntu-latest
node-version: 24
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
Expand All @@ -19,5 +32,5 @@ jobs:
cache: npm
- run: npm ci
- run: npm run check
- run: npm test
- run: npm run test:coverage
- run: npm audit --audit-level=high
26 changes: 26 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CodeQL

on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "23 4 * * 1"

permissions:
contents: read
security-events: write

jobs:
analyze:
name: CodeQL / JavaScript-TypeScript
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- uses: github/codeql-action/init@v3
with:
languages: javascript-typescript
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
46 changes: 38 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ on:
tags: ["v*"]

permissions:
contents: read
contents: write

jobs:
verify:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
Expand All @@ -18,13 +18,43 @@ jobs:
cache: npm
- run: npm ci
- run: npm run check
- run: npm test
- run: npm run test:coverage
- run: npm audit --audit-level=high
- name: Verify tag matches package version
- name: Verify metadata, license, package contents, and tag
env:
RELEASE_TAG: ${{ github.ref_name }}
run: npm run verify:package
- name: Extract matching changelog notes
run: npm run release:notes > release-notes.md
- name: Build package artifact
id: pack
shell: bash
run: |
expected="v$(node -p "require('./package.json').version")"
test "$GITHUB_REF_NAME" = "$expected" || {
echo "Tag $GITHUB_REF_NAME does not match package version $expected"
npm pack --json > pack-result.json
package_file="$(node -e "console.log(require('./pack-result.json')[0].filename)")"
echo "package_file=$package_file" >> "$GITHUB_OUTPUT"
- name: Preserve package as workflow artifact
uses: actions/upload-artifact@v4
with:
name: npm-package-${{ github.ref_name }}
path: ${{ steps.pack.outputs.package_file }}
if-no-files-found: error
retention-days: 30
- name: Refuse to replace an existing release
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "Release $GITHUB_REF_NAME already exists; immutable releases are never replaced."
exit 1
}
fi
- name: Create GitHub release and attach package
env:
GH_TOKEN: ${{ github.token }}
run: >-
gh release create "$GITHUB_REF_NAME"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate releases on commits contained in main

In .github/workflows/release.yml, this job is triggered by any pushed v* tag and reaches gh release create without verifying that GITHUB_SHA is an ancestor of origin/main. If someone with tag-push rights tags an unmerged branch or local commit whose package version matches the tag, the workflow will still publish a GitHub release, bypassing the documented reviewed-main release path; add an explicit fetch/merge-base --is-ancestor check before creating the release.

Useful? React with 👍 / 👎.

"${{ steps.pack.outputs.package_file }}"
--repo "$GITHUB_REPOSITORY"
--verify-tag
--title "$GITHUB_REF_NAME"
--notes-file release-notes.md
6 changes: 6 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dist/test/
test/
.env
.env.*
auth.json
credentials*
20 changes: 16 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,22 @@

### Added

- product, architecture, contributor, examples, and performance documentation;
- broader node:test coverage for anchor parsing and edit operations;
- reproducible hash benchmark and test coverage command;
- CI and tag/version release verification with dependency auditing.
- product, architecture, contributor, examples, performance, operations, branch-protection, security, and release documentation;
- focused coverage for anchor parsing, classified failures, file kinds, atomic filesystem edits, and cross-platform link/newline behavior;
- reproducible hash benchmark and enforced coverage thresholds;
- Linux, Windows, macOS, Node 22, and Node 24 CI coverage;
- package provenance/license verification and immutable GitHub release artifact creation;
- weekly Dependabot and CodeQL scanning workflows.

### Changed

- filesystem edits now classify paths before decoding and use same-directory atomic replacement with mode, BOM, newline, and cleanup guarantees;
- package contents exclude compiled tests and include complete repository provenance metadata.

### Security

- unsafe binary, image, special-file, symlink, null-byte, and lossy UTF-8 rewrites are rejected before writing;
- security reporting, sensitive-diagnostic handling, dependency review, and recovery responsibilities are documented.

## 0.1.0

Expand Down
20 changes: 20 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
MIT License

Copyright (c) 2026 T50 Systems

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ Edit `lines` must contain literal file content. Remove copied `LINE#HASH:` prefi

Use the filesystem adapter, which detects and preserves newline style. Include a CRLF regression test for adapter changes.

The adapter refuses directories, symbolic links, special files, images, null-byte/binary data, and invalid UTF-8 that would decode with replacement characters. Successful edits use a same-directory temporary file and atomic replacement, preserve UTF-8 BOMs and existing permission bits, and clean up temporary files after success or handled failure. Atomic replacement intentionally breaks the edited path out of a hard-link set; other hard links continue to reference the unchanged original inode.

## Development

```bash
Expand All @@ -98,18 +100,26 @@ npm run test:coverage
npm run benchmark
```

### Supported matrix

CI runs Node.js 22 on Ubuntu, Windows, and macOS, plus the Node.js 24 compatibility job on Ubuntu. Capability-sensitive symlink and permission assertions report a specific diagnostic when the host cannot provide that feature; unrelated filesystem and CRLF assertions continue to run. The thresholded coverage command enforces at least 85% line coverage and 75% branch coverage.

## Documentation

- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — components, control flow, and invariants.
- [`docs/BRANCH_PROTECTION.md`](docs/BRANCH_PROTECTION.md) — required review/check policy and read-only verification.
- [`docs/OPERATIONS.md`](docs/OPERATIONS.md) — classified errors, filesystem recovery, and safe escalation.
- [`docs/EXAMPLES.md`](docs/EXAMPLES.md) — parsing, editing, recovery, and adapter examples.
- [`docs/PERFORMANCE.md`](docs/PERFORMANCE.md) — reproducible hash baseline.
- [`docs/PRODUCT.md`](docs/PRODUCT.md) — vision and success metrics.
- [`docs/RELEASING.md`](docs/RELEASING.md) — package verification, immutable tags, release creation, and recovery.
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — contributor workflow.
- [`CHANGELOG.md`](CHANGELOG.md) — release history.
- [`SECURITY.md`](SECURITY.md) — supported versions, private reporting, trust boundaries, and security maintenance.

## Release workflow

Update `package.json` and `CHANGELOG.md`, merge validated changes, and create a matching `vX.Y.Z` tag. The release workflow verifies build/check/tests, dependency audit, and tag/version consistency.
Update `package.json` and add a matching version section to `CHANGELOG.md`, merge validated changes, and create a new immutable `vX.Y.Z` tag. The release workflow enforces coverage and audit gates, verifies provenance/license/package contents and tag/version/changelog consistency, builds an npm-format tarball, and creates one GitHub release with the tarball attached. See [`docs/RELEASING.md`](docs/RELEASING.md).

## License

Expand Down
30 changes: 30 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Security policy

## Supported versions

| Version | Supported |
|---|---|
| 0.1.x | Yes |
| Earlier or unreleased snapshots | No |

Security fixes are released as new immutable patch versions. Support may move to the newest minor release after an announcement in the changelog and GitHub release notes.

## Private reporting

Use [GitHub private vulnerability reporting](https://github.com/T50-Systems/pi-anchor-edit-core/security/advisories/new). Do not open a public issue for a suspected vulnerability. If GitHub does not present a private report form, contact a T50 Systems maintainer privately through the organization’s established contact channel and include only enough metadata to arrange a secure transfer; do not fall back to a public issue.

Never include credentials, access tokens, secrets, customer data, private source files, complete edited file contents, or stale-anchor diagnostic excerpts in a public issue, discussion, pull request, benchmark, or log. A safe initial report states the affected version, operating system, Node version, error prefix, and a minimal synthetic reproduction. Maintainers will request sensitive evidence through the private advisory.

We aim to acknowledge a private report within 3 business days, provide an initial severity/triage decision within 7 business days, and send status updates at least every 14 days until remediation or closure. Coordinated disclosure timing is agreed with the reporter after a fix and supported release are ready.

## Trust boundary

`pi-anchor-edit-core` reads and mutates caller-selected local paths. The caller is responsible for authorization, path selection, backups, and preventing untrusted users from choosing sensitive targets. The filesystem adapter rejects symbolic links, special files, directories, images, binary/null-byte content, and invalid UTF-8 rewrites; it does not create a sandbox or establish that a path is safe to edit.

Anchor diagnostics can quote nearby file content in `>>> LINE#HASH:content` retry lines. Treat all diagnostics as potentially sensitive. Redact or replace them with synthetic examples before sharing, and never send raw diagnostics to telemetry by default.

See [`docs/OPERATIONS.md`](docs/OPERATIONS.md) for safe recovery actions and [`docs/RELEASING.md`](docs/RELEASING.md) for immutable release recovery.

## Dependency and scanning maintenance

Dependabot checks npm and GitHub Actions dependencies weekly. CodeQL scans pushes, pull requests, and a weekly schedule. The maintainer responsible for the next release reviews high-severity `npm audit`, Dependabot, and code-scanning findings at least weekly and before every release. Security-related dependency updates use the normal reviewed pull-request and required-check path; emergency fixes do not bypass validation.
2 changes: 2 additions & 0 deletions dist/src/file-kind.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export type LoadedFile = {
kind: 'directory';
} | {
kind: 'symlink';
} | {
kind: 'image';
mimeType: string;
Expand Down
10 changes: 7 additions & 3 deletions dist/src/file-kind.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { open as fsOpen, stat as fsStat } from 'node:fs/promises';
import { lstat as fsLstat, open as fsOpen } from 'node:fs/promises';
import { fileTypeFromBuffer } from 'file-type';
const IMAGE_MIME_TYPES = new Set([
'image/jpeg',
Expand All @@ -19,7 +19,10 @@ function hasNullByte(buffer) {
return buffer.includes(0);
}
export async function loadFileKindAndText(filePath) {
const pathStat = await fsStat(filePath);
const pathStat = await fsLstat(filePath);
if (pathStat.isSymbolicLink()) {
return { kind: 'symlink' };
}
if (pathStat.isDirectory()) {
return { kind: 'directory' };
}
Expand All @@ -44,7 +47,8 @@ export async function loadFileKindAndText(filePath) {
if (hasNullByte(sample)) {
return { kind: 'binary', description: 'null bytes detected' };
}
const decoder = new TextDecoder('utf-8');
// Preserve a UTF-8 BOM as content so a read/edit round trip is byte-safe.
const decoder = new TextDecoder('utf-8', { ignoreBOM: true });
const fatalDecoder = new TextDecoder('utf-8', { fatal: true });
let hadUtf8DecodeErrors = false;
const noteUtf8DecodeErrors = (chunk) => {
Expand Down
2 changes: 2 additions & 0 deletions dist/src/filesystem-client.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { EditParams, PiClient, ReadParams } from './types.js';
export declare class FilesystemPiClient implements PiClient {
protected replaceTemporaryFile(temporaryPath: string, destinationPath: string): Promise<void>;
private atomicWrite;
read({ path, offset, limit }: ReadParams): Promise<string>;
edit({ path, edits }: EditParams): Promise<string>;
}
Loading
Loading