Base URL: https://api-staging.eewbot.net. The read endpoints below are GET only, have no
request body or query parameters, and require Cloudflare Access Service Token
authentication. This is an internal contract for a future Java
RemoteDestinationConfigSource / RevisionPoller; Java is unchanged in this phase.
The client sends CF-Access-Client-Id and CF-Access-Client-Secret to Cloudflare
Access over HTTPS. Access authenticates the token against a Service Auth policy
that includes the existing common Core token (now authorized for both reads and
cleanup writes). See setup in the README.
The Hono middleware reads the original runtime c.executionCtx.access using the
installed Workers ExecutionContext['access'] type. Hono's portable type lacks
this property, so a local intersection narrows that type without manufacturing
context or copying data from HTTP headers. The middleware requires a nonblank
ACCESS_AUD, an Access context, and strict equality of access.aud to that AUD.
Missing context (including Hono calls with no ExecutionContext) fails closed.
No JWT decoding, custom client-secret comparison or header-based identity is used.
The entire /internal subtree, its parent path, unknown versions/paths and all
methods pass through authentication before routing or DB binding access.
Authenticated non-GET read requests return 405 and Allow: GET; unknown GET paths
return 404. The cleanup writer uses the same native Access
middleware, application/AUD and Service Token. That common token can read and
perform cleanup. Authentication is separate from routing so a future deployment
can introduce distinct writer authorization. There are no test-only production
routes, CORS grants, Cache API calls or request/snapshot debug logs.
Every Worker response in this subtree, including errors, carries
Cache-Control: no-store. Do not cache snapshots in clients, proxies or Cloudflare
Cache Rules, or log bodies/headers. Snapshots contain Discord webhook credentials.
Edge responses generated by Cloudflare Access have Cloudflare's own status,
format and headers; they may be redirects or 401/403 before Worker execution.
| Request | Success JSON |
|---|---|
GET /internal/v1/meta |
{"apiVersion":1,"capabilities":["configRevision","deliverySnapshot"]} |
GET /internal/v1/config/revision |
{"revision":0} (empty initial DB) |
GET /internal/v1/config/snapshot |
{"revision":0,"destinations":[]} (empty initial DB) |
apiVersion is the HTTP protocol version. It is independent of SQL migration
versions, application versions and the changing configuration revision.
Capabilities list only implemented operations. All success and Worker error
bodies are JSON, except HEAD responses which have no body.
The revision is the existing D1 monotonically increasing configuration counter. It is a JSON integer, initially zero; transactional store mutations advance it, and read-only HTTP requests never change it. Revision is neither a timestamp nor a migration number. Store-level no-op updates need not advance it (see the D1 storage contract).
The snapshot directly uses the existing DestinationSnapshot and Destination
types in src/db/destination-types.ts. Each destination contains all fields:
| Field(s) | JSON type / meaning |
|---|---|
targetId, channelId |
Positive unsigned 64-bit Snowflake as canonical decimal string |
threadId, guildId |
Snowflake string or explicit null |
eewAlert, eewPrediction, eewDecimation, quakeInfo, tsunami |
JSON boolean |
minIntensity |
Java numeric code: 0 unknown, 1–4 intensity 1–4, 5 intensity 5-lower, 6 intensity 5-upper, 7 intensity 6-lower, 8 intensity 6-upper, 9 intensity 7 |
lang |
Stored language string, e.g. ja_jp or en_us |
webhookUrl |
Exact full stored URL, including its token/query, or explicit null |
webhookId |
Snowflake string parsed by the existing store, or explicit null |
Synthetic example (no real credentials):
{
"revision": 1,
"destinations": [{
"targetId": "1234567890123456791",
"channelId": "1234567890123456790",
"threadId": "1234567890123456791",
"guildId": "1234567890123456789",
"eewAlert": true,
"eewPrediction": false,
"eewDecimation": true,
"quakeInfo": false,
"tsunami": true,
"minIntensity": 4,
"lang": "en_us",
"webhookUrl": "https://discord.com/api/webhooks/1234567890123456792/synthetic-token",
"webhookId": "1234567890123456792"
}]
}The API calls DestinationConfigSource.getRevision() / loadSnapshot() on
D1DestinationStore. Handlers execute no SQL and do not change the store, schema
or migrations. loadSnapshot() reads revision and destinations in one SQLite
statement, so an empty or populated snapshot represents one committed DB state.
Sorting is the store's target ID text order; clients should index by targetId.
A future Core client should discover protocol/capabilities, poll revision, and load a complete snapshot after detecting a change. Use the snapshot's own revision when replacing the in-memory configuration atomically: a preceding revision read may observe an earlier state. Keep Snowflakes as strings; never parse them as floating-point JSON numbers. Preserve nulls, booleans, language and Java intensity codes. Keep full webhook URLs confidential in memory. On any authentication/service/parse failure, do not replace a last known good snapshot with empty configuration; retry with backoff and report a sanitized error.
| HTTP status | JSON error |
Meaning |
|---|---|---|
| 403 | access_required |
No trusted native Access context |
| 403 | access_forbidden |
Context AUD differs from the configured application |
| 503 | access_not_configured |
Expected AUD absent or blank; all internal requests denied |
| 503 | storage_unavailable |
DB binding missing or storage/handler failure |
| 405 | method_not_allowed |
Authenticated request uses a method other than GET |
| 404 | not_found |
Authenticated GET targets an unknown internal path |
Example: {"error":"storage_unavailable"}. Errors do not include exception
details, SQL, JWTs, client secrets, snapshot data or webhook URLs. Native context
and AUD checks happen before DB access. Meta requires authentication but no DB.
Production has neither ACCESS_AUD nor a DB binding added by this change, so its
internal API stays closed. /health, /interactions and the Java Core continue
using their existing behavior.