Skip to content

Commit 778fa55

Browse files
authored
feat: expose badge ids in details json (#43)
1 parent 703cebd commit 778fa55

9 files changed

Lines changed: 109 additions & 73 deletions

File tree

PRIVACY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ PolicyChecks is a free, read-only GitHub App that reports selected repository se
66

77
When someone requests a badge, Shields JSON response, details JSON response, or repository `info.json` response, PolicyChecks uses the requested repository owner/name to query GitHub for the supported repository settings. PolicyChecks does not request repository write permissions and does not read repository source code.
88

9-
PolicyChecks may temporarily cache API-derived claim results in memory for up to the configured cache TTL, currently one hour by default. This cache is used only to reduce repeated GitHub API requests.
9+
PolicyChecks may temporarily cache API-derived badge evaluation results in memory for up to the configured cache TTL, currently one hour by default. This cache is used only to reduce repeated GitHub API requests.
1010

1111
PolicyChecks may receive GitHub Marketplace lifecycle webhooks for Marketplace listing administration. These deliveries are verified and acknowledged, but PolicyChecks does not create customer accounts or store Marketplace webhook payloads.
1212

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,16 @@ If the GitHub API provides an endpoint that reliably tracks a specific repo sett
5858

5959
## Endpoints
6060

61-
Each claim supports the same endpoint shape:
61+
Each supported badge has a stable badge ID, such as `sha-pinning-required` or `community-health`, and supports the same endpoint shape:
6262

6363
```text
64-
GET /github/{owner}/{repo}/{setting}.svg # Returns an SVG badge with status `enabled`, `disabled`, or `unknown` for a given setting
65-
GET /github/{owner}/{repo}/{setting}.json # Returns a Shields-compatible JSON result for use in custom badge tooling
66-
GET /github/{owner}/{repo}/{setting}/details.json # Returns the PolicyChecks evaluation record and selected response-derived details for a given setting
64+
GET /github/{owner}/{repo}/{badge-id}.svg # Returns an SVG badge with status `enabled`, `disabled`, or `unknown`
65+
GET /github/{owner}/{repo}/{badge-id}.json # Returns a Shields-compatible JSON result for use in custom badge tooling
66+
GET /github/{owner}/{repo}/{badge-id}/details.json # Returns the PolicyChecks evaluation record and selected response-derived details
6767
GET /github/{owner}/{repo}/info.json # A general JSON response that provides collective information about multiple different settings
6868
```
6969

70-
Use the SVG endpoint for badges, the Shields-compatible JSON endpoint for badge tooling, and the details endpoint for the underlying PolicyChecks evaluation record. README badges can link directly to their details JSON:
70+
Use the SVG endpoint for badges, the Shields-compatible JSON endpoint for badge tooling, and the details endpoint for the underlying PolicyChecks evaluation record. Details JSON identifies the badge as `badgeId` and omits internal classification fields. README badges can link directly to their details JSON:
7171

7272
```markdown
7373
[![Immutable releases](https://policychecks.reponomics.org/github/OWNER/REPO/immutable-releases.svg)](https://policychecks.reponomics.org/github/OWNER/REPO/immutable-releases/details.json)
@@ -102,7 +102,7 @@ Use the SVG endpoint for badges, the Shields-compatible JSON endpoint for badge
102102

103103
</details>
104104

105-
The aggregate endpoint returns all currently supported claims for a repository:
105+
The aggregate endpoint returns all currently supported badge results for a repository:
106106

107107
```text
108108
https://policychecks.reponomics.org/github/OWNER/REPO/info.json

cloudflare/policychecks-worker.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createHmac, timingSafeEqual } from "node:crypto";
22

33
import { InMemoryClaimCache } from "../src/cache/cache.js";
4+
import { toDetailsJson } from "../src/badges/details-json.js";
45
import { renderBadgeSvg } from "../src/badges/svg.js";
56
import { toShieldsJson } from "../src/badges/shields-json.js";
67
import { claimDefinitions, getClaimDefinition } from "../src/claims/registry.js";
@@ -120,7 +121,7 @@ export default {
120121
}
121122

122123
if (route.kind === "details") {
123-
return json(result, 200, {
124+
return json(toDetailsJson(result), 200, {
124125
"Cache-Control": cacheControl
125126
});
126127
}

docs/adr/0001-badge-publication-consent.md

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ That creates a consent mismatch:
1616

1717
- Installing the GitHub App grants PolicyChecks read access to selected repository administration settings.
1818
- Public badge endpoints disclose the result of any supported setting check to anyone who knows or guesses the endpoint URL.
19-
- The aggregate `info.json` endpoint exposes all supported check results together, which makes the service look more like a public audit surface than a maintainer-selected badge service.
19+
- The aggregate `info.json` endpoint exposes all supported badge results together, which makes the service look more like a public audit surface than a maintainer-selected badge service.
2020

2121
This is especially sensitive because these settings are not all publicly available through the unauthenticated GitHub API. A maintainer may want to publish one badge, such as immutable releases, without also making every other supported repository setting easy to query.
2222

@@ -62,20 +62,20 @@ Both proposals require removing or disabling the public aggregate `info.json` en
6262

6363
Restrict PolicyChecks to public repositories and serve a badge only when the canonical badge URL appears in the repository's preferred README on the default branch.
6464

65-
Under this proposal, publication is controlled by the repository itself. A maintainer publishes a claim by committing the badge URL to the public repository README. If the badge URL is not present, PolicyChecks does not serve the badge or details endpoint for that claim.
65+
Under this proposal, publication is controlled by the repository itself. A maintainer publishes a badge by committing the badge URL to the public repository README. If the badge URL is not present, PolicyChecks does not serve the badge or details endpoint for that badge ID.
6666

6767
Example publication check:
6868

6969
```text
70-
GET /github/{owner}/{repo}/{claim}.svg
70+
GET /github/{owner}/{repo}/{badge-id}.svg
7171
```
7272

7373
PolicyChecks verifies:
7474

7575
1. The repository is public.
7676
2. The GitHub App is installed on the repository.
77-
3. The repository's preferred README on the default branch contains the canonical badge URL for that claim.
78-
4. The requested claim can be evaluated from GitHub API data.
77+
3. The repository's preferred README on the default branch contains the canonical badge URL for that badge ID.
78+
4. The requested badge can be evaluated from GitHub API data.
7979

8080
If any publication check fails, PolicyChecks returns a generic unavailable response, such as `404`. It should not return `disabled`, because `disabled` is the privileged information the publication check is meant to protect.
8181

@@ -114,20 +114,20 @@ This proposal is accepted because it keeps the project small and keeps the publi
114114

115115
Use tokenized public badge URLs and remove the public aggregate `info.json` endpoint.
116116

117-
Public badge and details endpoints would require a per-repository, per-claim token. A token authorizes disclosure of exactly one claim for exactly one repository installation. PolicyChecks can derive these tokens deterministically from a private server-side signing secret rather than storing each token in a database.
117+
Public badge and details endpoints would require a per-repository, per-badge token. A token authorizes disclosure of exactly one badge result for exactly one repository installation. PolicyChecks can derive these tokens deterministically from a private server-side signing secret rather than storing each token in a database.
118118

119119
Example endpoint shapes:
120120

121121
```text
122-
GET /github/{owner}/{repo}/{claim}.svg?token={token}
123-
GET /github/{owner}/{repo}/{claim}.json?token={token}
124-
GET /github/{owner}/{repo}/{claim}/details.json?token={token}
122+
GET /github/{owner}/{repo}/{badge-id}.svg?token={token}
123+
GET /github/{owner}/{repo}/{badge-id}.json?token={token}
124+
GET /github/{owner}/{repo}/{badge-id}/details.json?token={token}
125125
```
126126

127-
The token should be computed from stable GitHub identifiers and the claim identifier:
127+
The token should be computed from stable GitHub identifiers and the badge ID:
128128

129129
```text
130-
token = HMAC(BADGE_TOKEN_SECRET, installation_id + repository_id + claim_id)
130+
token = HMAC(BADGE_TOKEN_SECRET, installation_id + repository_id + badge_id)
131131
```
132132

133133
`BADGE_TOKEN_SECRET` is a private PolicyChecks runtime secret. It is never shown to installers. The public token derived from it is safe to place in README badge Markdown because it authorizes only a single public badge result.
@@ -136,7 +136,7 @@ Including `installation_id` means uninstalling and reinstalling the GitHub App r
136136

137137
## Publication Model
138138

139-
Under this model, "published" means that a maintainer has chosen to use the tokenized URL for a specific repository and claim. PolicyChecks does not need to detect where the URL is embedded.
139+
Under this model, "published" means that a maintainer has chosen to use the tokenized URL for a specific repository and badge ID. PolicyChecks does not need to detect where the URL is embedded.
140140

141141
Anyone can view a badge once they have the tokenized URL, but they cannot derive other badge URLs from it. For example, a token for `immutable-releases` on repository A cannot be used to query `sha-pinning-required` on repository A or `immutable-releases` on repository B.
142142

@@ -154,7 +154,7 @@ This preserves the practical workflow:
154154
2. Copy the badge snippets you want.
155155
3. Paste them where you want the badges to appear.
156156

157-
No per-claim database state is required for the initial version.
157+
No per-badge database state is required for the initial version.
158158

159159
### Consequences
160160

@@ -166,7 +166,7 @@ Positive:
166166
- The product remains close to "install and paste a badge."
167167
- No repository contents permission is required.
168168
- No committed configuration file is required.
169-
- No durable per-claim publication database is required.
169+
- No durable per-badge publication database is required.
170170
- Tokens can be regenerated for authorized maintainers because they are deterministic.
171171

172172
Negative:
@@ -181,7 +181,7 @@ Negative:
181181

182182
### Option A: Keep the current public endpoint model
183183

184-
Installation continues to expose every supported claim endpoint for an installed repository.
184+
Installation continues to expose every supported badge endpoint for an installed repository.
185185

186186
Benefits:
187187

@@ -200,7 +200,7 @@ This option conflicts with the desired consent model.
200200

201201
### Option B: Remove `info.json` only
202202

203-
Delete or disable the aggregate endpoint, but leave individual claim endpoints public and guessable.
203+
Delete or disable the aggregate endpoint, but leave individual badge endpoints public and guessable.
204204

205205
Benefits:
206206

@@ -210,14 +210,14 @@ Benefits:
210210

211211
Costs:
212212

213-
- Anyone can still query every individual supported claim for an installed repository.
214-
- Maintainers still cannot selectively publish one claim without exposing the rest.
213+
- Anyone can still query every individual supported badge for an installed repository.
214+
- Maintainers still cannot selectively publish one badge without exposing the rest.
215215

216216
This is a useful immediate mitigation, but it does not fully solve the disclosure model.
217217

218-
### Option C: Store a per-repository, per-claim publication allowlist
218+
### Option C: Store a per-repository, per-badge publication allowlist
219219

220-
Add a small configuration store that records which claims are publicly enabled for each repository.
220+
Add a small configuration store that records which badges are publicly enabled for each repository.
221221

222222
Example:
223223

@@ -226,7 +226,7 @@ repository_id
226226
installation_id
227227
owner
228228
repo
229-
published_claims
229+
published_badges
230230
updated_by
231231
updated_at
232232
```
@@ -275,14 +275,14 @@ This option is not recommended unless PolicyChecks intentionally expands into re
275275
276276
### Option E: Use tokenized per-badge URLs
277277
278-
Generate unguessable badge URLs for every repository and claim, and require the matching token on public badge and details endpoints.
278+
Generate unguessable badge URLs for every repository and badge ID, and require the matching token on public badge and details endpoints.
279279
280280
Benefits:
281281
282282
- Maintainers choose what to publish by choosing which badge URL to use.
283283
- No durable publication database is required.
284284
- No repository contents permission is required.
285-
- Public callers cannot enumerate unpublished claims.
285+
- Public callers cannot enumerate unpublished badges.
286286
- Tokens can be regenerated for authorized maintainers.
287287
- Keeps the product close to a no-configuration badge workflow.
288288
@@ -302,14 +302,14 @@ The public aggregate endpoint should be removed or disabled:
302302
GET /github/{owner}/{repo}/info.json
303303
```
304304

305-
If an aggregate endpoint is later reintroduced, it should return only claims that the maintainer has explicitly published. It should not act as a public repository policy profile.
305+
If an aggregate endpoint is later reintroduced, it should return only badges that the maintainer has explicitly published. It should not act as a public repository policy profile.
306306

307-
Badge, Shields JSON, and details JSON endpoints should all use the same publication rule. Under the README-presence proposal, the details endpoint is available only when the corresponding badge URL is present in the preferred README. Under the tokenized URL proposal, the details endpoint requires the same per-repository, per-claim token as the badge. Details JSON can disclose more context than a badge, so it should not remain available through an unauthenticated, guessable URL. The details endpoint is supporting evaluation context derived from GitHub API responses, not an audit report, guarantee, or independent compliance attestation.
307+
Badge, Shields JSON, and details JSON endpoints should all use the same publication rule. Under the README-presence proposal, the details endpoint is available only when the corresponding badge URL is present in the preferred README. Under the tokenized URL proposal, the details endpoint requires the same per-repository, per-badge token as the badge. Details JSON can disclose more context than a badge, so it should not remain available through an unauthenticated, guessable URL. The details endpoint is supporting evaluation context derived from GitHub API responses, not an audit report, guarantee, or independent compliance attestation.
308308

309309
For failed publication checks, responses should avoid distinguishing:
310310

311311
- GitHub App not installed.
312-
- Claim not published.
312+
- Badge not published.
313313
- Token invalid.
314314
- Repository not found.
315315
- Repository is private.
@@ -352,4 +352,4 @@ This keeps PolicyChecks closest to its core value: a small public badge service
352352

353353
Tokenized badge URLs remain the leading alternative if private repository support or non-README publication becomes important enough to justify the extra setup and authorization surface.
354354

355-
If schedule pressure requires a smaller immediate change, remove `info.json` first. The current all-claims-public posture should not be treated as the intended long-term model.
355+
If schedule pressure requires a smaller immediate change, remove `info.json` first. The current all-badges-public posture should not be treated as the intended long-term model.

docs/operations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ PolicyChecks is a current-state badge and details service. Operational safety de
66

77
## Cache Policy
88

9-
The internal claim-result cache defaults to `CACHE_TTL_SECONDS=3600`. This is intentionally conservative because the checked settings are low-volatility repository administration/security settings, not fast-changing CI state.
9+
The internal badge-result cache defaults to `CACHE_TTL_SECONDS=3600`. This is intentionally conservative because the checked settings are low-volatility repository administration/security settings, not fast-changing CI state.
1010

1111
Public badge, Shields JSON, details JSON, and `info.json` responses use `Cache-Control: public, max-age=300, stale-while-revalidate=300`. This keeps externally visible badge staleness modest while allowing the Worker to serve repeated requests from its longer internal cache when the same isolate remains warm.
1212

@@ -16,7 +16,7 @@ Tune cache settings with this policy:
1616
- Keep `CACHE_TTL_SECONDS` at least as large as the public `max-age` value, otherwise external clients can revalidate faster than the Worker cache can absorb.
1717
- Use `3600` seconds as the default.
1818
- Consider `21600` seconds for high-traffic public badges if rate-limit logs show repeated cache misses for the same repositories.
19-
- Do not lower below `300` seconds unless actively debugging a claim mapping.
19+
- Do not lower below `300` seconds unless actively debugging a badge mapping.
2020

2121
Current limitation: the cache is in-memory per Worker isolate. It is a quota reducer, not durable storage. Cold starts and separate isolates may still call GitHub for the same repository.
2222

0 commit comments

Comments
 (0)