Skip to content

feat(app): reach the box's own API, share a home read-only, and get back in - #16

Merged
frahlg merged 3 commits into
mainfrom
15-api-passthrough-energy-sharing
Aug 7, 2026
Merged

feat(app): reach the box's own API, share a home read-only, and get back in#16
frahlg merged 3 commits into
mainfrom
15-api-passthrough-energy-sharing

Conversation

@frahlg

@frahlg frahlg commented Aug 7, 2026

Copy link
Copy Markdown
Member

Needs srcfl/ftw#865 — merge that first.

Five things, and the first is why the rest are possible.

The box's own API, over the session

The app could ask its box six questions. The box's own page can ask 132, so every new view cost a box release. Now a view over a route the box already prices as read is this repo's work alone — 55 of the 132, which is every reading a house is drawn from.

It is not free for a route the box has never priced: that one is closed by default, and serving it means a line in the box's route table and a release. The docs say that rather than promising more, because somebody will plan work on the sentence.

Sharing, and the trap under it

Invite someone as a viewer, see who has access, take it away. The screen hides what a viewer cannot use — that is presentation; the box refusing is the part that counts.

This app asked for the role in a query string. The box read it from a body, and defaulted an absent one to owner. So the invite meant to let family look would have handed over the house. Every test passed because the simulator read it the same wrong way — a peer that shares the app's misreading hides it. The simulator now mirrors the box's route table, closed paths and all, and several tests changed as a result.

Getting back in

A phone that has lost its key can type the code the box shows. Crockford base32, decoded forgivingly — someone is reading digits aloud over a phone, and five wrong tries burns the code, so a typo the app could have normalised must not spend an attempt.

It was unreachable from the state it exists for: any surviving site row made the shell decide pairing was not needed, so a phone whose identity database had been evicted never mounted the screen. Verified in a production build at 375 px.

The energy view

The one Fredrik asked for, using the box's own ftw-bar-chart.js vendored byte for byte rather than a second bar chart written here.

The sealed copy

Opt-in, so a fresh install recovers with a passkey alone. Its claim — a sealed copy it cannot open, an opaque id, nothing beside itwas false six times, each one a layer below the last fix:

1 a managed database's point-in-time history
2 the proxy's access log
3 the storage engine's page layout, in write order
4 the journal written by the fix for 3
5 the time a read took — 28.7 µs empty against 109.6 µs occupied
6 the fsync an accepted write pays

Five are closed. A measured survey rejected every off-the-shelf store first — file-per-key and S3 keep an mtime, Redis keeps IDLETIME, LevelDB and Postgres keep arrival in their logs, LMDB and ndbm write different bytes for different arrival orders. The store is now one file of fixed slots: no arrival order to remove, 8.0 ms per write flat in capacity, reads 0.15 ms.

The sixth is written down with its numbers rather than chased. It names no household — an observer learns a write landed, around now, and attempts to tie it to an id came out a coin flip. Closing it would cost every read the same disk for a channel that identifies nobody. A test holds all four tellings of that count in step.

Two screens that invented facts

Both claimed something about the box before the box had answered — caps arrives empty and role defaults to owner, so a cold start printed "this box doesn't support changing how it runs" as if it were an answer.

Review notes

  • 693 tests, 527 files type-clean. Seven adversarial rounds; every fix has a test that fails when reverted.
  • contract/registry.yaml drifted three ways from the box's copy under a test that compared this file against itself. Now guarded for real.
  • One section of docs/architecture.md was destroyed mid-session by an agent running git checkout on an uncommitted tree, and recovered verbatim from an earlier agent's transcript.

🤖 Generated with Claude Code

Contract-pair: srcfl/ftw@agent/app-api-passthrough

…ack in

Five things, and the first is why the rest are possible.

The app could ask its box six questions. The box's own page can ask 132, so
every new view cost a box release. The session now carries the box's HTTP API,
and a view over a route the box already prices as read is this repo's work
alone. That is 55 of the 132 — every reading a house is drawn from. It is not
free for a route the box has never priced: that one is closed by default, and
serving it is a line in the box's table and a release. The docs say so rather
than promising more.

Sharing. A household can invite someone as a viewer, see who has access and
take it away. The screen hides what a viewer cannot use, and that is
presentation only — the box refuses, which is the part that counts. Ours asked
for the role in a query string while the box read a body and defaulted an
absent one to owner, so the invite meant to let family look would have handed
over the house. The simulator read it the same wrong way, which is why every
test passed; it now behaves like the box, closed paths and all.

Getting back in. A phone that has lost its key can type the code the box shows
— Crockford base32, decoded forgivingly, because someone is reading digits
aloud and five wrong tries burns it. The path was unreachable from the state it
exists for: any surviving site row made the shell decide pairing was not
needed, so the phone that needed the floor was the one that could not stand on
it.

The energy view Fredrik asked for, using the box's own bar chart vendored byte
for byte rather than a second one written here.

And a sealed copy at Sourceful, opt-in, so a fresh install recovers with a
passkey alone. Its claim — a sealed copy it cannot open, an opaque id, nothing
beside it — was false six times, each one a layer below the last fix: a managed
database's point-in-time history, the proxy's access log, the storage engine's
page layout, the journal written by the fix for that, the time a read took, and
the fsync an accepted write pays. Five are closed. The sixth is written down
with its numbers, because it names no household and closing it would cost every
read the same disk.

Two screens stopped inventing facts. Both claimed something about the box
before the box had answered — caps arrive empty and role defaults to owner, so
a cold start printed "this box doesn't support that" as if it were an answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 7, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
ftw-webapp a0407ab Aug 07 2026, 08:01 AM

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c50e3ed728

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread escrow/src/escrow.ts
if (!writeKey || writeKey.length !== WRITE_KEY_BYTES) return answer(400)
if (!signature || signature.length !== SIGNATURE_BYTES) return answer(400)

const outcome = await store.write({ id, version: version as number, blob, writeKey, signature })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Rate-limit unauthenticated first writes

Any internet client can generate an arbitrary ID and Ed25519 key, sign a version-1 blob, and reach this call; the signature only proves ownership of the caller-supplied key, so the first write consumes a permanent slot without authenticating an FTW household. With the default 128 buckets and 64 ways, an attacker can target IDs by the public bucket hash and fill every bucket in roughly 8,192 requests, after which every legitimate new opt-in receives 507 indefinitely. Put an admission/rate-limiting control in front of first writes so the fixed, non-evicting store cannot be exhausted anonymously.

Useful? React with 👍 / 👎.

Comment on lines +133 to +135
select(range: EnergyRangeKey): void {
this.range = range
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the displayed period coupled to its data

After a response for one period is displayed, selecting another period changes range and therefore the heading immediately but leaves days and its totals untouched until the new request succeeds. For example, tapping “30 days” while seven-day data is present shows those seven-day totals under “Last 30 days” during the request, and leaves them mislabeled indefinitely if the request fails. Clear the prior result or retain the prior displayed range until the matching response arrives.

Useful? React with 👍 / 👎.

Comment thread src/views/Energy.svelte
Comment on lines +70 to +73
const wanted = $derived(
site.session.caps.has(CAP_PASSTHROUGH)
? `energy ${energy.range} ${new Date().toDateString()}`
: null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Key daily refreshes to the box time zone

When the phone and home are in different time zones, this key changes at the phone's midnight even though /api/energy/daily defines its rows in the box's local day. If the box crosses midnight first, askWhenLive sees the same key and keeps showing the previous box day under “Today” until the handset reaches midnight, potentially hours later. Build the day key using the box time zone available in site.session.box?.tz.

Useful? React with 👍 / 👎.

frahlg and others added 2 commits August 7, 2026 09:57
The guard compares this repository's copy against the box's default branch, and
the box's compares against this one's. A change that lands in both at once
therefore waits for itself: neither side can go first, and the guard's own error
message tells you to change both copies in the same pair of pull requests, which
is the thing it forbids.

A pull request that names where its pair lives is now compared against that
branch. Everything else still meets the default branch, which is the drift this
job exists to catch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
github.event.pull_request.body is captured when the run is queued, so a pair
declared after the last push is invisible to it — the job compares against the
default branch and looks like it worked. Ask the API for the body instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@frahlg
frahlg merged commit 066a357 into main Aug 7, 2026
3 checks passed
@frahlg
frahlg deleted the 15-api-passthrough-energy-sharing branch August 7, 2026 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant