<feed xmlns='http://www.w3.org/2005/Atom'>
<title>progmaweb/apps/api-tests/AccountStoreSelectionTests.cs, 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>Ask for the Privacy and Security Policies at registration</title>
<updated>2026-11-06T09:57:27+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-11-06T09:57:27+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=53fb8b8bde857025d12655ae53857fc30226b186'/>
<id>53fb8b8bde857025d12655ae53857fc30226b186</id>
<content type='text'>
progmasoft.com/privacy and progmasoft.com/security publish the two
policies in en-US, de-DE, ru-RU and he-IL, and the footer links both.
The Privacy Policy says what an account keeps and that Progmasoft keeps
an IP address for one purpose only: with a ViGet upload, to establish
where an upload that turns out to be malicious software came from and
to give it to the competent authorities. It says that no data, the IP
address included, is sold, rented or traded. The Security Policy covers
passwords, sessions, HTTPS, the ban on malicious software in ViGet and
how to report a weakness.

The registration form asks the person to accept both policies, and the
API refuses a registration without that acceptance, with a password
(acceptPolicies, a validation error of that field) or through Google:
the start of a Google registration without acceptPolicies=true goes
back to /register?google=policies-required, and the completion creates
no account unless the authentication state carries the current version.
An account keeps the version it accepted and the moment, in two new
columns that schema step 2 adds; accounts created before it keep null.
PolicyAcceptance.CurrentVersion and policyVersion on the web side name
the same date, and a web test reads the C# constant to keep them equal.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
progmasoft.com/privacy and progmasoft.com/security publish the two
policies in en-US, de-DE, ru-RU and he-IL, and the footer links both.
The Privacy Policy says what an account keeps and that Progmasoft keeps
an IP address for one purpose only: with a ViGet upload, to establish
where an upload that turns out to be malicious software came from and
to give it to the competent authorities. It says that no data, the IP
address included, is sold, rented or traded. The Security Policy covers
passwords, sessions, HTTPS, the ban on malicious software in ViGet and
how to report a weakness.

The registration form asks the person to accept both policies, and the
API refuses a registration without that acceptance, with a password
(acceptPolicies, a validation error of that field) or through Google:
the start of a Google registration without acceptPolicies=true goes
back to /register?google=policies-required, and the completion creates
no account unless the authentication state carries the current version.
An account keeps the version it accepted and the moment, in two new
columns that schema step 2 adds; accounts created before it keep null.
PolicyAcceptance.CurrentVersion and policyVersion on the web side name
the same date, and a web test reads the C# constant to keep them equal.
</pre>
</div>
</content>
</entry>
<entry>
<title>Add the Tazel plugin catalog of ViGet to the API</title>
<updated>2026-10-29T06:53:57+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-29T06:53:57+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=58fa0d19d223aaebf9973ebd446c4bb855351ef9'/>
<id>58fa0d19d223aaebf9973ebd446c4bb855351ef9</id>
<content type='text'>
ViGet opens in stages: first only Progmasoft publishes Tazel plugins, later
every account. This adds the catalog and the server-side publishing for the
first stage; nothing publishes through the API yet.

- ViGet/: the catalog of Tazel plugins (in memory, or in PostgreSQL beside
  the accounts with its own numbered schema steps), the descriptor read from
  the JAR (META-INF/tazel-plugin.properties, with a reader of the Java
  properties format, and the ProjectPlugin service entry), and publishing:
  the publisher must be in ViGet:TazelPublishers (Progmasoft for now) and an
  existing account of that exact name, a version is never replaced, names
  that differ only in letter case are refused, and ViGet needs a
  description, an author and a license. The JAR is kept at
  dslplugins/tazel/&lt;Publisher&gt;/&lt;Name&gt;/&lt;Version&gt;/&lt;Name&gt;-&lt;Version&gt;.jar below
  ViGet:ArtifactRoot.
- Read endpoints under /api/v1/viget/dslplugins/tazel: the list, a plugin,
  and index.json, the index Tazel reads.
- 'viget publish-tazel-plugin &lt;jar&gt;' and 'viget yank-tazel-plugin' run with
  the configuration of the service instead of the service.
- The account store can find an account by name.

The pages, the Nginx routes and the deployment follow.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
ViGet opens in stages: first only Progmasoft publishes Tazel plugins, later
every account. This adds the catalog and the server-side publishing for the
first stage; nothing publishes through the API yet.

- ViGet/: the catalog of Tazel plugins (in memory, or in PostgreSQL beside
  the accounts with its own numbered schema steps), the descriptor read from
  the JAR (META-INF/tazel-plugin.properties, with a reader of the Java
  properties format, and the ProjectPlugin service entry), and publishing:
  the publisher must be in ViGet:TazelPublishers (Progmasoft for now) and an
  existing account of that exact name, a version is never replaced, names
  that differ only in letter case are refused, and ViGet needs a
  description, an author and a license. The JAR is kept at
  dslplugins/tazel/&lt;Publisher&gt;/&lt;Name&gt;/&lt;Version&gt;/&lt;Name&gt;-&lt;Version&gt;.jar below
  ViGet:ArtifactRoot.
- Read endpoints under /api/v1/viget/dslplugins/tazel: the list, a plugin,
  and index.json, the index Tazel reads.
- 'viget publish-tazel-plugin &lt;jar&gt;' and 'viget yank-tazel-plugin' run with
  the configuration of the service instead of the service.
- The account store can find an account by name.

The pages, the Nginx routes and the deployment follow.
</pre>
</div>
</content>
</entry>
<entry>
<title>Add Progmasoft OAuth, an OAuth 2.1 and OpenID Connect provider</title>
<updated>2026-10-23T01:49:52+00:00</updated>
<author>
<name>Leitwolf11</name>
</author>
<published>2026-10-23T01:49:52+00:00</published>
<link rel='alternate' type='text/html' href='https://git.progmasoft.com/progmaweb/commit/?id=cffc8d884ce3620b70c80a9ab98b8f1f03887402'/>
<id>cffc8d884ce3620b70c80a9ab98b8f1f03887402</id>
<content type='text'>
Progmasoft accounts can now sign in to any application, and any
developer can register one. The provider runs in the account API under
the Progmasoft.OAuth namespaces and answers on credentials.progmasoft.com.

Protocol: OpenIddict 7 (Apache-2.0) with its Entity Framework stores.
Authorization code with PKCE for every client, S256 only; refresh
tokens with rotation and no reuse leeway, so a replayed refresh token
revokes its chain; client credentials for confidential clients, without
account scopes. No implicit or password grant. Codes live 2 minutes,
access and identity tokens 15 minutes, refresh tokens 30 days. Signing
(RSA) and encryption keys are read from files in production and are
required there; other environments make keys per process.

Scopes, one grant each, as the consent page lists them: profile (the
account name), email (the sign-in address; email_verified is false,
since registration does not verify it), viget.packages.read,
viget.packages.publish and viget.packages.yank (access tokens for the
viget audience), and offline_access.

Pages, plain HTML without script, behind a content security policy that
allows the one stylesheet by digest: sign-in with the account password
(a cookie of its own, no account-site session), consent with the
application, its registering account and the return host, the developer
portal (register confidential or public clients, redirect address rules
from RFC 8252, scopes, client credentials, secret shown once and
rotatable, delete; 10 applications per account; other accounts'
applications are absent), and the applications an account has allowed,
with withdrawal that revokes their tokens. Forms carry antiforgery
tokens; return addresses stay on the host.

Storage: the oauth_* tables in the account database, created by a
numbered schema step like the account tables; the step is the script
Entity Framework generates for the model, and a test compares the two.
With the memory account store they live in a shared in-memory SQLite
database, which runs the bulk revocations the in-memory provider cannot.

A host filter keeps OAuth paths on the OAuth host and everything but
OAuth and /health off it. AllowedHosts names the new host, and a rate
limit policy covers the protocol endpoints and the portal. The Nginx
vhost for the host and the key set-up are in ops; docs/OAUTH.md is the
developer guide.

AccountService gains VerifyPasswordAsync, which LoginAsync now uses,
and FindAsync.

Verified: dotnet build with warnings as errors; dotnet test, 125 tests,
109 passed and 16 PostgreSQL tests skipped locally (CI runs them,
including a new one for the schema and refresh token chain revocation):
code flow with PKCE and the claims of each scope, single-use codes,
refresh rotation and reuse refusal, consent reuse and withdrawal, deny,
public client with a private-use scheme, client credentials, ViGet
audience by introspection, refusal without PKCE, of other flows and of
unpermitted scopes, unregistered redirect addresses, sign-in redirects
and prompt=none, the portal's address rules and ownership, secret
rotation, the page security policy, the host filter and the discovery
document.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Progmasoft accounts can now sign in to any application, and any
developer can register one. The provider runs in the account API under
the Progmasoft.OAuth namespaces and answers on credentials.progmasoft.com.

Protocol: OpenIddict 7 (Apache-2.0) with its Entity Framework stores.
Authorization code with PKCE for every client, S256 only; refresh
tokens with rotation and no reuse leeway, so a replayed refresh token
revokes its chain; client credentials for confidential clients, without
account scopes. No implicit or password grant. Codes live 2 minutes,
access and identity tokens 15 minutes, refresh tokens 30 days. Signing
(RSA) and encryption keys are read from files in production and are
required there; other environments make keys per process.

Scopes, one grant each, as the consent page lists them: profile (the
account name), email (the sign-in address; email_verified is false,
since registration does not verify it), viget.packages.read,
viget.packages.publish and viget.packages.yank (access tokens for the
viget audience), and offline_access.

Pages, plain HTML without script, behind a content security policy that
allows the one stylesheet by digest: sign-in with the account password
(a cookie of its own, no account-site session), consent with the
application, its registering account and the return host, the developer
portal (register confidential or public clients, redirect address rules
from RFC 8252, scopes, client credentials, secret shown once and
rotatable, delete; 10 applications per account; other accounts'
applications are absent), and the applications an account has allowed,
with withdrawal that revokes their tokens. Forms carry antiforgery
tokens; return addresses stay on the host.

Storage: the oauth_* tables in the account database, created by a
numbered schema step like the account tables; the step is the script
Entity Framework generates for the model, and a test compares the two.
With the memory account store they live in a shared in-memory SQLite
database, which runs the bulk revocations the in-memory provider cannot.

A host filter keeps OAuth paths on the OAuth host and everything but
OAuth and /health off it. AllowedHosts names the new host, and a rate
limit policy covers the protocol endpoints and the portal. The Nginx
vhost for the host and the key set-up are in ops; docs/OAUTH.md is the
developer guide.

AccountService gains VerifyPasswordAsync, which LoginAsync now uses,
and FindAsync.

Verified: dotnet build with warnings as errors; dotnet test, 125 tests,
109 passed and 16 PostgreSQL tests skipped locally (CI runs them,
including a new one for the schema and refresh token chain revocation):
code flow with PKCE and the claims of each scope, single-use codes,
refresh rotation and reuse refusal, consent reuse and withdrawal, deny,
public client with a private-use scheme, client credentials, ViGet
audience by introspection, refusal without PKCE, of other flows and of
unpermitted scopes, unregistered redirect addresses, sign-in redirects
and prompt=none, the portal's address rules and ownership, secret
rotation, the page security policy, the host filter and the discovery
document.
</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>
</feed>
