Skip to content
Open
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Versioning: strict [semver](https://semver.org/) — bundle schema changes
always bump at least minor; breaking schema changes bump major.

## [Unreleased]

### Added
- Add a submit-only, post-consent npm release-date check for the small audited
Better Auth, Lemon Squeezy, and Paddle package subset (#81). It runs only
after the remote-visibility gate, completes or aborts before upload, and
warns without blocking when a skill's first-seen date predates the earliest
complete mapped npm reference. npm receives only selected public package
names and normal connection data; failures are silent and no result is
cached. Ambiguous cross-ecosystem and Tier 2 slugs are excluded. The bundle,
schema, Redential upload body, and closed taxonomy are unchanged.

### Changed
- `--confirm-upload` help now states that it authorizes both the reviewed
upload and subsequent network steps, including eligible npm lookups (#81).

## [0.13.0] - 2026-08-14

### Changed
Expand Down
92 changes: 83 additions & 9 deletions docs/login-submit.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,14 @@ entry points share every invariant below identically. It then:
```
— the label is never part of the bundle JSON below; this is the one
place its exact value is shown before consent (principle 4).
5. A header line —
5. If the bundle contains an audited npm release-check candidate, a
disclosure explains that confirmation will allow selected public
package names to be queried from npm, which receives those names and
normal connection data such as the user's IP address. It also states
that npm never receives source, the repository URL, the bundle, or the
Redential token. This disclosure sits outside `WHAT GETS UPLOADED`
because that box describes data sent to Redential.
6. A header line —
```
Exact payload (byte-for-byte what gets sent):
```
Expand Down Expand Up @@ -266,17 +273,21 @@ entry points share every invariant below identically. It then:
or an identity lookup ever leaves the machine — this is `submit`-only
behavior; `scan` still only ever warns, never blocks, since `scan` has
no network access to make the real determination.
6. Fetches identity corroboration (below) and, if it succeeds, prints one
6. For the small audited subset of npm-backed skills described below,
checks the public npm package creation date. Every lookup completes or
aborts before the upload starts. Findings are warnings only; lookup
failures and incomplete data are silent and never block the upload.
7. Fetches identity corroboration (below) and, if it succeeds, prints one
informational line with the result — see that section for exactly what
is and isn't sent. Moved here, after the visibility gate (owner
directive, 2026-08): corroboration is informational, not part of the
reviewed payload, so it no longer justifies a network call before the
upload question — it fires only once the user has already said yes.
Never blocks or delays the next step: any failure here simply skips the
line and sends no header.
7. `POST {SITE_URL}/api/cli/bundles` with `Authorization: Bearer
8. `POST {SITE_URL}/api/cli/bundles` with `Authorization: Bearer
<access_token>` and the printed bundle JSON as the body — plus, if step
6's corroboration check succeeded, an
7's corroboration check succeeded, an
`X-Redential-Identity-Corroboration` header (below). On success:
`{id}`. Only the `id` is ever printed back — never the full response
body, so a change on the server side can't accidentally start echoing
Expand All @@ -286,16 +297,16 @@ entry points share every invariant below identically. It then:
relocated here from `scan`'s own summary, which used to show it
unconditionally before any upload happened; see
[scan.md](scan.md#the-summary-default-tty-output)).
8. `POST {SITE_URL}/api/cli/private-label` with `{bundle_id: <the id from
step 7>, private_label: <the label resolved in step 2>}` — only after
step 7 has already succeeded. See
9. `POST {SITE_URL}/api/cli/private-label` with `{bundle_id: <the id from
step 8>, private_label: <the label resolved in step 2>}` — only after
step 8 has already succeeded. See
[docs/private-label.md](private-label.md) for the full contract and
failure semantics: this request is never retried, and a failure here
never triggers a second bundle upload — it only prints a warning
(naming the label, so it can be set again from the web) and `submit`
still exits 0, since the bundle itself is already safely uploaded (see
[exit-codes.md](exit-codes.md)).
9. Records the upload locally (`last-submission.json`, above) — not part
10. Records the upload locally (`last-submission.json`, above) — not part
of what's sent, just local bookkeeping for a later `scan`'s next-step
hint. Unlike the version-check notice below, this is not best-effort:
a failure here (e.g. an unwritable config dir) surfaces as a real
Expand Down Expand Up @@ -335,6 +346,69 @@ this gate is its real, definitive answer:
inconclusive result, `submit` falls back to printing
`publicHostWarning`'s own (longer) message and proceeds.

## npm release-date check (submit-only)

After upload confirmation and after the remote-visibility gate has allowed
submission, `submit` checks a deliberately small, audited subset of detected
skills against the public npm registry. It requests the official full
packument with:

```text
GET https://registry.npmjs.org/{encoded-package-name}
Accept: application/json
```

A scoped package is encoded as one path segment (for example,
`%40scope%2Fpackage`). There is no query string, request body,
`Authorization` header, CLI-created `Cookie`, Redential header, or Redential
token. npm sees the selected public package name and ordinary connection
metadata, including the source IP. It never receives the bundle, repository
URL or remote, path, source, label, identity, or token.

The initial audited set is intentionally conservative:

- `better-auth` for `auth/better-auth`;
- `@lemonsqueezy/lemonsqueezy.js` for `payments/lemonsqueezy`;
- `@paddle/paddle-js` and `@paddle/paddle-node-sdk` for `payments/paddle`.

`signatures/package-map.json` stores only those eligible package keys. Their
slugs are still derived from the existing detection map; the list neither
detects skills nor records which package caused a detection. CI requires all
map keys for an eligible slug to be audited and excludes any slug reachable
through Tier 2. Consequently, ambiguous or cross-ecosystem slugs such as
`ai/openai-api` and `auth/firebase-auth` never trigger this check.

For each eligible detected slug, the CLI reads only `time.created`. If a slug
has several mapped npm references, all must return valid canonical UTC dates;
the earliest date is used. A warning appears only when the bundle's
`first_seen` is strictly earlier. Equality or a later date is not a finding.
Vendored code and private forks can legitimately predate a public npm release,
so the warning is non-accusatory, never adds a prompt, and explicitly says the
upload will continue.

The checker is bounded to four concurrent requests, 1.5 seconds per request,
and 3 seconds for the entire operation. Every started request is awaited until
it resolves or its abort timeout fires; npm work never continues in the
background or overlaps the Redential upload. A 404, 429, 5xx, network failure,
timeout, malformed response, missing/invalid date, incomplete multi-package
result, exhausted deadline, or unexpected checker error silently omits the
finding and allows upload. No result is cached: persisting it would create a
local technology history and require separate TTL, permissions, corruption,
and migration decisions.

TTY users see the npm disclosure after the private-label line and before the
payload header, still leaving the exact JSON immediately adjacent to the
upload question. Non-TTY stdout remains JSON-only with the bundle as its first
output; after `--confirm-upload` and a successful visibility gate, the same
disclosure is written to stderr immediately before npm is contacted. A decline,
a visibility refusal, or a bundle with no eligible skill produces no npm
request. `--confirm-upload` therefore authorizes both the reviewed upload and
these subsequent network steps.

The bundle schema and the byte-for-byte Redential upload body are unchanged.
This new network destination was discussed in issue #81; no bundle field,
Redential upload header, or schema change is involved.

## Identity corroboration (submit-only)

After the single upload confirmation (step 4 above) has been answered
Expand Down Expand Up @@ -382,7 +456,7 @@ identity is exactly as valid as before, just without an extra
corroboration marker server-side.

On upload, the two counts travel as a single optional HTTP header on the
`POST /api/cli/bundles` request (step 7 above):
`POST /api/cli/bundles` request (step 8 above):
`X-Redential-Identity-Corroboration: {"corroborated_count": N,
"total_claimed": M}` (compact JSON). This is the only place they go — they
are never added to the bundle body, so the bundle stays byte-for-byte
Expand Down
3 changes: 3 additions & 0 deletions docs/privacy-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ wrong, not the test.
|---|---|
| `test/scan.test.ts` → "requires explicit confirmation before producing a bundle" | `runScan` throws unless `confirmed` is explicitly `true` — `--author` alone (non-interactive identity selection) never implies authorization. |
| `test/prompt.test.ts` (both cases) | If the interactive attestation/author prompt hits EOF (closed stdin) before an answer, the CLI fails loudly (throws) instead of silently proceeding — no confirmation can be assumed by default. |
| `test/privacy/zero-network.test.ts` → Better Auth hand-off declined at the upload question | Even when the reviewed bundle contains an audited npm release-check candidate, the complete `scan` → `submit` hand-off makes zero `fetch`/HTTP/HTTPS calls when upload consent is declined. |
| `test/privacy/submit-guardrail.test.ts` → npm release lookup privacy boundary | The npm GET happens only after confirmation and the visibility gate, finishes before identity/upload, carries only the audited public package path, and contains no token, bundle, remote, repository URL, label, body, query, authorization, or CLI cookie. |

## 3. Bounded output

Expand All @@ -35,6 +37,7 @@ wrong, not the test.
|---|---|
| `test/scan.test.ts` → single-commit / multiple-author cases (implicit) | `runScan` is a pure function of its inputs (repo state + explicit `now`): given the same repository and the same `now`, it returns byte-identical JSON on every call — there is no hidden enrichment step between what a caller inspects and what `submit` would later send, since both would come from calling the same function on the same reviewed bundle. |
| `test/privacy/submit-guardrail.test.ts` → "the request body equals the exact string logged before the upload confirmation" | Closes the gap noted below: `submit` prints the bundle via the same `buildBundleInteractively` path `scan` uses, then uploads that **exact printed string** (`postRawJson`, never a re-serialization of the parsed object) — proven by asserting the mock server's received request body is `===` the printed line, not just deep-equal after re-parsing. |
| `test/privacy/submit-guardrail.test.ts` → npm release lookup privacy boundary | Adding the npm check does not enrich or reserialize the Redential payload: the exact JSON printed for review is still byte-for-byte the bundle POST body, while any timeline warning stays stderr-only and non-blocking. |

_Gap closed: `submit` now exists (see [login-submit.md](login-submit.md))_
_and sends the exact bytes `scan`'s bundle-building path printed, verified_
Expand Down
26 changes: 26 additions & 0 deletions docs/signatures.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,32 @@ run anything, or name a slug outside `taxonomy.json` (enforced at load
time — see "Closed vocabulary" below). This is the whole reason Tier 1 can
scale to hundreds of entries without hundreds of regexes to audit.

### Audited npm release-check metadata

`signatures/package-map.json` also contains `npmReleaseCheckPackages`, a
small list of public npm package keys eligible for `submit`'s post-consent
release-date check. This list is **not a detection tier** and never creates a
skill match. Detection still uses only `map`; the checker looks up each listed
key in that map to derive its slug, so package-to-slug knowledge is not
duplicated.

The initial list contains only `better-auth`,
`@lemonsqueezy/lemonsqueezy.js`, `@paddle/paddle-js`, and
`@paddle/paddle-node-sdk`. Tests require every key to exist in `map`, every
map key pointing to an eligible slug to appear in the list, and no eligible
slug to be produced by a Tier 2 signature. Adding another map key for one of
those slugs therefore fails CI until the entire slug is reaudited. Slugs that
are shared with another ecosystem or reachable through Tier 2 are excluded;
for example, `ai/openai-api` and `auth/firebase-auth` never trigger an npm
lookup merely because their slugs appear in a bundle.

This conservative metadata deliberately does not claim which package caused a
historical detection: the bundle records only a slug. It allows `submit` to
compare a small unambiguous subset while leaving `scan`, skill detection, the
bundle, and the closed vocabulary unchanged. See
[login-submit.md](login-submit.md#npm-release-date-check-submit-only) for the
network and warning contract.

### PHP scope, honestly

`composer.json`'s `require` block is parsed as structured JSON — exact,
Expand Down
8 changes: 7 additions & 1 deletion signatures/package-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -686,5 +686,11 @@
"hvac": "security/hashicorp-vault",
"node-vault": "security/hashicorp-vault",
"pgvector": "ai/vector-search"
}
},
"npmReleaseCheckPackages": [
"better-auth",
"@lemonsqueezy/lemonsqueezy.js",
"@paddle/paddle-js",
"@paddle/paddle-node-sdk"
]
}
9 changes: 5 additions & 4 deletions src/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,11 @@ export async function headRequest(url: string, timeoutMs: number): Promise<{ sta
* Anonymous-or-authenticated GET with a timeout. Returns null (never
* throws) on any failure — network error, timeout, non-2xx status, or a
* body that isn't valid JSON — so a broken or offline endpoint can never
* delay or fail the command it's attached to. Two best-effort callers:
* version-check.ts's npm-registry freshness check (no headers), and
* submit.ts's fetchVerifiedEmails (a bearer `Authorization` header, for
* the identity-corroboration lookup) — both share this fail-open contract.
* delay or fail the command it's attached to. Best-effort callers are
* version-check.ts's npm-registry freshness check, submit.ts's npm
* packument transport (an `Accept` header only), and submit.ts's
* fetchVerifiedEmails (a bearer `Authorization` header for identity
* corroboration). All share this fail-open contract.
*/
export async function getJson<T>(
url: string,
Expand Down
Loading