Merge branch 'setup'

This commit is contained in:
2026-08-02 02:34:42 +02:00
135 changed files with 5735 additions and 976 deletions
@@ -3,7 +3,9 @@ import Configuration
extension AbsoluteConfigKey {
/// A namespace for the static files cache configuration keys, as absolute keys.
public enum Cache {
/// The absolute configuration key for the max-age, in seconds, applied to text-based static files.
/// The absolute configuration key for the max-age, in seconds, applied to fingerprinted assets and fonts.
public static let maxAgeAsset: AbsoluteConfigKey = .init(.Cache.maxAgeAsset)
/// The absolute configuration key for the max-age, in seconds, applied to unversioned text-based static files.
public static let maxAgeText: AbsoluteConfigKey = .init(.Cache.maxAgeText)
/// The absolute configuration key for the max-age, in seconds, applied to image static files.
public static let maxAgeImage: AbsoluteConfigKey = .init(.Cache.maxAgeImage)
@@ -50,6 +52,15 @@ extension AbsoluteConfigKey {
/// The absolute configuration key for the minimum log level.
public static let level: AbsoluteConfigKey = .init(.Log.level)
}
/// A namespace for the rate limit configuration keys, as absolute keys.
public enum RateLimit {
/// The absolute configuration key for the number of requests admitted per client per window.
public static let limit: AbsoluteConfigKey = .init(.RateLimit.limit)
/// The absolute configuration key for the window length, in seconds.
public static let window: AbsoluteConfigKey = .init(.RateLimit.window)
/// The absolute configuration key for keying clients by the first `X-Forwarded-For` entry.
public static let trustForwardedFor: AbsoluteConfigKey = .init(.RateLimit.trustForwardedFor)
}
/// A namespace for the path configuration keys, as absolute keys.
public enum Path {
/// The absolute configuration key for the directory the static files are served from.
@@ -3,7 +3,9 @@ import Configuration
extension ConfigKey {
/// A namespace for the static files cache configuration keys.
public enum Cache {
/// The configuration key for the max-age, in seconds, applied to text-based static files (CSS, JavaScript, plain text).
/// The configuration key for the max-age, in seconds, applied to fingerprinted assets (CSS, JavaScript) and fonts.
public static let maxAgeAsset: ConfigKey = "cache.maxAge.asset"
/// The configuration key for the max-age, in seconds, applied to unversioned text-based static files (e.g. plain text).
public static let maxAgeText: ConfigKey = "cache.maxAge.text"
/// The configuration key for the max-age, in seconds, applied to image static files (ICO, PNG, SVG).
public static let maxAgeImage: ConfigKey = "cache.maxAge.image"
@@ -50,6 +52,15 @@ extension ConfigKey {
/// The configuration key for the minimum log level.
public static let level: ConfigKey = "log.level"
}
/// A namespace for the rate limit configuration keys.
public enum RateLimit {
/// The configuration key for the number of requests admitted per client per window.
public static let limit: ConfigKey = "rateLimit.limit"
/// The configuration key for the window length, in seconds.
public static let window: ConfigKey = "rateLimit.window"
/// The configuration key for keying clients by the first `X-Forwarded-For` entry (enable only behind a trusted proxy).
public static let trustForwardedFor: ConfigKey = "rateLimit.trustForwardedFor"
}
/// A namespace for the path configuration keys.
public enum Path {
/// The configuration key for the directory the static files are served from.
@@ -1,10 +0,0 @@
import HTTPTypes
extension HTTPField.Name {
/// The `Permissions-Policy` field name (not provided as a standard `HTTPField.Name`).
static let permissionsPolicy = Self("Permissions-Policy")!
/// The `Referrer-Policy` field name (not provided as a standard `HTTPField.Name`).
static let referrerPolicy = Self("Referrer-Policy")!
/// The `X-Frame-Options` field name (not provided as a standard `HTTPField.Name`).
static let frameOptions = Self("X-Frame-Options")!
}
@@ -1,7 +1,9 @@
extension Int {
/// A namespace for the cache's default configuration values.
public enum Cache {
/// The default max-age, in seconds, applied to text-based static files (1 hour).
/// The default max-age, in seconds, applied to fingerprinted assets and fonts (1 year).
public static let maxAgeAsset = 31_536_000
/// The default max-age, in seconds, applied to unversioned text-based static files (1 hour).
public static let maxAgeText = 3_600
/// The default max-age, in seconds, applied to image static files (1 week).
public static let maxAgeImage = 604_800
@@ -0,0 +1,13 @@
import Foundation
import Localization
public extension LanguageList {
// MARK: Initializers
/// Creates a language list backed by the module's String Catalog.
init() {
self.init(bundle: .module)
}
}
@@ -0,0 +1,13 @@
import Foundation
import Infrastructure
public extension LocalizationMiddleware {
// MARK: Initializers
/// Creates a localization middleware that negotiates against the module's String Catalog languages.
init() {
self.init(bundle: .module)
}
}
@@ -0,0 +1,21 @@
import Foundation
import Infrastructure
public extension NotFoundMiddleware {
// MARK: Initializers
/// Creates a not-found middleware that renders the website's error page, localized to the module's String Catalog languages.
/// - Parameter assetVersion: the version token appended to the page's asset URLs, or `nil` (the default) to leave them unversioned.
init(
assetVersion: String? = nil
) {
self.init(bundle: .module) {
NotFoundPage(
locale: $0,
assetVersion: assetVersion
)
}
}
}
@@ -1,80 +0,0 @@
import Hummingbird
/// A result builder that collects ``RouteCollection`` values into a stack.
///
/// Mirrors the `MiddlewareFixedTypeBuilder` Hummingbird uses for `addMiddleware`, letting route
/// collections be listed declaratively rather than added one statement at a time.
@resultBuilder
public enum RouteCollectionBuilder<Context: RequestContext> {
public static func buildExpression(
_ collection: RouteCollection<Context>
) -> [RouteCollection<Context>] {
[collection]
}
public static func buildBlock(
_ collections: [RouteCollection<Context>]...
) -> [RouteCollection<Context>] {
collections.flatMap { $0 }
}
public static func buildOptional(
_ collections: [RouteCollection<Context>]?
) -> [RouteCollection<Context>] {
collections ?? []
}
public static func buildEither(
first collections: [RouteCollection<Context>]
) -> [RouteCollection<Context>] {
collections
}
public static func buildEither(
second collections: [RouteCollection<Context>]
) -> [RouteCollection<Context>] {
collections
}
public static func buildArray(
_ collections: [[RouteCollection<Context>]]
) -> [RouteCollection<Context>] {
collections.flatMap { $0 }
}
}
// MARK: - Helpers
public extension RouterMethods {
// MARK: Methods
/// Adds route collections to the router using the ``RouteCollectionBuilder`` result builder.
///
/// Mirrors `addMiddleware`, letting controllers be listed declaratively:
///
/// ```swift
/// router.addRoutes {
/// RootController<AppRequestContext>().routes
/// HealthController<AppRequestContext>().routes
/// }
/// ```
///
/// Each collection is added at the router's root, exactly as a sequence of
/// `addRoutes(_:)` calls would.
/// - Parameter build: the route-collection stack result builder.
/// - Returns: the router, so calls can be chained.
@discardableResult
func addRoutes(
@RouteCollectionBuilder<Context> _ build: () -> [RouteCollection<Context>]
) -> Self {
for collection in build() {
addRoutes(collection)
}
return self
}
}
@@ -23,27 +23,6 @@ extension String {
/// The directory, relative to the working directory, that the website's static files are served from.
public static let staticResources = "Resources/Static"
}
/// A namespace for the security headers' default configuration values.
///
/// `Strict-Transport-Security` is intentionally absent: it is only safe over HTTPS and is
/// "sticky" in browsers, so it stays off unless explicitly configured in production.
public enum Security {
/// The default `Content-Security-Policy`.
///
/// Restricts every resource to the site's own origin (`default-src 'self'`), blocks plugins
/// (`object-src 'none'`), pins the document base URL (`base-uri 'self'`), and forbids framing
/// (`frame-ancestors 'none'`). Both pages link external stylesheets, so no inline-style
/// exception is required.
public static let contentSecurityPolicy = "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
/// The default `X-Content-Type-Options` (disables MIME sniffing).
public static let contentTypeOptions = "nosniff"
/// The default `X-Frame-Options` (forbids framing the page).
public static let frameOptions = "DENY"
/// The default `Referrer-Policy`.
public static let referrerPolicy = "strict-origin-when-cross-origin"
/// The default `Permissions-Policy` (denies access to powerful browser features the site does not use).
public static let permissionsPolicy = "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
}
/// A namespace for the server string constants.
public enum Server {
/// The website server's name.