Repository: curveball/a12n-server
Pull request #647: #646 feat: add seed data for dummy users
Author: login: usrrname
Created: 2025-03-14T16:41:00Z | Merged: 2025-05-23T18:52:57Z

# Changes

- adds `seeds/` folder and script for opting in to seed dummy users

- include `env_file` property on docker-compose to read from `.env` variable changes a user might make during development

Testing:

<img width="1493" alt="Screenshot 2025-03-24 at 9 55 59 AM" src="https://github.com/user-attachments/assets/1686f5fa-5892-425f-9b97-1032c6514cc2" />


<img width="690" alt="Screenshot 2025-03-24 at 9 55 07 AM" src="https://github.com/user-attachments/assets/ea093187-208e-4ba2-8215-16d038d760ed" />

created `jane.smith@gmail.com`, was able to log in with autogenerated password

#646

TODO: 
- [x] make use of seed opt-in


Diff:
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 4cf66a1c..c2999473 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -68,6 +68,8 @@ Everything prepended with `_` is either a hidden file or a build artifact.
 └── tsconfig.json
 ```
 
+Note: The `Makefile` contains all commands for codebase operations, while the `package.json` scripts contains basic commands for startup and build. 
+
 ## 🔄 Database Migrations
 
 Database migrations scripts are in `src/migrations` and run in chronological order when the server starts. 
@@ -86,6 +88,13 @@ node ./bin/generate-db-types.mjs
 
 This will generate a `src/db-types.ts` file that will be used by Knex to type the database tables.
 
+## Seed Users
+
+To quickly get some test data set up, we've included database seed files. To insert this data, run `make knex-seed`. This should only be done after onboarding has been completed.
+
+You'll be able to login as admin or any of the dummy users with `password123`
+:warning: This is only for development ease and should not be used on production.
+
 
 ## Type Conventions
 
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 025f30a7..8e2623b9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -2,17 +2,21 @@
 # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
 
 name: Test
-
 on:
   push:
     branches: [ main ]
   pull_request:
-
+permissions:
+  contents: read
+  checks: write
 jobs:
+
   build:
+    strategy:
+      matrix:
+        node-version: ['18.x', '20.x', '22.x']
     name: Build
     runs-on: ubuntu-latest
-
     steps:
     - uses: actions/checkout@v4
     - name: Use Node.js ${{ matrix.node-version }}
@@ -27,67 +31,39 @@ jobs:
     needs: build
     steps:
     - uses: actions/checkout@v4
-    - name: Use Node.js ${{ matrix.node-version }}
+    - name: Use Node.js
       uses: actions/setup-node@v4
-      with:
-        node-version: ${{ matrix.node-version }}
     - run: npm ci
     - run: make lint
   test:
     name: Test
+    runs-on: ubuntu-latest
     strategy:
       matrix:
-        node-version: [18.x, 20.x, 22.x]
-    runs-on: ubuntu-latest
-    needs: build
-    steps:
-    - uses: actions/checkout@v4
-    - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v4
-      with:
-        node-version: ${{ matrix.node-version }}
-    - run: npm ci
-    - run: make test
-  mysql:
-    name: Test MySQL migrations
+        node-version: ['18.x', '20.x', '22.x']
+        db-driver: ['mysql', 'postgres', 'sqlite3']
     needs: build
-    runs-on: ubuntu-latest
     services:
       mysql:
         image: mysql
+        ports:
+          - 3306:3306
         env:
+          MYSQL_ROOT_PASSWORD: password
+          MYSQL_DATABASE: a12nserver
           MYSQL_USER: a12nserver
           MYSQL_PASSWORD: password
-          MYSQL_DATABASE: a12nserver
-          MYSQL_ROOT_PASSWORD: password
-        ports:
-          - 3306:3306
-    steps:
-    - uses: actions/checkout@v4
-    - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v4
-      with:
-        node-version: ${{ matrix.node-version }}
-    - run: npm ci
-    - name: Run migrations
-      env:
-        MYSQL_USER: a12nserver
-        MYSQL_PASSWORD: password
-        MYSQL_DATABASE: a12nserver
-        MYSQL_HOST: 127.0.0.1
-      run: make knex-migrate
-  postgres:
-    name: Test Postgres migrations
-    needs: build
-    runs-on: ubuntu-latest
-    services:
       postgres:
         image: postgres
+        ports:
+          - 5432:5432
         env:
           POSTGRES_USER: a12nserver
           POSTGRES_PASSWORD: password
-        ports:
-          - 5432:5432
+          POSTGRES_DATABASE: a12nserver
+          POSTGRES_HOST: 127.0.0.1
+          POSTGRES_PORT: 5432
+          POSTGRES_DATA_DIR: "${{ github.workspace }}/var/lib/postgresql/data"
     steps:
     - uses: actions/checkout@v4
     - name: Use Node.js ${{ matrix.node-version }}
@@ -95,26 +71,34 @@ jobs:
       with:
         node-version: ${{ matrix.node-version }}
     - run: npm ci
-    - run: PGPASSWORD=password psql -h 127.0.0.1 -d a12nserver -U a12nserver -c 'CREATE SCHEMA a12nserver;'
-    - name: Run migrations
+    - run: make test
+    - name: Test MySQL migrations
+      if: matrix.db-driver == 'mysql'
       env:
-        PG_USER: a12nserver
-        PG_PASSWORD: password
-        PG_DATABASE: a12nserver
-        PG_HOST: 127.0.0.1
-      run: make knex-migrate
-  sqlite:
-    name: Test Sqlite migrations
-    needs: build
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v4
-    - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v4
-      with:
-        node-version: ${{ matrix.node-version }}
-    - run: npm ci
-    - name: Run migrations
+        DB_DRIVER: mysql
+        DB_USER: a12nserver
+        DB_PASSWORD: password
+        DB_DATABASE: a12nserver
+        DB_ROOT_PASSWORD: password
+      run: |
+        make knex-migrate
+        make knex-seed
+    - name: Test Postgres migrations
+      if: matrix.db-driver == 'postgres'
+      env:
+        DB_USER: a12nserver
+        DB_PASSWORD: password
+        DB_DATABASE: a12nserver
+        DB_HOST: 127.0.0.1
+        DB_DRIVER: pg
+      run: |
+        make knex-migrate
+        make knex-seed
+    - name: Test SQLite migrations
+      if: matrix.db-driver == 'sqlite3'
       env:
         DB_DRIVER: sqlite3
-      run: make knex-migrate
+        DB_FILENAME: "${{ github.workspace }}/a12nserver.sqlite3"
+      run: |
+        make knex-migrate
+        make knex-seed
\ No newline at end of file
diff --git a/Makefile b/Makefile
index cbf3eded..bba99b0d 100644
--- a/Makefile
+++ b/Makefile
@@ -34,6 +34,9 @@ knex-migrate: dist/build
 knex-make-migration:
 	cd src; npx knex migrate:make migration_name -x ts
 
+knex-seed: build
+	bin/db-seed.mjs
+
 start-dev:
 	npx tsc-watch --onSuccess 'node --inspect=9339 dist/app.js'
 
diff --git a/bin/db-seed.mjs b/bin/db-seed.mjs
new file mode 100755
index 00000000..07ba3eaa
--- /dev/null
+++ b/bin/db-seed.mjs
@@ -0,0 +1,5 @@
+#!/usr/bin/env node
+
+import db, { seed } from '../dist/database.js';
+await seed();
+await db.destroy();
diff --git a/docker-compose.yml b/docker-compose.yml
index f93f8954..a80a69c9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -22,6 +22,8 @@ services:
       dockerfile: Dockerfile
     ports:
       - "8531:8531"
+    env_file:
+      - .env
     environment:
         - NODE_ENV=development
         - PORT=8531
diff --git a/docs/server-settings.md b/docs/server-settings.md
index 8d309491..199dbf42 100644
--- a/docs/server-settings.md
+++ b/docs/server-settings.md
@@ -6,20 +6,22 @@ The following table lists all configurable options for a12nserver. These can be
 The value for each field is stored as a JSON string. This means that in order to store ``'null'``, you need the literal string ``'null'``.  \
 To store a string, it needs to be surrounded by double-quotes. For example, a legal value for the `smpt.emailFrom` field might be `"info@example.org"`.
 
+Some of the following server settings are also enabled as environment variables for ease of local development.
 
-| Name                      | Type | Default value | Description |
-|:--------------------------|------|--------------:|-------------|
-| `cors.allowOrigin`          | Integer | `null`          | List of allowed origins that may directly talk to the server. This should only ever be 1st party, trusted domains. By default CORS is not enabled     
-| `jwt.privateKey`            | String  | `null` | The RSA private key to sign JWT access tokens. Usually this value has the contents of a .pem file. If not set, JWT will be disabled
-| `login.defaultRedirect`     | String  | `/`         | The url that the user will be redirected to after the log in to a12nserver, and no other redirect_uri is provided by the application. It's a good idea to set this to your application URL. 
+
+| Name                      | Type | Default value | Description | Environment Variable
+|:--------------------------|------|--------------:|-------------|----------------------:|
+| `cors.allowOrigin`          | String | `null`          | List of allowed origins that may directly talk to the server. This should only ever be 1st party, trusted domains. By default CORS is not enabled. 
+| `jwt.privateKey`            | String  | `null` | The RSA private key to sign JWT access tokens. Usually this value has the contents of a .pem file. If not set, JWT will be disabled | `JWT_PRIVATE_KEY`
+| `login.defaultRedirect`     | String  | `/`         | The url that the user will be redirected to after the log in to a12nserver, and no other redirect_uri is provided by the application. It's a good idea to set this to your application URL.
 | `logo_url`                  | String  | `null` | The application logo to display on the a12nserver pages. If no logo url is provided, the application will show the Curveball logo by default. |
-| `oauth2.code.expiry`        | Integer | `600` | The expiry time (in seconds) for the \'code\' from the oauth2 authorization code grant type |
-| `oauth2.accessToken.expiry` | Integer | `600` | The expiry time (in seconds) for OAuth2 access token. |
-| `oauth2.refreshToken.expiry`| Integer | `3600 * 6` | The expiry time (in seconds) for OAuth2 refresh tokens. |
-| `registration.enabled`      | Boolean | `true`    | Allow users to register new accounts. By default new accounts will be disabled and have no permissions.|
+| `oauth2.code.expiry`        | Integer | `600` | The expiry time (in seconds) for the \'code\' from the oauth2 authorization code grant type | `OAUTH2_CODE_EXPIRY`
+| `oauth2.accessToken.expiry` | Integer | `600` | The expiry time (in seconds) for OAuth2 access token. | `OAUTH2_ACCESSTOKEN_EXPIRY`
+| `oauth2.refreshToken.expiry`| Integer | `3600 * 6` | The expiry time (in seconds) for OAuth2 refresh tokens. | `OAUTH2_REFRESHTOKEN_EXPIRY`
+| `registration.enabled`      | Boolean | `true`    | Allow users to register new accounts. By default new accounts will be disabled and have no permissions.| `REGISTRATION_ENABLED`
 | `registration.mf.enabled`   | Boolean | `true` | Allow users to register new accounts. By default new accounts will be disabled and have no permissions.
-| `smtp.url`                  | String  | `null`  | The url to the SMTP server. See the node-mailer documentation for possible values
-|  `smtp.emailFrom`            | String  | `null` | The "from" address that should be used for all outgoing emails
+| `smtp.url`                  | String  | `null`  | The url to the SMTP server. See the node-mailer documentation for possible values | `SMTP_URL`
+| `smtp.emailFrom`            | String  | `null` | The "from" address that should be used for all outgoing emails | `SMTP_EMAIL_FROM`
 | `totp`                      | String  | `enabled` | Whether TOTP is enabled. TOTP uses authenticator apps like Google Authenticator and Authy. This can be set to "enabled", "disabled", and "required"
 | `totp.serviceName`          | String  | `a12n-server API` | The name of the application that should show up in authenticator apps
 | `webauthn`                  | String  | `enabled` | Whether webauthn is "enabled", "disabled" or "required".'
diff --git a/src/database.ts b/src/database.ts
index c2e8980c..d294cf86 100644
--- a/src/database.ts
+++ b/src/database.ts
@@ -8,10 +8,12 @@ let settings: Knex.Config | null = null;
 const db: Knex = knex(getSettings());
 
 export async function init() {
-
+  settings = getSettings();
+  if (!settings) {
+    throw new Error('Database settings not found');
+  }
   console.info('Running database migrations');
   await db.migrate.latest();
-
 }
 
 export default db;
@@ -66,8 +68,6 @@ export async function insertAndGetId<T extends Record<string, any>> (
   return result[0]?.id ?? result[0];
 
 }
-
-
 export function getSettings(): Knex.Config {
 
   let connection: Knex.MySql2ConnectionConfig | Knex.PgConnectionConfig | Knex.Sqlite3ConnectionConfig;
@@ -163,7 +163,6 @@ export function getSettings(): Knex.Config {
         if (process.env.DB_DRIVER === undefined) {
           console.warn('No database settings were found, so we\'re creating a sqlite database in the current directory. This is not recommended for production');
         }
-
         connection = {
           filename: process.env.DB_FILENAME || 'a12nserver.sqlite3',
         };
@@ -173,10 +172,16 @@ export function getSettings(): Knex.Config {
       default:
         throw new Error(`Unknown value for DB_DRIVER: ${process.env.DB_DRIVER}`);
 
-
     }
 
   }
+  const seedDirectory = path.dirname(fileURLToPath(import.meta.url)) + '/seeds';
+
+  /** Knex Seed API @link {https://knexjs.org/guide/migrations.html#seed-api} */
+  const seedConfig: Knex.SeederConfig | undefined = {
+    directory: seedDirectory,
+    loadExtensions: ['.js'],
+  };
 
   settings = {
     client,
@@ -187,9 +192,19 @@ export function getSettings(): Knex.Config {
       loadExtensions: ['.js'],
     },
     pool,
+    seeds: seedConfig,
     debug: process.env.DEBUG ? true : false,
     useNullAsDefault: useNullAsDefault,
   };
 
   return settings;
 }
+
+/**
+ * Executes the 'seed' files, which populates the database with test data.
+ */
+export async function seed() {
+
+  await db.seed.run();
+
+}
diff --git a/src/seeds/001_users.ts b/src/seeds/001_users.ts
new file mode 100644
index 00000000..47919c42
--- /dev/null
+++ b/src/seeds/001_users.ts
@@ -0,0 +1,80 @@
+import * as bcrypt from 'bcrypt';
+import { generatePublicId } from '../crypto.ts';
+import { Knex } from 'knex';
+import { insertAndGetId } from '../database.ts';
+const initialUsers = [
+  {
+    nickname: 'admin',
+    given_name: 'Admin',
+    family_name: 'User',
+    privilege: 'admin',
+    resource: '*',
+    scope: '*',
+    external_id: await generatePublicId(),
+  },
+  {
+    nickname: 'apple',
+    given_name: 'Apple',
+    family_name: 'Cake',
+    external_id: await generatePublicId(),
+  },
+  {
+    nickname: 'banana',
+    given_name: 'Banana',
+    family_name: 'Bread',
+    external_id: await generatePublicId(),
+  },
+  {
+    nickname: 'cherry',
+    given_name: 'Cherry',
+    family_name: 'Tart',
+    external_id: await generatePublicId(),
+  }
+] as const;
+
+export async function seed(knex: Knex): Promise<void> {
+  console.info('Seeding users...');
+
+  for (const user of initialUsers) {
+    // Insert principal and get the generated ID
+    const principalId = await insertAndGetId('principals', {
+      active: 1,
+      nickname: user.nickname,
+      type: 1,
+      external_id: user.external_id,
+      created_at: new Date().getTime(),
+      modified_at: new Date().getTime(),
+    });
+
+    await knex('principal_identities').insert({
+      principal_id: principalId,
+      external_id: user.external_id,
+      uri: `mailto:${user.nickname}@example.com`,
+      is_primary: 1,
+      created_at: new Date().getTime(),
+      modified_at: new Date().getTime(),
+      verified_at: new Date().getTime(),
+    });
+
+    // Use the principal_id for related tables
+    await knex('user_info').insert({
+      principal_id: principalId,
+      name: `${user.given_name} ${user.family_name}`,
+      given_name: user.given_name,
+      family_name: user.family_name,
+      locale: 'en-US',
+    });
+
+    await knex('user_passwords').insert({
+      user_id: principalId,
+      password: await bcrypt.hash('password123', 12)
+    });
+  }
+  // Add admin privilege to admin user
+  await knex('user_privileges').insert({
+    user_id: 2,
+    privilege: 'admin',
+    resource: '*',
+  });
+  console.info('Seeding users complete!');
+}
diff --git a/src/server-settings.ts b/src/server-settings.ts
index c2486692..8bee00a0 100644
--- a/src/server-settings.ts
+++ b/src/server-settings.ts
@@ -5,7 +5,6 @@ export type Settings = {
   'registration.enabled': boolean;
   'registration.mfa.enabled': boolean;
   'cors.allowOrigin': string[] | null;
-
   /*
   'db.driver': 'mysql2' | 'pg' | 'sqlite3' | 'mysql';
   'db.host': string | null;
@@ -90,7 +89,6 @@ export const settingsRules: SettingsRules = {
     fromDb: true,
     default: null,
   },
-
   /*
   'db.driver': {
     description: 'Database client to use. Only "pg", "sqlite3" and "mysql" are supported',
@@ -143,7 +141,6 @@ export const settingsRules: SettingsRules = {
     env: 'DB_FILENAME',
   },
   */
-
   'smtp.url' : {
     description: 'The url to the SMTP server. See the node-mailer documentation for possible values',
     env: 'SMTP_URL',
@@ -302,13 +299,13 @@ export async function load(): Promise<void> {
   for (const row of result) {
 
     if (!isValidSettingName(row.setting)) {
-      // eslint-disable-next-line no-console
+
       console.warn('Unknown setting in database: %s. We ignored it.', row.setting);
       continue;
     }
 
     if (!(settingsRules as any)[row.setting].fromDb) {
-      // eslint-disable-next-line no-console
+
       console.warn('The setting %s may not be set from the database. We ignored it');
       continue;
     }


Reviews:
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T00:57:49Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T00:58:15Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T01:12:57Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T01:20:06Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T01:20:53Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T01:26:46Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T01:28:04Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T01:32:44Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T18:45:51Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T18:53:26Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T21:29:40Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T21:39:54Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T21:42:40Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T23:20:42Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T02:04:26Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T02:33:11Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T02:34:47Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T12:43:49Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T12:45:27Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T13:32:49Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T13:33:40Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T13:35:17Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T14:42:43Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T14:43:27Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T14:44:36Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T14:45:43Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T14:48:53Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T14:56:50Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T15:10:17Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T15:13:33Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T16:22:44Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T16:24:31Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T19:26:18Z
user: login: evert
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-23T22:45:13Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T01:01:00Z
user: login: evert
body: Very minimal last 2 comments, but mostly good to go! Was a long road but looks great
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-11T23:41:33Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T01:07:20Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T01:26:48Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T01:43:07Z
user: login: evert
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T14:27:39Z
user: login: usrrname
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T00:45:58Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds a seed script for dummy users and wires seed support into the database configuration, local development tools, and documentation.  
- Introduces `src/seeds/001_users.ts` to populate initial user data  
- Extends Knex config and CLI targets (`Makefile`, GitHub Actions) to run seeds  
- Updates docs (`docs/server-settings.md`, `.github/CONTRIBUTING.md`) and `docker-compose.yml` to document and enable environment-driven settings

### Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

<details>
<summary>Show a summary per file</summary>

| File                          | Description                                          |
| ----------------------------- | ---------------------------------------------------- |
| src/server-settings.ts        | Removed stray blank lines                             |
| src/seeds/001_users.ts        | New seed script to insert dummy users                 |
| src/database.ts               | Registers the seed directory and adds a `seed()` API  |
| docs/server-settings.md       | Expanded settings table with environment-variable column |
| docker-compose.yml            | Added `env_file` reference for `.env`                 |
| Makefile                      | Added `knex-seed` target                              |
| .github/workflows/test.yml    | Runs seeds after migrations in matrix CI jobs         |
| .github/CONTRIBUTING.md       | Documented seed usage                                 |
</details>



<details>
<summary>Comments suppressed due to low confidence (3)</summary>

**src/server-settings.ts:309**
* The `%s` placeholder is never supplied an argument here, so `row.setting` won’t appear in the log. Pass `row.setting` as a second parameter to `console.warn`.
```
console.warn('The setting %s may not be set from the database. We ignored it');
```
**docs/server-settings.md:16**
* The new table header includes an “Environment Variable” column, but this row (and some below) has only four columns. Add the appropriate env var or a placeholder (`—`) to keep the table aligned.
```
| `login.defaultRedirect`     | String  | `/`         | The url that the user will be redirected to after the log in to a12nserver, and no other redirect_uri is provided by the application. It's a good idea to set this to your application URL.|
```
**src/seeds/001_users.ts:35**
* The new seed functionality is not covered by any tests. Adding a simple integration test to verify that `seed()` inserts the expected records could prevent regressions.
```
export async function seed(knex: Knex): Promise<void> {
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T01:22:11Z
user: login: evert
body: Lets go!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-23T18:52:47Z

Review comments:
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
+
+  // Insert seed users
+  const users = await knex('principals').insert([
+    {
+      identity: 'admin@example.com',
path: seeds/001_users.ts
user: login: evert
body: This field should no longer be used. It's from before there was a `principal_identity` table which is now used instead. 
created_at: 2025-03-15T00:57:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
path: seeds/001_users.ts
user: login: evert
body: This feels like a really bad idea. Is this how seed files normally work?
created_at: 2025-03-15T00:58:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
+
+  // Insert seed users
+  const users = await knex('principals').insert([
+    {
+      identity: 'admin@example.com',
path: seeds/001_users.ts
user: login: usrrname
body: LOL shite. how does one stay up to date with most current db/schema state? do they just look at what the table headings are when they load the datagrip/tableplus?
created_at: 2025-03-15T01:12:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
path: seeds/001_users.ts
user: login: usrrname
body: no... this was claude bs. I unquestionably agreed with it after seeing delete statements at the beginning of local dev schema files. 
created_at: 2025-03-15T01:20:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
path: seeds/001_users.ts
user: login: usrrname
body: However... you mentioning that does make me recall that you intended to set up local dev database to persist test users... so this approach is counter to your intent... 
created_at: 2025-03-15T01:20:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
path: seeds/001_users.ts
user: login: evert
body: Everything should already persist by default. I think it's just your docker container right now that doesn't but there's no problem with the normal dev setup for this i think.
created_at: 2025-03-15T01:26:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
+
+  // Insert seed users
+  const users = await knex('principals').insert([
+    {
+      identity: 'admin@example.com',
path: seeds/001_users.ts
user: login: evert
body: well, the 'internal api' is really the specification. So it's always more reliable to call the principalService and 'save()' functions instead of directly inserting into the database.

This has the advantage that you get full typing, and it also continuous to work with code changes and does business logic validation. I don't know if this is atypical to do things like that in seed files though :shrug: 
created_at: 2025-03-15T01:28:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
path: seeds/001_users.ts
user: login: usrrname
body: I dunno If it's just me but I think the users I created have been persisting... but so far I cant validate/verify them without supporting the mail part I think
created_at: 2025-03-15T01:32:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
path: seeds/001_users.ts
user: login: evert
body: You're not able to see those users when you navigate to http://localhost:8531/user ?
created_at: 2025-03-15T18:45:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
+
+  // Insert seed users
+  const users = await knex('principals').insert([
+    {
+      identity: 'admin@example.com',
path: seeds/001_users.ts
user: login: evert
body: Also for additional context, I didn't drop that column right away because *just* in case someone runs into a bug with that migration, it's nice if the old column is still around. So it's a bit of a precaution to keep the old schema around and then later on remove it once it had some time to settle.
created_at: 2025-03-15T18:53:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
+
+  // Insert seed users
+  const users = await knex('principals').insert([
+    {
+      identity: 'admin@example.com',
path: seeds/001_users.ts
user: login: usrrname
body: Makes sense from a backward compatibility perspective! Lol when I worked corporate we announced deprecations like 3 months in advance 
created_at: 2025-03-15T21:29:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
path: seeds/001_users.ts
user: login: usrrname
body: I see the users, I just can't login as them as the system will claim they haven't verified (existing docker setup with 2 other manually entered registrations) 
created_at: 2025-03-15T21:39:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
path: seeds/001_users.ts
user: login: evert
body: That makes sense! Because email verification needs to happen
created_at: 2025-03-15T21:42:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
path: seeds/001_users.ts
user: login: usrrname
body: Yep, that's what I meant, we don't have a way to support that in dev env yet 
created_at: 2025-03-15T23:20:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+import * as bcrypt from 'bcrypt';
+import { Knex } from 'knex';
+
+export async function seed(knex: Knex): Promise<void> {
+  // First, delete existing entries to start fresh
+  await knex('user_info').del();
+  await knex('user_passwords').del();
+  await knex('principals').del();
path: seeds/001_users.ts
user: login: usrrname
body: ```suggestion
```
created_at: 2025-03-16T02:04:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,6 +14,18 @@ KEEP_ALIVE_TIMEOUT_MS=
 # functioning correctly.
 CURVEBALL_ORIGIN="http://localhost:8531"
 
+# Allow CORS from any origin by running the seed script 002_cors_allowed_origin.js
+# ie. CORS_ALLOW_ORIGIN=["http://localhost:8531", "http://localhost:3000"]
+CORS_ALLOW_ORIGIN=
+
path: .env.defaults
user: login: evert
body: I was gonna leave a comment that this shouldn't use seed files and instead use the built-in facility for loading config from .env, but it looks like you partially did this?

This is not the type of thing that should ever exists in a seed file. Remember that seed files are contributor only and just meant for populating development / dummy data. They can also only run once, so any changes to this env will be ignored after the seed file ran once.

created_at: 2025-03-23T02:22:00Z
author_association: MEMBER
diff_hunk: @@ -14,6 +14,18 @@ KEEP_ALIVE_TIMEOUT_MS=
 # functioning correctly.
 CURVEBALL_ORIGIN="http://localhost:8531"
 
+# Allow CORS from any origin by running the seed script 002_cors_allowed_origin.js
+# ie. CORS_ALLOW_ORIGIN=["http://localhost:8531", "http://localhost:3000"]
+CORS_ALLOW_ORIGIN=
+
+# NOTE: Seed variables should only be set in development mode. 
+# Seed scripts can only run if tables don't already contain values.
+# Seed scripts WILL CLEAR EXISTING DATA.
+
+# Run user seed scripts only
+# Seed scripts WILL CLEAR EXISTING principals and principal_identities tables.
+SEED_USERS=
path: .env.defaults
user: login: evert
body: I mentioned this earlier,  I think the seed scripts should only ever run by calling an explicit `make` target, not as part of the server script. Adding seed-data is a one-time choice a contributor might make, not something that's an 'always on flag' or server-configuration feature, which is what environment files are a bit more intended for.

You already did this with `make knex-seed`, so I really think that's enough.
created_at: 2025-03-23T02:24:42Z
author_association: MEMBER
diff_hunk: @@ -5,13 +5,21 @@ import { fileURLToPath } from 'node:url';
 import './env.ts';
 
 let settings: Knex.Config | null = null;
-const db: Knex = knex(getSettings());
+const db: Knex = knex(await getSettings());
 
 export async function init() {
-
   console.info('Running database migrations');
-  await db.migrate.latest();
-
+  await db.migrate.latest()
+    .then(() => {
+      if (process.env.NODE_ENV === 'development') {
+        console.info('Running database seeds');
+        return db.seed.run();
+      }
+    })
+    .catch((error) => {
+      console.error('Migrations failed', error);
+      process.exit(1);
+    });
 }
path: src/database.ts
user: login: evert
body: This should all be reverted.
created_at: 2025-03-23T02:31:58Z
author_association: MEMBER
diff_hunk: @@ -187,6 +224,7 @@ export function getSettings(): Knex.Config {
       loadExtensions: ['.js'],
     },
     pool,
+    seeds: isDevelopment ? seedInfo : undefined,
path: src/database.ts
user: login: evert
body: It's ok for seeds to always be in here, but they shouldn't always run. So this doesn't need to be conditional.
created_at: 2025-03-23T02:32:50Z
author_association: MEMBER
diff_hunk: @@ -14,6 +14,18 @@ KEEP_ALIVE_TIMEOUT_MS=
 # functioning correctly.
 CURVEBALL_ORIGIN="http://localhost:8531"
 
+# Allow CORS from any origin by running the seed script 002_cors_allowed_origin.js
+# ie. CORS_ALLOW_ORIGIN=["http://localhost:8531", "http://localhost:3000"]
+CORS_ALLOW_ORIGIN=
+
path: .env.defaults
user: login: evert
body: Seed files should effectively behave as a second set of migration files, ran in order and maintaining consistency over time.
created_at: 2025-03-23T02:34:39Z
author_association: MEMBER
diff_hunk: @@ -14,6 +14,18 @@ KEEP_ALIVE_TIMEOUT_MS=
 # functioning correctly.
 CURVEBALL_ORIGIN="http://localhost:8531"
 
+# Allow CORS from any origin by running the seed script 002_cors_allowed_origin.js
+# ie. CORS_ALLOW_ORIGIN=["http://localhost:8531", "http://localhost:3000"]
+CORS_ALLOW_ORIGIN=
+
path: .env.defaults
user: login: usrrname
body: The addition of `CORS_ALLOW_ORIGIN` is meant to benefit both end users and contributors by allowing them to have a quick way to manipulate those values without having to directly run database code 
created_at: 2025-03-23T12:43:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -187,6 +224,7 @@ export function getSettings(): Knex.Config {
       loadExtensions: ['.js'],
     },
     pool,
+    seeds: isDevelopment ? seedInfo : undefined,
path: src/database.ts
user: login: usrrname
body: So far these are only supposed to run IF the env is development _AND_ env variables are set , so if it's not clear I need to add more doc around that
created_at: 2025-03-23T12:45:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,13 +5,21 @@ import { fileURLToPath } from 'node:url';
 import './env.ts';
 
 let settings: Knex.Config | null = null;
-const db: Knex = knex(getSettings());
+const db: Knex = knex(await getSettings());
 
 export async function init() {
-
   console.info('Running database migrations');
-  await db.migrate.latest();
-
+  await db.migrate.latest()
+    .then(() => {
+      if (process.env.NODE_ENV === 'development') {
+        console.info('Running database seeds');
+        return db.seed.run();
+      }
+    })
+    .catch((error) => {
+      console.error('Migrations failed', error);
+      process.exit(1);
+    });
 }
path: src/database.ts
user: login: usrrname
body: Ah I thought the server script command was necessary in order for this to work.

But accordingly if I'm changing the seed config in settings, it should run when the `knex seed:run ` is called I hope
created_at: 2025-03-23T13:32:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,6 +14,18 @@ KEEP_ALIVE_TIMEOUT_MS=
 # functioning correctly.
 CURVEBALL_ORIGIN="http://localhost:8531"
 
+# Allow CORS from any origin by running the seed script 002_cors_allowed_origin.js
+# ie. CORS_ALLOW_ORIGIN=["http://localhost:8531", "http://localhost:3000"]
+CORS_ALLOW_ORIGIN=
+
+# NOTE: Seed variables should only be set in development mode. 
+# Seed scripts can only run if tables don't already contain values.
+# Seed scripts WILL CLEAR EXISTING DATA.
+
+# Run user seed scripts only
+# Seed scripts WILL CLEAR EXISTING principals and principal_identities tables.
+SEED_USERS=
path: .env.defaults
user: login: usrrname
body: Putting this here with no value doesn't make it "always on" though...`envVarExists()` used in `src/database.ts` checks whether the value is null or undefined. The env var is included here so contributors and users know about what server env variables are at their disposal to set
created_at: 2025-03-23T13:33:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,13 +5,21 @@ import { fileURLToPath } from 'node:url';
 import './env.ts';
 
 let settings: Knex.Config | null = null;
-const db: Knex = knex(getSettings());
+const db: Knex = knex(await getSettings());
 
 export async function init() {
-
   console.info('Running database migrations');
-  await db.migrate.latest();
-
+  await db.migrate.latest()
+    .then(() => {
+      if (process.env.NODE_ENV === 'development') {
+        console.info('Running database seeds');
+        return db.seed.run();
+      }
+    })
+    .catch((error) => {
+      console.error('Migrations failed', error);
+      process.exit(1);
+    });
path: src/database.ts
user: login: usrrname
body: ```suggestion
```
created_at: 2025-03-23T13:35:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,6 +14,18 @@ KEEP_ALIVE_TIMEOUT_MS=
 # functioning correctly.
 CURVEBALL_ORIGIN="http://localhost:8531"
 
+# Allow CORS from any origin by running the seed script 002_cors_allowed_origin.js
+# ie. CORS_ALLOW_ORIGIN=["http://localhost:8531", "http://localhost:3000"]
+CORS_ALLOW_ORIGIN=
+
path: .env.defaults
user: login: evert
body: Yes, 100% agree. This is a useful environment variable. I mentioned I wanted to add this at our last meeting. But the problem is that you implemented it with seed files. Seed files are *not* used by end-users.


created_at: 2025-03-23T14:42:43Z
author_association: MEMBER
diff_hunk: @@ -14,6 +14,18 @@ KEEP_ALIVE_TIMEOUT_MS=
 # functioning correctly.
 CURVEBALL_ORIGIN="http://localhost:8531"
 
+# Allow CORS from any origin by running the seed script 002_cors_allowed_origin.js
+# ie. CORS_ALLOW_ORIGIN=["http://localhost:8531", "http://localhost:3000"]
+CORS_ALLOW_ORIGIN=
+
+# NOTE: Seed variables should only be set in development mode. 
+# Seed scripts can only run if tables don't already contain values.
+# Seed scripts WILL CLEAR EXISTING DATA.
+
+# Run user seed scripts only
+# Seed scripts WILL CLEAR EXISTING principals and principal_identities tables.
+SEED_USERS=
path: .env.defaults
user: login: evert
body: I meant that turning `SEED_USERS` off after the fact does not remove users. This makes it not a 'flag' but an action. 
created_at: 2025-03-23T14:43:27Z
author_association: MEMBER
diff_hunk: @@ -187,6 +224,7 @@ export function getSettings(): Knex.Config {
       loadExtensions: ['.js'],
     },
     pool,
+    seeds: isDevelopment ? seedInfo : undefined,
path: src/database.ts
user: login: evert
body: Seed files should not automatically run, so this conditional is not useful. Keeping `seeds` always set here should not have an effect whether we're in dev or production mode.
created_at: 2025-03-23T14:44:36Z
author_association: MEMBER
diff_hunk: @@ -14,6 +14,18 @@ KEEP_ALIVE_TIMEOUT_MS=
 # functioning correctly.
 CURVEBALL_ORIGIN="http://localhost:8531"
 
+# Allow CORS from any origin by running the seed script 002_cors_allowed_origin.js
+# ie. CORS_ALLOW_ORIGIN=["http://localhost:8531", "http://localhost:3000"]
+CORS_ALLOW_ORIGIN=
+
+# NOTE: Seed variables should only be set in development mode. 
+# Seed scripts can only run if tables don't already contain values.
+# Seed scripts WILL CLEAR EXISTING DATA.
+
+# Run user seed scripts only
+# Seed scripts WILL CLEAR EXISTING principals and principal_identities tables.
+SEED_USERS=
path: .env.defaults
user: login: usrrname
body: Oooohhh! great point about flag vs action
created_at: 2025-03-23T14:45:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,6 +14,18 @@ KEEP_ALIVE_TIMEOUT_MS=
 # functioning correctly.
 CURVEBALL_ORIGIN="http://localhost:8531"
 
+# Allow CORS from any origin by running the seed script 002_cors_allowed_origin.js
+# ie. CORS_ALLOW_ORIGIN=["http://localhost:8531", "http://localhost:3000"]
+CORS_ALLOW_ORIGIN=
+
path: .env.defaults
user: login: usrrname
body: So what may be the alternative or better practice?
created_at: 2025-03-23T14:48:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,6 +14,18 @@ KEEP_ALIVE_TIMEOUT_MS=
 # functioning correctly.
 CURVEBALL_ORIGIN="http://localhost:8531"
 
+# Allow CORS from any origin by running the seed script 002_cors_allowed_origin.js
+# ie. CORS_ALLOW_ORIGIN=["http://localhost:8531", "http://localhost:3000"]
+CORS_ALLOW_ORIGIN=
+
path: .env.defaults
user: login: usrrname
body: >  built-in facility for loading config from .env, but it looks like you partially did this?

Like... `src/env.ts`? Or `src/main-mw.ts`?
created_at: 2025-03-23T14:56:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,6 +14,18 @@ KEEP_ALIVE_TIMEOUT_MS=
 # functioning correctly.
 CURVEBALL_ORIGIN="http://localhost:8531"
 
+# Allow CORS from any origin by running the seed script 002_cors_allowed_origin.js
+# ie. CORS_ALLOW_ORIGIN=["http://localhost:8531", "http://localhost:3000"]
+CORS_ALLOW_ORIGIN=
+
path: .env.defaults
user: login: usrrname
body: Gonna leave CORS_ALLOW_ORIGIN for a differnet PR
created_at: 2025-03-23T15:10:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,13 +5,21 @@ import { fileURLToPath } from 'node:url';
 import './env.ts';
 
 let settings: Knex.Config | null = null;
-const db: Knex = knex(getSettings());
+const db: Knex = knex(await getSettings());
 
 export async function init() {
-
   console.info('Running database migrations');
-  await db.migrate.latest();
-
+  await db.migrate.latest()
+    .then(() => {
+      if (process.env.NODE_ENV === 'development') {
+        console.info('Running database seeds');
+        return db.seed.run();
+      }
+    })
+    .catch((error) => {
+      console.error('Migrations failed', error);
+      process.exit(1);
+    });
 }
path: src/database.ts
user: login: evert
body: Yes it should. `knex seed:run` should run all the seed files unconditionally. I think seed files are very useful to get a whole bunch of test data, but there really should be no need for any configuration. This PR just needs to be simplified a bit
created_at: 2025-03-23T15:13:33Z
author_association: MEMBER
diff_hunk: @@ -14,6 +14,15 @@ KEEP_ALIVE_TIMEOUT_MS=
 # functioning correctly.
 CURVEBALL_ORIGIN="http://localhost:8531"
 
+# NOTE: Seed variables should only be set in development mode. 
+# Seed scripts can only run if tables don't already contain values.
+# Seed scripts MAY CLEAR EXISTING DATA.
+
+# Run user seed scripts only
+# Seed users script will not run if there are existing duplicate users in the database.
+# Note: if you set this, removing this flag will not remove existing users.
+# SEED_USERS=
+
path: .env.defaults
user: login: evert
body: Should still be reverted! 
created_at: 2025-03-23T16:14:50Z
author_association: MEMBER
diff_hunk: @@ -68,6 +68,8 @@ Everything prepended with `_` is either a hidden file or a build artifact.
 └── tsconfig.json
 ```
 
+Note: The `MAKEFILE` contains all commands for codebase operations, while the `package.json` scripts contains basic commands for startup and build. 
path: .github/CONTRIBUTING.md
user: login: evert
body: If you're missing anything in `Makefile` that exists in `package.json`, we should add these in `Makefile`. Contributers shouldn't have to look in 2 places for this i think.


created_at: 2025-03-23T16:16:04Z
author_association: MEMBER
diff_hunk: @@ -86,6 +88,13 @@ node ./bin/generate-db-types.mjs
 
 This will generate a `src/db-types.ts` file that will be used by Knex to type the database tables.
 
+## Seed Users
+
+For ease of contribution or bootstrapping client integrations, seeding of dummy users was included and can be enabled if env variable for `SEED_USERS=true` in `.env`
path: .github/CONTRIBUTING.md
user: login: evert
body: ```suggestion
To quickly get some test data set up, we've inluded database seed files. To insert this data, run `make knex-seed`. This should only be done after onboarding has been completed.
```
created_at: 2025-03-23T16:17:49Z
author_association: MEMBER
diff_hunk: @@ -173,10 +180,26 @@ export function getSettings(): Knex.Config {
       default:
         throw new Error(`Unknown value for DB_DRIVER: ${process.env.DB_DRIVER}`);
 
-
     }
 
   }
+  const isSeedUsersEnabled = !!process.env.SEED_USERS;
+  const seedDirectory = path.dirname(fileURLToPath(import.meta.url)) + '/seeds';
+
+  /** Knex Seed API @link {https://knexjs.org/guide/migrations.html#seed-api} */
+  let seedConfig: Knex.SeederConfig | undefined = {
+    directory: '',
+    loadExtensions: ['.js'],
+  };
+
+  if (isSeedUsersEnabled) {
+    seedConfig.directory = seedDirectory + '/users';
+    seedConfig.specific = '001_users.js';
+  } else {
+    // no env variable set, no seeding needed
+    seedConfig = undefined;
+  }
+
path: src/database.ts
user: login: evert
body: ```suggestion
  const seedDirectory = path.dirname(fileURLToPath(import.meta.url)) + '/seeds';

  /** Knex Seed API @link {https://knexjs.org/guide/migrations.html#seed-api} */
  const seedConfig: Knex.SeederConfig | undefined = {
    directory: seedDirectory,
    loadExtensions: ['.js'],
  };
```
created_at: 2025-03-23T16:19:54Z
author_association: MEMBER
diff_hunk: @@ -187,6 +210,7 @@ export function getSettings(): Knex.Config {
       loadExtensions: ['.js'],
     },
     pool,
+    seeds: seedConfig ?? {},
path: src/database.ts
user: login: evert
body: ```suggestion
    seeds: seedConfig,
```
created_at: 2025-03-23T16:20:03Z
author_association: MEMBER
diff_hunk: @@ -67,8 +65,17 @@ export async function insertAndGetId<T extends Record<string, any>> (
 
 }
 
+/**
+ * Checks if an environment variable exists and is not empty.
+ * @param key - The key of the environment variable to check.
+ * @returns `true` if the environment variable exists and is not empty, `false` otherwise.
+ */
+export function envVarExists(key: string): boolean {
+  const value = process.env[key];
+  return value !== undefined && value !== '';
+};
path: src/database.ts
user: login: evert
body: This is not used
created_at: 2025-03-23T16:20:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,82 @@
+import * as bcrypt from 'bcrypt';
+import { randomUUID } from 'crypto';
+import { Knex } from 'knex';
+
+const initialUsers = [
+  {
+    nickname: 'admin',
+    given_name: 'Admin',
+    family_name: 'User',
+    privilege: 'admin',
+    resource: '*',
+    scope: '*',
+    external_id: randomUUID().slice(0, 6),
path: src/seeds/users/001_users.ts
user: login: evert
body: Any chance we could use the same algorithm for these as the actual server also uses? 

https://github.com/curveball/a12n-server/blob/main/src/crypto.ts#L29
created_at: 2025-03-23T16:21:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,82 @@
+import * as bcrypt from 'bcrypt';
+import { randomUUID } from 'crypto';
+import { Knex } from 'knex';
+
+const initialUsers = [
+  {
+    nickname: 'admin',
+    given_name: 'Admin',
+    family_name: 'User',
+    privilege: 'admin',
+    resource: '*',
+    scope: '*',
path: src/seeds/users/001_users.ts
user: login: evert
body: I don't think these 2 are used.
created_at: 2025-03-23T16:22:20Z
author_association: MEMBER
diff_hunk: @@ -68,6 +68,8 @@ Everything prepended with `_` is either a hidden file or a build artifact.
 └── tsconfig.json
 ```
 
+Note: The `MAKEFILE` contains all commands for codebase operations, while the `package.json` scripts contains basic commands for startup and build. 
path: .github/CONTRIBUTING.md
user: login: evert
body: ```suggestion
Note: The `Makefile` contains all commands for codebase operations, while the `package.json` scripts contains basic commands for startup and build. 
```
created_at: 2025-03-23T16:24:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,82 @@
+import * as bcrypt from 'bcrypt';
+import { randomUUID } from 'crypto';
+import { Knex } from 'knex';
+
+const initialUsers = [
+  {
+    nickname: 'admin',
+    given_name: 'Admin',
+    family_name: 'User',
+    privilege: 'admin',
+    resource: '*',
+    scope: '*',
path: src/seeds/users/001_users.ts
user: login: usrrname
body: I was copying what came in through the initial admin registration 😜
created_at: 2025-03-23T19:26:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,4 +27,5 @@ To store a string, it needs to be surrounded by double-quotes. For example, a le
 | `webauthn`                  | String  | `enabled` | Whether webauthn is "enabled", "disabled" or "required".'
 | `webauthn.serviceName`      | String  | `a12n-server` | The service name that should appear in Webauthn dialogs.
 | `webauthn.expectedOrigin`   | String  | `null` | The "origin" of this server. This must be set for webauthn to work
-| `webauthn.relyingPartyId`   | String  | `null` | The origin of the application performing the login.
\ No newline at end of file
+| `webauthn.relyingPartyId`   | String  | `null` | The origin of the application performing the login.
+|`seed.users`| boolean | `null` | Whether to populate local database with dummy users | `SEED_USERS`
path: docs/server-settings.md
user: login: evert
body: This one is still left over ! I appreciate the fixes for the other missing env variables though!
created_at: 2025-03-23T22:44:03Z
author_association: MEMBER
diff_hunk: @@ -67,8 +65,17 @@ export async function insertAndGetId<T extends Record<string, any>> (
 
 }
 
+/**
+ * Checks if an environment variable exists and is not empty.
+ * @param key - The key of the environment variable to check.
+ * @returns `true` if the environment variable exists and is not empty, `false` otherwise.
+ */
+export function envVarExists(key: string): boolean {
path: src/database.ts
user: login: evert
body: Should this still be in there?
created_at: 2025-03-23T22:44:56Z
author_association: MEMBER
diff_hunk: @@ -34,6 +34,9 @@ knex-migrate: dist/build
 knex-make-migration:
 	cd src; npx knex migrate:make migration_name -x ts
 
+knex-seed:
path: Makefile
user: login: evert
body: ```suggestion
knex-seed: build
```

This ensures that "build" runs before "knex-seed" (or skips it if it's not outdated)
created_at: 2025-03-24T01:01:00Z
author_association: MEMBER
diff_hunk: @@ -12,6 +12,9 @@ jobs:
   build:
     name: Build
     runs-on: ubuntu-latest
+    strategy:
path: .github/workflows/test.yml
user: login: evert
body: Hey is there any reason you added all these?

This increases the number of github jobs for any PR to 24, which is quite large and can take a long time to complete. It's the most complete way to to test, but unittests themselves and the typescript build does already run on these 3 versions.
created_at: 2025-05-11T23:38:26Z
author_association: MEMBER
diff_hunk: @@ -103,10 +124,20 @@ jobs:
         PG_DATABASE: a12nserver
         PG_HOST: 127.0.0.1
       run: make knex-migrate
+    - name: Test seed migrations
+      env:
path: .github/workflows/test.yml
user: login: evert
body: I noticed these environment variables are duplicated. Is there a way these can be specified in a way that they are re-used across steps? Maybe `env` can be specified on the job level? 
created_at: 2025-05-11T23:39:29Z
author_association: MEMBER
diff_hunk: @@ -12,6 +12,9 @@ jobs:
   build:
     name: Build
     runs-on: ubuntu-latest
+    strategy:
path: .github/workflows/test.yml
user: login: usrrname
body: I dont remember why I did...  perhaps it was a debugging step... I can remove them
created_at: 2025-05-12T01:07:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,6 +12,9 @@ jobs:
   build:
     name: Build
     runs-on: ubuntu-latest
+    strategy:
path: .github/workflows/test.yml
user: login: usrrname
body: Dropped the commit
created_at: 2025-05-12T01:26:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,10 +124,20 @@ jobs:
         PG_DATABASE: a12nserver
         PG_HOST: 127.0.0.1
       run: make knex-migrate
+    - name: Test seed migrations
+      env:
path: .github/workflows/test.yml
user: login: usrrname
body: I think they might be needed on every one of these, seems like I borked it by refactoring them onto the parent job
created_at: 2025-05-12T01:43:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,10 +124,20 @@ jobs:
         PG_DATABASE: a12nserver
         PG_HOST: 127.0.0.1
       run: make knex-migrate
+    - name: Test seed migrations
+      env:
path: .github/workflows/test.yml
user: login: evert
body: Ah sorry, I thought maybe there was a way to de-duplicate these but it's a tiny comment I don't care. 
created_at: 2025-05-12T14:27:39Z
author_association: MEMBER
diff_hunk: @@ -6,7 +6,9 @@ on:
   push:
     branches: [ main ]
   pull_request:
- 
+permissions:
+  contents: read
+  checks: write
path: .github/workflows/test.yml
user: login: usrrname
body: Added due to seeing this CodeQL warning 

<img width="720" alt="Screenshot 2025-05-19 at 8 44 22 PM" src="https://github.com/user-attachments/assets/39fa4efc-cbc6-403d-887a-d0404eaa0448" />

created_at: 2025-05-20T00:45:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import * as bcrypt from 'bcrypt';
+import { generatePublicId } from '../crypto.ts';
+import { Knex } from 'knex';
+import { insertAndGetId } from '../database.ts';
+const initialUsers = [
+  {
+    nickname: 'admin',
+    given_name: 'Admin',
+    family_name: 'User',
+    privilege: 'admin',
+    resource: '*',
+    scope: '*',
+    external_id: await generatePublicId(),
+  },
+  {
+    nickname: 'apple',
+    given_name: 'Apple',
+    family_name: 'Cake',
+    external_id: await generatePublicId(),
+  },
+  {
+    nickname: 'banana',
+    given_name: 'Banana',
+    family_name: 'Bread',
+    external_id: await generatePublicId(),
+  },
+  {
+    nickname: 'cherry',
+    given_name: 'Cherry',
+    family_name: 'Tart',
+    external_id: await generatePublicId(),
+  }
+] as const;
+
+export async function seed(knex: Knex): Promise<void> {
+  console.info('Seeding users...');
+
+  for (const user of initialUsers) {
+    // Insert principal and get the generated ID
+    const principalId = await insertAndGetId('principals', {
+      active: 1,
+      nickname: user.nickname,
+      type: 1,
+      external_id: user.external_id,
+      created_at: new Date().getTime(),
+      modified_at: new Date().getTime(),
+    });
+
+    await knex('principal_identities').insert({
+      principal_id: principalId,
+      external_id: user.external_id,
+      uri: `mailto:${user.nickname}@example.com`,
+      is_primary: 1,
+      created_at: new Date().getTime(),
+      modified_at: new Date().getTime(),
+      verified_at: new Date().getTime(),
+    });
+
+    // Use the principal_id for related tables
+    await knex('user_info').insert({
+      principal_id: principalId,
+      name: `${user.given_name} ${user.family_name}`,
+      given_name: user.given_name,
+      family_name: user.family_name,
+      locale: 'en-US',
+    });
+
+    await knex('user_passwords').insert({
+      user_id: principalId,
+      password: await bcrypt.hash('password123', 12)
+    });
+  }
+  // Add admin privilege to admin user
+  await knex('user_privileges').insert({
+    user_id: 2,
+    privilege: 'admin',
+    resource: '*',
+  });
path: src/seeds/001_users.ts
user: login: Copilot
body: Hard-coding `user_id: 2` for the admin privilege will likely assign it to the wrong user if IDs change. Instead capture the `principalId` returned for the admin user or query by nickname.
```suggestion
  if (adminPrincipalId !== null) {
    await knex('user_privileges').insert({
      user_id: adminPrincipalId,
      privilege: 'admin',
      resource: '*',
    });
  } else {
    throw new Error('Admin user not found during seeding.');
  }
```
created_at: 2025-05-22T01:22:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,6 +34,9 @@ knex-migrate: dist/build
 knex-make-migration:
 	cd src; npx knex migrate:make migration_name -x ts
 
+knex-seed: build
path: Makefile
user: login: Copilot
body: [nitpick] All other database targets depend on `dist/build`, but this uses `build`. Consider aligning it with the rest (`knex-seed: dist/build`) for consistency.
```suggestion
knex-seed: dist/build
```
created_at: 2025-05-22T01:22:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -86,6 +88,13 @@ node ./bin/generate-db-types.mjs
 
 This will generate a `src/db-types.ts` file that will be used by Knex to type the database tables.
 
+## Seed Users
+
+To quickly get some test data set up, we've inluded database seed files. To insert this data, run `make knex-seed`. This should only be done after onboarding has been completed.
path: .github/CONTRIBUTING.md
user: login: Copilot
body: There's a typo: `inluded` should be `included`.
```suggestion
To quickly get some test data set up, we've included database seed files. To insert this data, run `make knex-seed`. This should only be done after onboarding has been completed.
```
created_at: 2025-05-22T01:22:11Z
author_association: CONTRIBUTOR

PR comments:
user: login: evert
created_at: 2025-03-15T00:57:16Z
body: Was this tested if you got an error? 
author_association: MEMBER
user: login: usrrname
created_at: 2025-03-16T05:05:15Z
body: > Was this tested if you got an error?

Yep... working through these... tons of ` null value in column "field" of relation "table-name" violates not-null constraint`

author_association: CONTRIBUTOR
user: login: evert
created_at: 2025-03-17T03:44:16Z
body: > Last night I did get something going in, but it was still breaking

I noticed this was force-pushed after. Was this still an issue? Don't 100% understand how that could happen!
author_association: MEMBER
user: login: usrrname
created_at: 2025-03-17T23:21:39Z
body: The force-push was a result of me making changes, squashing a commit and pushing it up :p 
author_association: CONTRIBUTOR
user: login: usrrname
created_at: 2025-03-17T23:29:58Z
body: Rebased... and still err-ing: 

of note: `PostgreSQL Database directory appears to contain a database; Skipping initialization` 
Seems like if there was already some data on a mounted volume, the migrations wont run.
<details><summary>Full Trace</summary>  

```
Attaching to a12n-server-1, db-1
db-1           | 
db-1           | PostgreSQL Database directory appears to contain a database; Skipping initialization
db-1           | 
db-1           | 2025-03-17 23:20:34.425 UTC [1] LOG:  starting PostgreSQL 17.2 (Debian 17.2-1.pgdg120+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
db-1           | 2025-03-17 23:20:34.425 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
db-1           | 2025-03-17 23:20:34.425 UTC [1] LOG:  listening on IPv6 address "::", port 5432
db-1           | 2025-03-17 23:20:34.426 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
db-1           | 2025-03-17 23:20:34.429 UTC [29] LOG:  database system was shut down at 2025-03-17 23:19:02 UTC
db-1           | 2025-03-17 23:20:34.434 UTC [1] LOG:  database system is ready to accept connections
a12n-server-1  | ⚾ @curveball/a12n-server 0.30.0
a12n-server-1  | PUBLIC_URI environment variable was not set, defaulting to http://localhost:8531/
a12n-server-1  | Running database migrations
a12n-server-1  | Running database seeds
db-1           | 2025-03-17 23:20:35.294 UTC [33] ERROR:  column "principal_id" of relation "user_info" does not exist at character 73
db-1           | 2025-03-17 23:20:35.294 UTC [33] STATEMENT:  insert into "user_info" ("family_name", "given_name", "locale", "name", "principal_id") values ($1, $2, $3, $4, $5)
a12n-server-1  | Migrations failed Error: Error while executing "/opt/app/dist/seeds/001_users.js" seed: insert into "user_info" ("family_name", "given_name", "locale", "name", "principal_id") values ($1, $2, $3, $4, $5) - column "principal_id" of relation "user_info" does not exist
a12n-server-1  |     at Seeder._waterfallBatch (/opt/app/node_modules/knex/lib/migrations/seed/Seeder.js:118:23)
a12n-server-1  | error: insert into "user_info" ("family_name", "given_name", "locale", "name", "principal_id") values ($1, $2, $3, $4, $5) - column "principal_id" of relation "user_info" does not exist
a12n-server-1  |     at Parser.parseErrorMessage (/opt/app/node_modules/pg-protocol/dist/parser.js:283:98)
a12n-server-1  |     at Parser.handlePacket (/opt/app/node_modules/pg-protocol/dist/parser.js:122:29)
a12n-server-1  |     at Parser.parse (/opt/app/node_modules/pg-protocol/dist/parser.js:35:38)
a12n-server-1  |     at Socket.<anonymous> (/opt/app/node_modules/pg-protocol/dist/index.js:11:42)
a12n-server-1  |     at Socket.emit (node:events:524:28)
a12n-server-1  |     at addChunk (node:internal/streams/readable:561:12)
a12n-server-1  |     at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
a12n-server-1  |     at Readable.push (node:internal/streams/readable:392:5)
a12n-server-1  |     at TCP.onStreamRead (node:internal/stream_base_commons:191:23) {
a12n-server-1  |   original: error: insert into "user_info" ("family_name", "given_name", "locale", "name", "principal_id") values ($1, $2, $3, $4, $5) - column "principal_id" of relation "user_info" does not exist
a12n-server-1  |       at Parser.parseErrorMessage (/opt/app/node_modules/pg-protocol/dist/parser.js:283:98)
a12n-server-1  |       at Parser.handlePacket (/opt/app/node_modules/pg-protocol/dist/parser.js:122:29)
a12n-server-1  |       at Parser.parse (/opt/app/node_modules/pg-protocol/dist/parser.js:35:38)
a12n-server-1  |       at Socket.<anonymous> (/opt/app/node_modules/pg-protocol/dist/index.js:11:42)
a12n-server-1  |       at Socket.emit (node:events:524:28)
a12n-server-1  |       at addChunk (node:internal/streams/readable:561:12)
a12n-server-1  |       at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
a12n-server-1  |       at Readable.push (node:internal/streams/readable:392:5)
a12n-server-1  |       at TCP.onStreamRead (node:internal/stream_base_commons:191:23) {
a12n-server-1  |     length: 134,
a12n-server-1  |     severity: 'ERROR',
a12n-server-1  |     code: '42703',
a12n-server-1  |     detail: undefined,
a12n-server-1  |     hint: undefined,
a12n-server-1  |     position: '73',
a12n-server-1  |     internalPosition: undefined,
a12n-server-1  |     internalQuery: undefined,
a12n-server-1  |     where: undefined,
a12n-server-1  |     schema: undefined,
a12n-server-1  |     table: undefined,
a12n-server-1  |     column: undefined,
a12n-server-1  |     dataType: undefined,
a12n-server-1  |     constraint: undefined,
a12n-server-1  |     file: 'parse_target.c',
a12n-server-1  |     line: '1065',
a12n-server-1  |     routine: 'checkInsertTargets'
a12n-server-1  |   }
a12n-server-1  | }
a12n-server-1 exited with code 1
Gracefully stopping... (press Ctrl+C again to force)
```

</details>

author_association: CONTRIBUTOR
user: login: usrrname
created_at: 2025-03-22T18:56:07Z
body: Now.. it seems I can set `seed.user` / `process.env.SEED_USER` or `cors.allowOrigin`/`process.env.CORS_ALLOW_ORIGIN` in `.env` independently. However, if I set both, I'll get this error when I try to login with the admin user 

<img width="1006" alt="Screenshot 2025-03-22 at 2 53 47 PM" src="https://github.com/user-attachments/assets/52d24d2b-ea32-44f3-99b8-a10cfb6da4b2" />


author_association: CONTRIBUTOR
user: login: evert
created_at: 2025-03-23T22:45:22Z
body: Lgtm!
author_association: MEMBER
user: login: usrrname
created_at: 2025-03-23T23:50:46Z
body: Does not look good to me, but I had to go cook dinner LOL
couple loose ends... I was part way through writing feedback on the UofT code too
author_association: CONTRIBUTOR
user: login: usrrname
created_at: 2025-03-24T21:56:56Z
body: LOL WHY SQLITE WHY https://github.com/curveball/a12n-server/actions/runs/14046723175/job/39329127972
author_association: CONTRIBUTOR
user: login: evert
created_at: 2025-04-05T01:32:13Z
body: I think the most likely problem with that error is that maybe it tries to use a different database. If you connect with sqlite to a non-existing file, it just creates the database with 0 tables on the spot.

So might be another relative path issue. :')
author_association: MEMBER
user: login: usrrname
created_at: 2025-05-10T21:05:42Z
body: FINALLY 🟢 🥗 🚥 📗 🍏 
author_association: CONTRIBUTOR
user: login: usrrname
created_at: 2025-05-10T21:13:34Z
body: This passed and all green, but unable to merge because: 

<img width="851" alt="Screenshot 2025-05-10 at 5 13 20 PM" src="https://github.com/user-attachments/assets/a18d73a6-bf15-4453-82d1-fec5aa334e14" />

author_association: CONTRIBUTOR
user: login: evert
created_at: 2025-05-11T23:47:49Z
body: This should definitely be a squashed merge btw !
author_association: MEMBER
user: login: usrrname
created_at: 2025-05-14T17:32:07Z
body: <img width="878" alt="Screenshot 2025-05-14 at 1 31 58 PM" src="https://github.com/user-attachments/assets/ccee74a4-068f-4375-a938-2d9f23935fbb" />
Not sure what's going on here 
author_association: CONTRIBUTOR
user: login: evert
created_at: 2025-05-14T19:16:42Z
body: I think it's because the .yml is invalid :')
author_association: MEMBER
user: login: usrrname
created_at: 2025-05-15T01:45:23Z
body: Ohhh yeah I remember why I added the extra `strategy` with node versions now!

<img width="1464" alt="Screenshot 2025-05-14 at 9 44 54 PM" src="https://github.com/user-attachments/assets/59640f2b-66bf-44c3-ae2e-c9cd05c33e24" />


author_association: CONTRIBUTOR
user: login: usrrname
created_at: 2025-05-21T15:22:58Z
body: <img width="901" alt="Screenshot 2025-05-21 at 11 22 37 AM" src="https://github.com/user-attachments/assets/739fd5aa-c530-444d-903f-735c1c46ba8c" />
I think this one is still there because that is what's on the `test.yml` on `main`
author_association: CONTRIBUTOR