Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- **N0 compose.** The embedded Vector configuration is regenerated for the new route. Together with the optional `NODE_STATS_READ_TOKEN` env line, the reviewed N0 compose template changes to `79469f97…` (raw `32a002ef…`; `docs/releasing.md`).
- **`current_kv` C-index warning codes.** The log sidecar maps the three constant warnings of the TC-1274 `current_kv` C-index migration to `current_kv_c_index_missing`, `current_kv_c_index_invalid` and `current_kv_c_index_wrong_shape` (TC-1274); until then they would ship as `unclassified`. This is part of the same regenerated Vector configuration.
- **Ingress.** The `/admin/stats` label takes effect once the ingress image is rebuilt from `deploy/ingress` and its digest is pinned in the compose; until then the deployed ingress logs it as `<other>`.
- Add `GET /admin/stats` (TC-1482), a read-only list of the lowercase wallet addresses that hold data on the node, for the scoreboard's protocol-user count. It is disabled (`404`, like an unknown route) unless the new `NODE_STATS_READ_TOKEN` is set to a value of at least 32 bytes (`openssl rand -hex 32`); a shorter value, or one equal to `TINYCLOUD_ADMIN_SECRET`, also disables it. The token is separate from `TINYCLOUD_ADMIN_SECRET` (neither opens the other's routes). An owner counts when they hold live KV in any space except `account`, or database artifacts in any space except `account` and `secrets`, except KV in the `public` space whose key starts with `.well-known/vault-` (vault metadata). Automatic sign-in records are excluded: the `account` space, the `secrets` table OpenKey's bootstrap creates, and the vault metadata `vault.unlock()` writes. App first-use databases (for example listen's `conversations`) count. So an owner who only signed in, or only signed in and unlocked the vault, is not listed. One SQL statement however many spaces exist. A result past 200,000 owners (or 400,000 distinct counted spaces) is a bare `507`, never a partial `200`. Neither the token nor any address is logged. The deployment workflow and compose files do not pass the variable yet.

## [1.21.2] - 2026-10-19

Expand Down
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ The SDK checks this endpoint during sign-in and requires an exact protocol versi
| `POST` | `/delegate` | Yes | Create capability delegations |
| `GET` | `/peer/generate/<space>` | No | Generate space host key pair |
| `GET` | `/healthz` | No | Health check |
| `GET` | `/admin/stats` | `NODE_STATS_READ_TOKEN` bearer | Distinct owner addresses that hold data beyond automatic sign-in records and vault metadata (read-only; disabled unless the token is set, at least 32 bytes) |

### Storage usage and full-space writes

Expand Down Expand Up @@ -110,6 +111,75 @@ so the service bearer is sent only on the configured billing request. A `3xx`
answer uses the existing quota fallback policy, with the existing 3-second total
and 2-second connect deadlines.

### Admin stats (`GET /admin/stats`)

Read-only list of the wallet addresses that hold data on this node, for the
scoreboard's protocol-user count (TC-1482). It is **disabled** (answers `404`,
exactly like an unknown route) unless `NODE_STATS_READ_TOKEN` is set to a
value of at least 32 bytes that differs from `TINYCLOUD_ADMIN_SECRET`. Send it as
`Authorization: Bearer <NODE_STATS_READ_TOKEN>`; a missing or wrong credential
is a bare `401`. The token is separate from the admin secret: the admin secret
does not open this route and this token opens no other `/admin/*` route. Use a
random value of at least 32 bytes (a shorter value disables the route; use
`openssl rand -hex 32`, 64 hex characters), kept in the secret store; the node never
logs it or any address.

```json
{ "owners": ["0x…", "0x…"], "count": 2, "generatedAt": "2026-10-19T12:00:00Z" }
```

- **What is counted** (the complete rule).
- **Counts:** an owner holding live (`deleted = false`) KV in any space
except `account`, or a SQL/DuckDB database artifact in any space except
`account` and `secrets`, **except** KV in the `public` space whose key
starts with `.well-known/vault-` (vault metadata).
- **Excluded** (automatic sign-in records, not use of the node): the
`account` space entirely (its KV and its databases); the `secrets` table
that OpenKey's bootstrap creates (database artifacts in `secrets`; KV
stored in `secrets` still counts); and the vault metadata that
`vault.unlock()` writes (`.well-known/vault-pubkey`,
`.well-known/vault-version`, `.well-known/vault-space` in `public`). An
app may unlock the vault automatically, so counting those rows would
inflate "users with data" the same way the `secrets` table did.
- **App first-use data counts.** A database an app creates on first use
(for example listen's `conversations` database or feed-learn's tables) is
a person actually using an app that stores on TinyCloud. Artifact rows are
never deleted, so a database that was created and later emptied or dropped
still counts.

The space match is on the exact space name (the last `:`-separated segment
of the space id), so `secrets2`, `my-account`, `Secrets` or `Account` are
ordinary spaces. The vault exclusion is exactly the `public` space and keys
that start with `.well-known/vault-` (case-sensitive, starts-with): other
`.well-known/*` keys (including `.well-known/vault`), any other KV in
`public`, and the same prefix in any other space all count, and a `public`
space with at least one live KV row that is not vault metadata makes its
owner count. An owner who only signed in, or only signed in and unlocked the
vault, is therefore not listed. This is stricter than "spaces with
`usage_bytes > 0`" (`/admin/usage`), which counts the `account` space's
bytes.
- **Owner.** Parsed from `tinycloud:pkh:eip155:<chain>:0x<address>:<name>`,
lowercased and de-duplicated across spaces and chains. Spaces whose owner is
not an `eip155` address (a `did:key` owner) are skipped. `owners` is sorted.
- **Cost.** One SQL statement (one round trip) however many spaces exist; no
per-space `store_size` call. For each space it makes one primary-key probe of
`current_kv`, plus one pass over `database_artifact`'s space column. The cost
grows with the number of `space` rows (about six per bootstrapped account),
not only with listed owners: 4.35 s warm on a local PostgreSQL 14 with
300,000 spaces and 1.5 million keys (measured before the vault-metadata
condition, which adds one prefix comparison per `current_kv` row a probe
reads; in a `public` space that is at most the vault rows before the first
other live row). Re-measure before the node holds several times that many
spaces; the consumer gives up after 15 s.
- **Bound, never a partial list.** The consumer makes no partial use of a
response, so the route never answers a shorter list with `200`. More than
200,000 owners (or more than 400,000 distinct counted spaces, which bounds
memory; spaces that do not count are not included) is a
bare `507`. Every entry is exactly `0x` followed by 40 lowercase hex digits;
a space whose owner is not such an `eip155` address is left out.
- **Nothing else** about spaces, keys, sizes or timing is returned, and the
response is `Cache-Control: no-store`.

### Signed KV URLs

`POST /signed/kv` accepts a normal TinyCloud invocation whose authorized capabilities include a `tinycloud.kv/get` capability that can be attenuated to the requested `{space, path}`. The invocation may include other capabilities. The JSON body is:
Expand Down Expand Up @@ -201,6 +271,7 @@ The following common options are available:
| spaces.allowlist | TINYCLOUD_SPACES_ALLOWLIST | Set the URL of an allowlist service for gating the creation of Space Peers |
| telemetry.enabled | TINYCLOUD_TELEMETRY__ENABLED | Enable Prometheus latency metrics on the configured Prometheus port |
| (n/a) | ROCKET_MAX_BLOCKING | Set Rocket's blocking-thread-pool cap (default 512). Note the `ROCKET_` prefix, not `TINYCLOUD_` — Rocket builds its tokio runtime from its own Figment before TinyCloud's config is loaded, so `TINYCLOUD_MAX_BLOCKING` is silently ignored for this setting |
| (n/a) | NODE_STATS_READ_TOKEN | Bearer token for the read-only `GET /admin/stats` (TC-1482). Unset, blank, shorter than 32 bytes, or equal to `TINYCLOUD_ADMIN_SECRET` disables the route (`404`). Generate it with `openssl rand -hex 32` (64 hex characters). Separate from `TINYCLOUD_ADMIN_SECRET`, which it does not replace and which does not open it. Secret: set it in the deployment secret store, never in `tinycloud.toml` or a committed env file. See [Admin stats](#admin-stats-get-adminstats) |

### Database Config

Expand Down
8 changes: 8 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ Environment settings (Settings → Environments → `production`):
must not be selected for the organization secret `PHALA_CLOUD_API_KEY`. A
same-named repository or organization secret is still readable by a job
without `environment: production`.
- `NODE_STATS_READ_TOKEN` (TC-1482, read-only `GET /admin/stats`) is not in
that list and is not passed to the CVM by `docker.yml` or the compose files,
so the route is disabled (`404`) in a deployment until a reviewed change adds
it: a `PROD_TINYCLOUD_NODE_STATS_READ_TOKEN` environment secret, the
`PHALA_ENV_` pass-through and a compose `environment` line (which changes the
reviewed compose hash). Its value must be at least 32 bytes (generate it with
`openssl rand -hex 32`; a shorter value disables the route) and must differ from
`PROD_TINYCLOUD_ADMIN_SECRET`; the node disables the route if they are equal.

What this blocks: a dispatch on an unlocked branch fails the environment's
branch policy, even if that branch edits `docker.yml` to name the environment.
Expand Down
Loading
Loading