Skip to content
Open
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,8 @@ SENTRY_DSN=
# Time zone for /events, /schedule, and {{date}} tokens in user-set posts. Default Europe/Amsterdam.
# PIXEL_TIMEZONE=Europe/Amsterdam
# PIXEL_CONTENT_DIR=content

# Private RevBank gateway. Both or neither; HTTPS (HTTP only for loopback tests).
# Do not point this at the backup repository. Pixel never reads that repository.
REVBANK_API_URL=
REVBANK_API_TOKEN=
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Until 1.0.0, releases stay on 0.x: `Added` bumps minor, `Fixed`/`Changed` (and t

### Added

- Optional private RevBank gateway integration for friends, members and admins: verified link requests, private balance and last-ten-booking commands, owner unlinking, and separately authorised `/revbank manage` commands. The website includes the account-linking guide and command reference. Pixel never reads the backup or persists financial responses. Invalid gateway URLs produce a clear configuration error without exposing the value.

- CHANGELOG conflicts resolve themselves when that is safe. When `main`'s `CHANGELOG.md` changes (and after each Release), a new workflow merges `main` into every open PR from this repo that edits it. If `CHANGELOG.md` is the only conflict, it keeps both sides' `[Unreleased]` entries, pushes the merge and comments on the PR. It never adds a section twice, puts entries into the new `[Unreleased]` after a release, and comments with the reason instead when both sides changed the same entry or the file isn't plain Keep a Changelog. Locally, `just changelog-merge` does the same in a stopped merge or rebase, and `just changelog-merge-driver` makes git do it by itself. For CI to run on those merges, a maintainer sets up a GitHub App (runbook: "Changelog conflicts").

### Changed
Expand Down
4 changes: 4 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ export default defineConfig({
text: "0017 · One writer for app secrets",
link: "/maintainers/adr/0017-one-writer-for-app-secrets",
},
{
text: "0018 · Private RevBank gateway",
link: "/maintainers/adr/0018-private-revbank-gateway",
},
],
},
],
Expand Down
7 changes: 6 additions & 1 deletion docs/.vitepress/theme/components/PxCommandTray.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const props = withDefaults(
caption?: string;
}>(),
{
queries: () => ["/status", "/ha", "/info", "/schedule", "/events"],
queries: () => ["/status", "/ha", "/revbank", "/info", "/schedule", "/events"],
rows: 5,
query: undefined,
selected: 0,
Expand All @@ -53,6 +53,11 @@ const COMMANDS: readonly Command[] = [
description:
"Send feedback to Pixel's maintainers. Your message and username may be made public",
},
{ name: "/revbank link", description: "Request a verified link to your RevBank account" },
{ name: "/revbank status", description: "Show your RevBank link or pending request" },
{ name: "/revbank balance", description: "Show your balance from the latest usable backup" },
{ name: "/revbank transactions", description: "Show your last ten confirmed bookings" },
{ name: "/revbank unlink", description: "Remove your link and any pending request" },
{ name: "/closing-time", description: "Post the closing-time reminder" },
{ name: "/ha list", description: "The devices you may use, and their state" },
{ name: "/ha status", description: "The live state of one device" },
Expand Down
6 changes: 6 additions & 0 deletions docs/.vitepress/theme/components/PxHome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ const features = [
body: "/events lists what's on at Pixelbar, from the Discord server's scheduled events.",
link: "/commands#events",
},
{
eyebrow: "RevBank",
title: "Your credits, in private",
body: "Link your RevBank account once, then check your balance and latest bookings right in Discord.",
link: "/guide/#your-revbank-account",
},
{
eyebrow: "Info",
title: "How do I join?",
Expand Down
32 changes: 32 additions & 0 deletions docs/adr/0018-private-revbank-gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 0018. Personal RevBank access through a private gateway

Status: accepted for implementation; deployment requires operator configuration.

## Context

People want to read their own prepaid balance and bookings in Pixel. RevBank and its existing Git backup must remain unchanged. Pixel is public source; the backup contains private account records, terminal input and transaction descriptions.

## Decision

Pixel calls a separately deployed private gateway. Only that gateway can read a local mirror of the backup. Pixel has no Git credentials, backup mount, account catalogue, or persisted financial data. The gateway's code and operations live in a separate private repository. Public code contains only the typed API port, HTTP client, commands and fictional fixtures.

- Personal commands require at least friend tier: friends, members and admins can use them, while guests cannot. A RevBank account does not grant Pixel membership, and membership does not grant access to a RevBank account.
- The authenticated Discord actor is the only identity used for personal requests. There is no personal `user` or `nick` selector on balance/history requests. Replies and failures are private from the initial deferral onward.
- A request carries a nickname, expires after 24 hours and receives a unique request ID. It reveals no account existence. A manager verifies ownership independently in person, then approves that exact request for the selected Discord user. Self-approval is refused by the gateway.
- Link management requires the `revbank-link-admin` capability at member tier in Pixel **and** a separately maintained immutable-ID allow-list in the gateway. Managers can inspect pending/link metadata and approve, reject or revoke; they cannot read another person's balance. Approvals enforce one Discord identity per account. Unlinking is available to the owner without a manager.
- The gateway reads fixed filenames from one immutable Git commit. It never checks out or executes backup content. Account creation markers bind links to account incarnations, so a missing or recreated account fails closed. Unrecognised formats and account/log inconsistencies fail closed. Historic data is taken from confirmed booking records, not commit messages or Git diffs.
- Only the last ten confirmed bookings are exposed. Labels come from recognised catalogue descriptions; other descriptions become generic credit/debit/adjustment labels. Raw terminal input, counterparties and free-form log content never cross the API.
- Pixel stores no financial response. HTTP requests have an eight-second deadline, a streamed 16 KiB response cap, strict schemas, no redirects and no automatic retries. Errors contain fixed codes only. There is no financial/request-body logging.
- The TypeScript gateway runs as a separate Azure Container App in Pixel’s existing Container Apps environment, with internal HTTPS ingress and a dedicated bearer token. Its private repository provides the deployment recipes and separate Terraform state keys. HTTP is allowed only for loopback testing. No public ingress or Tailscale Funnel. Dev must use a separate gateway, token and fictional backup.

## Consequences and limits

The gateway trusts Pixel to identify the authenticated Discord actor correctly. A stolen Pixel service token can impersonate linked users; a private repository does not remove that trust. Token rotation and network ACLs remain necessary. Neither Pixel operators nor gateway operators are cryptographically prevented from accessing the data they administer.

Data is as recent as the backup commit, not live. The private service enforces a configured maximum age and reports the commit time; old commits may mean either a quiet space or an interrupted backup. No financial writes, payments, reminders, public statistics or automated membership changes are included.

RevBank's terminal operates on trust. A nickname entered there is not proof of ownership. Renames require unlinking and a fresh, independently verified request. Restore procedures must not silently resurrect revoked links. The private service owns the link-state audit and retention; the Pixel privacy notice describes the integration separately from ordinary Pixel storage.

## Validation

Tests cover dispatcher gating, private replies, actor-only requests, schema/size failures, missing or expired approvals, request replacement, duplicate links, self-approval, revocation, recreated accounts, inconsistent snapshots and removal of private descriptions. Operators must review the gateway, configure access and verify its private deployment before enabling production.
16 changes: 16 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,8 @@ Environment variables are validated by `config.ts` (zod). Nothing else reads `pr
| `PIXEL_CLOSING_TIME_FILE` | | Optional. Operator-authored closing-time body. Default `{PIXEL_DATA_DIR}/closing-time.md`. Missing or empty → built-in default. Not a secret |
| `DISCORD_ROLE_MEMBER` | | Optional. A Discord role name or ID that the `member` level is mirrored to. Unset means not mirrored |
| `DISCORD_ROLE_FRIEND` | | Optional. A Discord role name or ID that the `friend` level is mirrored to. Unset means not mirrored |
| `REVBANK_API_URL` | | Optional HTTPS origin of the private RevBank gateway; both URL and token or neither |
| `REVBANK_API_TOKEN` | | Secret bearer token for the private RevBank gateway; never backup Git credentials |
| `HOME_ASSISTANT_URL` | | Optional, with the token. Where Pixel reaches Home Assistant (http or https). On Azure prefer the Tailscale sidecar address; a Nabu Casa URL still works |
| `HOME_ASSISTANT_TOKEN` | | Optional, with the URL. A long-lived access token from a **non-admin** Home Assistant user. A secret |
| `MASTODON_INSTANCE` | | Optional, with the token. Origin of the Mastodon instance (http or https). A path is stripped so only the origin is used. Never hard-coded |
Expand Down Expand Up @@ -582,4 +584,18 @@ Record significant decisions as short ADRs in `docs/adr/NNNN-title.md`. Numbers
| 0014 | Release registers prod slash commands; `pixel-github-prod` may read only `discord-token` | accepted |
| 0015 | Secrets manifest (`infra/secrets.manifest.yaml`) is the one list of Key Vault secrets; Terraform reads it and a test fails on drift | accepted |
| 0016 | `just secrets-pull` / `just secrets-push` sync the manifest's app secrets between Key Vault and `.env.<env>`, diffing against the live vault before a push; `just pull` / `just push` add the shared `terraform-tfvars` and read-only share mirrors | accepted |
| 0018 | Personal RevBank reads through a private gateway with independently approved account links | accepted |
| 0017 | `just secrets-push` / `just push` are the only writers of the app secrets; Terraform's `write_secrets` writes only the infra group, and `removed` blocks drop the old app secrets from state without deleting them | accepted |


## Personal RevBank access

The optional RevBank integration reads personal balances and the last ten bookings through a separately hosted private gateway. Pixel never reads the Git backup itself. The gateway owns link requests, approvals, immutable Discord-ID bindings and financial-data filtering. All `/revbank` replies are private. Friends, members and admins may request their own link; managing links requires the `revbank-link-admin` capability and independent authorisation in the gateway. No financial writes or local cache. See [ADR 0018](adr/0018-private-revbank-gateway.md) and the [runbook](runbook.md#revbank).

```mermaid
flowchart LR
BACKUP[Private existing Git backup] -->|read-only mirror| GATEWAY[Private RevBank gateway]
PIXEL[Pixel] -->|HTTPS, service token, caller Discord ID| GATEWAY
GATEWAY -->|only caller's balance or bookings| PIXEL
PIXEL -->|private reply| DISCORD[Discord caller]
```
68 changes: 68 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,46 @@ Sends a message to Pixel's maintainers, with your Discord name and ID so they ca

</PxCommand>

## Friends

For friends, members and admins.

### RevBank

Your credits and latest bookings, in private. [Link your account once](/guide/#your-revbank-account), then check it from Discord.

<PxCommandTray query="/revbank" :selected="1" :rows="5" caption="Your RevBank commands, together in one place." />

<PxCommand :level="4" sig="/revbank link" tier="friend" reply="private" :options="{ nick: 'Your RevBank nickname. Fill in this required field in Discord before sending the command.' }">

Requests a link to your RevBank account. A link manager verifies that the account is yours and approves the request.

</PxCommand>

<PxCommand :level="4" sig="/revbank status" tier="friend" reply="private">

Shows your linked account or pending request.

</PxCommand>

<PxCommand :level="4" sig="/revbank balance" tier="friend" reply="private">

Your current balance in the latest available backup, with its date and time.

</PxCommand>

<PxCommand :level="4" sig="/revbank transactions" tier="friend" reply="private">

Your last ten confirmed bookings, including the amount and balance after each booking.

</PxCommand>

<PxCommand :level="4" sig="/revbank unlink" tier="friend" reply="private">

Removes your account link and any pending request.

</PxCommand>

## Members

For paid members. Commands marked Friend also work with a friend membership.
Expand All @@ -63,6 +103,34 @@ Posts the closing-time reminder in the closing-time channel. Pixel also posts it

</PxCommand>

### RevBank link management

For members with the `revbank-link-admin` capability who are also authorised by the RevBank service. Link managers verify account ownership and manage links; they do not gain access to other people's balances.

<PxCommand :level="4" sig="/revbank manage review" tier="member" reply="private" :caps="['revbank-link-admin']" :options="{ user: 'Choose the person whose account link you want to review.' }">

Shows a person's linked account or pending request, including its request ID.

</PxCommand>

<PxCommand :level="4" sig="/revbank manage approve" tier="member" reply="private" :caps="['revbank-link-admin']" :options="{ user: 'Choose the person whose request you verified.', request: 'The request ID from the review.' }">

Approves the reviewed request after verifying account ownership in person.

</PxCommand>

<PxCommand :level="4" sig="/revbank manage reject" tier="member" reply="private" :caps="['revbank-link-admin']" :options="{ user: 'Choose the person whose request to reject.', request: 'The request ID from the review.' }">

Rejects the reviewed request.

</PxCommand>

<PxCommand :level="4" sig="/revbank manage revoke" tier="member" reply="private" :caps="['revbank-link-admin']" :options="{ user: 'Choose the person whose link to remove.' }">

Removes a person's link and pending request.

</PxCommand>

### Home Assistant

Lights, switches and doors in the space. You only ever see the devices you're allowed to see, and Pixel checks again whatever autocomplete offered.
Expand Down
13 changes: 13 additions & 0 deletions docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,23 @@ A few you'll use most:
- `/status`: is Pixelbar open right now, and for how long?
- `/events`: what's coming up.
- `/info`: short answers about Pixelbar. With no topic, it lists them.
- `/revbank balance`: your credits, once your RevBank account is linked.
- `/help`: the commands you can use. It only lists the ones your tier allows.

The [commands](/commands) page has every command, who can use it and what it needs.

## Your RevBank account

When RevBank is enabled, friends, members and admins can check their credits and latest bookings privately in Discord.

1. Select `/revbank link` and fill in your RevBank nickname in the **nick** field.
2. Ask a link manager in the space to verify that the account is yours and approve your request.
3. Use `/revbank balance` for your credits or `/revbank transactions` for your last ten bookings.

`/revbank status` shows your link or pending request. You can remove it yourself with `/revbank unlink`. You need at least the Friend tier to use RevBank. Linking an account does not change your Pixel membership tier.

[All RevBank commands](/commands#revbank) · [Privacy notice](/privacy#optional-revbank-integration)

## Who sees the reply

Every command says how it replies.
Expand Down
8 changes: 8 additions & 0 deletions docs/guide/tiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ A capability is a named permission for one person, for things that shouldn't rea
| Capability | What it allows |
| --- | --- |
| `schedule-posts` | Schedule messages and polls in channels you can post in, with `/schedule`. |
| `revbank-link-admin` | Review, approve and revoke account links with `/revbank manage`. Members only, with separate authorisation from the RevBank service. |
| `spacestate` | Set the space open or closed, with `/spacestate set`. Members only. |
| `ha-lights` | Change lights in Home Assistant, with `/ha set`. |
| `ha-switches` | Change switches in Home Assistant, with `/ha set`. |
Expand All @@ -41,3 +42,10 @@ A capability is a named permission for one person, for things that shouldn't rea
## Home Assistant devices

Each device has its own minimum tier to be used. Seeing a device and its state needs only that tier. Changing it needs the tier, plus `ha-admin` or the capability for that kind of device, plus an action that device allows.


## Your RevBank account

RevBank is available to friends, members and admins. Guests cannot use the RevBank commands. A link manager verifies and approves the link before Pixel can show your balance or bookings. Managing links requires the separate `revbank-link-admin` capability and service authorisation; it does not give access to another person's financial data.

[Set up your account link](/guide/#your-revbank-account).
5 changes: 5 additions & 0 deletions docs/identity-and-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,8 @@ These are recorded so that phase 1 doesn't make them harder. Each will get an AD
4. Both accounts are notified. `/unlink` from either side. Guests can't link (no entry to add to). Admins' Telegram IDs are added by hand in both files.
- ~~**Database-backed grants**~~: done without a database (#25; #18 closed). Admin commands rewrite `members.yaml`.
- **Freshness:** once tiers come from outside sources, re-verify a tier older than 24 hours before privileged commands, and reconcile every night.


## RevBank links

RevBank links are separate from membership and platform-account linking. The private gateway, not `members.yaml`, stores the approved mapping from an immutable Discord ID to a RevBank account. `/revbank` personal commands require at least friend tier but the gateway returns financial data only after independent ownership verification. `/revbank manage` requires member tier plus `revbank-link-admin`; the gateway separately checks its manager ID allow-list. Link managers have no financial-read override. See [ADR 0018](adr/0018-private-revbank-gateway.md).
7 changes: 7 additions & 0 deletions docs/infra.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,10 @@ just deploy dev # this tree → GHCR SHA → pixel-dev, then just regi
```

Do not apply Terraform from CD. Do not copy `pixel-dev-kv` tokens into prod. After first create, **dev** apply ignores the Pixel container image (#73). Prod apply sets `container_image`; a Release then rolls the published SHA.


## Optional RevBank gateway

The private RevBank repository deploys a separate TypeScript/Node.js Container App into the **same resource group and Container Apps environment as Pixel**. Its API uses internal HTTPS ingress, so the Pixel container can reach it without public ingress or another Tailscale route. The gateway owns a private registry, managed identity, Key Vault and separate Azure Files shares for backup data and account links. Pixel receives only the API URL and token.

The gateway's `just configure dev|prod` imports Pixel's environment settings and discovers the existing Container Apps environment. Its Terraform stacks use the existing remote state account/container with their own `revbank-api/foundation.tfstate` and `revbank-api/app.tfstate` keys. It does not manage the Pixel Container App or Pixel's Terraform state. The private repo documents `just provision`, `just secrets-push`, `just deploy`, rollback and production guards.
1 change: 1 addition & 0 deletions docs/maintainers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Architecture decision records, newest last.
| [0015](./adr/0015-secrets-manifest) | One manifest lists every Key Vault secret. |
| [0016](./adr/0016-secrets-sync) | `just pull` / `just push` (and `secrets-pull` / `secrets-push`) sync tfvars and app secrets between Key Vault and the laptop. |
| [0017](./adr/0017-one-writer-for-app-secrets) | Only the sync tool writes app secrets; Terraform writes just the infra group. |
| [0018](./adr/0018-private-revbank-gateway) | Personal RevBank reads through a separate private gateway; verified account links. |

## Working on the site

Expand Down
Loading
Loading