Skip to content

Commit b001554

Browse files
committed
reduce api surface, rework
1 parent b804c12 commit b001554

66 files changed

Lines changed: 1315 additions & 1800 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/pages.yml‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ jobs:
1717
runs-on: ubuntu-latest
1818
permissions:
1919
contents: read
20-
pages: write
2120
steps:
2221
- uses: actions/checkout@v5
2322
- uses: actions/setup-node@v5
@@ -27,12 +26,6 @@ jobs:
2726
cache-dependency-path: runtime/package-lock.json
2827
- run: npm ci
2928
working-directory: runtime
30-
# - run: npm run check
31-
# working-directory: runtime
32-
# - run: npm run lint
33-
# working-directory: runtime
34-
# - run: npm test
35-
# working-directory: runtime
3629
- run: npm run build
3730
working-directory: runtime
3831
- name: Stage Pages metadata

‎.gitignore‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,5 @@ __marimo__/
224224
# Streamlit
225225
.streamlit/secrets.toml
226226

227-
config/data/*
227+
config/data/*
228+
vps-config/

‎README.md‎

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -78,23 +78,35 @@ const resource = resourceFromDocument({
7878
});
7979
```
8080

81-
The bundled read-only discussion example can render the feedback site's test
82-
thread, including labels, canonical reaction counters, minimized comments, and
83-
replies:
81+
The bundled example exercises multiple threads, batched native upvotes, main-post
82+
reactions, labels, polls, comments, minimized/deleted comments, replies, accepted
83+
answers, author associations, and GitHub links. Supply
84+
comma-separated resource keys with `keys`:
8485

85-
`https://feedback.cpp.social/example/?site=feedback-cpp-social&key=feedback%2Fexample&github=link`
86+
`https://feedback.cpp.social/example/?site=feedback-cpp-social&keys=feedback%2Fexample%2Cfeedback%2Fexample-two&github=link`
8687

87-
## Counter cache
88+
## API and intent configuration
8889

89-
The service database is a cache of GitHub discussions. It keeps
90-
lightweight discussion and comment metadata in `discussions` and `comments`,
91-
content separately in `content`, main-post reactions in `reactions`, comment
92-
reactions in `comment_reactions`, and discussion labels through
93-
`discussion_labels`. Reaction rows retain GitHub account IDs when the API
94-
returns them; an aggregate remainder row is used for accounts not included in
95-
the returned user page. The public API is read-only for discussion content and
96-
counters; discussion creation, voting, starring, and comment moderation are not
97-
exposed.
90+
Every site must list its required `intents`. Disabled intents return 404 and do
91+
not activate their refresh paths. The route contract, intent matrix, and audit
92+
of every server-side GitHub request path are in [docs/API.md](docs/API.md).
93+
94+
The browser runtime sends user-specific viewer queries, native upvotes, and comments
95+
directly to GitHub. The service handles OAuth, discussion discovery/creation, and
96+
shared anonymous reads where caching prevents every visitor consuming a GitHub
97+
request.
98+
99+
Use `--verbose` for privacy-safe route/status/timing and cache-decision logs. It
100+
does not log credentials, OAuth codes, bodies, origins, resource URLs, or client
101+
addresses.
102+
103+
## Counter cache and discussion content
104+
105+
The service database keeps discussion identifiers and aggregate counters only.
106+
Discussion/comment bodies are never persisted or
107+
reused between requests, preventing deleted content from being served from a
108+
stale cache. Only concurrent reads for the same thread share an in-flight GitHub
109+
call.
98110

99111
`cache_fresh_seconds` is the age at which a requested tracked counter needs an
100112
authoritative GitHub refresh. The default is five seconds. A batched request
@@ -111,28 +123,26 @@ batch. The default is five seconds.
111123
`refresh_sweep_seconds` controls the low-priority full maintenance cycle. The
112124
default is 86400 seconds (daily). The service walks only discussions whose last
113125
authoritative snapshot is that old, in batches of 50 with pacing between full
114-
batches. Targeted requests and successful votes continue independently.
126+
batches. Targeted requests and successful upvotes continue independently.
115127

116-
Votes made through the runtime update SQLite immediately using an atomic,
117-
confirmed delta. These local values are tentative: they do not change the last
118-
GitHub-refresh timestamp, and the next successful targeted or maintenance refresh
119-
replaces them with GitHub's absolute counts. Counter responses use `no-cache`, so
128+
Browser mutations do not write speculative server values. The next successful
129+
targeted or maintenance refresh replaces counters with GitHub's absolute counts.
130+
Counter responses use `no-cache`, so
120131
browsers revalidate with this service; that does not imply a GitHub request while
121132
the relevant snapshot remains fresh.
122133

123134
The browser runtime keeps the last counter snapshot and the last confirmed viewer
124-
vote/star state in local storage for up to seven days. Consumers can render them
135+
native-upvote state in local storage for up to seven days. Consumers can render them
125136
synchronously while the API request is in flight, avoiding a flash of zero counters
126137
or unselected vote buttons. Snapshots contain only the site/resource key, discussion
127-
node ID, counts, viewer state, star state, and save time; authentication tokens are not part of
138+
node ID, counts, viewer state, and save time; authentication tokens are not part of
128139
this cache. Storage is optional and failures fall back to the network normally.
129140

130-
Once authenticated, the runtime also resolves the current user's vote and star
141+
Once authenticated, the runtime also resolves the current user's native-upvote
131142
state for all visible discussions in one batched GitHub query. That viewer-specific
132143
snapshot is reused for five minutes, including across reloads, and then refreshed
133144
on demand. This makes reactions created on GitHub or another device visible without
134-
turning every counter request into an authenticated GitHub request. Stars map to
135-
GitHub's `EYES` reaction because Discussions does not provide a star reaction.
145+
turning every counter request into an authenticated GitHub request.
136146

137147
Operational logs are emitted at GitHub boundaries rather than for every HTTP
138148
request. Reaction-refresh lines include the site, trigger (`requested` or

‎config/sites.dev.toml‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ http_request_timeout_seconds = 10
1212

1313
[sites.feedback-cpp-social]
1414
origins = ["https://feedback.cpp.social"]
15+
mode = "discussion"
1516
mapping = "key"
1617
repository = "cppsocial/feedback"
1718
repository_id = "R_kgDOUc9png"
1819
installation_id = 162105624
19-
category = "General"
20-
category_id = "DIC_kwDOUc9pns4DFwQt"
20+
default_category = "general"
21+
intents = ["upvotes", "reactions", "discussion", "comments", "labels", "github_link"]
22+
23+
[sites.feedback-cpp-social.categories.general]
24+
name = "General"
25+
id = "DIC_kwDOUc9pns4DFwQt"

‎config/sites.example.toml‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,30 @@ http_request_timeout_seconds = 10
1010

1111
[sites.example]
1212
origins = ["https://www.example.com"]
13+
mode = "discussion"
1314
mapping = "key"
1415
repository = "example/site"
1516
repository_id = "replace-with-repository-node-id"
1617
installation_id = 1
17-
category = "Resources"
18-
category_id = "replace-with-category-node-id"
18+
default_category = "articles"
19+
discussion_body = "Discussion for [{title}]({url})."
1920

2021
# Optional cache tuning; these are the defaults.
2122
# cache_fresh_seconds = 5
2223
# refresh_cooldown_seconds = 5
2324
# refresh_sweep_seconds = 86400
2425
# max_batch_size = 100
25-
features = ["counters", "viewer_reactions", "voting", "discussion", "comments", "labels", "github_link"]
26+
intents = ["upvotes", "reactions", "discussion", "comments", "answers", "polls", "authors", "moderation", "comment_reactions", "comment_upvotes", "labels", "github_link"]
2627
reaction_counters = ["LAUGH", "HOORAY", "CONFUSED", "HEART", "ROCKET", "EYES"]
27-
upvote_source = "both" # thumbsup, native, or both
28-
downvotes = true
28+
29+
[sites.example.categories.articles]
30+
name = "Articles"
31+
id = "replace-with-articles-category-node-id"
32+
33+
[sites.example.categories.tips]
34+
name = "Tips"
35+
id = "replace-with-tips-category-node-id"
36+
37+
[sites.example.categories.updates]
38+
name = "Updates"
39+
id = "replace-with-updates-category-node-id"

‎config/sites.toml‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,30 @@ http_request_timeout_seconds = 10
1010

1111
[sites.feedback-cpp-social]
1212
origins = ["https://feedback.cpp.social"]
13+
mode = "discussion"
1314
mapping = "key"
1415
repository = "cppsocial/feedback"
1516
repository_id = "R_kgDOUc9png"
1617
installation_id = 162105624
17-
category = "General"
18-
category_id = "DIC_kwDOUc9pns4DFwQt"
19-
features = ["counters", "viewer_reactions", "voting", "discussion", "comments", "labels", "github_link"]
18+
default_category = "general"
19+
discussion_body = "Feedback for [{title}]({url})"
20+
intents = ["upvotes", "reactions", "discussion", "comments", "labels", "github_link"]
2021
reaction_counters = ["LAUGH", "HOORAY", "CONFUSED", "HEART", "ROCKET", "EYES"]
21-
upvote_source = "both"
22-
downvotes = true
22+
23+
[sites.feedback-cpp-social.categories.general]
24+
name = "General"
25+
id = "DIC_kwDOUc9pns4DFwQt"
2326

2427
[sites.cpp-social]
2528
origins = ["https://cpp.social"]
29+
mode = "ranking"
2630
mapping = "key"
2731
repository = "cppsocial/site"
2832
repository_id = "R_kgDOSOfNQQ"
2933
installation_id = 162105624
30-
category = "Resources"
31-
category_id = "DIC_kwDOSOfNQc4DFW7W"
34+
default_category = "resources"
35+
intents = ["upvotes"]
36+
37+
[sites.cpp-social.categories.resources]
38+
name = "Resources"
39+
id = "DIC_kwDOSOfNQc4DFW7W"

‎docs/API.md‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Feedback API and browser package
2+
3+
The system maps a site-defined resource key to one GitHub Discussion. Resource
4+
keys are stable and known by the consuming site; use category-qualified keys such
5+
as `articles/ranges`, `tips/vector-growth`, and `updates/2026-09`. The first path
6+
component selects a configured GitHub Discussion category. Unrecognised prefixes
7+
use `default_category`.
8+
9+
There are two site modes:
10+
11+
- `ranking` exposes batched native GitHub Discussion upvote counts. It may also
12+
expose explicitly selected reaction counts. It has no discussion-content API.
13+
- `discussion` can expose a full thread, post reactions, comments and replies,
14+
answers, polls, author associations, moderation state, labels, comment
15+
reactions/upvotes, and GitHub links. Each extra group is enabled by an intent.
16+
17+
An unauthenticated visitor can read configured data but cannot mutate it. OAuth
18+
is initiated only by user interaction. The browser then talks directly to
19+
GitHub for viewer state and mutations; the service never receives the user's
20+
GitHub token after exchange and provides no general user-token proxy.
21+
22+
## Configuration and intents
23+
24+
Every site declares `mode`, `intents`, one or more categories, and a default
25+
category. `discussion_body` is the template used only when creating a thread and
26+
supports `{key}`, `{title}`, and `{url}`.
27+
28+
| Intent | Returned or enabled data |
29+
| --- | --- |
30+
| `upvotes` | Native Discussion `upvoteCount`; required for `/reactions` |
31+
| `reactions` | Main-post reaction groups selected by `reaction_counters` |
32+
| `discussion` | Authoritative thread read and discussion creation |
33+
| `comments` | Comments and replies |
34+
| `answers` | Accepted-answer state |
35+
| `polls` | Poll question, options, and totals |
36+
| `authors` | Author identity and `authorAssociation` |
37+
| `moderation` | Minimized state and reason |
38+
| `comment_reactions` | Comment/reply reaction totals |
39+
| `comment_upvotes` | Comment/reply native upvote totals |
40+
| `labels` | Discussion labels |
41+
| `github_link` | Discussion/comment URLs and discussion number in counter results |
42+
43+
Ranking mode accepts only `upvotes` and `reactions`. Discussion metadata intents
44+
require `discussion`; comment-specific metadata requires `comments`. Disabled
45+
features return 404 and do not add fields to GitHub queries or API responses.
46+
47+
## HTTP routes
48+
49+
All JSON responses contain `v: 1`. Errors are
50+
`{"v":1,"error":{"code":"...","message":"..."}}`.
51+
52+
| Method | Route | Purpose |
53+
| --- | --- | --- |
54+
| `GET` | `/v1/sites/{site}/reactions?keys=a,b` | Sorted, deduplicated batch of up to `max_batch_size` counters |
55+
| `POST` | `/v1/sites/{site}/oauth/authorize` | Start PKCE OAuth with `challenge` and `nonce` |
56+
| `POST` | `/v1/sites/{site}/oauth/exchange` | Exchange `code`, `state`, and `verifier`; return token plus an origin-bound creation grant |
57+
| `POST` | `/v1/sites/{site}/discussions/ensure` | Find or create a thread for a validated resource and grant |
58+
| `GET` | `/v1/sites/{site}/discussion?keys=a` | Fetch one authoritative configured thread |
59+
60+
A minimal counter response is:
61+
62+
```json
63+
{"v":1,"site":"cpp-social","items":{"resources/42":{"id":"D_...","upvotes":17}}}
64+
```
65+
66+
`number` is included only for `github_link`; `reactions` is included only when
67+
`reaction_counters` is non-empty. Cache age and server internals are not exposed.
68+
Unknown resources have `id: null` and zero counts. Counter responses revalidate
69+
with an ETag. Discussion and OAuth responses are `no-store`.
70+
71+
## Browser API
72+
73+
`FeedbackClient.reactions(keys)` performs the multi-key service read.
74+
`createUpvoteControls()` creates accessible buttons, batches their initial read,
75+
authenticates on activation, creates a missing discussion lazily, and sends the
76+
native upvote directly to GitHub. `createAuthenticationStatus()` is deliberately
77+
independent so a site can mount login/status anywhere.
78+
79+
The package also exports direct, typed GitHub helpers for comment/reply creation,
80+
comment editing/deletion, reactions, poll votes, accepted answers, native
81+
upvotes, and batched viewer-upvote state. A discussion UI can compose these
82+
without routing user actions through the service. Authentication tokens are held
83+
in session storage; counter/viewer snapshots contain no token.
84+
85+
## GitHub request audit and abuse boundaries
86+
87+
The server can contact GitHub only in these places:
88+
89+
1. OAuth code exchange. State, PKCE, exact origins, expiry, and single-purpose
90+
creation grants bound the flow.
91+
2. GitHub App installation-token acquisition. Tokens are cached until shortly
92+
before expiry and acquisition is serialized.
93+
3. Counter refresh. Stale discussion IDs are grouped into `nodes(ids:)` batches
94+
of at most 50. Per-site in-flight work is shared, failures have a cooldown,
95+
global GitHub concurrency is capped, and maintenance sweeps are paced.
96+
4. Discussion discovery/creation. Work is serialized per site/resource; exact
97+
repository and category matches are required, and the resulting ID is stored.
98+
5. Discussion reads. A read is authoritative; simultaneous reads of the same
99+
thread share only the in-flight request. The response is never retained.
100+
101+
Request sizes, key syntax, JSON fields, body sizes, origins, and batch size are
102+
bounded. Security headers are applied globally. Logs omit tokens, OAuth codes,
103+
comment bodies, URLs, origins, and client addresses. `--verbose` enables safe
104+
route timing and cache/GitHub-boundary diagnostics.
105+
106+
The database stores discussion identity and aggregate counters only. It does not
107+
store discussion or comment bodies. Consequently a later request cannot serve a
108+
cached copy of deleted content; deleted nodes returned by GitHub are represented
109+
only by their deletion state, and frontend renderers must not render their body.

‎runtime/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@cppsocial/feedback-runtime",
33
"version": "0.1.0",
4-
"private": true,
4+
"private": false,
55
"type": "module",
66
"files": [
77
"dist/package"

‎runtime/pages/example/index.html‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
<body>
1212
<main>
1313
<h1>Feedback integration example</h1>
14-
<p>This page renders the feedback discussion, canonical reactions, labels, comments, minimized comments, and replies.</p>
14+
<p>This page exercises batched native upvotes, multiple discussion threads, reactions, labels, polls, comments, minimized or deleted comments, replies, accepted answers, and GitHub links.</p>
1515
<p>API: <code id="api-origin"></code></p>
1616
<div id="authentication-status" class="authentication-status"></div>
1717
<section id="thread" class="thread"></section>
1818
<form id="comment-form" class="comment-form">
19+
<label>Thread <select id="comment-key"></select></label>
1920
<textarea id="comment-body" rows="4" maxlength="16000" placeholder="Add a comment"></textarea>
2021
<button type="submit">Sign in and comment</button>
2122
<button id="clear-reply" type="button" hidden>Cancel reply</button>

‎runtime/pages/example/style.css‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ main {
3131
}
3232

3333
.thread {
34-
border: 1px solid #ddd;
35-
border-radius: 0.5rem;
36-
padding: 1rem;
34+
display: grid;
35+
gap: 1rem;
3736
}
3837

38+
.discussion { border: 1px solid #ddd; border-radius: 0.5rem; padding: 1rem; }
39+
.controls { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 1rem; }
40+
3941
.thread h2 {
4042
font-size: 1rem;
4143
margin: 0;

0 commit comments

Comments
 (0)