<feed xmlns='http://www.w3.org/2005/Atom'>
<title>progmaweb/ops, branch ops/git-mirrors</title>
<subtitle>The site and account service of Progmasoft: a Next.js frontend and an ASP.NET Core API.</subtitle>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/'/>
<entry>
<title>Serve only listed mirrors and report a failed default-branch lookup</title>
<updated>2026-11-12T22:57:15+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-11-12T22:57:15+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=05d42b1a4ca923ee7726c35c261bfc75d32ddea3'/>
<id>05d42b1a4ca923ee7726c35c261bfc75d32ddea3</id>
<content type='text'>
- git-http-backend no longer exports every directory under the mirror
  root: GIT_HTTP_EXPORT_ALL is gone, git-mirror marks each listed mirror
  with git-daemon-export-ok, and a mirror taken off the list loses the
  mark and is deleted on the next run. Tried live: an unlisted bare
  repository answered 404 and was removed by the next run.
- git-mirror checks git ls-remote before it reads the default branch
  and fails the run when the lookup fails.
- The README explains the /usr/bin spelling of the fcgiwrap label (an
  equivalence of /usr/sbin), the stock label of /srv/*/www, and where
  the DNS record is kept.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- git-http-backend no longer exports every directory under the mirror
  root: GIT_HTTP_EXPORT_ALL is gone, git-mirror marks each listed mirror
  with git-daemon-export-ok, and a mirror taken off the list loses the
  mark and is deleted on the next run. Tried live: an unlisted bare
  repository answered 404 and was removed by the next run.
- git-mirror checks git ls-remote before it reads the default branch
  and fails the run when the lookup fails.
- The README explains the /usr/bin spelling of the fcgiwrap label (an
  equivalence of /usr/sbin), the stock label of /srv/*/www, and where
  the DNS record is kept.
</pre>
</div>
</content>
</entry>
<entry>
<title>Add read-only Git mirrors on git.progmasoft.com</title>
<updated>2026-11-12T20:41:27+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-11-12T20:41:27+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=83e0ad4586d90bf02c3a5bfeb1ab92fd90e51703'/>
<id>83e0ad4586d90bf02c3a5bfeb1ab92fd90e51703</id>
<content type='text'>
git.progmasoft.com shows the public repositories under development
(tazel, tazelish, tazel.net, catch3, progmasoft-cpp, progmaweb, website)
as read-only mirrors with cgit, and serves clones over smart HTTP, such
as git clone https://git.progmasoft.com/tazel. GitHub stays where
development happens; private and archived repositories are not mirrored.

ops/git holds what runs on the server:

- cgitrc, the header, footer and stylesheet: the look of progmasoft.com
  (its tokens, the four-tile brand with "Git" beside it, the dark
  footer), light like its default, no scripts, no rendered READMEs.
- git-mirror.sh with git-mirror.service/.timer: every ten minutes, as
  gitmirror, branches and tags only from GitHub, pruned, the default
  branch followed, repacked with a bitmap when something changed.
- fcgiwrap@cgit drop-ins: cgit and git-http-backend run as cgit with
  systemd sandboxing (no network, read-only system, exposure 0.7), on
  a socket only nginx can open.
- selinux/progmasoft_cgit.te: fcgiwrap runs in httpd_t and the two
  CGIs move into git_script_t; the module allows only that move under
  the NoNewPrivileges that the sandboxing sets.
- nginx/git.conf: HSTS, a content security policy without scripts,
  per-address request and connection limits, 403 for receive-pack and
  the dumb protocol, buffered clones, and the inline handlers of cgit's
  menus removed.

ops/README.md describes the files, the labels, and why the name is not
behind the Cloudflare proxy: through it a clone of catch3 never reached
the client.

Deployed and checked live: every page answers 200, unknown ones 404,
full clones of tazel, progmaweb and catch3 work, a push answers 403,
no SELinux denials, no console errors, no horizontal scroll at 390 px.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
git.progmasoft.com shows the public repositories under development
(tazel, tazelish, tazel.net, catch3, progmasoft-cpp, progmaweb, website)
as read-only mirrors with cgit, and serves clones over smart HTTP, such
as git clone https://git.progmasoft.com/tazel. GitHub stays where
development happens; private and archived repositories are not mirrored.

ops/git holds what runs on the server:

- cgitrc, the header, footer and stylesheet: the look of progmasoft.com
  (its tokens, the four-tile brand with "Git" beside it, the dark
  footer), light like its default, no scripts, no rendered READMEs.
- git-mirror.sh with git-mirror.service/.timer: every ten minutes, as
  gitmirror, branches and tags only from GitHub, pruned, the default
  branch followed, repacked with a bitmap when something changed.
- fcgiwrap@cgit drop-ins: cgit and git-http-backend run as cgit with
  systemd sandboxing (no network, read-only system, exposure 0.7), on
  a socket only nginx can open.
- selinux/progmasoft_cgit.te: fcgiwrap runs in httpd_t and the two
  CGIs move into git_script_t; the module allows only that move under
  the NoNewPrivileges that the sandboxing sets.
- nginx/git.conf: HSTS, a content security policy without scripts,
  per-address request and connection limits, 403 for receive-pack and
  the dumb protocol, buffered clones, and the inline handlers of cgit's
  menus removed.

ops/README.md describes the files, the labels, and why the name is not
behind the Cloudflare proxy: through it a clone of catch3 never reached
the client.

Deployed and checked live: every page answers 200, unknown ones 404,
full clones of tazel, progmaweb and catch3 work, a push answers 403,
no SELinux denials, no console errors, no horizontal scroll at 390 px.
</pre>
</div>
</content>
</entry>
<entry>
<title>Merge remote-tracking branch 'origin/main' into security/cloudflare-real-ip</title>
<updated>2026-11-08T22:18:34+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-11-08T22:18:34+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=c0bafe9050161ea6ca0c49f54897f908ca16ce84'/>
<id>c0bafe9050161ea6ca0c49f54897f908ca16ce84</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>Rate-limit sign-in by visitor behind Cloudflare and stop forwarding h2c</title>
<updated>2026-11-08T21:38:47+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-11-08T21:38:47+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=66f3cc196a9b91d471a90e021b12630100181c4a'/>
<id>66f3cc196a9b91d471a90e021b12630100181c4a</id>
<content type='text'>
Cloudflare proxies the Progmasoft hosts, so Nginx saw a Cloudflare edge as
its peer and appended that edge to X-Forwarded-For. The API takes the last
hop as the client, so the account-auth limit (12 attempts in 3 minutes) was
counted per edge: 38 failed sign-ins from one address in a row were all
answered with 401, none with 429, and unrelated visitors behind one edge
shared a bucket.

- ops/nginx/cloudflare-real-ip.conf trusts CF-Connecting-IP only from the
  published Cloudflare ranges; a direct peer keeps its own address, so the
  header cannot be forged around Cloudflare. With it installed the 13th
  failed sign-in gets 429, a forged X-Forwarded-For changes nothing, and
  another address is not affected.
- Every proxied location forwarded the client's Upgrade header with
  "Connection: upgrade" on each request. Only a WebSocket upgrade is now
  forwarded; other requests carry no Upgrade and an empty Connection.
- c8 reached glob 13 (GHSA-3g3m-2fjr-6pw4) and minimatch 10
  (GHSA-g3hv-75wg-m249) through test-exclude; both are overridden to the
  fixed majors. pnpm audit reports nothing, coverage, lint and check pass.
- The CodeQL action pins carry their exact version comment (v4.38.2).
- ops/README.md describes the address handling and no longer names a
  script that left the repository with AI Support.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Cloudflare proxies the Progmasoft hosts, so Nginx saw a Cloudflare edge as
its peer and appended that edge to X-Forwarded-For. The API takes the last
hop as the client, so the account-auth limit (12 attempts in 3 minutes) was
counted per edge: 38 failed sign-ins from one address in a row were all
answered with 401, none with 429, and unrelated visitors behind one edge
shared a bucket.

- ops/nginx/cloudflare-real-ip.conf trusts CF-Connecting-IP only from the
  published Cloudflare ranges; a direct peer keeps its own address, so the
  header cannot be forged around Cloudflare. With it installed the 13th
  failed sign-in gets 429, a forged X-Forwarded-For changes nothing, and
  another address is not affected.
- Every proxied location forwarded the client's Upgrade header with
  "Connection: upgrade" on each request. Only a WebSocket upgrade is now
  forwarded; other requests carry no Upgrade and an empty Connection.
- c8 reached glob 13 (GHSA-3g3m-2fjr-6pw4) and minimatch 10
  (GHSA-g3hv-75wg-m249) through test-exclude; both are overridden to the
  fixed majors. pnpm audit reports nothing, coverage, lint and check pass.
- The CodeQL action pins carry their exact version comment (v4.38.2).
- ops/README.md describes the address handling and no longer names a
  script that left the repository with AI Support.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Remove Xide and ides.progmasoft.com</title>
<updated>2026-11-08T15:46:08+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-11-08T15:46:08+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=efa1f584205e93c292ea40b564d92a03f12ab33c'/>
<id>efa1f584205e93c292ea40b564d92a03f12ab33c</id>
<content type='text'>
Xide is no longer developed as of 2026-11-08 (Progmasoft/xide#29), so
Progmaweb stops presenting it:

- product.progmasoft.com: Xide leaves the catalog, its card and its
  detail page; /xide on the product host is a 404 like any unknown
  product. The catalog's description in every language names the Tala
  language with its tools and Catch3.
- ides.progmasoft.com served only the IDE list and the Xide page, so the
  whole site goes: the pages under app/ides, its texts in every
  language, idesRoute and its host in the proxy, and both nginx server
  blocks. The certificate of progmasoft.com no longer has to name it.

Tests: the catalog lists the remaining products and refuses xide.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Xide is no longer developed as of 2026-11-08 (Progmasoft/xide#29), so
Progmaweb stops presenting it:

- product.progmasoft.com: Xide leaves the catalog, its card and its
  detail page; /xide on the product host is a 404 like any unknown
  product. The catalog's description in every language names the Tala
  language with its tools and Catch3.
- ides.progmasoft.com served only the IDE list and the Xide page, so the
  whole site goes: the pages under app/ides, its texts in every
  language, idesRoute and its host in the proxy, and both nginx server
  blocks. The certificate of progmasoft.com no longer has to name it.

Tests: the catalog lists the remaining products and refuses xide.
</pre>
</div>
</content>
</entry>
<entry>
<title>Make the policies say what ViGet and support do today</title>
<updated>2026-11-06T12:00:16+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-11-06T12:00:16+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=8f7f90b57ecc0bf5fba555b389c149ac068929b0'/>
<id>8f7f90b57ecc0bf5fba555b389c149ac068929b0</id>
<content type='text'>
The review found four promises the policies made beyond what exists:

- ViGet takes no uploads from accounts yet; only Progmasoft publishes,
  through a server command. The Privacy Policy now says so, and that
  ViGet will record the IP address of each upload, for that one purpose
  and for the same time, once uploads open. ops/README.md tells the
  upload endpoint to do exactly that.
- A yank leaves a JAR served by nginx and cached by Cloudflare for a
  year. The Security Policy says a malicious version is taken off ViGet
  and kept out of reach as evidence; ops/README.md gives the four steps:
  yank, move the JAR into a root-only quarantine, purge the Cloudflare
  cache, close the account.
- The API cannot delete an account. ops/README.md gives the procedure
  support follows on a request from the account's address: a dump, then
  one transaction that removes the OpenIddict tokens and authorizations
  of the account, the applications it alone owns, and the account with
  its sessions and ownerships.
- Pages allow Cloudflare's analytics script besides their own, and the
  Security Policy now names it.

ops/README.md also records the nginx logs without client addresses on
both servers, which make the Privacy Policy's statement about web-server
logs true.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The review found four promises the policies made beyond what exists:

- ViGet takes no uploads from accounts yet; only Progmasoft publishes,
  through a server command. The Privacy Policy now says so, and that
  ViGet will record the IP address of each upload, for that one purpose
  and for the same time, once uploads open. ops/README.md tells the
  upload endpoint to do exactly that.
- A yank leaves a JAR served by nginx and cached by Cloudflare for a
  year. The Security Policy says a malicious version is taken off ViGet
  and kept out of reach as evidence; ops/README.md gives the four steps:
  yank, move the JAR into a root-only quarantine, purge the Cloudflare
  cache, close the account.
- The API cannot delete an account. ops/README.md gives the procedure
  support follows on a request from the account's address: a dump, then
  one transaction that removes the OpenIddict tokens and authorizations
  of the account, the applications it alone owns, and the account with
  its sessions and ownerships.
- Pages allow Cloudflare's analytics script besides their own, and the
  Security Policy now names it.

ops/README.md also records the nginx logs without client addresses on
both servers, which make the Privacy Policy's statement about web-server
logs true.
</pre>
</div>
</content>
</entry>
<entry>
<title>Merge remote-tracking branch 'origin/main' into feature/provision-reserved-accounts</title>
<updated>2026-10-29T12:52:11+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-29T12:52:11+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=3749e30da561652a226017d9360b84e34a8b74fc'/>
<id>3749e30da561652a226017d9360b84e34a8b74fc</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>Provision the reserved accounts on the server</title>
<updated>2026-10-29T12:35:06+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-29T12:35:06+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=21b515565b73f86477afe436bb3588f3e5929443'/>
<id>21b515565b73f86477afe436bb3588f3e5929443</id>
<content type='text'>
Registration refuses the reserved names Progmasoft and Leitwolf, which are
meant to be created by trusted operations tooling only, and that tooling did
not exist: the Progmasoft account, the publisher of Progmasoft's Tazel
plugins on ViGet, could not be created at all.

- 'accounts provision-reserved &lt;Name&gt; &lt;email&gt;' runs with the configuration
  of the service, like the viget commands. It accepts only the reserved
  names, applies the rules of registration to the email address and the
  password, never replaces an existing account, and brings the account
  tables to the current schema first.
- The password is read from the terminal without echo, twice, or from one
  line of standard input when it is not a terminal; it never appears on a
  command line or in the output.
- AccountNamePolicy.TryNormalizeReserved checks a reserved name;
  TryNormalize, which registration uses, still refuses it.

Tests cover a reserved account created and signing in with its password,
a name that is not reserved, two different passwords, a weak password, an
existing account and the usage line. ops/README.md shows the command with
systemd-run --pty; docs/ACCOUNT-API.md points to it.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Registration refuses the reserved names Progmasoft and Leitwolf, which are
meant to be created by trusted operations tooling only, and that tooling did
not exist: the Progmasoft account, the publisher of Progmasoft's Tazel
plugins on ViGet, could not be created at all.

- 'accounts provision-reserved &lt;Name&gt; &lt;email&gt;' runs with the configuration
  of the service, like the viget commands. It accepts only the reserved
  names, applies the rules of registration to the email address and the
  password, never replaces an existing account, and brings the account
  tables to the current schema first.
- The password is read from the terminal without echo, twice, or from one
  line of standard input when it is not a terminal; it never appears on a
  command line or in the output.
- AccountNamePolicy.TryNormalizeReserved checks a reserved name;
  TryNormalize, which registration uses, still refuses it.

Tests cover a reserved account created and signing in with its password,
a name that is not reserved, two different passwords, a weak password, an
existing account and the usage line. ops/README.md shows the command with
systemd-run --pty; docs/ACCOUNT-API.md points to it.
</pre>
</div>
</content>
</entry>
<entry>
<title>Publish ViGet plugins without putting the environment on a command line</title>
<updated>2026-10-29T12:04:15+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-29T12:04:15+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=e644c9bb28ca59d93f09c2d732aa7f070ecdaf21'/>
<id>e644c9bb28ca59d93f09c2d732aa7f070ecdaf21</id>
<content type='text'>
The documented command passed the variables of account.env to env as
arguments, which shows them in the process list to every user while it
runs. systemd-run reads the environment file itself, runs as the service
user with the production environment, and the JAR goes through a private
incoming directory. The publisher must be an existing account.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The documented command passed the variables of account.env to env as
arguments, which shows them in the process list to every user while it
runs. systemd-run reads the environment file itself, runs as the service
user with the production environment, and the JAR goes through a private
incoming directory. The publisher must be an existing account.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Show the Tazel plugins of ViGet, open the Gradle catalog, and route their files</title>
<updated>2026-10-29T09:32:23+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-29T09:32:23+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=3d1c6d52b830d201a68b0440a8a81c13b74c8b92'/>
<id>3d1c6d52b830d201a68b0440a8a81c13b74c8b92</id>
<content type='text'>
The API of the previous commit holds the catalog; this adds what people and
Tazel reach.

Pages:
- /dslplugins/tazel lists the published plugins, read from the API on its
  loopback address, and says when there is none.
- /dslplugins/tazel/&lt;Publisher&gt;/&lt;Name&gt; is the page of a plugin: its
  description, publisher, latest version, license, authors, homepage and
  repository, the plugins block that uses it, and every version with its
  date, plugin API, size, SHA-256, yanked mark and JAR.
- /dslplugins/gradle opens the Gradle catalog. It only lists: Gradle takes
  com.progmasoft.tazel.plugin from the Gradle Plugin Portal.
- The availability note says that Progmasoft publishes first and every
  account later. Texts are in the four languages of the site.

Nginx (ops/nginx/progmaweb.conf):
- On viget.progmasoft.com, index.json of a plugin goes to the API and the
  JARs come from /srv/viget/files with immutable caching; /api/ stays
  closed.
- maven.progmasoft.com serves /srv/maven, whose /tazel holds tazel-api.

ops/README.md describes the directories, their SELinux labels, the publish
and yank commands, and how a release of Tazel copies the API into the Maven
repository.

Tests cover the texts in every language, the plugins block, the Gradle list,
and that a page asks the API only for identifiers.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The API of the previous commit holds the catalog; this adds what people and
Tazel reach.

Pages:
- /dslplugins/tazel lists the published plugins, read from the API on its
  loopback address, and says when there is none.
- /dslplugins/tazel/&lt;Publisher&gt;/&lt;Name&gt; is the page of a plugin: its
  description, publisher, latest version, license, authors, homepage and
  repository, the plugins block that uses it, and every version with its
  date, plugin API, size, SHA-256, yanked mark and JAR.
- /dslplugins/gradle opens the Gradle catalog. It only lists: Gradle takes
  com.progmasoft.tazel.plugin from the Gradle Plugin Portal.
- The availability note says that Progmasoft publishes first and every
  account later. Texts are in the four languages of the site.

Nginx (ops/nginx/progmaweb.conf):
- On viget.progmasoft.com, index.json of a plugin goes to the API and the
  JARs come from /srv/viget/files with immutable caching; /api/ stays
  closed.
- maven.progmasoft.com serves /srv/maven, whose /tazel holds tazel-api.

ops/README.md describes the directories, their SELinux labels, the publish
and yank commands, and how a release of Tazel copies the API into the Maven
repository.

Tests cover the texts in every language, the plugins block, the Gradle list,
and that a page asks the API only for identifiers.
</pre>
</div>
</content>
</entry>
</feed>
