diff --git a/DEPLOY.md b/DEPLOY.md
index 4e8d26d..500ab53 100644
--- a/DEPLOY.md
+++ b/DEPLOY.md
@@ -1,6 +1,7 @@
# Deployment
[`.env` Example](example.env) ·
+[Configuration Parameters](env.md) ·
[GHCR Docker Image](https://github.com/wuespace/deno-asn-generator/pkgs/container/deno-asn-generator)
· Docker Hub Docker Image (coming soon) ·
[JSR Package](https://jsr.io/@wuespace/asn-generator) ·
diff --git a/README.md b/README.md
index 82b92eb..0e7ad22 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,9 @@
[![Deno CI](https://github.com/wuespace/deno-asn-generator/actions/workflows/deno-ci.yml/badge.svg)](https://github.com/wuespace/deno-asn-generator/actions/workflows/deno-ci.yml)
[![Docker](https://github.com/wuespace/deno-asn-generator/actions/workflows/docker-publish.yml/badge.svg)](https://github.com/wuespace/deno-asn-generator/actions/workflows/docker-publish.yml)
-[Deployment](DEPLOY.md) · [License (MIT)](./LICENSE) ·
+[Deployment](DEPLOY.md) ·
+[Configuration](env.md) ·
+[License (MIT)](./LICENSE) ·
[Contributing](./CONTRIBUTING.md)
---
diff --git a/compose.yml b/compose.yml
index 0a92ef4..0a821d9 100644
--- a/compose.yml
+++ b/compose.yml
@@ -1,16 +1,36 @@
name: deno-asn-generator
services:
- app:
- image: asngenerator
+ asn-generator:
+ image: ghcr.io/wuespace/deno-asn-generator
build:
context: .
dockerfile: Dockerfile
ports:
- - "127.0.0.1:8080:8080"
- env_file:
- - .env
+ - "127.0.0.1:8080:${PORT:-8080}"
+ volumes:
+ - asn-generator-data:/app/data
environment:
- ASN_NAMESPACE_RANGE: 50
- ASN_PREFIX: WBD
- ASN_BARCODE_TYPE: CODE128
\ No newline at end of file
+ - PORT=${PORT:-8080}
+ - ASN_PREFIX
+ - ASN_NAMESPACE_RANGE
+ - ASN_ENABLE_NAMESPACE_EXTENSION
+ - ADDITIONAL_MANAGED_NAMESPACES
+ - ASN_LOOKUP_URL
+ - ASN_LOOKUP_INCLUDE_PREFIX
+ - ASN_BARCODE_TYPE
+ - DATA_DIR
+ - DB_FILE_NAME
+ - DENO_KV_ACCESS_TOKEN
+ - OIDC_ISSUER
+ - OIDC_AUTH_SECRET
+ - OIDC_CLIENT_ID
+ - OIDC_CLIENT_SECRET
+ - OIDC_REDIRECT_URI
+ - OIDC_SCOPES
+ - OIDC_UID_CLAIM
+ - OIDC_NAME_CLAIM
+ - OIDC_ROLES_CLAIM
+
+volumes:
+ asn-generator-data:
\ No newline at end of file
diff --git a/deno.json b/deno.json
index 9fbcf3b..08220ea 100644
--- a/deno.json
+++ b/deno.json
@@ -13,7 +13,10 @@
"test": "DATA_DIR=data-test ASN_PREFIX=ASN ASN_NAMESPACE_RANGE=50 ASN_ENABLE_NAMESPACE_EXTANSION=1 deno test -A",
"docs": "deno doc --reload --html main.ts",
"lint": "DENO_FUTURE=1 deno lint && deno doc --lint main.ts lib/*/mod.ts",
- "check": "deno check main.ts lib/**/*.ts"
+ "check": "deno check main.ts lib/**/*.ts",
+ "env:all": "deno task env:example && deno task env:docs",
+ "env:example": "deno run --allow-read --allow-write jsr:@wuespace/envardoc@0.4.3 example -r example.env",
+ "env:docs": "deno run --allow-read --allow-write jsr:@wuespace/envardoc@0.4.3 docs -o env.md example.env"
},
"imports": {
"$/": "./",
@@ -27,23 +30,15 @@
"@std/assert": "jsr:@std/assert@^1.0.4",
"@std/cli": "jsr:@std/cli@^1.0.5",
"@std/datetime": "jsr:@std/datetime@^0.225.2",
- "@std/dotenv": "jsr:@std/dotenv@^0.225.2"
+ "@std/dotenv": "jsr:@std/dotenv@^0.225.2",
+ "@wuespace/envar": "jsr:@wuespace/envar@^1.1.1"
},
- "unstable": [
- "kv"
- ],
+ "unstable": ["kv"],
"compilerOptions": {
"jsx": "precompile",
"jsxImportSource": "@hono/hono/jsx"
},
"lint": {
- "exclude": [
- "deps/",
- "dist/",
- "docs/",
- "node_modules/",
- "vendor/",
- "data/"
- ]
+ "exclude": ["deps/", "dist/", "docs/", "node_modules/", "vendor/", "data/"]
}
}
diff --git a/deno.lock b/deno.lock
index 00d597b..d7b0cd5 100644
--- a/deno.lock
+++ b/deno.lock
@@ -5,7 +5,12 @@
"jsr:@std/assert@^1.0.4": "1.0.4",
"jsr:@std/cli@^1.0.5": "1.0.5",
"jsr:@std/dotenv@~0.225.2": "0.225.2",
+ "jsr:@std/fmt@^1.0.3": "1.0.3",
+ "jsr:@std/fs@^1.0.6": "1.0.6",
"jsr:@std/internal@^1.0.3": "1.0.3",
+ "jsr:@std/io@0.225": "0.225.0",
+ "jsr:@std/log@~0.224.9": "0.224.11",
+ "jsr:@wuespace/envar@^1.1.1": "1.1.1",
"npm:@hono/oidc-auth@^1.2.0": "1.2.0_hono@4.6.9",
"npm:@types/node@*": "18.16.19",
"npm:bwip-js@^4.5.1": "4.5.1",
@@ -27,8 +32,31 @@
"@std/dotenv@0.225.2": {
"integrity": "e2025dce4de6c7bca21dece8baddd4262b09d5187217e231b033e088e0c4dd23"
},
+ "@std/fmt@1.0.3": {
+ "integrity": "97765c16aa32245ff4e2204ecf7d8562496a3cb8592340a80e7e554e0bb9149f"
+ },
+ "@std/fs@1.0.6": {
+ "integrity": "42b56e1e41b75583a21d5a37f6a6a27de9f510bcd36c0c85791d685ca0b85fa2"
+ },
"@std/internal@1.0.3": {
"integrity": "208e9b94a3d5649bd880e9ca38b885ab7651ab5b5303a56ed25de4755fb7b11e"
+ },
+ "@std/io@0.225.0": {
+ "integrity": "c1db7c5e5a231629b32d64b9a53139445b2ca640d828c26bf23e1c55f8c079b3"
+ },
+ "@std/log@0.224.11": {
+ "integrity": "df5e5a6d6ab8bcea016a17982cd2435f65234d6618bf631925587c0b2eae2a4e",
+ "dependencies": [
+ "jsr:@std/fmt",
+ "jsr:@std/fs",
+ "jsr:@std/io"
+ ]
+ },
+ "@wuespace/envar@1.1.1": {
+ "integrity": "0d21905ac25f2db6f2dc6bd7e0154d5c4db90157327880eed28d007a06c69426",
+ "dependencies": [
+ "jsr:@std/log"
+ ]
}
},
"npm": {
@@ -62,6 +90,7 @@
"jsr:@std/cli@^1.0.5",
"jsr:@std/datetime@~0.225.2",
"jsr:@std/dotenv@~0.225.2",
+ "jsr:@wuespace/envar@^1.1.1",
"npm:@hono/oidc-auth@^1.2.0",
"npm:bwip-js@^4.5.1",
"npm:zod@^3.23.8"
diff --git a/env.md b/env.md
new file mode 100644
index 0000000..e4706db
--- /dev/null
+++ b/env.md
@@ -0,0 +1,327 @@
+# Configuration Environment Variables
+
+Note that for every parameter, you can also set "[PARAMETER]_FILE"
+to a file that contains the value. This is especially useful for things
+like mounted secrets in Docker Swarm or Kubernetes.
+
+## ASN Generation Settings
+
+### `ADDITIONAL_MANAGED_NAMESPACES` (optional)
+
+Additional namespaces managed by the system outside of the ASN_NAMESPACE_RANGE.
+
+Namespaces are notated as "<Namespace Label><Namespace Label>..." where:
+- Namespace is the numeric ID of the namespace.
+- Label is the label for the namespace.
+Optionally, commas and spaces can be used to separate namespaces.
+
+If empty, no additional namespaces are managed and only the ASN_NAMESPACE_RANGE is used.
+
+#### Default Value (used by the application if not provided)
+
+```env
+ADDITIONAL_MANAGED_NAMESPACES=
+```
+
+#### Other Examples
+
+```env
+ADDITIONAL_MANAGED_NAMESPACES=<700 NDA-Covered Documents (Generic)><800 Personal Data Documents (Generic)>
+```
+
+### `ASN_BARCODE_TYPE` (optional)
+
+The type of barcode to generate for the ASN.
+
+#### Default Value (used by the application if not provided)
+
+```env
+ASN_BARCODE_TYPE=CODE128
+```
+
+#### Other Examples
+
+```env
+ASN_BARCODE_TYPE=CODE39
+```
+
+```env
+ASN_BARCODE_TYPE=CODE93
+```
+
+### `ASN_ENABLE_NAMESPACE_EXTENSION` (optional)
+
+Enable namespace extension. If true, the ADDITIONAL_MANAGED_NAMESPACES can have more digits than
+the ASN_NAMESPACE_RANGE.
+If false, the ADDITIONAL_MANAGED_NAMESPACES must have the same number of digits as the ASN_NAMESPACE_RANGE.
+
+This works by reserving leading `9`s for namespace extension:
+For example, let's say the ASN_NAMESPACE_RANGE is 60. Therefore, without the extension, our ADDITIONAL_MANAGED_NAMESPACES
+could only be 6X-9X, meaning we only have 39 available namespaces.
+With the extension, in the two-digit namespace range, we actually lose 9X (leaving 6X-8X). However, leading 9s
+expand the namespace by another digit. This can also be chained, giving us theoretically infinite additional namespaces:
+6X-8X, 90X-98X, 990X-998X, 9990X-9998X, etc.
+
+Note that behind the leading 9s, the namespace must still be the same number of digits as the ASN_NAMESPACE_RANGE.
+
+#### Default Value (used by the application if not provided)
+
+```env
+ASN_ENABLE_NAMESPACE_EXTENSION=false
+```
+
+#### Other Examples
+
+```env
+ASN_ENABLE_NAMESPACE_EXTENSION=true
+```
+
+### `ASN_NAMESPACE_RANGE`
+
+The namespace range. The number of digits must not change after the first run.
+For example, if the range is 600, auto-generated ASNs will be in the range of 100XXX to 599XXX.
+600XXX to 999XXX will be reserved for manual ASNs in that case.
+
+#### Default Value (from the example environment file, must be provided)
+
+```env
+ASN_NAMESPACE_RANGE=600
+```
+
+### `ASN_PREFIX`
+
+Prefix for the ASN. Must not change after the first run.
+
+#### Default Value (from the example environment file, must be provided)
+
+```env
+ASN_PREFIX=ASN
+```
+
+## ASN Lookup Settings
+
+### `ASN_LOOKUP_URL` (optional)
+
+URL to look up existing ASN data. "{asn}" will be replaced with the ASN.
+If empty, the lookup feature will be disabled.
+
+#### Default Value (used by the application if not provided)
+
+```env
+ASN_LOOKUP_URL=
+```
+
+#### Other Examples
+
+```env
+ASN_LOOKUP_URL="https://dms.example.com/documents?archive_serial_number
+```
+
+### `ASN_LOOKUP_URL_INCLUDE_PREFIX` (optional)
+
+Include the ASN_PREFIX in the {asn} replacement of the lookup URL. If false, the prefix will be removed.
+Default is false.
+
+#### Default Value (used by the application if not provided)
+
+```env
+ASN_LOOKUP_URL_INCLUDE_PREFIX=false
+```
+
+## Network Settings
+
+### `PORT` (optional)
+
+The port the server will listen on.
+
+#### Default Value (used by the application if not provided)
+
+```env
+PORT=8080
+```
+
+#### Other Examples
+
+```env
+PORT=80
+```
+
+## OIDC (OpenID Connect) Configuration
+
+### `OIDC_AUTH_SECRET`
+
+Secret key used for signing and verifying tokens.
+Must be at least 32 characters long for security purposes.
+
+#### Default Value (from the example environment file, must be provided)
+
+```env
+OIDC_AUTH_SECRET=RANDOM_SECRET_WITH_MIN_32_CHARS_CHANGE_ME_IMMEDIATELY_UPON_COPYING
+```
+
+### `OIDC_CLIENT_ID`
+
+Client ID provided by your OIDC provider.
+Replace "XXX" with your actual client ID.
+
+#### Default Value (from the example environment file, must be provided)
+
+```env
+OIDC_CLIENT_ID="XXX"
+```
+
+### `OIDC_CLIENT_SECRET`
+
+Client Secret provided by your OIDC provider.
+Replace "XXX" with your actual client secret.
+
+#### Default Value (from the example environment file, must be provided)
+
+```env
+OIDC_CLIENT_SECRET="XXX"
+```
+
+### `OIDC_ISSUER`
+
+The URL of the OIDC provider's authorization server.
+This is where your application will redirect users to authenticate.
+
+#### Default Value (from the example environment file, must be provided)
+
+```env
+OIDC_ISSUER=https://logto.example.com/oidc # Logto
+```
+
+#### Other Examples
+
+```env
+OIDC_ISSUER=https://authentik.example.com/application/o/dms/ # Authentik
+```
+
+```env
+OIDC_ISSUER=https://authelia.example.com # Authelia
+```
+
+```env
+OIDC_ISSUER=https://keycloak.example.com/realms/[REALM] # Keycloak
+```
+
+### `OIDC_NAME_CLAIM` (optional)
+
+The claim in the ID token that contains the user's name.
+
+#### Default Value (used by the application if not provided)
+
+```env
+OIDC_NAME_CLAIM=name
+```
+
+#### Other Examples
+
+```env
+OIDC_NAME_CLAIM=preferred_username
+```
+
+### `OIDC_REDIRECT_URI`
+
+The URL to which the OIDC provider will redirect users after authentication.
+This should match the redirect URI registered with your OIDC provider.
+
+#### Default Value (from the example environment file, must be provided)
+
+```env
+OIDC_REDIRECT_URI=http://localhost:41319/oidc/callback
+```
+
+### `OIDC_ROLES_CLAIM` (optional)
+
+The claim in the ID token that contains the user's roles.
+
+#### Default Value (used by the application if not provided)
+
+```env
+OIDC_ROLES_CLAIM=roles
+```
+
+#### Other Examples
+
+```env
+OIDC_ROLES_CLAIM=groups
+```
+
+```env
+OIDC_ROLES_CLAIM=custom-roles-claim
+```
+
+### `OIDC_SCOPES`
+
+Scopes requested from the OIDC provider.
+These determine the information returned in the ID token.
+
+#### Default Value (from the example environment file, must be provided)
+
+```env
+OIDC_SCOPES="openid profile roles"
+```
+
+### `OIDC_UID_CLAIM` (optional)
+
+The claim in the ID token that contains the user's unique identifier.
+
+#### Default Value (used by the application if not provided)
+
+```env
+OIDC_UID_CLAIM=sub
+```
+
+#### Other Examples
+
+```env
+OIDC_UID_CLAIM=uid
+```
+
+```env
+OIDC_UID_CLAIM=email
+```
+
+```env
+OIDC_UID_CLAIM=custom-uid-claim
+```
+
+## Storage Settings
+
+### `DATA_DIR` (optional)
+
+Data directory.
+
+#### Default Value (used by the application if not provided)
+
+```env
+DATA_DIR=data
+```
+
+### `DB_FILE_NAME` (optional)
+
+Name of the SQLite3 database file within the data directory.
+The database gets created if it does not exist.
+To use a distributed database, set this to a URL beginning with "http" or "https".
+If it starts with "http" or "https", this uses the KV Connect Protocol:
+https://github.com/denoland/denokv/blob/main/proto/kv-connect.md
+
+#### Default Value (used by the application if not provided)
+
+```env
+DB_FILE_NAME=denokv.sqlite3
+```
+
+### `DENO_KV_ACCESS_TOKEN` (optional)
+
+The access token for the KV Connect Protocol.
+This is required if DB_FILE_NAME is a URL.
+The token must be set in the environment variable DENO_KV_ACCESS_TOKEN as per Deno's requirements.
+
+#### Default Value (used by the application if not provided)
+
+```env
+DENO_KV_ACCESS_TOKEN=XXX
+```
diff --git a/example.env b/example.env
index 442c03f..1c8e125 100644
--- a/example.env
+++ b/example.env
@@ -1,147 +1,122 @@
-# ############################################
-# #### Deno ASN Generator Configuration ######
-# ############################################
+# Note that for every parameter, you can also set "[PARAMETER]_FILE"
+# to a file that contains the value. This is especially useful for things
+# like mounted secrets in Docker Swarm or Kubernetes.
+
+# ASN Generation Settings
#
-# This file contains configuration options for the ASN generator.
-# Copy this file to .env and modify the values as needed.
+# Additional namespaces managed by the system outside of the ASN_NAMESPACE_RANGE.
#
-# For Docker, pass the `.env` file to the container using the `--env-file` option.
-# For example:
-# docker run --env-file .env -p 8080:8080 deno-asn-generator
+# Namespaces are notated as "..." where:
+# - Namespace is the numeric ID of the namespace.
+# - Label is the label for the namespace.
+# Optionally, commas and spaces can be used to separate namespaces.
#
-# Some settings are commented out by default. These are settings that should only be
-# changed when not (!) using Docker. If you are using Docker, leave these settings
-# commented out. These settings are:
-# - PORT
-# - DATA_DIR
-# - DB_FILE_NAME
+# If empty, no additional namespaces are managed and only the ASN_NAMESPACE_RANGE is used.
+# ADDITIONAL_MANAGED_NAMESPACES=
+# ADDITIONAL_MANAGED_NAMESPACES=<700 NDA-Covered Documents (Generic)><800 Personal Data Documents (Generic)>
#
-# This also allows importing the `env.example` file into Docker configuration UIs such as
-# Portainer, which will ignore the comments and use the correct settings.
+# The type of barcode to generate for the ASN.
+# ASN_BARCODE_TYPE=CODE128
+# ASN_BARCODE_TYPE=CODE39
+# ASN_BARCODE_TYPE=CODE93
#
-
-# ## General Settings
-# The port the server will listen on.
-# FOR DOCKER, THIS DEFAULTS TO 8080 AND SHOULD NOT BE CHANGED
-# PORT=8080
-
-# Docker-specific setting to set the host port.
-# Allows convenient configuration of the host port in Docker environments.
-# Ignored by the application itself.
-HOST_PORT=8080
-
-# URL to look up existing ASN data. "{asn}" will be replaced with the ASN.
-# If empty, the lookup feature will be disabled.
-# Default is empty.
-ASN_LOOKUP_URL="https://dms.example.com/documents?archive_serial_number={asn}"
-
-# Include the ASN_PREFIX in the {asn} replacement of the lookup URL. If false, the prefix will be removed.
-# Default is false.
-ASN_LOOKUP_URL_INCLUDE_PREFIX=false
-
-# The type of barcode to generate for the ASN. Options:
-# - CODE128
-# - CODE39
-# - CODE93
-# Default is CODE128.
-ASN_BARCODE_TYPE=CODE128
-
-# ## ASN Generation Settings
-
-# Prefix for the ASN. Must not change after the first run.
-ASN_PREFIX=ASN
-
-# The namespace range. The number of digits must not change after the first run.
-# For example, if the range is 600, auto-generated ASNs will be in the range of 100XXX to 599XXX.
-# 600XXX to 999XXX will be reserved for manual ASNs in that case.
-ASN_NAMESPACE_RANGE=600
-
# Enable namespace extension. If true, the ADDITIONAL_MANAGED_NAMESPACES can have more digits than
# the ASN_NAMESPACE_RANGE.
# If false, the ADDITIONAL_MANAGED_NAMESPACES must have the same number of digits as the ASN_NAMESPACE_RANGE.
-#
+#
# This works by reserving leading `9`s for namespace extension:
# For example, let's say the ASN_NAMESPACE_RANGE is 60. Therefore, without the extension, our ADDITIONAL_MANAGED_NAMESPACES
# could only be 6X-9X, meaning we only have 39 available namespaces.
# With the extension, in the two-digit namespace range, we actually lose 9X (leaving 6X-8X). However, leading 9s
# expand the namespace by another digit. This can also be chained, giving us theoretically infinite additional namespaces:
# 6X-8X, 90X-98X, 990X-998X, 9990X-9998X, etc.
-#
+#
# Note that behind the leading 9s, the namespace must still be the same number of digits as the ASN_NAMESPACE_RANGE.
-#
-# Default is false.
-ASN_ENABLE_NAMESPACE_EXTENSION=false
-
-# Additional namespaces managed by the system outside of the ASN_NAMESPACE_RANGE.
+# ASN_ENABLE_NAMESPACE_EXTENSION=false
+# ASN_ENABLE_NAMESPACE_EXTENSION=true
#
-# Namespaces are notated as "..." where:
-# - Namespace is the numeric ID of the namespace.
-# - Label is the label for the namespace.
-# Optionally, commas and spaces can be used to separate namespaces.
+# The namespace range. The number of digits must not change after the first run.
+# For example, if the range is 600, auto-generated ASNs will be in the range of 100XXX to 599XXX.
+# 600XXX to 999XXX will be reserved for manual ASNs in that case.
+ASN_NAMESPACE_RANGE=600
#
-# If empty, no additional namespaces are managed and only the ASN_NAMESPACE_RANGE is used.
+# Prefix for the ASN. Must not change after the first run.
+ASN_PREFIX=ASN
+
+# ASN Lookup Settings
+#
+# URL to look up existing ASN data. "{asn}" will be replaced with the ASN.
+# If empty, the lookup feature will be disabled.
+# ASN_LOOKUP_URL=
+# ASN_LOOKUP_URL="https://dms.example.com/documents?archive_serial_number
#
-# For example:
-# <700 NDA-Covered Documents (Generic)><800 Personal Data Documents (Generic)>
-ADDITIONAL_MANAGED_NAMESPACES="<700 NDA-Covered Documents (Generic)><800 Personal Data Documents (Generic)>"
+# Include the ASN_PREFIX in the {asn} replacement of the lookup URL. If false, the prefix will be removed.
+# Default is false.
+# ASN_LOOKUP_URL_INCLUDE_PREFIX=false
+# Network Settings
+#
+# The port the server will listen on.
+# PORT=8080
+# PORT=80
-# ## File paths
+# OIDC (OpenID Connect) Configuration
+#
+# Secret key used for signing and verifying tokens.
+# Must be at least 32 characters long for security purposes.
+OIDC_AUTH_SECRET=RANDOM_SECRET_WITH_MIN_32_CHARS_CHANGE_ME_IMMEDIATELY_UPON_COPYING
+#
+# Client ID provided by your OIDC provider.
+# Replace "XXX" with your actual client ID.
+OIDC_CLIENT_ID="XXX"
+#
+# Client Secret provided by your OIDC provider.
+# Replace "XXX" with your actual client secret.
+OIDC_CLIENT_SECRET="XXX"
+#
+# The URL of the OIDC provider's authorization server.
+# This is where your application will redirect users to authenticate.
+OIDC_ISSUER=https://logto.example.com/oidc # Logto
+# OIDC_ISSUER=https://authentik.example.com/application/o/dms/ # Authentik
+# OIDC_ISSUER=https://authelia.example.com # Authelia
+# OIDC_ISSUER=https://keycloak.example.com/realms/[REALM] # Keycloak
+#
+# The claim in the ID token that contains the user's name.
+# OIDC_NAME_CLAIM=name
+# OIDC_NAME_CLAIM=preferred_username
+#
+# The URL to which the OIDC provider will redirect users after authentication.
+# This should match the redirect URI registered with your OIDC provider.
+OIDC_REDIRECT_URI=http://localhost:41319/oidc/callback
+#
+# The claim in the ID token that contains the user's roles.
+# OIDC_ROLES_CLAIM=roles
+# OIDC_ROLES_CLAIM=groups
+# OIDC_ROLES_CLAIM=custom-roles-claim
+#
+# Scopes requested from the OIDC provider.
+# These determine the information returned in the ID token.
+OIDC_SCOPES="openid profile roles"
+#
+# The claim in the ID token that contains the user's unique identifier.
+# OIDC_UID_CLAIM=sub
+# OIDC_UID_CLAIM=uid
+# OIDC_UID_CLAIM=email
+# OIDC_UID_CLAIM=custom-uid-claim
+# Storage Settings
+#
# Data directory.
-# FOR DOCKER, THIS DEFAULTS TO /data AND SHOULD NOT BE CHANGED
# DATA_DIR=data
-
+#
# Name of the SQLite3 database file within the data directory.
# The database gets created if it does not exist.
# To use a distributed database, set this to a URL beginning with "http" or "https".
# If it starts with "http" or "https", this uses the KV Connect Protocol:
# https://github.com/denoland/denokv/blob/main/proto/kv-connect.md
# DB_FILE_NAME=denokv.sqlite3
-
+#
# The access token for the KV Connect Protocol.
# This is required if DB_FILE_NAME is a URL.
# The token must be set in the environment variable DENO_KV_ACCESS_TOKEN as per Deno's requirements.
# DENO_KV_ACCESS_TOKEN=XXX
-
-# OIDC (OpenID Connect) Configuration
-#
-# The URL of the OIDC provider's authorization server.
-# This is where your application will redirect users to authenticate.
-# If not set, OIDC authentication will be disabled.
-# OIDC_ISSUER=https://logto.example.com/oidc
-#
-# Secret key used for signing and verifying tokens.
-# Must be at least 32 characters long for security purposes.
-# OIDC_AUTH_SECRET=RANDOM_SECRET_WITH_MIN_32_CHARS_CHANGE_ME_IMMEDIATELY_UPON_COPYING
-#
-# Client ID provided by your OIDC provider.
-# Replace "XXX" with your actual client ID.
-# OIDC_CLIENT_ID="XXX"
-#
-# Client Secret provided by your OIDC provider.
-# Replace "XXX" with your actual client secret.
-# OIDC_CLIENT_SECRET="XXX"
-#
-# The URL to which the OIDC provider will redirect users after authentication.
-# This should match the redirect URI registered with your OIDC provider.
-# OIDC_REDIRECT_URI=http://localhost:41319/oidc/callback
-#
-# Scopes requested from the OIDC provider.
-# These determine the information returned in the ID token.
-# OIDC_SCOPES="openid profile roles"
-#
-# Optional: Uncomment and set if your application uses roles.
-# The claim in the ID token that contains the user's roles.
-# Defaults to "roles".
-# OIDC_ROLES_CLAIM=The claim in the ID token that contains the user's roles.
-#
-# Optional: Uncomment and set if your application uses a custom UID claim.
-# The claim in the ID token that contains the user's unique identifier.
-# Defaults to "sub".
-# OIDC_UID_CLAIM=The claim in the ID token that contains the user's unique identifier.
-#
-# Optional: Uncomment and set if your application uses a custom name claim.
-# The claim in the ID token that contains the user's name.
-# Defaults to "name".
-# OIDC_NAME_CLAIM=The claim in the ID token that contains the user's name.
diff --git a/lib/cli/bump.ts b/lib/cli/bump.ts
index 22e1fd1..ca2e4d5 100644
--- a/lib/cli/bump.ts
+++ b/lib/cli/bump.ts
@@ -1,7 +1,7 @@
import z from "@collinhacks/zod";
import {
allManagedNamespaces,
- CONFIG,
+ getConfig,
generateASN,
isManagedNamespace,
} from "$common/mod.ts";
@@ -17,6 +17,7 @@ const bumpArgs = z.object({
* @param args.count the number of ASNs to generate (default: 1)
*/
export async function runBump(args: unknown) {
+ const CONFIG = getConfig();
const parsedParams = bumpArgs.parse(args);
if (parsedParams.namespace && !isManagedNamespace(parsedParams.namespace)) {
diff --git a/lib/cli/server.ts b/lib/cli/server.ts
index dfb6da2..5e76c11 100644
--- a/lib/cli/server.ts
+++ b/lib/cli/server.ts
@@ -1,13 +1,8 @@
import { z } from "@collinhacks/zod";
-import { CONFIG, logPaths } from "$common/mod.ts";
+import { getConfig, logPaths } from "$common/mod.ts";
import { httpApp } from "../http/mod.ts";
import metadata from "$/deno.json" with { type: "json" };
-const serverArgs = z.object({
- port: z.number().default(CONFIG.PORT),
- host: z.string().default("0.0.0.0"),
-});
-
/**
* Runs the web server.
* @param args the arguments to the command
@@ -15,6 +10,11 @@ const serverArgs = z.object({
* @param args.host the hostname to listen on (default: 0.0.0.0)
*/
export function runServer(args: unknown): Promise {
+ const serverArgs = z.object({
+ port: z.number().default(getConfig().PORT),
+ host: z.string().default("0.0.0.0"),
+ });
+
console.log(`Running ${metadata.name} v${metadata.version}`);
console.log();
@@ -22,7 +22,7 @@ export function runServer(args: unknown): Promise {
console.log(`Starting server on ${parsedArgs.host}:${parsedArgs.port}`);
- console.log("Environment Configuration:", CONFIG);
+ console.log("Environment Configuration:", getConfig());
console.log("Arguments:", parsedArgs);
console.log("Paths:");
logPaths();
diff --git a/lib/cli/stats.ts b/lib/cli/stats.ts
index 42b07ca..288add9 100644
--- a/lib/cli/stats.ts
+++ b/lib/cli/stats.ts
@@ -1,5 +1,5 @@
import z from "@collinhacks/zod";
-import { allManagedNamespaces, CONFIG, TimeStats } from "$common/mod.ts";
+import { allManagedNamespaces, getConfig, TimeStats } from "$common/mod.ts";
const generateArgs = z.object({
namespace: z.number({ coerce: true }).optional(),
@@ -26,7 +26,7 @@ export async function runStats(args: unknown) {
);
const strings = stats.map((stats) => {
- return `${CONFIG.ASN_PREFIX}${stats.namespace}XXX: ` + stats.toString();
+ return `${getConfig().ASN_PREFIX}${stats.namespace}XXX: ` + stats.toString();
});
console.log(strings.join("\n"));
@@ -35,7 +35,7 @@ export async function runStats(args: unknown) {
"Maximum Rate of ASN registrations per hour per namespace in the above namespaces:",
);
console.log(
- `Filtered to only include namespaces with more than 3 ${CONFIG.ASN_PREFIX} numbers.`,
+ `Filtered to only include namespaces with more than 3 ${getConfig().ASN_PREFIX} numbers.`,
);
function maxHourlyRate(sigma: number) {
@@ -48,22 +48,22 @@ export async function runStats(args: unknown) {
console.log(
"1σ (68.27 %):",
maxHourlyRate(1).toPrecision(5),
- `registered ${CONFIG.ASN_PREFIX} numbers per namespace per hour`,
+ `registered ${getConfig().ASN_PREFIX} numbers per namespace per hour`,
);
console.log(
"2σ (95.45 %):",
maxHourlyRate(2).toPrecision(5),
- `registered ${CONFIG.ASN_PREFIX} numbers per namespace per hour`,
+ `registered ${getConfig().ASN_PREFIX} numbers per namespace per hour`,
);
console.log(
"3σ (99.73 %):",
maxHourlyRate(3).toPrecision(5),
- `registered ${CONFIG.ASN_PREFIX} numbers per namespace per hour`,
+ `registered ${getConfig().ASN_PREFIX} numbers per namespace per hour`,
);
console.log(
"6σ (99.99 %):",
maxHourlyRate(6).toPrecision(5),
- `registered ${CONFIG.ASN_PREFIX} numbers per namespace per hour`,
+ `registered ${getConfig().ASN_PREFIX} numbers per namespace per hour`,
);
console.log(
diff --git a/lib/common/additional-managed-namespaces.ts b/lib/common/additional-managed-namespaces.ts
index 861c39f..faeacad 100644
--- a/lib/common/additional-managed-namespaces.ts
+++ b/lib/common/additional-managed-namespaces.ts
@@ -1,4 +1,4 @@
-import { CONFIG, isValidNamespace } from "$common/mod.ts";
+import { type Config, getConfig, isValidNamespace } from "$common/mod.ts";
/**
* An additional managed namespace outside of the default range.
@@ -102,7 +102,7 @@ export function deserializeAdditionalManagedNamespaces(
*/
export function isValidAdditionalManagedNamespace(
namespace: number,
- config = CONFIG,
+ config: Config = getConfig(),
): boolean {
if (!isValidNamespace(namespace, config)) {
return false;
diff --git a/lib/common/asn.ts b/lib/common/asn.ts
index 80140df..f9a6b72 100644
--- a/lib/common/asn.ts
+++ b/lib/common/asn.ts
@@ -1,6 +1,7 @@
-import { CONFIG } from "$common/config.ts";
+import { getConfig } from "$common/config.ts";
import {
addTimestampToNamespaceStats,
+ type Config,
ensureFileContent,
getCounterPath,
getMaximumGenericRangeNamespace,
@@ -38,7 +39,7 @@ export interface ASNData {
metadata: Record;
}
-function getCurrentNamespace(config = CONFIG): number {
+function getCurrentNamespace(config = getConfig()): number {
const date = Date.now();
const range = getMinimumGenericRangeNamespace(config);
return range + date % (config.ASN_NAMESPACE_RANGE - range);
@@ -79,6 +80,7 @@ export async function generateASN(
metadata: Record = {},
namespace?: number,
deltaCounter = 1,
+ config: Config = getConfig(),
): Promise {
if (deltaCounter < 1) {
throw new Error("Delta counter must be at least 1");
@@ -111,7 +113,7 @@ export async function generateASN(
const asnData = {
asn: formatASN(namespace, counter),
namespace,
- prefix: CONFIG.ASN_PREFIX,
+ prefix: config.ASN_PREFIX,
counter: counter,
metadata,
};
@@ -147,7 +149,7 @@ export function isValidCounter(counter: number): boolean {
export function formatASN(
namespace: number,
counter: number,
- config = CONFIG,
+ config: Config = getConfig(),
): string {
if (!isValidNamespace(namespace, config)) {
throw new Error("Invalid namespace: " + namespace);
@@ -171,7 +173,7 @@ export function formatASN(
* @returns a human-readable description of the ASN format that explains the prefix, the namespace, and the counter.
* @remark The description is intended to be used in the console output or other monospaced text.
*/
-export function getFormatDescription(config = CONFIG): string {
+export function getFormatDescription(config: Config = getConfig()): string {
const {
ASN_PREFIX,
ASN_NAMESPACE_RANGE,
@@ -244,7 +246,7 @@ export function nthNinerExtensionRange(
* @param config The configuration to use for validation. Defaults to the global configuration.
* @returns `true` if the ASN is valid, `false` otherwise
*/
-export function isValidASN(asn: string, config = CONFIG): boolean {
+export function isValidASN(asn: string, config: Config = getConfig()): boolean {
return new RegExp(
`^(${config.ASN_PREFIX})?(\\d{${
`${config.ASN_NAMESPACE_RANGE}`.length
@@ -262,7 +264,7 @@ export function isValidASN(asn: string, config = CONFIG): boolean {
* @param config The configuration to use for parsing. Defaults to the global configuration.
* @returns An {@link ASNData} object with the parsed ASN data
*/
-export function parseASN(asn: string, config = CONFIG): ASNData {
+export function parseASN(asn: string, config: Config = getConfig()): ASNData {
if (!isValidASN(asn, config)) {
throw new Error("Invalid ASN");
}
@@ -286,7 +288,7 @@ export function parseASN(asn: string, config = CONFIG): ASNData {
return {
asn: formatASN(namespace, counter, config),
- prefix: CONFIG.ASN_PREFIX,
+ prefix: config.ASN_PREFIX,
namespace,
counter,
metadata: {},
diff --git a/lib/common/config.ts b/lib/common/config.ts
index 7ce3217..f044733 100644
--- a/lib/common/config.ts
+++ b/lib/common/config.ts
@@ -1,11 +1,14 @@
-import { z } from "@collinhacks/zod";
import {
type AdditionalManagedNamespace,
deserializeAdditionalManagedNamespaces,
getDB,
isValidAdditionalManagedNamespace,
+ toBoolean,
+ toNumber,
zBoolString,
} from "$common/mod.ts";
+import { z, type ZodSchema } from "@collinhacks/zod";
+import { initVariable } from "@wuespace/envar";
/**
* The application configuration.
@@ -138,79 +141,146 @@ export interface Config {
readonly DB_FILE_NAME: string;
}
-const configSchema = z.object({
- PORT: z.number({ coerce: true }).default(8080),
+const configSchema: ZodSchema = z.object({
+ PORT: z.number().positive().int(),
ASN_PREFIX: z.string().min(1).max(10).regex(/^[A-Z]+$/),
- ASN_NAMESPACE_RANGE: z.number({ coerce: true }),
- ASN_ENABLE_NAMESPACE_EXTENSION: zBoolString().default(false),
- ADDITIONAL_MANAGED_NAMESPACES: z.string().default("").transform((v) =>
- deserializeAdditionalManagedNamespaces(v)
- ).or(z.array(z.object({
- namespace: z.number(),
+ ASN_NAMESPACE_RANGE: z.number().int().positive(),
+ ASN_ENABLE_NAMESPACE_EXTENSION: z.boolean(),
+ ADDITIONAL_MANAGED_NAMESPACES: z.array(z.object({
+ namespace: z.number().int().positive(),
label: z.string().min(1),
- }))).default([]),
- ASN_LOOKUP_URL: z.string().regex(/^https?\:\/\/.*\{asn\}.*$/).optional(),
- ASN_LOOKUP_INCLUDE_PREFIX: zBoolString().default(false),
- ASN_BARCODE_TYPE: z.preprocess(
- (v) => v && String(v).toUpperCase(),
- z.literal("CODE128")
- .or(z.literal("CODE39"))
- .or(z.literal("CODE93"))
- .default("CODE128"),
- ).transform((v) => v.toLowerCase()),
- DATA_DIR: z.string().min(1).default("data"),
- DB_FILE_NAME: z.string().min(1).default("denokv.sqlite3"),
-}).superRefine((config, ctx) => {
+ })),
+ ASN_LOOKUP_URL: z.string().optional(),
+ ASN_LOOKUP_INCLUDE_PREFIX: z.boolean(),
+ ASN_BARCODE_TYPE: z.literal("CODE128")
+ .or(z.literal("CODE39"))
+ .or(z.literal("CODE93")),
+ DATA_DIR: z.string(),
+ DB_FILE_NAME: z.string(),
+});
+
+/**
+ * Initializes the environment variable based configuration, including validation and defaults.
+ * This function should be called at the start of the application, befor any calls to {@link getConfig}.
+ */
+export async function initConfig() {
+ await Promise.all([
+ initVariable("PORT", z.number({ coerce: true }).int().positive(), "8080"),
+ initVariable("ASN_PREFIX", z.string().min(1).max(10).regex(/^[A-Z]+$/)),
+ initVariable(
+ "ASN_NAMESPACE_RANGE",
+ z.number({ coerce: true }).int().positive(),
+ ),
+ initVariable("ASN_ENABLE_NAMESPACE_EXTENSION", zBoolString(), "false"),
+ initVariable(
+ "ADDITIONAL_MANAGED_NAMESPACES",
+ z.string().transform((v) => deserializeAdditionalManagedNamespaces(v)),
+ "",
+ ),
+ initVariable(
+ "ASN_LOOKUP_URL",
+ z.string().regex(/^https?\:\/\/.*\{asn\}.*$/).optional(),
+ ),
+ initVariable("ASN_LOOKUP_INCLUDE_PREFIX", zBoolString(), "false"),
+ initVariable(
+ "ASN_BARCODE_TYPE",
+ z.preprocess(
+ (s) => s && String(s).toUpperCase(),
+ z.literal("CODE128")
+ .or(z.literal("CODE39"))
+ .or(z.literal("CODE93")),
+ ),
+ "CODE128",
+ ),
+ initVariable("DATA_DIR", z.string().min(1), "data"),
+ initVariable("DB_FILE_NAME", z.string().min(1), "denokv.sqlite3"),
+ initVariable("DENO_KV_ACCESS_TOKEN", z.string().optional()),
+ // OIDC
+ initVariable("OIDC_ISSUER", z.string().url().optional()),
+ initVariable("OIDC_AUTH_SECRET", z.string().optional()),
+ initVariable("OIDC_CLIENT_ID", z.string().optional()),
+ initVariable("OIDC_CLIENT_SECRET", z.string().optional()),
+ initVariable("OIDC_REDIRECT_URI", z.string().url().optional()),
+ initVariable("OIDC_SCOPES", z.string().optional()),
+ initVariable("OIDC_UID_CLAIM", z.string(), "sub"),
+ initVariable("OIDC_NAME_CLAIM", z.string(), "name"),
+ initVariable("OIDC_ROLES_CLAIM", z.string(), "roles"),
+ ]);
+
+ // Additional checks
+ const config = getConfig();
+
if (
config.ASN_ENABLE_NAMESPACE_EXTENSION &&
(config.ASN_NAMESPACE_RANGE - 1).toString().charAt(0) === "9"
) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- params: {
- ASN_NAMESPACE_RANGE: config.ASN_NAMESPACE_RANGE,
- ASN_ENABLE_NAMESPACE_EXTENSION: config.ASN_ENABLE_NAMESPACE_EXTENSION,
- invalidGenericNamespace: config.ASN_NAMESPACE_RANGE - 1,
- },
- message:
- `Semantic configuration error: ASN_NAMESPACE_RANGE includes namespaces with leading 9s.\n` +
+ throw new Error(
+ `Semantic configuration error: ASN_NAMESPACE_RANGE includes namespaces with leading 9s.\n` +
`This is not allowed when ASN_ENABLE_NAMESPACE_EXTENSION is true.`,
- });
+ {
+ cause: {
+ ASN_NAMESPACE_RANGE: config.ASN_NAMESPACE_RANGE,
+ ASN_ENABLE_NAMESPACE_EXTENSION: config.ASN_ENABLE_NAMESPACE_EXTENSION,
+ invalidGenericNamespace: config.ASN_NAMESPACE_RANGE - 1,
+ },
+ },
+ );
}
- if (
- !config.ADDITIONAL_MANAGED_NAMESPACES.every((a) =>
+ const hasInvalidAdditionalNamespaces = !config
+ .ADDITIONAL_MANAGED_NAMESPACES.every((a) =>
isValidAdditionalManagedNamespace(a.namespace, config)
- )
+ );
+
+ if (
+ hasInvalidAdditionalNamespaces
) {
console.debug(config.ADDITIONAL_MANAGED_NAMESPACES);
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- params: {
- ASN_ENABLE_NAMESPACE_EXTENSION: config.ASN_ENABLE_NAMESPACE_EXTENSION,
- ASN_NAMESPACE_RANGE: config.ASN_NAMESPACE_RANGE,
- invalidAdditionalManagedNamespaces: config.ADDITIONAL_MANAGED_NAMESPACES
- .filter(
- (a) => !isValidAdditionalManagedNamespace(a.namespace, config),
- ).map((v) => `${config.ASN_PREFIX}${v.namespace}XXX - ${v.label}`),
- },
- message:
- `Semantic configuration error: Additional managed namespaces contain invalid namespace numbers.\n` +
+ throw new Error(
+ `Semantic configuration error: Additional managed namespaces contain invalid namespace numbers.\n` +
`The namespace numbers must have the same amount of digits as ASN_NAMESPACE_RANGE.\n` +
`If ASN_ENABLE_NAMESPACE_EXTENSION is true, the leading 9s are stripped from this calculation.\n` +
`For example, if your ASN_NAMESPACE_RANGE has two digits, instead of only XX, you can then also have 9XX, 99XX, etc.\n` +
`Note that in this case, 9X would not be valid.`,
- });
+ {
+ cause: {
+ ASN_ENABLE_NAMESPACE_EXTENSION: config.ASN_ENABLE_NAMESPACE_EXTENSION,
+ ASN_NAMESPACE_RANGE: config.ASN_NAMESPACE_RANGE,
+ invalidAdditionalManagedNamespaces: config
+ .ADDITIONAL_MANAGED_NAMESPACES
+ .filter(
+ (a) => !isValidAdditionalManagedNamespace(a.namespace, config),
+ ).map((v) => `${config.ASN_PREFIX}${v.namespace}XXX - ${v.label}`),
+ },
+ },
+ );
}
-});
+}
/**
- * The current application configuration, based on environment variables, `.env` files, and defaults.
- * @see {@link Config}
+ * Returns the current configuration. Should only be called after initializing the configuration with {@link initConfig}.
+ * @returns The current configuration.
*/
-export const CONFIG: Config = Object.freeze(
- configSchema.parse(Deno.env.toObject()),
-);
+export function getConfig(): Config {
+ return configSchema.parse({
+ PORT: toNumber(Deno.env.get("PORT")),
+ ASN_PREFIX: Deno.env.get("ASN_PREFIX"),
+ ASN_NAMESPACE_RANGE: toNumber(Deno.env.get("ASN_NAMESPACE_RANGE")),
+ ASN_ENABLE_NAMESPACE_EXTENSION: toBoolean(Deno.env.get(
+ "ASN_ENABLE_NAMESPACE_EXTENSION",
+ )),
+ ADDITIONAL_MANAGED_NAMESPACES: deserializeAdditionalManagedNamespaces(
+ z.string().parse(Deno.env.get("ADDITIONAL_MANAGED_NAMESPACES")),
+ ),
+ ASN_LOOKUP_URL: Deno.env.get("ASN_LOOKUP_URL"),
+ ASN_LOOKUP_INCLUDE_PREFIX: toBoolean(
+ Deno.env.get("ASN_LOOKUP_INCLUDE_PREFIX"),
+ ),
+ ASN_BARCODE_TYPE: Deno.env.get("ASN_BARCODE_TYPE")?.toUpperCase(),
+ DATA_DIR: Deno.env.get("DATA_DIR"),
+ DB_FILE_NAME: Deno.env.get("DB_FILE_NAME"),
+ }) satisfies Config;
+}
const DB_CONFIG_KEY = "config";
@@ -224,46 +294,46 @@ const DB_CONFIG_KEY = "config";
* @returns A promise that resolves if the database configuration is valid.
* @throws {Error} If the configuration has changed in an incompatible way.
*/
-export async function validateDB(): Promise {
+export async function validateDB(config: Config = getConfig()): Promise {
const db = await getDB();
const dbConfigRes = await db.get([DB_CONFIG_KEY]);
if (!dbConfigRes.value) {
- await db.set([DB_CONFIG_KEY], CONFIG);
+ await db.set([DB_CONFIG_KEY], config);
return;
}
const dbConfig = configSchema.parse(dbConfigRes.value);
- if (dbConfig.ASN_PREFIX !== CONFIG.ASN_PREFIX) {
+ if (dbConfig.ASN_PREFIX !== config.ASN_PREFIX) {
throw new Error(
`Database configuration mismatch: ASN_PREFIX.\n` +
` Old: ${dbConfig.ASN_PREFIX},\n` +
- ` New: ${CONFIG.ASN_PREFIX}.\n` +
+ ` New: ${config.ASN_PREFIX}.\n` +
`The prefix must be the same.`,
);
}
if (
dbConfig.ASN_NAMESPACE_RANGE?.toString().length !==
- CONFIG.ASN_NAMESPACE_RANGE.toString().length
+ config.ASN_NAMESPACE_RANGE.toString().length
) {
throw new Error(
`Database configuration mismatch: ASN_NAMESPACE_RANGE.\n` +
` Old: ${dbConfig.ASN_NAMESPACE_RANGE},\n` +
- ` New: ${CONFIG.ASN_NAMESPACE_RANGE}.\n` +
+ ` New: ${config.ASN_NAMESPACE_RANGE}.\n` +
`The number of digits must be the same.`,
);
}
- if (dbConfig.ASN_BARCODE_TYPE !== CONFIG.ASN_BARCODE_TYPE) {
+ if (dbConfig.ASN_BARCODE_TYPE !== config.ASN_BARCODE_TYPE) {
console.warn(
`Warning: ASN_BARCODE_TYPE has changed. This will affect the barcode generation.\n` +
` Old: ${dbConfig.ASN_BARCODE_TYPE},\n` +
- ` New: ${CONFIG.ASN_BARCODE_TYPE}.\n` +
+ ` New: ${config.ASN_BARCODE_TYPE}.\n` +
`Any future barcodes will be generated using the new barcode type which may not be compatible with the old ones.`,
);
}
- await db.set([DB_CONFIG_KEY], CONFIG);
+ await db.set([DB_CONFIG_KEY], config);
}
diff --git a/lib/common/db.ts b/lib/common/db.ts
index 26609ca..7c3c029 100644
--- a/lib/common/db.ts
+++ b/lib/common/db.ts
@@ -1,12 +1,13 @@
import { ensureParentDirExists, getDatabasePath } from "$common/path.ts";
-import { CONFIG } from "$common/config.ts";
+import { getConfig } from "$common/config.ts";
+import type { Config } from "$common/mod.ts";
/**
* Ensures that the database file exists and returns its API.
* @param config The configuration object to use. Defaults to the global configuration.
* @returns the key-value store for the application database.
*/
-export async function getDB(config = CONFIG): Promise {
+export async function getDB(config: Config = getConfig()): Promise {
const databasePath = getDatabasePath(config);
if (!databasePath.startsWith("http")) {
await ensureParentDirExists(databasePath);
@@ -38,7 +39,7 @@ export async function getDB(config = CONFIG): Promise {
*/
export async function performAtomicTransaction(
fn: (db: Deno.Kv) => Promise,
- config = CONFIG,
+ config: Config = getConfig(),
) {
const db = await getDB(config);
let res = { ok: false };
diff --git a/lib/common/namespaces.ts b/lib/common/namespaces.ts
index 4d62208..500a0fb 100644
--- a/lib/common/namespaces.ts
+++ b/lib/common/namespaces.ts
@@ -1,11 +1,11 @@
-import { CONFIG } from "$common/mod.ts";
+import { type Config, getConfig } from "$common/mod.ts";
/**
* Returns a list of all managed namespaces.
* This includes the generic namespaces and the additional managed namespaces.
* @returns all managed namespaces
*/
-export function allManagedNamespaces(config = CONFIG): number[] {
+export function allManagedNamespaces(config: Config = getConfig()): number[] {
const minGeneric = getMinimumGenericRangeNamespace(config);
const maxGeneric = getMaximumGenericRangeNamespace(config);
@@ -23,7 +23,7 @@ export function allManagedNamespaces(config = CONFIG): number[] {
* This is the maximum value smaller than the `ASN_NAMESPACE_RANGE` configuration parameter.
* @returns the maximum namespace value for the generic range
*/
-export function getMaximumGenericRangeNamespace(config = CONFIG): number {
+export function getMaximumGenericRangeNamespace(config: Config = getConfig()): number {
return config.ASN_NAMESPACE_RANGE - 1;
}
@@ -33,7 +33,7 @@ export function getMaximumGenericRangeNamespace(config = CONFIG): number {
* the `ASN_NAMESPACE_RANGE` configuration parameter.
* @returns the minimum namespace value for the generic range
*/
-export function getMinimumGenericRangeNamespace(config = CONFIG): number {
+export function getMinimumGenericRangeNamespace(config: Config = getConfig()): number {
return Number.parseInt(
"1" + "0".repeat(config.ASN_NAMESPACE_RANGE.toString().length - 1),
);
@@ -54,7 +54,7 @@ export function getMinimumGenericRangeNamespace(config = CONFIG): number {
* @param namespace the namespace to check
* @returns `true` if the namespace is a valid namespace, `false` otherwise
*/
-export function isValidNamespace(namespace: number, config = CONFIG): boolean {
+export function isValidNamespace(namespace: number, config: Config = getConfig()): boolean {
if (
!Number.isSafeInteger(namespace) ||
namespace < getMinimumGenericRangeNamespace(config)
@@ -82,7 +82,7 @@ export function isValidNamespace(namespace: number, config = CONFIG): boolean {
*/
export function isManagedNamespace(
namespace: number,
- config = CONFIG,
+ config: Config = getConfig(),
): boolean {
if (namespace < getMinimumGenericRangeNamespace(config)) {
return false;
diff --git a/lib/common/path.ts b/lib/common/path.ts
index 6077bfd..35ba345 100644
--- a/lib/common/path.ts
+++ b/lib/common/path.ts
@@ -1,12 +1,12 @@
import { resolve } from "node:path";
-import { CONFIG } from "$common/mod.ts";
+import { type Config, getConfig } from "$common/mod.ts";
/**
* Resolves the full path to the {@link Config.DATA_DIR}.
* @param config The configuration object to use. Defaults to the global configuration.
* @returns Full path to the data directory.
*/
-export function getDataDirectoryPath(config = CONFIG): string {
+export function getDataDirectoryPath(config: Config = getConfig()): string {
return resolve(config.DATA_DIR);
}
@@ -17,7 +17,7 @@ export function getDataDirectoryPath(config = CONFIG): string {
* @param config The configuration object to use. Defaults to the global configuration.
* @returns Full path to the database file.
*/
-export function getDatabasePath(config = CONFIG): string {
+export function getDatabasePath(config: Config = getConfig()): string {
if (config.DB_FILE_NAME.startsWith("http")) {
return config.DB_FILE_NAME;
}
@@ -28,7 +28,7 @@ export function getDatabasePath(config = CONFIG): string {
/**
* Logs relevant paths to the console.
*/
-export function logPaths(config = CONFIG) {
+export function logPaths(config: Config = getConfig()) {
console.log(`DATA_PATH: ${getDataDirectoryPath(config)}`);
console.log(`DB_FILE_PATH: ${getDatabasePath(config)}`);
}
@@ -43,7 +43,7 @@ export function logPaths(config = CONFIG) {
export function getCounterPath(
namespace: number,
counter: number,
- config = CONFIG,
+ config: Config = getConfig(),
): string {
return resolve(
getDataDirectoryPath(config),
@@ -60,7 +60,7 @@ export function getCounterPath(
*/
export function getNamespaceMetadataPath(
namespace: number,
- config = CONFIG,
+ config: Config = getConfig(),
): string {
return resolve(
getDataDirectoryPath(config),
diff --git a/lib/common/zod-helpers.ts b/lib/common/zod-helpers.ts
index c02bfaf..85dcdd3 100644
--- a/lib/common/zod-helpers.ts
+++ b/lib/common/zod-helpers.ts
@@ -15,3 +15,31 @@ export function zBoolString(): z.ZodEffects {
return false;
}, z.boolean());
}
+
+/**
+ * Parses a boolean from an environment variable value.
+ * @param value the environment variable value to parse
+ * @returns the boolean value of the environment variable
+ */
+export function toBoolean(value: string | undefined): boolean | undefined {
+ if (
+ value === undefined ||
+ value === ""
+ ) return undefined;
+
+ return ["1", "true", "yes", "on", "enabled"].includes(value.toLowerCase());
+}
+
+/**
+ * Parses a number from an environment variable value.
+ * @param value the environment variable value to parse
+ * @returns the number value of the environment variable
+ */
+export function toNumber(value: string | undefined): number | undefined {
+ if (
+ value === undefined ||
+ value === ""
+ ) return undefined;
+
+ return Number(value);
+}
diff --git a/lib/http/barcode-svg.ts b/lib/http/barcode-svg.ts
index d1ccb4c..a6aaf47 100644
--- a/lib/http/barcode-svg.ts
+++ b/lib/http/barcode-svg.ts
@@ -1,5 +1,5 @@
import * as bwip from "@metafloor/bwip-js";
-import { CONFIG } from "$common/config.ts";
+import { getConfig } from "$common/config.ts";
/**
* Creates an SVG barcode for the given data and current configuration.
@@ -9,7 +9,7 @@ import { CONFIG } from "$common/config.ts";
*/
export function createBarcodeSVG(data: string, embedded = false): string {
return bwip.toSVG({
- bcid: CONFIG.ASN_BARCODE_TYPE, // Barcode type
+ bcid: getConfig().ASN_BARCODE_TYPE, // Barcode type
text: data, // Text to encode
scale: 3, // 3x scaling factor
height: 10, // Bar height, in millimeters
diff --git a/lib/http/lookup-url.ts b/lib/http/lookup-url.ts
index 015db2a..40cfb88 100644
--- a/lib/http/lookup-url.ts
+++ b/lib/http/lookup-url.ts
@@ -1,4 +1,4 @@
-import { CONFIG, isValidASN } from "$common/mod.ts";
+import { type Config, getConfig, isValidASN } from "$common/mod.ts";
/**
* Builds the URL to lookup the ASN based on the configuration.
@@ -6,8 +6,8 @@ import { CONFIG, isValidASN } from "$common/mod.ts";
* @returns the URL to lookup the ASN if the ASN lookup is enabled
* @throws {Error} when the ASN lookup is disabled or the ASN is invalid
*/
-export function getLookupURL(asn: string): string {
- const baseUrl = CONFIG.ASN_LOOKUP_URL;
+export function getLookupURL(asn: string, config: Config = getConfig()): string {
+ const baseUrl = config.ASN_LOOKUP_URL;
if (!baseUrl) {
throw new Error("ASN Lookup is disabled");
@@ -17,8 +17,8 @@ export function getLookupURL(asn: string): string {
throw new Error("Invalid ASN");
}
- if (!CONFIG.ASN_LOOKUP_INCLUDE_PREFIX) {
- asn = asn.slice(CONFIG.ASN_PREFIX.length);
+ if (!config.ASN_LOOKUP_INCLUDE_PREFIX) {
+ asn = asn.slice(config.ASN_PREFIX.length);
}
return baseUrl.replaceAll("{asn}", asn);
diff --git a/lib/http/routes/lookup.ts b/lib/http/routes/lookup.ts
index f07cdb2..dde3064 100644
--- a/lib/http/routes/lookup.ts
+++ b/lib/http/routes/lookup.ts
@@ -2,7 +2,7 @@ import { Hono } from "@hono/hono";
import { validator } from "@hono/hono/validator";
import { z } from "@collinhacks/zod";
-import { CONFIG, isValidASN } from "$common/mod.ts";
+import { getConfig, isValidASN } from "$common/mod.ts";
import { getLookupURL } from "$http/mod.ts";
@@ -22,7 +22,7 @@ lookupRoutes.post(
return parsed.data;
}),
(c) => {
- const asn = CONFIG.ASN_PREFIX + c.req.valid("form").asn;
+ const asn = getConfig().ASN_PREFIX + c.req.valid("form").asn;
return c.redirect("/go/" + asn);
},
);
@@ -38,7 +38,7 @@ lookupRoutes.get(
(c) => {
const asn = c.req.valid("param").asn;
- if (!CONFIG.ASN_LOOKUP_URL) {
+ if (!getConfig().ASN_LOOKUP_URL) {
return c.text("ASN Lookup is disabled", 400);
}
diff --git a/lib/http/routes/ui.tsx b/lib/http/routes/ui.tsx
index 26565b3..0e26070 100644
--- a/lib/http/routes/ui.tsx
+++ b/lib/http/routes/ui.tsx
@@ -1,7 +1,7 @@
import { Hono } from "@hono/hono";
import { jsxRenderer } from "@hono/hono/jsx-renderer";
-import { CONFIG, generateASN } from "$common/mod.ts";
+import { getConfig, generateASN } from "$common/mod.ts";
import { createMetadata } from "../mod.ts";
@@ -19,7 +19,7 @@ uiRoutes.get(
"/",
async (c) =>
await c.render(
- ,
+ ,
),
);
diff --git a/lib/http/ui/index.tsx b/lib/http/ui/index.tsx
index 0910967..19cfb7f 100644
--- a/lib/http/ui/index.tsx
+++ b/lib/http/ui/index.tsx
@@ -1,4 +1,4 @@
-import { CONFIG, type Config } from "$common/mod.ts";
+import type { Config } from "$common/mod.ts";
import { Search } from "$http/ui/search.tsx";
import { css, cx } from "@hono/hono/css";
import { BUTTON_STYLE } from "$http/ui/common/button-styles.ts";
@@ -44,7 +44,7 @@ export function IndexPage({ config }: { config: Config }) {
accessible to all members of the organization.
- Generate generic {CONFIG.ASN_PREFIX} number
+ Generate generic {config.ASN_PREFIX} number
{config.ASN_PREFIX}
@@ -53,7 +53,7 @@ export function IndexPage({ config }: { config: Config }) {
{genericRangeEnd}XXX
- {CONFIG.ADDITIONAL_MANAGED_NAMESPACES.length
+ {config.ADDITIONAL_MANAGED_NAMESPACES.length
? (
Manually generate {config.ASN_PREFIX}{" "}
@@ -62,7 +62,7 @@ export function IndexPage({ config }: { config: Config }) {
)
: ""}
- {CONFIG.ADDITIONAL_MANAGED_NAMESPACES.map(({ namespace, label }) => (
+ {config.ADDITIONAL_MANAGED_NAMESPACES.map(({ namespace, label }) => (
+ css`
display: block;
border: 1px solid var(--primary-color);
padding: 0.5rem;
@@ -53,9 +52,10 @@ border: 1px solid var(--primary-color);
`;
export function Search() {
+ const isLookupEnabled = Boolean(getConfig().ASN_LOOKUP_URL);
return (