<feed xmlns='http://www.w3.org/2005/Atom'>
<title>progmaweb/.github/workflows, branch main</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>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>Merge main into the Renovate branch</title>
<updated>2026-10-10T20:16:50+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-10T20:16:50+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=107a257bb5313828ca5a36e4f1c2963dfb24b294'/>
<id>107a257bb5313828ca5a36e4f1c2963dfb24b294</id>
<content type='text'>
main gained the workflow analysis change, which adds a line to the
checkout step right above the action whose version comment this branch
corrects. Both changes are kept: the checkout does not persist its
credentials, and the comment names v4.9.0.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
main gained the workflow analysis change, which adds a line to the
checkout step right above the action whose version comment this branch
corrects. Both changes are kept: the checkout does not persist its
credentials, and the comment names v4.9.0.
</pre>
</div>
</content>
</entry>
<entry>
<title>Name the pinned version of the dependency review action</title>
<updated>2026-10-10T19:35:08+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-10T19:35:08+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=65f4b4f309958def71b9b8956c06d0d55b75e700'/>
<id>65f4b4f309958def71b9b8956c06d0d55b75e700</id>
<content type='text'>
The action is pinned to the commit of v4.9.0, and the comment beside
the pin said v4. Renovate reads the comment as the version in use and
looked for a tag named v5 to update to; the action has v5.0.0 and no
v5, so the lookup failed and Renovate warned about it. The comment now
names the version the commit is.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The action is pinned to the commit of v4.9.0, and the comment beside
the pin said v4. Renovate reads the comment as the version in use and
looked for a tag named v5 to update to; the action has v5.0.0 and no
v5, so the lookup failed and Renovate warned about it. The comment now
names the version the commit is.
</pre>
</div>
</content>
</entry>
<entry>
<title>Leave out the Scorecard workflow</title>
<updated>2026-10-10T17:26:52+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-10T17:26:52+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=04622baa18a5d52b60ec0f7ea882896956a91c3a'/>
<id>04622baa18a5d52b60ec0f7ea882896956a91c3a</id>
<content type='text'>
The owner decided against running OpenSSF Scorecard here. The workflow
added by the commit before this one is removed; nothing else changes.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The owner decided against running OpenSSF Scorecard here. The workflow
added by the commit before this one is removed; nothing else changes.
</pre>
</div>
</content>
</entry>
<entry>
<title>Add workflow and repository analysis</title>
<updated>2026-10-10T17:23:08+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-10T17:23:08+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=525f3c0566247bbb6a81b5a2e1488147688c089a'/>
<id>525f3c0566247bbb6a81b5a2e1488147688c089a</id>
<content type='text'>
zizmor audits the workflows themselves for what makes a workflow
unsafe, and uploads what it finds to code scanning. Its first run here
found one thing, 6 times: a checkout that leaves the job's token in
the checked-out repository, where any later step can read it. No
workflow pushes or needs the token after the checkout, so every checkout
now sets persist-credentials to false, and zizmor reports nothing.

OpenSSF Scorecard grades how the repository is kept: branch protection,
pinned dependencies, token permissions and the like. It runs on the
default branch and weekly, publishes its score and uploads its findings to
code scanning.

Every new action is pinned to a commit, each job names the permissions
it needs, and no new workflow has a default permission.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
zizmor audits the workflows themselves for what makes a workflow
unsafe, and uploads what it finds to code scanning. Its first run here
found one thing, 6 times: a checkout that leaves the job's token in
the checked-out repository, where any later step can read it. No
workflow pushes or needs the token after the checkout, so every checkout
now sets persist-credentials to false, and zizmor reports nothing.

OpenSSF Scorecard grades how the repository is kept: branch protection,
pinned dependencies, token permissions and the like. It runs on the
default branch and weekly, publishes its score and uploads its findings to
code scanning.

Every new action is pinned to a commit, each job names the permissions
it needs, and no new workflow has a default permission.
</pre>
</div>
</content>
</entry>
<entry>
<title>Keep accounts and sessions in PostgreSQL</title>
<updated>2026-10-06T19:53:46+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-06T19:53:46+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=1c986ff99b3c8f349e4a63c0e922276ddf7dc836'/>
<id>1c986ff99b3c8f349e4a63c0e922276ddf7dc836</id>
<content type='text'>
The account API kept everything in the memory of its process, so every
restart and every deployment of the API deleted all accounts and signed
everyone out.

PostgresAccountStore implements IAccountStore on PostgreSQL through Npgsql.
The three uniqueness rules are unique constraints, so two requests that race
for one name, address or Google identity are decided by the database. A
refused account is an ordinary answer, not an exception, and the rules are
reported in the order the in-memory store reports them. A session refers to
its account, and a session of an account that is not stored is refused; the
in-memory store now refuses it as well.

The schema is a list of numbered steps. The service takes the missing ones
when it starts, in one transaction under an advisory lock, and does not
start when the database cannot be reached or is newer than the release.
/health answers 503 while the database does not answer.

ConnectionStrings:Accounts selects PostgreSQL. A production service that is
given no database does not start; keeping accounts in memory there has to
be asked for with Accounts:Store=Memory. Development and the tests keep the
in-memory store by default.

The store gains RemoveExpiredSessionsAsync, and a background service calls
it at the start and once an hour: a session whose token is never presented
again no longer keeps its row for ever.

PostgreSQL keeps a timestamp to the microsecond. The store cuts a moment to
that precision and returns what it stored, and the service uses the
returned account, so the creation time in the registration response equals
the one every later read returns.

Tests. AccountStoreContractTests states the rules once and runs them on
both stores. Further tests cover the schema steps applied concurrently and a
database that is too new, the choice of the store from the configuration,
the removal of expired sessions, and an account with its session surviving
a restart of the service over HTTP. The PostgreSQL tests read their
database from PROGMAWEB_TEST_POSTGRES and work in a schema of their own
each. Without the variable they are skipped; CI starts PostgreSQL 16 for
them, and a test fails the run when the variable is missing there.

Operations. ops/README.md describes the database: the Unix socket with peer
authentication, so that no password exists, the one-time preparation, and
rollback across a schema step. The service unit starts after PostgreSQL.
docs/LIMITATIONS.md no longer lists the in-memory store and now lists the
missing backup and the missing deletion of accounts.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The account API kept everything in the memory of its process, so every
restart and every deployment of the API deleted all accounts and signed
everyone out.

PostgresAccountStore implements IAccountStore on PostgreSQL through Npgsql.
The three uniqueness rules are unique constraints, so two requests that race
for one name, address or Google identity are decided by the database. A
refused account is an ordinary answer, not an exception, and the rules are
reported in the order the in-memory store reports them. A session refers to
its account, and a session of an account that is not stored is refused; the
in-memory store now refuses it as well.

The schema is a list of numbered steps. The service takes the missing ones
when it starts, in one transaction under an advisory lock, and does not
start when the database cannot be reached or is newer than the release.
/health answers 503 while the database does not answer.

ConnectionStrings:Accounts selects PostgreSQL. A production service that is
given no database does not start; keeping accounts in memory there has to
be asked for with Accounts:Store=Memory. Development and the tests keep the
in-memory store by default.

The store gains RemoveExpiredSessionsAsync, and a background service calls
it at the start and once an hour: a session whose token is never presented
again no longer keeps its row for ever.

PostgreSQL keeps a timestamp to the microsecond. The store cuts a moment to
that precision and returns what it stored, and the service uses the
returned account, so the creation time in the registration response equals
the one every later read returns.

Tests. AccountStoreContractTests states the rules once and runs them on
both stores. Further tests cover the schema steps applied concurrently and a
database that is too new, the choice of the store from the configuration,
the removal of expired sessions, and an account with its session surviving
a restart of the service over HTTP. The PostgreSQL tests read their
database from PROGMAWEB_TEST_POSTGRES and work in a schema of their own
each. Without the variable they are skipped; CI starts PostgreSQL 16 for
them, and a test fails the run when the variable is missing there.

Operations. ops/README.md describes the database: the Unix socket with peer
authentication, so that no password exists, the one-time preparation, and
rollback across a schema step. The service unit starts after PostgreSQL.
docs/LIMITATIONS.md no longer lists the in-memory store and now lists the
missing backup and the missing deletion of accounts.
</pre>
</div>
</content>
</entry>
<entry>
<title>Require documentation for every item of the API reference</title>
<updated>2026-10-03T15:27:22+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-03T15:27:22+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=b1cf9a3bea0c81c1cb3ed2e0ddce2b9a56e175ef'/>
<id>b1cf9a3bea0c81c1cb3ed2e0ddce2b9a56e175ef</id>
<content type='text'>
The DocFX gate only failed on a broken link or an unresolved reference.
DocFX does not report a declaration without a comment, and the API had
none, so the gate passed over a reference with no descriptions at all.

The gate now has three parts:

- DocFX still builds the reference with warnings as errors.
- The API project compiles its XML documentation. With warnings as errors
  already on, an undocumented public declaration or a wrong cref now fails
  the build.
- docs/api/CheckDocumentation.cs reads the metadata DocFX generated and
  fails when a namespace, type or member has no summary. This covers
  internal and private members, which the compiler does not check. It runs
  as a file-based .NET program, so the gate needs no tool beyond the SDK.
  It refuses to pass when the metadata is missing or defines nothing.

To satisfy the gate, every declaration of apps/api has documentation
comments, 131 reference items in all, and the two namespaces are described
by overwrite files in docs/api/namespaces. Only comment lines were added to
the C# sources; no statement changed.

docs/api/filterConfig.yml leaves generator-written code out of the
reference: the generated regular-expression namespace, and the one partial
method whose generated half replaces the comment of its declaration.

Verified locally: the solution builds with no warnings, the 23 API tests
pass, DocFX builds with no warnings and the coverage check passes. Removing
one summary and one namespace file made the check fail and name both.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The DocFX gate only failed on a broken link or an unresolved reference.
DocFX does not report a declaration without a comment, and the API had
none, so the gate passed over a reference with no descriptions at all.

The gate now has three parts:

- DocFX still builds the reference with warnings as errors.
- The API project compiles its XML documentation. With warnings as errors
  already on, an undocumented public declaration or a wrong cref now fails
  the build.
- docs/api/CheckDocumentation.cs reads the metadata DocFX generated and
  fails when a namespace, type or member has no summary. This covers
  internal and private members, which the compiler does not check. It runs
  as a file-based .NET program, so the gate needs no tool beyond the SDK.
  It refuses to pass when the metadata is missing or defines nothing.

To satisfy the gate, every declaration of apps/api has documentation
comments, 131 reference items in all, and the two namespaces are described
by overwrite files in docs/api/namespaces. Only comment lines were added to
the C# sources; no statement changed.

docs/api/filterConfig.yml leaves generator-written code out of the
reference: the generated regular-expression namespace, and the one partial
method whose generated half replaces the comment of its declaration.

Verified locally: the solution builds with no warnings, the 23 API tests
pass, DocFX builds with no warnings and the coverage check passes. Removing
one summary and one namespace file made the check fail and name both.
</pre>
</div>
</content>
</entry>
<entry>
<title>Add a DocFX documentation gate for the API</title>
<updated>2026-10-03T10:04:35+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-03T10:04:35+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=ca41877fafbac6cffc3e78db1d3306112e8acc60'/>
<id>ca41877fafbac6cffc3e78db1d3306112e8acc60</id>
<content type='text'>
The backend workflow now builds the API reference with DocFX and treats
every warning as an error, so an unresolved reference or a broken link
fails the check.

- docs/api holds the DocFX project: it reads apps/api/Progmaweb.Api.csproj
  and includes internal members, because the service is an application and
  most of its types are internal.
- .config/dotnet-tools.json pins docfx 2.81.0, so CI and a local run use the
  same version through `dotnet tool restore`.
- The workflow also runs when the DocFX project or the tool manifest change.
- `just api-docs` runs the same two commands locally.
- The generated site and metadata are ignored.

The gate does not require documentation comments: the API has none yet, and
DocFX does not report an undocumented member. Requiring them is a separate
change to the C# sources.

Verified locally with docfx 2.81.0 on the .NET 10 SDK: the build passes with
no warnings, and a broken link in the overview page makes it fail.

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 backend workflow now builds the API reference with DocFX and treats
every warning as an error, so an unresolved reference or a broken link
fails the check.

- docs/api holds the DocFX project: it reads apps/api/Progmaweb.Api.csproj
  and includes internal members, because the service is an application and
  most of its types are internal.
- .config/dotnet-tools.json pins docfx 2.81.0, so CI and a local run use the
  same version through `dotnet tool restore`.
- The workflow also runs when the DocFX project or the tool manifest change.
- `just api-docs` runs the same two commands locally.
- The generated site and metadata are ignored.

The gate does not require documentation comments: the API has none yet, and
DocFX does not report an undocumented member. Requiring them is a separate
change to the C# sources.

Verified locally with docfx 2.81.0 on the .NET 10 SDK: the build passes with
no warnings, and a broken link in the overview page makes it fail.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Harden Progmaweb account API and CI coverage credentials</title>
<updated>2026-09-27T22:10:33+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-09-27T22:10:33+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=bcf10c927358157bedf6ab09ff6bff0a8aa86a14'/>
<id>bcf10c927358157bedf6ab09ff6bff0a8aa86a14</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>Add Progmaweb frontend tests and full-stack security and coverage workflows</title>
<updated>2026-09-26T13:57:36+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-09-26T13:57:36+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=07b91c2966ffe5daa9501f41436b3a45b3a2d774'/>
<id>07b91c2966ffe5daa9501f41436b3a45b3a2d774</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
</feed>
