From 400b8700dfa66659db9ac99b3aa9467f44a29da5 Mon Sep 17 00:00:00 2001 From: Vishal T <54330666+vish727@users.noreply.github.com> Date: Tue, 22 Sep 2026 00:09:29 -0400 Subject: [PATCH 1/2] feat: nook_data.py and nook-data.js, and the skill section for a nook with a server The two clients a nook's own code uses, which are to server code what nook.js is to a page: the same calls and the same rules, so a me/ collection means the same thing on both sides. viewer(headers) is the whole of the design. It takes the act-as token out of the request rather than an email address, so a nook acts for someone who is using it and can never act for someone it merely names. Passing headers straight through is also the easier thing to write, which is the point. Neither needs configuring: nookd puts NOOK_DATA_URL and NOOK_TOKEN in the environment and nook-init writes both files beside the nook's own, so they match the server they talk to and nobody carries a stale copy around. The skill file gains a section on when a nook needs a server at all, the four manifest fields, secrets, what the code is given and what it is not, and what the machine can reach. --- helpers/__pycache__/nook_data.cpython-314.pyc | Bin 0 -> 7616 bytes helpers/embed.go | 19 +++ helpers/embed_test.go | 35 +++++ helpers/nook-data.js | 108 ++++++++++++++ helpers/nook_data.py | 133 ++++++++++++++++++ skill/SKILL.md | 71 ++++++++++ 6 files changed, 366 insertions(+) create mode 100644 helpers/__pycache__/nook_data.cpython-314.pyc create mode 100644 helpers/embed.go create mode 100644 helpers/embed_test.go create mode 100644 helpers/nook-data.js create mode 100644 helpers/nook_data.py diff --git a/helpers/__pycache__/nook_data.cpython-314.pyc b/helpers/__pycache__/nook_data.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b8642cb340e8396967e19fd7f3efbb8c0ac7e65 GIT binary patch literal 7616 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 sdzioe7oJ(E ``` +## When a nook needs a server + +Most nooks are a page and nothing else. Add a `run` command to `nook.json` and the nook runs code +on a server of its own instead: use it when the page cannot do the job, which usually means an API +key that must not reach a browser, a call to a service that will not allow one, or work on a +schedule. + +```json +{ "name": "board", "run": "python app.py", "port": 8000, + "secrets": ["SLACK_TOKEN"], "egress": ["hooks.slack.com"], + "cron": [{ "schedule": "0 9 * * 1-5", "run": "python digest.py" }] } +``` + +- `run` is the command. `port` is what it listens on, 1024 or above. Listen on `0.0.0.0`, and + read the port from `PORT` if you like; it is in the environment. +- Python 3.12 and Node 22 are available. There is no build step: the folder is what runs. +- `secrets` names the values the nook needs. Set each one with `nook secret set board SLACK_TOKEN`, + which reads the value from a pipe or a prompt. **A value is never an argument, never in + `nook.json`, and never readable again** — not by the owner, not by us. The nook will not start + until every declared name is set, and the deploy says which are missing. +- `egress` is every host the code may reach. Anything not listed is blocked. Leave it out and the + nook has no outbound network at all. +- `cron` runs a command on a schedule, in UTC, five fields, at most five entries. + +Add `"source": "viewers"` if you want anyone who can open the nook to read its code; by default +that is the owner and its editors, because code on a server never reaches a browser the way a +page does. + +### Reading and writing data from server code + +`nook_data.py` and `nook-data.js` are written into the nook's folder for you. They are the same +thing `nook.js` is for a page, with the same rules. + +```python +import nook_data + +# For whoever is making this request: pass the request's headers straight through. +me = nook_data.viewer(self.headers) +me.create("me/notes", {"text": "only mine"}) +me.list("notes") +me.user["email"] # who they are; the nook never sees a password or a token of theirs + +# From a cron entry, with nobody on the other end. Shared collections only. +nook_data.list("notes") +``` + +```js +const nook = require('./nook-data.js') +const me = nook.viewer(req.headers) +await me.create('me/notes', { text: 'only mine' }) +await nook.list('notes') // as the nook itself +``` + +**Pass the headers, not an email address.** `viewer()` takes a token out of them that lets the +nook act for that person while they are using it. There is no way to act for someone by naming +them, which is what keeps a `me/` collection private even from the nook's own code. + +### What the nook is given, and what it is not + +The proxy sets `X-Nook-User`, `X-Nook-Name`, `X-Nook-Role`, and the token `viewer()` uses. It +strips `Authorization` and Nook's own cookies, so **the code never sees a credential**, only who +someone is. Cookies the nook sets itself pass through normally, so it can keep a session of its +own if it needs one. + +Everything else is fenced: the machine reaches only the hosts in `egress` plus Nook itself, holds +nothing between restarts, and is stopped when nobody has opened the nook for ten minutes and +started again on the next request. Keep anything that must last in the data API. + +`nook logs board` shows what the code printed, including scheduled jobs and any restarts. + ## Sharing ``` @@ -216,6 +286,7 @@ yourself, so nothing you build is stuck anywhere. ``` nook list nook versions nook rollback [version] nook open nook delete nook account whoami|plan|upgrade|logout +nook logs nook secret set|list|unset KEY ``` Run these inside the nook's folder, or add `--nook `. From 4628add2877e46a152a03c0979033def322be707 Mon Sep 17 00:00:00 2001 From: Vishal Tiruveedi Date: Tue, 22 Sep 2026 00:11:08 -0400 Subject: [PATCH 2/2] chore: drop a stray __pycache__ from the helpers and ignore it --- .gitignore | 2 ++ helpers/__pycache__/nook_data.cpython-314.pyc | Bin 7616 -> 0 bytes 2 files changed, 2 insertions(+) delete mode 100644 helpers/__pycache__/nook_data.cpython-314.pyc diff --git a/.gitignore b/.gitignore index 07a846b..86d0ef2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /bin/ .DS_Store +__pycache__/ +*.pyc diff --git a/helpers/__pycache__/nook_data.cpython-314.pyc b/helpers/__pycache__/nook_data.cpython-314.pyc deleted file mode 100644 index 8b8642cb340e8396967e19fd7f3efbb8c0ac7e65..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7616 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 sdzioe7oJ(E