lotuspod 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lotuspod-0.1.0/LICENSE +21 -0
- lotuspod-0.1.0/PKG-INFO +123 -0
- lotuspod-0.1.0/README.md +113 -0
- lotuspod-0.1.0/pyproject.toml +20 -0
- lotuspod-0.1.0/setup.cfg +4 -0
- lotuspod-0.1.0/src/lotuspod/__init__.py +3 -0
- lotuspod-0.1.0/src/lotuspod/__main__.py +6 -0
- lotuspod-0.1.0/src/lotuspod/_templates/artifact.html +59 -0
- lotuspod-0.1.0/src/lotuspod/_templates/index.html +130 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/attachments.css +126 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/base.css +576 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/comments.css +1491 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/decisions.css +344 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/forms.css +89 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/image-viewer.css +100 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/index.css +238 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/live-page.css +65 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/narrow.css +239 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/panel-resize.css +66 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/prose.css +243 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/report.css +119 -0
- lotuspod-0.1.0/src/lotuspod/_theme/css/table-expand.css +56 -0
- lotuspod-0.1.0/src/lotuspod/_theme/favicon.svg +1 -0
- lotuspod-0.1.0/src/lotuspod/_theme/js/attachments.js +352 -0
- lotuspod-0.1.0/src/lotuspod/_theme/js/comments.js +2395 -0
- lotuspod-0.1.0/src/lotuspod/_theme/js/decisions.js +268 -0
- lotuspod-0.1.0/src/lotuspod/_theme/js/image-viewer.js +131 -0
- lotuspod-0.1.0/src/lotuspod/_theme/js/live-page.js +185 -0
- lotuspod-0.1.0/src/lotuspod/_theme/js/narrow.js +542 -0
- lotuspod-0.1.0/src/lotuspod/_theme/js/page-close.js +1 -0
- lotuspod-0.1.0/src/lotuspod/_theme/js/page-open.js +494 -0
- lotuspod-0.1.0/src/lotuspod/_theme/js/panel-resize.js +216 -0
- lotuspod-0.1.0/src/lotuspod/_theme/js/table-expand.js +144 -0
- lotuspod-0.1.0/src/lotuspod/_theme/js/tables.js +35 -0
- lotuspod-0.1.0/src/lotuspod/_theme/tokens.json +63 -0
- lotuspod-0.1.0/src/lotuspod/access.py +324 -0
- lotuspod-0.1.0/src/lotuspod/agents.py +505 -0
- lotuspod-0.1.0/src/lotuspod/api.py +551 -0
- lotuspod-0.1.0/src/lotuspod/backup.py +496 -0
- lotuspod-0.1.0/src/lotuspod/cli.py +2505 -0
- lotuspod-0.1.0/src/lotuspod/comments.py +153 -0
- lotuspod-0.1.0/src/lotuspod/db.py +897 -0
- lotuspod-0.1.0/src/lotuspod/decisions.py +374 -0
- lotuspod-0.1.0/src/lotuspod/machine.py +780 -0
- lotuspod-0.1.0/src/lotuspod/markdown.py +269 -0
- lotuspod-0.1.0/src/lotuspod/media.py +360 -0
- lotuspod-0.1.0/src/lotuspod/py.typed +0 -0
- lotuspod-0.1.0/src/lotuspod/responder.py +784 -0
- lotuspod-0.1.0/src/lotuspod/routing.py +162 -0
- lotuspod-0.1.0/src/lotuspod/sections.py +84 -0
- lotuspod-0.1.0/src/lotuspod.egg-info/PKG-INFO +123 -0
- lotuspod-0.1.0/src/lotuspod.egg-info/SOURCES.txt +96 -0
- lotuspod-0.1.0/src/lotuspod.egg-info/dependency_links.txt +1 -0
- lotuspod-0.1.0/src/lotuspod.egg-info/entry_points.txt +2 -0
- lotuspod-0.1.0/src/lotuspod.egg-info/top_level.txt +1 -0
- lotuspod-0.1.0/tests/test_access.py +677 -0
- lotuspod-0.1.0/tests/test_agent_pull.py +901 -0
- lotuspod-0.1.0/tests/test_agent_pull_witness.py +482 -0
- lotuspod-0.1.0/tests/test_answers_witness.py +431 -0
- lotuspod-0.1.0/tests/test_api.py +1038 -0
- lotuspod-0.1.0/tests/test_backup.py +459 -0
- lotuspod-0.1.0/tests/test_backup_witness.py +393 -0
- lotuspod-0.1.0/tests/test_browser_checks.py +48 -0
- lotuspod-0.1.0/tests/test_capture_site.py +462 -0
- lotuspod-0.1.0/tests/test_ci_workflow.py +126 -0
- lotuspod-0.1.0/tests/test_claims.py +611 -0
- lotuspod-0.1.0/tests/test_cli_surface.py +153 -0
- lotuspod-0.1.0/tests/test_comments.py +419 -0
- lotuspod-0.1.0/tests/test_decisions.py +599 -0
- lotuspod-0.1.0/tests/test_demo_build.py +424 -0
- lotuspod-0.1.0/tests/test_demo_checks.py +48 -0
- lotuspod-0.1.0/tests/test_demo_deploy.py +235 -0
- lotuspod-0.1.0/tests/test_demo_shapes.py +83 -0
- lotuspod-0.1.0/tests/test_deploy.py +368 -0
- lotuspod-0.1.0/tests/test_docs_links.py +329 -0
- lotuspod-0.1.0/tests/test_forms.py +175 -0
- lotuspod-0.1.0/tests/test_history.py +89 -0
- lotuspod-0.1.0/tests/test_leak_guard.py +180 -0
- lotuspod-0.1.0/tests/test_machine.py +438 -0
- lotuspod-0.1.0/tests/test_manifest_v2.py +1846 -0
- lotuspod-0.1.0/tests/test_markdown.py +590 -0
- lotuspod-0.1.0/tests/test_media.py +278 -0
- lotuspod-0.1.0/tests/test_page_policy.py +195 -0
- lotuspod-0.1.0/tests/test_palette.py +126 -0
- lotuspod-0.1.0/tests/test_public_ready.py +132 -0
- lotuspod-0.1.0/tests/test_publish.py +581 -0
- lotuspod-0.1.0/tests/test_publish_remote.py +561 -0
- lotuspod-0.1.0/tests/test_publish_remote_witness.py +191 -0
- lotuspod-0.1.0/tests/test_publish_witness.py +281 -0
- lotuspod-0.1.0/tests/test_release.py +415 -0
- lotuspod-0.1.0/tests/test_render_commits.py +228 -0
- lotuspod-0.1.0/tests/test_resolutions.py +212 -0
- lotuspod-0.1.0/tests/test_responder.py +718 -0
- lotuspod-0.1.0/tests/test_responder_witness.py +483 -0
- lotuspod-0.1.0/tests/test_sections.py +222 -0
- lotuspod-0.1.0/tests/test_serve_theme.py +122 -0
- lotuspod-0.1.0/tests/test_theme_hash.py +136 -0
- lotuspod-0.1.0/tests/test_theme_sources.py +217 -0
lotuspod-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ko Vial
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lotuspod-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lotuspod
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lotuspod: publish pages people comment on, with decision forms and agents that answer.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# Lotuspod
|
|
12
|
+
|
|
13
|
+
Lotuspod publishes pages for a small, signed-in circle of readers, and lets
|
|
14
|
+
those readers answer on them. A page is written in markdown or HTML and
|
|
15
|
+
published in one command, drawn in one shared lotus theme. Readers comment on
|
|
16
|
+
a section or on a passage they highlight, and answer the decision tables a
|
|
17
|
+
plan page asks the maintainer. Agents (a Claude Code or Codex session, or
|
|
18
|
+
the default responder) pull the comments and answers meant for them, reply in
|
|
19
|
+
the thread, and revise the page when asked.
|
|
20
|
+
|
|
21
|
+
Try the demo: [https://lotuspod.kovial.co/](https://lotuspod.kovial.co/) (your comments stay in your browser; replies are scripted)
|
|
22
|
+
|
|
23
|
+
The site runs on one writer host, behind Cloudflare Access, and is reached
|
|
24
|
+
through a Cloudflare Tunnel or over a tailnet.
|
|
25
|
+
|
|
26
|
+
Lotuspod is a personal project, and issues are off.
|
|
27
|
+
|
|
28
|
+
## See it
|
|
29
|
+
|
|
30
|
+
A reader comments on a section of a plan page, the thread waits for the
|
|
31
|
+
page's agent, `hermes`, and its reply appears; then the reader answers the
|
|
32
|
+
page's decisions and comments on a passage:
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
The pictures come from a sample page served on loopback by the test fixture;
|
|
43
|
+
[Development](docs/development.md#captures) makes them again.
|
|
44
|
+
|
|
45
|
+
## Layout
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
lotuspod/
|
|
49
|
+
├── src/lotuspod/ # package + minimal CLI (`lotuspod render|publish|manifest|index|serve|answers|backup|restore`)
|
|
50
|
+
│ ├── _templates/artifact.html # artifact template ({{placeholder}} substitution)
|
|
51
|
+
│ ├── _templates/index.html # index-page template
|
|
52
|
+
│ └── _theme/ # tokens.json (colors, fonts, radii) + lotuspod.css
|
|
53
|
+
├── artifacts/ # rendered output (gitignored)
|
|
54
|
+
└── lotuspod-media/ # markdown pages' images, beside artifacts/ (gitignored)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Beside these, `docs/` holds the reference pages listed below, `deploy/` the
|
|
58
|
+
tunnel config and the systemd units, `tests/` the unit tests and `e2e/` the
|
|
59
|
+
Playwright captures and browser checks.
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
Install the package, write a markdown page, publish it and serve it:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
pip install -e .
|
|
67
|
+
printf '# Opening the Pond\n\n## The pond\n\nHello from the pond.\n' > pond.md
|
|
68
|
+
lotuspod publish pond.md --local --summary "Why we started Lotuspod."
|
|
69
|
+
lotuspod serve
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`--local` publishes on this machine, even when the config names a
|
|
73
|
+
`[publish] host` to send pages to. `publish` writes `artifacts/pond.html`,
|
|
74
|
+
keeps the source beside it as `artifacts/pond.md`, and rebuilds
|
|
75
|
+
`artifacts/index.html`. `serve` prints the address it listens on; open
|
|
76
|
+
`http://127.0.0.1:8000/` for the index. Serve listens on `127.0.0.1`, this
|
|
77
|
+
machine only, unless `--host` names another address, such as
|
|
78
|
+
`--host "$(tailscale ip -4)"` to serve on a tailnet.
|
|
79
|
+
|
|
80
|
+
Comments and answers need the reader's routes, which sit behind Cloudflare
|
|
81
|
+
Access: until the config has an `[access]` section they answer 503, and the
|
|
82
|
+
page itself is served as usual.
|
|
83
|
+
|
|
84
|
+
## Where to read next
|
|
85
|
+
|
|
86
|
+
- [Publishing pages](docs/publishing.md): for people publishing pages.
|
|
87
|
+
Markdown and HTML pages, images, the manifest and the index, decision
|
|
88
|
+
tables, the template and the theme, and what a page may run.
|
|
89
|
+
- [Reading and answering pages](docs/comments.md): for readers and the people
|
|
90
|
+
answering them. Answers and comments, the comments panel, the popover and
|
|
91
|
+
bottom sheet, and comments on passages.
|
|
92
|
+
- [Agents](docs/agents.md): for agents and their operators. Agent
|
|
93
|
+
credentials, the pull loop and the default responder.
|
|
94
|
+
- [Operating the site](docs/operating.md): for the operator of the site.
|
|
95
|
+
Serve, Cloudflare Access, publishing through the tunnel, deploying from
|
|
96
|
+
`main`, and backups.
|
|
97
|
+
- [Development](docs/development.md): for people changing Lotuspod. The
|
|
98
|
+
tests and the captures.
|
|
99
|
+
- [Architecture](docs/architecture.md): for reviewers and new contributors.
|
|
100
|
+
One diagram of how the parts fit together.
|
|
101
|
+
|
|
102
|
+
## How it was built
|
|
103
|
+
|
|
104
|
+
Lotuspod is built by [Holophyte](https://github.com/wevial/holophyte), an
|
|
105
|
+
agent factory. Each change starts as a ticket with acceptance criteria and
|
|
106
|
+
verify commands. An agent implements it in an isolated worktree, the verify
|
|
107
|
+
commands run, and a different agent reviews it. A pull request then runs the
|
|
108
|
+
required `unit` check and merges. The
|
|
109
|
+
[merged pull requests](https://github.com/wevial/lotuspod/pulls?q=is%3Apr+is%3Amerged)
|
|
110
|
+
are the record.
|
|
111
|
+
|
|
112
|
+
## Security
|
|
113
|
+
|
|
114
|
+
Report a vulnerability privately ([SECURITY.md](SECURITY.md)). `src/lotuspod/access.py` checks Cloudflare
|
|
115
|
+
Access's RS256 assertions with the standard library: the package has no runtime dependencies, the check only
|
|
116
|
+
verifies (never signs or makes keys), and RS256 is one modular exponentiation plus a comparison of the whole
|
|
117
|
+
re-encoded PKCS#1 v1.5 block, leaving no parser to trick. It refuses any `alg` but RS256, a key id not in
|
|
118
|
+
the team's key set, keys under 2048 bits, and a wrong issuer, audience, time or email; it never reads the
|
|
119
|
+
plain email header. Pinned by `tests.test_access.VerifierTests.test_header_must_name_rs256_and_a_listed_key`,
|
|
120
|
+
`tests.test_access.VerifierTests.test_signature_block_is_compared_whole`,
|
|
121
|
+
`tests.test_access.VerifierTests.test_keys_under_2048_bits_are_not_listed`,
|
|
122
|
+
`tests.test_access.VerifierTests.test_malformed_tokens_are_invalid` and
|
|
123
|
+
`tests.test_access.WhoamiTests.test_email_header_naming_someone_else_is_not_read`.
|
lotuspod-0.1.0/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Lotuspod
|
|
2
|
+
|
|
3
|
+
Lotuspod publishes pages for a small, signed-in circle of readers, and lets
|
|
4
|
+
those readers answer on them. A page is written in markdown or HTML and
|
|
5
|
+
published in one command, drawn in one shared lotus theme. Readers comment on
|
|
6
|
+
a section or on a passage they highlight, and answer the decision tables a
|
|
7
|
+
plan page asks the maintainer. Agents (a Claude Code or Codex session, or
|
|
8
|
+
the default responder) pull the comments and answers meant for them, reply in
|
|
9
|
+
the thread, and revise the page when asked.
|
|
10
|
+
|
|
11
|
+
Try the demo: [https://lotuspod.kovial.co/](https://lotuspod.kovial.co/) (your comments stay in your browser; replies are scripted)
|
|
12
|
+
|
|
13
|
+
The site runs on one writer host, behind Cloudflare Access, and is reached
|
|
14
|
+
through a Cloudflare Tunnel or over a tailnet.
|
|
15
|
+
|
|
16
|
+
Lotuspod is a personal project, and issues are off.
|
|
17
|
+
|
|
18
|
+
## See it
|
|
19
|
+
|
|
20
|
+
A reader comments on a section of a plan page, the thread waits for the
|
|
21
|
+
page's agent, `hermes`, and its reply appears; then the reader answers the
|
|
22
|
+
page's decisions and comments on a passage:
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
The pictures come from a sample page served on loopback by the test fixture;
|
|
33
|
+
[Development](docs/development.md#captures) makes them again.
|
|
34
|
+
|
|
35
|
+
## Layout
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
lotuspod/
|
|
39
|
+
├── src/lotuspod/ # package + minimal CLI (`lotuspod render|publish|manifest|index|serve|answers|backup|restore`)
|
|
40
|
+
│ ├── _templates/artifact.html # artifact template ({{placeholder}} substitution)
|
|
41
|
+
│ ├── _templates/index.html # index-page template
|
|
42
|
+
│ └── _theme/ # tokens.json (colors, fonts, radii) + lotuspod.css
|
|
43
|
+
├── artifacts/ # rendered output (gitignored)
|
|
44
|
+
└── lotuspod-media/ # markdown pages' images, beside artifacts/ (gitignored)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Beside these, `docs/` holds the reference pages listed below, `deploy/` the
|
|
48
|
+
tunnel config and the systemd units, `tests/` the unit tests and `e2e/` the
|
|
49
|
+
Playwright captures and browser checks.
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
Install the package, write a markdown page, publish it and serve it:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
pip install -e .
|
|
57
|
+
printf '# Opening the Pond\n\n## The pond\n\nHello from the pond.\n' > pond.md
|
|
58
|
+
lotuspod publish pond.md --local --summary "Why we started Lotuspod."
|
|
59
|
+
lotuspod serve
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`--local` publishes on this machine, even when the config names a
|
|
63
|
+
`[publish] host` to send pages to. `publish` writes `artifacts/pond.html`,
|
|
64
|
+
keeps the source beside it as `artifacts/pond.md`, and rebuilds
|
|
65
|
+
`artifacts/index.html`. `serve` prints the address it listens on; open
|
|
66
|
+
`http://127.0.0.1:8000/` for the index. Serve listens on `127.0.0.1`, this
|
|
67
|
+
machine only, unless `--host` names another address, such as
|
|
68
|
+
`--host "$(tailscale ip -4)"` to serve on a tailnet.
|
|
69
|
+
|
|
70
|
+
Comments and answers need the reader's routes, which sit behind Cloudflare
|
|
71
|
+
Access: until the config has an `[access]` section they answer 503, and the
|
|
72
|
+
page itself is served as usual.
|
|
73
|
+
|
|
74
|
+
## Where to read next
|
|
75
|
+
|
|
76
|
+
- [Publishing pages](docs/publishing.md): for people publishing pages.
|
|
77
|
+
Markdown and HTML pages, images, the manifest and the index, decision
|
|
78
|
+
tables, the template and the theme, and what a page may run.
|
|
79
|
+
- [Reading and answering pages](docs/comments.md): for readers and the people
|
|
80
|
+
answering them. Answers and comments, the comments panel, the popover and
|
|
81
|
+
bottom sheet, and comments on passages.
|
|
82
|
+
- [Agents](docs/agents.md): for agents and their operators. Agent
|
|
83
|
+
credentials, the pull loop and the default responder.
|
|
84
|
+
- [Operating the site](docs/operating.md): for the operator of the site.
|
|
85
|
+
Serve, Cloudflare Access, publishing through the tunnel, deploying from
|
|
86
|
+
`main`, and backups.
|
|
87
|
+
- [Development](docs/development.md): for people changing Lotuspod. The
|
|
88
|
+
tests and the captures.
|
|
89
|
+
- [Architecture](docs/architecture.md): for reviewers and new contributors.
|
|
90
|
+
One diagram of how the parts fit together.
|
|
91
|
+
|
|
92
|
+
## How it was built
|
|
93
|
+
|
|
94
|
+
Lotuspod is built by [Holophyte](https://github.com/wevial/holophyte), an
|
|
95
|
+
agent factory. Each change starts as a ticket with acceptance criteria and
|
|
96
|
+
verify commands. An agent implements it in an isolated worktree, the verify
|
|
97
|
+
commands run, and a different agent reviews it. A pull request then runs the
|
|
98
|
+
required `unit` check and merges. The
|
|
99
|
+
[merged pull requests](https://github.com/wevial/lotuspod/pulls?q=is%3Apr+is%3Amerged)
|
|
100
|
+
are the record.
|
|
101
|
+
|
|
102
|
+
## Security
|
|
103
|
+
|
|
104
|
+
Report a vulnerability privately ([SECURITY.md](SECURITY.md)). `src/lotuspod/access.py` checks Cloudflare
|
|
105
|
+
Access's RS256 assertions with the standard library: the package has no runtime dependencies, the check only
|
|
106
|
+
verifies (never signs or makes keys), and RS256 is one modular exponentiation plus a comparison of the whole
|
|
107
|
+
re-encoded PKCS#1 v1.5 block, leaving no parser to trick. It refuses any `alg` but RS256, a key id not in
|
|
108
|
+
the team's key set, keys under 2048 bits, and a wrong issuer, audience, time or email; it never reads the
|
|
109
|
+
plain email header. Pinned by `tests.test_access.VerifierTests.test_header_must_name_rs256_and_a_listed_key`,
|
|
110
|
+
`tests.test_access.VerifierTests.test_signature_block_is_compared_whole`,
|
|
111
|
+
`tests.test_access.VerifierTests.test_keys_under_2048_bits_are_not_listed`,
|
|
112
|
+
`tests.test_access.VerifierTests.test_malformed_tokens_are_invalid` and
|
|
113
|
+
`tests.test_access.WhoamiTests.test_email_header_naming_someone_else_is_not_read`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lotuspod"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lotuspod: publish pages people comment on, with decision forms and agents that answer."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
lotuspod = "lotuspod.cli:main"
|
|
15
|
+
|
|
16
|
+
[tool.setuptools.packages.find]
|
|
17
|
+
where = ["src"]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.package-data]
|
|
20
|
+
lotuspod = ["py.typed", "_templates/*.html", "_theme/*.json", "_theme/*.svg", "_theme/css/*.css", "_theme/js/*.js"]
|
lotuspod-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta http-equiv="Content-Security-Policy" content="{{policy}}">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<title>{{title}} · Lotuspod</title>
|
|
8
|
+
<meta name="generator" content="lotuspod theme {{theme_name}}">
|
|
9
|
+
<meta name="lotuspod:visible" content="{{visible}}">
|
|
10
|
+
{{#revision}} <meta name="lotuspod:revision" content="{{revision}}">
|
|
11
|
+
{{/revision}}{{#owner}} <meta name="lotuspod:owner" content="{{owner}}">
|
|
12
|
+
{{/owner}} <link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
13
|
+
<link rel="stylesheet" href="lotuspod.css?v={{theme_hash}}">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<main class="artifact{{variant_class}}" id="top">
|
|
17
|
+
<div class="artifact-topbar">
|
|
18
|
+
<a class="artifact-topbar-brand" href="index.html">Lotuspod</a><span class="artifact-topbar-sep">:</span>
|
|
19
|
+
<a class="artifact-topbar-title" href="#top">{{title}}</a>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<header class="artifact-header">
|
|
23
|
+
{{#kicker}} <p class="artifact-kicker">{{kicker}}</p>
|
|
24
|
+
{{/kicker}} <h1 class="artifact-title">{{title}}</h1>
|
|
25
|
+
<p class="artifact-meta"><time datetime="{{date}}">{{date}}</time></p>
|
|
26
|
+
{{#owner}} <p class="artifact-owner">Published by {{owner}}</p>
|
|
27
|
+
{{/owner}} {{summary_block}}
|
|
28
|
+
</header>
|
|
29
|
+
|
|
30
|
+
<div class="artifact-main">
|
|
31
|
+
{{#outline}}<nav class="artifact-outline" aria-label="On this page">
|
|
32
|
+
<details class="artifact-outline-disclosure">
|
|
33
|
+
<summary class="artifact-outline-summary">On this page</summary>
|
|
34
|
+
<ol class="artifact-outline-list" role="list">
|
|
35
|
+
{{outline_items}}
|
|
36
|
+
</ol>
|
|
37
|
+
</details>
|
|
38
|
+
</nav>{{/outline}}
|
|
39
|
+
|
|
40
|
+
<section class="artifact-body">
|
|
41
|
+
{{body}}
|
|
42
|
+
</section>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<footer class="artifact-footer">
|
|
46
|
+
<span aria-hidden="true">✿</span> lotuspod
|
|
47
|
+
</footer>
|
|
48
|
+
</main>
|
|
49
|
+
{{#mermaid}} <script type="module">
|
|
50
|
+
import mermaid from "{{mermaid_dir}}dist/mermaid.esm.min.mjs";
|
|
51
|
+
mermaid.initialize({
|
|
52
|
+
startOnLoad: true,
|
|
53
|
+
theme: "base",
|
|
54
|
+
themeVariables: {{mermaid_theme_variables}}
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
{{/mermaid}}{{#page_script_needed}} <script src="{{page_script}}?v={{theme_hash}}" defer></script>
|
|
58
|
+
{{/page_script_needed}}</body>
|
|
59
|
+
</html>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Lotuspod</title>
|
|
7
|
+
<meta name="generator" content="lotuspod theme {{theme_name}}">
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
9
|
+
<link rel="stylesheet" href="lotuspod.css?v={{theme_hash}}">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<main class="index">
|
|
13
|
+
<header class="index-header">
|
|
14
|
+
<h1 class="index-title">Lotuspod <span class="index-updated">updated <time datetime="{{generated}}">{{generated}}</time></span></h1>
|
|
15
|
+
</header>
|
|
16
|
+
|
|
17
|
+
<section class="index-body">
|
|
18
|
+
<div class="index-controls" hidden>
|
|
19
|
+
<label class="index-search-label" for="index-search">Search</label>
|
|
20
|
+
<input id="index-search" class="index-search" type="search" autocomplete="off"
|
|
21
|
+
spellcheck="false" placeholder="Search pages by title, date or summary">
|
|
22
|
+
<p class="index-count" role="status" aria-live="polite"></p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
{{entries_block}}
|
|
26
|
+
|
|
27
|
+
<p class="index-empty index-no-match" hidden>Nothing matches that search.</p>
|
|
28
|
+
</section>
|
|
29
|
+
|
|
30
|
+
<footer class="artifact-footer">
|
|
31
|
+
<span aria-hidden="true">✿</span> lotuspod
|
|
32
|
+
</footer>
|
|
33
|
+
</main>
|
|
34
|
+
|
|
35
|
+
<script>
|
|
36
|
+
// Progressive enhancement over a listing that is already complete: the table
|
|
37
|
+
// above stands on its own, and this adds sorting and the search filter on
|
|
38
|
+
// top. The search field ships hidden and is revealed here, so a reader
|
|
39
|
+
// without scripting never sees a control that cannot filter anything.
|
|
40
|
+
(() => {
|
|
41
|
+
const table = document.querySelector(".index-table");
|
|
42
|
+
if (!table) return;
|
|
43
|
+
|
|
44
|
+
const body = table.tBodies[0];
|
|
45
|
+
const headers = Array.from(table.tHead.rows[0].cells);
|
|
46
|
+
// Document order is filename order - the listing's stable base. Sorts run
|
|
47
|
+
// from it rather than from the current DOM order, so a sort is repeatable
|
|
48
|
+
// and equal keys keep falling back to filename order.
|
|
49
|
+
const rows = Array.from(body.rows);
|
|
50
|
+
|
|
51
|
+
const cell = (row, column) => row.cells[column].textContent.trim();
|
|
52
|
+
|
|
53
|
+
const sorted = (column, ascending) => {
|
|
54
|
+
const numeric = headers[column].dataset.sortType === "number";
|
|
55
|
+
const direction = ascending ? 1 : -1;
|
|
56
|
+
return rows.slice().sort((a, b) => {
|
|
57
|
+
const left = cell(a, column);
|
|
58
|
+
const right = cell(b, column);
|
|
59
|
+
// Blanks (no episode number, no date) sink in both directions, so the
|
|
60
|
+
// rows that carry the field stay together at the top either way.
|
|
61
|
+
if (!left || !right) return left ? -1 : right ? 1 : 0;
|
|
62
|
+
if (numeric) {
|
|
63
|
+
const [x, y] = [Number(left), Number(right)];
|
|
64
|
+
if (!Number.isNaN(x) && !Number.isNaN(y)) return (x - y) * direction;
|
|
65
|
+
}
|
|
66
|
+
return left.localeCompare(right, undefined, {
|
|
67
|
+
numeric: true,
|
|
68
|
+
sensitivity: "base",
|
|
69
|
+
}) * direction;
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let sortColumn = -1;
|
|
74
|
+
let ascending = true;
|
|
75
|
+
|
|
76
|
+
const sortBy = (column) => {
|
|
77
|
+
ascending = column === sortColumn ? !ascending : true;
|
|
78
|
+
sortColumn = column;
|
|
79
|
+
for (const row of sorted(column, ascending)) body.append(row);
|
|
80
|
+
headers.forEach((header, index) => {
|
|
81
|
+
header.setAttribute(
|
|
82
|
+
"aria-sort",
|
|
83
|
+
index !== column ? "none" : ascending ? "ascending" : "descending"
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// The header label becomes a button so the sort is reachable by keyboard;
|
|
89
|
+
// aria-sort carries the state, and the theme draws the arrow from it.
|
|
90
|
+
headers.forEach((header, index) => {
|
|
91
|
+
const button = document.createElement("button");
|
|
92
|
+
button.type = "button";
|
|
93
|
+
button.className = "sort-button";
|
|
94
|
+
button.textContent = header.textContent;
|
|
95
|
+
button.addEventListener("click", () => sortBy(index));
|
|
96
|
+
header.textContent = "";
|
|
97
|
+
header.append(button);
|
|
98
|
+
header.setAttribute("aria-sort", "none");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const controls = document.querySelector(".index-controls");
|
|
102
|
+
const search = document.querySelector(".index-search");
|
|
103
|
+
const count = document.querySelector(".index-count");
|
|
104
|
+
const noMatch = document.querySelector(".index-no-match");
|
|
105
|
+
|
|
106
|
+
// One case-insensitive substring test against the whole row, so a query
|
|
107
|
+
// matches any column without the reader having to pick one.
|
|
108
|
+
const filter = () => {
|
|
109
|
+
const query = search.value.trim().toLowerCase();
|
|
110
|
+
let shown = 0;
|
|
111
|
+
for (const row of rows) {
|
|
112
|
+
const match = !query || row.textContent.toLowerCase().includes(query);
|
|
113
|
+
row.hidden = !match;
|
|
114
|
+
if (match) shown += 1;
|
|
115
|
+
}
|
|
116
|
+
const noun = rows.length === 1 ? "page" : "pages";
|
|
117
|
+
count.textContent =
|
|
118
|
+
shown === rows.length
|
|
119
|
+
? `${rows.length} ${noun}`
|
|
120
|
+
: `${shown} of ${rows.length} ${noun}`;
|
|
121
|
+
noMatch.hidden = shown > 0;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
search.addEventListener("input", filter);
|
|
125
|
+
controls.hidden = false;
|
|
126
|
+
filter();
|
|
127
|
+
})();
|
|
128
|
+
</script>
|
|
129
|
+
</body>
|
|
130
|
+
</html>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/* Images on a comment (attachments.js). In a composer the images attached
|
|
2
|
+
sit under the field's frame, each a thumbnail with its Remove button,
|
|
3
|
+
beside the "Add image" button, and the status line moves below them.
|
|
4
|
+
Under a comment its images are a row of thumbnails, each a link to the
|
|
5
|
+
full size. A thumbnail's width and height attributes are its drawn size,
|
|
6
|
+
so its box is there before its bytes. Every rule here is one class more
|
|
7
|
+
specific than the composer's button rules in comments.css, which come
|
|
8
|
+
later. */
|
|
9
|
+
:is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach {
|
|
10
|
+
align-items: center;
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-wrap: wrap;
|
|
13
|
+
gap: 0.5rem;
|
|
14
|
+
grid-column: 1 / -1;
|
|
15
|
+
grid-row: 2;
|
|
16
|
+
margin: 0.45rem 0.6rem 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:is(.artifact-comment-form, .artifact-comment-reply) .artifact-comment-actions .artifact-comment-status {
|
|
20
|
+
grid-row: 3;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.artifact-attach .artifact-attach-tray,
|
|
24
|
+
.artifact-comment-col .artifact-comment-images {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-wrap: wrap;
|
|
27
|
+
gap: 0.5rem;
|
|
28
|
+
list-style: none;
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.artifact-attach .artifact-attach-tray[hidden] {
|
|
34
|
+
display: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.artifact-attach .artifact-attach-item,
|
|
38
|
+
.artifact-comment-images > li {
|
|
39
|
+
margin: 0;
|
|
40
|
+
padding: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.artifact-attach .artifact-attach-item {
|
|
44
|
+
align-items: flex-start;
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: 0.2rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.artifact-attach-thumb {
|
|
51
|
+
background: var(--color-glow);
|
|
52
|
+
border: 1px solid var(--hairline-strong);
|
|
53
|
+
border-radius: var(--radius-block);
|
|
54
|
+
display: block;
|
|
55
|
+
object-fit: cover;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.artifact-attach .artifact-attach-holder {
|
|
59
|
+
align-items: center;
|
|
60
|
+
background: var(--color-glow);
|
|
61
|
+
border: 1px dashed var(--hairline-strong);
|
|
62
|
+
border-radius: var(--radius-block);
|
|
63
|
+
color: var(--color-muted-lavender);
|
|
64
|
+
display: flex;
|
|
65
|
+
font-size: 0.78rem;
|
|
66
|
+
height: 4.5rem;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
width: 4.5rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
:is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach :is(.artifact-attach-add, .artifact-attach-remove) {
|
|
72
|
+
appearance: none;
|
|
73
|
+
background: none;
|
|
74
|
+
border: 1px solid var(--hairline-strong);
|
|
75
|
+
border-radius: var(--radius-pill);
|
|
76
|
+
color: var(--color-lavender);
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
font-family: var(--font-body);
|
|
79
|
+
font-size: 0.8rem;
|
|
80
|
+
height: auto;
|
|
81
|
+
line-height: 1.4;
|
|
82
|
+
margin: 0;
|
|
83
|
+
overflow: visible;
|
|
84
|
+
padding: 0.15rem 0.65rem;
|
|
85
|
+
position: static;
|
|
86
|
+
width: auto;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
:is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach .artifact-attach-remove {
|
|
90
|
+
border-color: transparent;
|
|
91
|
+
color: var(--color-muted-lavender);
|
|
92
|
+
padding: 0 0.3rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
:is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach :is(.artifact-attach-add, .artifact-attach-remove)::before,
|
|
96
|
+
:is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach :is(.artifact-attach-add, .artifact-attach-remove)::after {
|
|
97
|
+
content: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
:is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach :is(.artifact-attach-add, .artifact-attach-remove):hover {
|
|
101
|
+
background: none;
|
|
102
|
+
color: var(--color-pale-lavender);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
:is(.artifact-comment-form, .artifact-comment-reply) .artifact-attach :is(.artifact-attach-add, .artifact-attach-remove):disabled {
|
|
106
|
+
cursor: default;
|
|
107
|
+
opacity: 0.6;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.artifact-comment-col .artifact-comment-images {
|
|
111
|
+
margin-top: 0.35rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.artifact-comment-item--reader .artifact-comment-images {
|
|
115
|
+
justify-content: flex-end;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.artifact-comment-image {
|
|
119
|
+
border-radius: var(--radius-block);
|
|
120
|
+
display: block;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.artifact-comment-image:focus-visible {
|
|
124
|
+
outline: 2px solid var(--color-lavender);
|
|
125
|
+
outline-offset: 2px;
|
|
126
|
+
}
|