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);
""";
}
}