Skip to content

Commit 40f6249

Browse files
committed
refactor(build): adopt canonical make-package-exports generator + browser field
Generate package.json exports + the browser builtin-stub field via the wheelhouse-canonical generator + an opt-in scripts/repo/package-exports.config.mts. Retire the bespoke generator. browser-fetch → fetch/browser (fetchResponse); docs/api-index.md → docs/api.md. Includes the cascaded scanner + token-guard fixture fixes. On top of the 6.1.0 bump.
1 parent 65c4567 commit 40f6249

180 files changed

Lines changed: 4212 additions & 1036 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* @file Single source of truth for "is this a fleet-approved CDN / package
3+
* registry host?" — shared by the cdn-allowlist-guard Claude hook
4+
* (PreToolUse, blocks a fetch/download to an off-allowlist host) and the
5+
* commit-time check, so the two never drift (code is law, DRY).
6+
*
7+
* The allowlist holds ONLY public package-registry and public CDN hosts —
8+
* the canonical registries every ecosystem advertises (crates.io, pypi.org,
9+
* …) plus the browser CDNs a front-end's CSP already exposes. These are
10+
* public knowledge, so the list is not sensitive: it is an allowlist, not a
11+
* secret, and the enforcement (not the secrecy of the list) is the value.
12+
*
13+
* 🚨 NEVER add an internal host here. A naive `https://` grep of a Socket
14+
* service repo surfaces `*.svc.cluster.local` Kubernetes service names
15+
* (artifact-search, github-interposer, metadata, nats, pgbouncer,
16+
* pipeline-gateway, svix, typosquat, …). Those are infra topology — a
17+
* public-surface-hygiene violation if committed. Seed this list from the
18+
* typed ecosystem-registry CONSTANTS that name fetch targets, never from a
19+
* blanket URL grep, and keep it to public registries / public CDNs only.
20+
*/
21+
22+
import { findInvocation } from './shell-command.mts'
23+
24+
// Public package-registry + download hosts the fleet's tooling legitimately
25+
// fetches from (seeded from depscan's ecosystem registry constants). Public
26+
// knowledge; all are canonical registries. Sorted alphabetically.
27+
export const ALLOWED_CDN_HOSTS: readonly string[] = [
28+
'bower.io',
29+
'chromewebstore.google.com',
30+
'clojars.org',
31+
'conda-forge.org',
32+
'cran.r-project.org',
33+
'crates.io',
34+
'deno.land',
35+
'elpa.gnu.org',
36+
'forge.puppet.com',
37+
'formulae.brew.sh',
38+
'github.com',
39+
'hackage.haskell.org',
40+
'hex.pm',
41+
'hub.docker.com',
42+
'huggingface.co',
43+
'juliahub.com',
44+
'metacpan.org',
45+
'npmjs.org',
46+
'nuget.org',
47+
'open-vsx.org',
48+
'package.elm-lang.org',
49+
'packagist.org',
50+
'pkgs.racket-lang.org',
51+
'proxy.golang.org',
52+
'pub.dev',
53+
'pypi.org',
54+
'repo1.maven.org',
55+
'rubygems.org',
56+
'swiftpackageindex.com',
57+
'vcpkg.io',
58+
]
59+
60+
// Public CDN hosts a fleet front-end's CSP exposes (wildcard subdomains).
61+
// Public-by-design (sent in browser response headers). `*.` matches any
62+
// subdomain depth of the suffix.
63+
export const ALLOWED_CDN_WILDCARDS: readonly string[] = [
64+
'*.apicdn.sanity.io',
65+
'*.api.sanity.io',
66+
'*.cloudfront.net',
67+
'*.githubusercontent.com',
68+
'*.jsdelivr.net',
69+
'*.unpkg.com',
70+
]
71+
72+
// True when `hostname` exactly matches an allowed host, or matches an allowed
73+
// wildcard suffix (`*.example.com` matches `a.example.com` and
74+
// `a.b.example.com`, but not the bare `example.com`). Compares
75+
// case-insensitively. Pass a bare hostname, not a URL.
76+
export function isAllowedCdnHost(hostname: string): boolean {
77+
const host = hostname.toLowerCase()
78+
for (let i = 0, { length } = ALLOWED_CDN_HOSTS; i < length; i += 1) {
79+
if (host === ALLOWED_CDN_HOSTS[i]) {
80+
return true
81+
}
82+
}
83+
for (let i = 0, { length } = ALLOWED_CDN_WILDCARDS; i < length; i += 1) {
84+
const suffix = ALLOWED_CDN_WILDCARDS[i]!.slice(1)
85+
if (host.endsWith(suffix) && host.length > suffix.length) {
86+
return true
87+
}
88+
}
89+
return false
90+
}
91+
92+
// Extract the hostname from a URL string, or undefined when it doesn't parse.
93+
export function hostnameOf(url: string): string | undefined {
94+
try {
95+
return new URL(url).hostname
96+
} catch {
97+
return undefined
98+
}
99+
}
100+
101+
// Find the first http(s) URL in a Bash command whose host is NOT allowed,
102+
// returning { url, host }. Used by the guard. Only flags fetch/download tools
103+
// (curl / wget / fetch) so unrelated URL mentions don't trip it. AST-matched
104+
// binary detection (no regex on the command), then a URL scan of the string.
105+
export interface DisallowedCdnHit {
106+
url: string
107+
host: string
108+
}
109+
110+
const FETCH_BINARIES: readonly string[] = ['curl', 'wget', 'fetch', 'http', 'https']
111+
112+
const URL_RE = /https?:\/\/[^\s"'`)>\]]+/g
113+
114+
export function findDisallowedCdn(command: string): DisallowedCdnHit | undefined {
115+
let invokesFetch = false
116+
for (let i = 0, { length } = FETCH_BINARIES; i < length; i += 1) {
117+
if (findInvocation(command, { binary: FETCH_BINARIES[i]! })) {
118+
invokesFetch = true
119+
break
120+
}
121+
}
122+
if (!invokesFetch) {
123+
return undefined
124+
}
125+
const matches = command.match(URL_RE)
126+
if (!matches) {
127+
return undefined
128+
}
129+
for (let i = 0, { length } = matches; i < length; i += 1) {
130+
const url = matches[i]!
131+
const host = hostnameOf(url)
132+
if (host && !isAllowedCdnHost(host)) {
133+
return { url, host }
134+
}
135+
}
136+
return undefined
137+
}

.claude/hooks/fleet/_shared/package-manager-auto-update.mts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,36 @@ export const AUTO_UPDATE_CHECKS: readonly AutoUpdateCheck[] = [
265265
},
266266
]
267267

268+
export interface MacosPkgAutoUpdateEnv {
269+
// The env-var name a shell `export` sets.
270+
name: string
271+
// The value that disables auto-update (always '1' today).
272+
value: string
273+
// The AutoUpdateCheck ids this knob disables, for traceability back to the
274+
// source-of-truth detectors above.
275+
managerIds: readonly string[]
276+
}
277+
278+
// The env knobs setup-security-tools persists into the managed shell-rc block on
279+
// macOS so a mid-task `brew` / `npm` / `pnpm` run can't auto-update a tool under
280+
// a build/scan. Single source of truth shared with the detectors above — the
281+
// shell-rc bridge imports this list instead of hardcoding a divergent copy, so
282+
// adding a future macOS knob here flows into the persisted block automatically.
283+
// HOMEBREW_NO_AUTO_UPDATE maps to the 'homebrew' check; NO_UPDATE_NOTIFIER is
284+
// honored by both 'npm' and 'pnpm'. Listed alphabetically by env name.
285+
export const MACOS_PKG_AUTO_UPDATE_ENV: readonly MacosPkgAutoUpdateEnv[] = [
286+
{
287+
name: 'HOMEBREW_NO_AUTO_UPDATE',
288+
value: '1',
289+
managerIds: ['homebrew'],
290+
},
291+
{
292+
name: 'NO_UPDATE_NOTIFIER',
293+
value: '1',
294+
managerIds: ['npm', 'pnpm'],
295+
},
296+
]
297+
268298
// True when `name` (a platform string) applies to the current OS.
269299
export function platformApplies(platform: PkgManagerPlatform): boolean {
270300
return platform === 'all' || platform === os.platform()

.claude/hooks/fleet/_shared/token-patterns.mts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,73 @@ export const SENSITIVE_NAME_FRAGMENTS: readonly string[] = [
211211
'AUTH',
212212
'CREDENTIAL',
213213
]
214+
215+
export interface SecretValuePattern {
216+
// The regex that matches the literal secret VALUE shape (not the env-var
217+
// name) — `AKIA…`, `ghp_…`, `sktsec_…`, a JWT, a PEM header.
218+
re: RegExp
219+
// Human label naming the vendor / kind, used in the block message.
220+
label: string
221+
}
222+
223+
// Literal secret-VALUE shapes — if any matches in arbitrary text, a real
224+
// credential has been pasted somewhere it shouldn't be. Distinct from the
225+
// `*_TOKEN_PATTERNS` above (those match an env-var KEY name). This is the
226+
// single source of truth shared by the Bash-time `token-guard`, the edit-time
227+
// `secret-content-guard`, and the commit-time scanners — one catalog so a new
228+
// vendor shape is added once and every gate picks it up (code is law, DRY).
229+
export const SECRET_VALUE_PATTERNS: readonly SecretValuePattern[] = [
230+
{ re: /sktsec_[A-Za-z0-9]{20,}/, label: 'Socket API key (sktsec_)' },
231+
{ re: /\bvtwn_[A-Za-z0-9_-]{8,}/, label: 'Val Town token (vtwn_)' },
232+
{ re: /\blin_api_[A-Za-z0-9_-]{8,}/, label: 'Linear API token (lin_api_)' },
233+
{ re: /\bsk-ant-[A-Za-z0-9_-]{20,}/, label: 'Anthropic API key (sk-ant-)' },
234+
{ re: /\bsk-proj-[A-Za-z0-9_-]{20,}/, label: 'OpenAI project key (sk-proj-)' },
235+
{ re: /\bhf_[A-Za-z0-9]{30,}/, label: 'Hugging Face token (hf_)' },
236+
{ re: /\bnpm_[A-Za-z0-9]{36}/, label: 'npm access token (npm_)' },
237+
{ re: /\bdop_v1_[a-f0-9]{64}/, label: 'DigitalOcean PAT (dop_v1_)' },
238+
{ re: /\bsk-[A-Za-z0-9_-]{20,}/, label: 'OpenAI/Anthropic-style secret key (sk-)' },
239+
{ re: /\bsk_live_[A-Za-z0-9_-]{16,}/, label: 'Stripe live secret (sk_live_)' },
240+
{ re: /\bsk_test_[A-Za-z0-9_-]{16,}/, label: 'Stripe test secret (sk_test_)' },
241+
{ re: /\bpk_live_[A-Za-z0-9_-]{16,}/, label: 'Stripe live publishable (pk_live_)' },
242+
{ re: /\brk_live_[A-Za-z0-9_-]{16,}/, label: 'Stripe live restricted (rk_live_)' },
243+
{ re: /\bghp_[A-Za-z0-9]{30,}/, label: 'GitHub personal access token (ghp_)' },
244+
{ re: /\bgho_[A-Za-z0-9]{30,}/, label: 'GitHub OAuth token (gho_)' },
245+
// ghs_ / ghu_ char classes include `.` and `_` to match both the classic
246+
// opaque format AND the stateless JWT format (≥36 is the min for both).
247+
{ re: /\bghs_[A-Za-z0-9._]{36,}/, label: 'GitHub app server token (ghs_)' },
248+
{ re: /\bghu_[A-Za-z0-9._]{36,}/, label: 'GitHub user access token (ghu_)' },
249+
{ re: /\bghr_[A-Za-z0-9]{30,}/, label: 'GitHub refresh token (ghr_)' },
250+
{ re: /\bgithub_pat_[A-Za-z0-9_]{20,}/, label: 'GitHub fine-grained PAT' },
251+
{ re: /\bglpat-[A-Za-z0-9_-]{16,}/, label: 'GitLab PAT (glpat-)' },
252+
{ re: /\bAKIA[0-9A-Z]{16}/, label: 'AWS access key ID (AKIA)' },
253+
{ re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/, label: 'Slack token (xox_-)' },
254+
{ re: /\bAIza[0-9A-Za-z_-]{35}/, label: 'Google API key (AIza)' },
255+
{
256+
re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/,
257+
label: 'JWT',
258+
},
259+
{
260+
re: /-----BEGIN [A-Z ]*PRIVATE KEY( BLOCK)?-----/,
261+
label: 'private key (PEM block)',
262+
},
263+
]
264+
265+
export interface SecretValueHit {
266+
label: string
267+
// The matched secret substring, for the block message. Callers MUST redact
268+
// before logging if the surface could be public.
269+
match: string
270+
}
271+
272+
// Return the first secret-VALUE shape matched in `text`, or undefined. Used by
273+
// every secret gate (Bash / edit / commit) so they share one detection list.
274+
export function scanSecretValues(text: string): SecretValueHit | undefined {
275+
for (let i = 0, { length } = SECRET_VALUE_PATTERNS; i < length; i += 1) {
276+
const { label, re } = SECRET_VALUE_PATTERNS[i]!
277+
const m = re.exec(text)
278+
if (m) {
279+
return { label, match: m[0] }
280+
}
281+
}
282+
return undefined
283+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# cascade-first-triage-reminder
2+
3+
**Type:** Stop hook (soft reminder — never blocks).
4+
5+
## Trigger
6+
7+
Fires when the last assistant turn shows all of:
8+
9+
1. a "not found" / "missing" / "unregistered" error shape, AND
10+
2. mention of a fleet-canonical artifact kind (`socket/*` rule, oxlint plugin,
11+
`.config/fleet/`, `scripts/fleet/`, `.claude/hooks/fleet/`, `_shared/`, a
12+
`check-*.mts`), AND
13+
3. evidence the assistant patched the **member** repo's copy (edit verbs aimed
14+
at a `socket-*` path, "fixed the cascaded/live copy", `git apply`),
15+
16+
without acknowledging the cascade-first path (re-cascade / "check the
17+
wheelhouse" / "incomplete cascade").
18+
19+
## Why
20+
21+
Member repos hold byte-copies of wheelhouse-canonical content. A missing or
22+
unregistered canonical artifact in a member is almost always an **incomplete
23+
cascade** (the cascade skips a fleet dir whose template source is git-dirty),
24+
not a real bug. Debugging or hand-patching the member's copy wastes cycles on
25+
code you don't own there — the fix lives upstream in the wheelhouse template,
26+
and re-cascading propagates it.
27+
28+
## Bypass
29+
30+
None — it's a non-blocking reminder. Acknowledge the cascade-first path (or
31+
genuinely confirm the artifact is absent from the wheelhouse too) and it stays
32+
quiet.
33+
34+
See CLAUDE.md "Never fork fleet-canonical files locally" (cascade-first triage).
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env node
2+
// Claude Code Stop hook — cascade-first-triage-reminder.
3+
//
4+
// Nudges when the assistant reacted to a "not found" / "missing" /
5+
// "unregistered" error for a fleet-CANONICAL artifact by debugging or
6+
// hand-patching the MEMBER repo's copy, instead of checking the wheelhouse
7+
// template first and re-cascading. Member repos hold byte-copies of
8+
// wheelhouse-canonical content (`.config/fleet/**`, `scripts/fleet/**`,
9+
// `.claude/hooks/fleet/**`, the `socket/*` oxlint plugin, `_shared/` libs).
10+
// When one of those goes missing in a member, it is almost always an
11+
// incomplete cascade — the cascade SKIPS a fleet dir whose template source
12+
// is git-dirty — not a real bug to fix in the member.
13+
//
14+
// What this catches: an assistant turn that BOTH
15+
// (a) shows a not-found-shaped error naming a canonical artifact, AND
16+
// (b) describes editing / patching a member-repo copy of fleet content,
17+
// WITHOUT acknowledging the cascade-first path (check wheelhouse, re-cascade).
18+
//
19+
// Heuristic; false positives expected. Soft reminder, never blocks.
20+
// Per CLAUDE.md "Never fork fleet-canonical files locally" (cascade-first
21+
// triage).
22+
23+
import process from 'node:process'
24+
25+
import {
26+
readLastAssistantText,
27+
readStdin,
28+
stripCodeFences,
29+
} from '../_shared/transcript.mts'
30+
31+
interface StopPayload {
32+
readonly transcript_path?: string | undefined
33+
}
34+
35+
// A "not found"-shaped error for a canonical artifact: a rule, hook, lib,
36+
// or check that the tooling reports as absent/unregistered.
37+
const NOT_FOUND_RE =
38+
/\b(not found|no such (?:rule|file|module|hook)|cannot find|missing|unregistered|does not exist|isn't registered|is not registered)\b/i
39+
40+
// Mentions of a fleet-canonical artifact KIND (the things that live in the
41+
// wheelhouse and cascade out). A bare "file not found" without one of these
42+
// is too generic to flag. `plugin ['"]?socket` catches the oxlint loader's
43+
// own `Rule '…' not found in plugin 'socket'` message shape.
44+
const CANONICAL_ARTIFACT_RE =
45+
/(socket\/[a-z0-9-]+|oxlint[- ]plugin|plugin ['"]?socket|\.config\/fleet\/|scripts\/fleet\/|\.claude\/hooks\/fleet\/|_shared\/|fleet[- ]canonical|check-[a-z-]+\.mts)/i
46+
47+
// Evidence the assistant DEBUGGED / PATCHED the member copy rather than
48+
// re-cascading: edit verbs aimed at a member-repo path or "the copy".
49+
const MEMBER_PATCH_RE =
50+
/\b(edited|patched|hand-?patch|fixed (?:the )?(?:member|downstream|live|cascaded) (?:copy|file)|git apply|added .* to (?:socket-(?:lib|cli|bin|btm|registry|sdk-js|mcp|packageurl-js|addon)))\b/i
51+
52+
// The cascade-first acknowledgement that satisfies the check.
53+
const CASCADE_ACK_RE =
54+
/\b(re-?cascade|cascade-first|check(?:ed)? the wheelhouse|wheelhouse (?:has|template)|sync-scaffolding|incomplete cascade|cascade issue|cascade incompleteness)\b/i
55+
56+
async function main(): Promise<void> {
57+
const payloadRaw = await readStdin()
58+
let payload: StopPayload
59+
try {
60+
payload = JSON.parse(payloadRaw) as StopPayload
61+
} catch {
62+
process.exit(0)
63+
}
64+
const rawText = readLastAssistantText(payload.transcript_path)
65+
if (!rawText) {
66+
process.exit(0)
67+
}
68+
const text = stripCodeFences(rawText)
69+
70+
if (!NOT_FOUND_RE.test(text)) {
71+
process.exit(0)
72+
}
73+
if (!CANONICAL_ARTIFACT_RE.test(text)) {
74+
process.exit(0)
75+
}
76+
if (!MEMBER_PATCH_RE.test(text)) {
77+
process.exit(0)
78+
}
79+
if (CASCADE_ACK_RE.test(text)) {
80+
process.exit(0)
81+
}
82+
83+
const lines = [
84+
'[cascade-first-triage-reminder] A canonical artifact looked "not found" in a member repo and you patched the member copy.',
85+
'',
86+
' Member repos hold byte-copies of wheelhouse-canonical content',
87+
' (.config/fleet/**, scripts/fleet/**, .claude/hooks/fleet/**, the',
88+
' socket/* oxlint plugin, _shared/ libs). A missing/unregistered one is',
89+
' almost always an INCOMPLETE CASCADE, not a bug to fix in the member.',
90+
'',
91+
' Cascade-first triage:',
92+
' 1. Check the wheelhouse template/ for the artifact.',
93+
' 2. If present → re-cascade the member:',
94+
' node scripts/repo/sync-scaffolding/cli.mts --target <repo> --fix',
95+
' 3. If the cascade SKIPS a fleet dir, its template source is git-dirty',
96+
' (WIP / a parallel session) — commit/reconcile the template, re-cascade.',
97+
' 4. Only if genuinely absent from the wheelhouse is it a real authoring task.',
98+
'',
99+
' Per CLAUDE.md "Never fork fleet-canonical files locally".',
100+
'',
101+
]
102+
process.stderr.write(lines.join('\n') + '\n')
103+
process.exit(0)
104+
}
105+
106+
main().catch(() => {
107+
process.exit(0)
108+
})

0 commit comments

Comments
 (0)