namespace ZB.MOM.WW.Auth.ApiKeys.SqlServer; /// /// Schema constants and DDL for the shared SQL Server API-key store. Mirrors /// ZB.MOM.WW.Auth.ApiKeys.Sqlite.SqliteAuthSchema column-for-column so a row means exactly /// the same thing in either store. /// /// /// /// Timestamps are DATETIMEOFFSET(7), not ISO-8601 text. The sibling /// ZB.MOM.WW.Secrets SQL Server store deliberately stores its timestamps as /// nvarchar because its rows replicate with a SQLite store and last-writer-wins /// compares those timestamps for equality, where a sub-tick round-trip difference would turn a tie /// into a spurious "newer". No such rationale applies here: this store never replicates with the /// SQLite one — a host picks exactly one provider, and the one-shot SQLite import is a copy, not an /// ongoing sync — so the native type is used, which sorts and range-scans correctly and keeps the /// tables readable to a DBA. /// /// /// SqliteAuthSchema.CurrentVersion is 3 only because that store carries the donor /// MxAccessGateway's version history (v1/v2 were byte-identical; v3 added expires_utc). This /// schema is created whole, with expires_utc present from the start, so its own version line /// starts at 1. /// /// public static class SqlServerAuthSchema { /// /// The schema version this build creates and supports. The migrator refuses a database whose /// recorded version is newer than this. /// public const int CurrentVersion = 1; /// Default schema the API-key tables live in. public const string DefaultSchemaName = "zbauth"; /// Name of the single-row table tracking the applied schema version. public const string SchemaVersionTable = "schema_version"; /// Name of the table storing API-key records. public const string ApiKeysTable = "api_keys"; /// Name of the append-only audit table. public const string ApiKeyAuditTable = "api_key_audit"; /// /// Returns the idempotent DDL that provisions the schema, the three tables and the two /// supporting indexes in . /// /// The SQL schema name to create the objects in. /// The DDL batch. /// /// is interpolated, not parameterized — T-SQL has no parameter /// form for an object name. It is re-validated here against the same strict identifier /// allow-list enforces, and bracket-quoted below. /// The duplicate check is deliberate: this method is public, so it must defend itself /// rather than trust that every caller routed through the options type. The version row is NOT /// written here — writes it with a MERGE that also /// updates on match, so a future upgrade re-stamps the stored version. /// /// is not a plain identifier. public static string CreateSchemaDdl(string schemaName) { SqlServerApiKeyOptions.ValidateIdentifier(schemaName); return $""" IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = N'{schemaName}') EXEC(N'CREATE SCHEMA [{schemaName}]'); IF OBJECT_ID(N'[{schemaName}].[{SchemaVersionTable}]', N'U') IS NULL CREATE TABLE [{schemaName}].[{SchemaVersionTable}] ( id INT NOT NULL CONSTRAINT PK_{schemaName}_{SchemaVersionTable} PRIMARY KEY CHECK (id = 1), version INT NOT NULL, applied_utc DATETIMEOFFSET(7) NOT NULL ); IF OBJECT_ID(N'[{schemaName}].[{ApiKeysTable}]', N'U') IS NULL CREATE TABLE [{schemaName}].[{ApiKeysTable}] ( key_id NVARCHAR(64) NOT NULL CONSTRAINT PK_{schemaName}_{ApiKeysTable} PRIMARY KEY, key_prefix NVARCHAR(16) NOT NULL, secret_hash VARBINARY(64) NOT NULL, display_name NVARCHAR(256) NOT NULL, scopes NVARCHAR(MAX) NOT NULL, constraints NVARCHAR(MAX) NULL, created_utc DATETIMEOFFSET(7) NOT NULL, last_used_utc DATETIMEOFFSET(7) NULL, revoked_utc DATETIMEOFFSET(7) NULL, expires_utc DATETIMEOFFSET(7) NULL ); IF OBJECT_ID(N'[{schemaName}].[{ApiKeyAuditTable}]', N'U') IS NULL CREATE TABLE [{schemaName}].[{ApiKeyAuditTable}] ( audit_id BIGINT IDENTITY(1,1) NOT NULL CONSTRAINT PK_{schemaName}_{ApiKeyAuditTable} PRIMARY KEY, key_id NVARCHAR(64) NULL, event_type NVARCHAR(64) NOT NULL, remote_address NVARCHAR(64) NULL, created_utc DATETIMEOFFSET(7) NOT NULL, details NVARCHAR(MAX) NULL ); IF NOT EXISTS ( SELECT 1 FROM sys.indexes WHERE name = N'ix_api_keys_revoked_utc' AND object_id = OBJECT_ID(N'[{schemaName}].[{ApiKeysTable}]')) CREATE INDEX ix_api_keys_revoked_utc ON [{schemaName}].[{ApiKeysTable}] (revoked_utc); IF NOT EXISTS ( SELECT 1 FROM sys.indexes WHERE name = N'ix_api_key_audit_key_id_created_utc' AND object_id = OBJECT_ID(N'[{schemaName}].[{ApiKeyAuditTable}]')) CREATE INDEX ix_api_key_audit_key_id_created_utc ON [{schemaName}].[{ApiKeyAuditTable}] (key_id, created_utc); """; } }