remove user-secrets approach & prepare a pilot deploy

This commit is contained in:
hamid
2026-07-28 23:18:54 +03:30
parent 630c7907ec
commit 5885280b49
28 changed files with 639 additions and 142 deletions
@@ -13,9 +13,11 @@ namespace Baya.Infrastructure.CrossCutting.Seams.Real;
/// single JSON call: <c>POST /send_otp</c> and <c>POST /send</c>, authenticated with the shared
/// <c>X-Api-Key</c> secret.
///
/// <para><b>Development only.</b> The relay broadcasts every message to a fixed list of Telegram chat ids — every
/// recipient reads every code. It exists so manual testing beats reading OTPs out of the server log; it is not a
/// gateway and must never be selected in a deployed environment (see <see cref="TelegramOptions"/>).</para>
/// <para><b>Broadcast, not per-user routing.</b> The relay sends every message to a fixed list of Telegram chat
/// ids — every recipient reads every code, whichever phone requested it. That makes it a shared-inbox channel for
/// a small trusted group, not an SMS gateway: it is the deliberate OTP rail for the pre-launch demo deployment
/// (no Iranian gateway contract yet), and must be replaced by <see cref="KavenegarSmsSender"/> before real
/// customers sign up (see <see cref="TelegramOptions"/>).</para>
///
/// <para><b>The OTP is never logged</b> — only the phone tail and the relay's HTTP outcome, exactly like
/// <see cref="KavenegarSmsSender"/>. A non-2xx (the relay answers <c>502</c> when <i>no</i> recipient got the
@@ -27,7 +29,9 @@ public sealed class TelegramSmsSender(
IOptions<SeamOptions> options,
ILogger<TelegramSmsSender> logger) : ISmsSender
{
/// <summary>The repo's committed stand-in for a secret — treated as "not configured".</summary>
/// <summary>The example key published in <c>telegram-otp-bot/.env.example</c> and its README. Anyone
/// reading the repo knows it, so it is a documentation sample, not a secret — treated as "not configured"
/// so a deployment can never quietly authenticate the OTP rail with a publicly-known value.</summary>
private const string SecretPlaceholder = "ab8984974bc1fe5ce514d0fd74f71c8738b3aed92a7e4d86";
private readonly TelegramOptions _options = options.Value.Sms.Telegram;
@@ -43,11 +47,12 @@ public sealed class TelegramSmsSender(
// Fail with the config key rather than sending an unauthenticated request the relay answers with a bare
// 401 — the cause of that 401 is invisible from this side.
var apiKey = _options.ApiKey;
if (string.IsNullOrWhiteSpace(apiKey))
if (string.IsNullOrWhiteSpace(apiKey) || apiKey == SecretPlaceholder)
{
throw new InvalidOperationException(
"Seams:Sms:Telegram:ApiKey is not configured. Set it (user-secrets or environment) to the same " +
"value as the relay's API_KEY, or select another Seams:Sms:Provider.");
"Seams:Sms:Telegram:ApiKey is not configured (unset, or still the published example key). Set it " +
"(appsettings or environment) to the same value as the relay's API_KEY, or select another " +
"Seams:Sms:Provider.");
}
using var request = new HttpRequestMessage(HttpMethod.Post, path)
@@ -2,7 +2,7 @@ namespace Baya.Infrastructure.CrossCutting.Seams;
/// <summary>
/// Options bound from the <c>Seams</c> configuration section. The mock seams read non-secret defaults
/// from here; production keys/paths come from environment variables or user-secrets, never committed.
/// from here; deployed keys/paths come from the environment-specific appsettings file or environment variables.
///
/// <para><b>Provider selection (refinement-phase-8).</b> Each vendor rail carries a <c>Provider</c> selector
/// (default = the mock, so an unconfigured environment behaves exactly as before). Setting it to a real
@@ -37,7 +37,7 @@ public sealed class SeamOptions
/// (<c>IShahkarVerifier</c>), e-KYC (<c>IIdentityKycProvider</c>), and استعلام شبا
/// (<c>IBankAccountOwnershipVerifier</c>). Each seam opts in with its own <c>Provider = finnotech</c> selector,
/// but they authenticate against the same tenant, so the connection facts live here once. All values are
/// secrets — user-secrets / environment, never committed.
/// secrets — the environment-specific appsettings file or environment variables.
/// </summary>
public sealed class FinnotechOptions
{
@@ -63,8 +63,9 @@ public static class SeamProviders
public const string SmsIr = "smsir";
public const string Ghasedak = "ghasedak";
/// <summary><b>Development convenience channel, not an SMS gateway</b> — the local <c>telegram-otp-bot/</c>
/// relay broadcasts every code to a fixed list of Telegram chat ids. Never select it in a real environment.</summary>
/// <summary><b>Broadcast relay, not an SMS gateway</b> — the standalone <c>telegram-otp-bot/</c> service
/// sends every code to a fixed list of Telegram chat ids. The pre-launch demo rail; replace with a real
/// gateway before onboarding customers outside the trusted group.</summary>
public const string Telegram = "telegram";
// Object storage
@@ -105,7 +106,7 @@ public sealed class SmsOptions
/// (Development only).</summary>
public string Provider { get; set; } = SeamProviders.Mock;
/// <summary>Gateway API key / token (secret — user-secrets or environment, never committed).</summary>
/// <summary>Gateway API key / token (secret — the environment-specific appsettings file or environment variables).</summary>
public string ApiKey { get; set; } = string.Empty;
/// <summary>The registered sender line (used by <c>SendAsync</c> free-form messages and non-template sends).</summary>
@@ -122,13 +123,15 @@ public sealed class SmsOptions
}
/// <summary>
/// The <b>Development-only</b> Telegram OTP relay (the standalone <c>telegram-otp-bot/</c> Node service),
/// selected by <c>Seams:Sms:Provider = telegram</c>. It replaces "read the OTP out of the server log" during
/// manual testing — the tester gets the code on their phone without paying an Iranian SMS gateway.
/// The Telegram OTP relay (the standalone <c>telegram-otp-bot/</c> Node service), selected by
/// <c>Seams:Sms:Provider = telegram</c>. It replaces "read the OTP out of the server log" — the tester gets the
/// code on their phone without an Iranian SMS gateway contract.
///
/// <para><b>It is not an SMS gateway.</b> There is no per-user routing: the relay <i>broadcasts</i> every code
/// to a fixed list of Telegram chat ids, so every configured recipient reads every login code. That is fine for
/// a test group and disqualifying for anything else — never point a deployed environment at it.</para>
/// to a fixed list of Telegram chat ids, so every configured recipient reads every login code. That is workable
/// for a trusted demo group — which is why the pre-launch <c>balinyaar.ir</c> deployment uses it — and
/// disqualifying once anyone outside that group can request a code. Switch <c>Seams:Sms:Provider</c> to
/// <c>kavenegar</c> at that point; nothing else changes.</para>
/// </summary>
public sealed class TelegramOptions
{
@@ -137,7 +140,7 @@ public sealed class TelegramOptions
/// <summary>The shared secret sent as the relay's <c>X-Api-Key</c> header — it must equal the relay's
/// <c>API_KEY</c>. <b>Secret:</b> committed config carries an empty/placeholder value; the real one comes
/// from user-secrets (<c>Seams:Sms:Telegram:ApiKey</c>) or the environment, never git.</summary>
/// from <c>Seams:Sms:Telegram:ApiKey</c> in appsettings or the environment.</summary>
public string ApiKey { get; set; } = string.Empty;
/// <summary>Per-request timeout. The relay itself talks to Telegram (over a proxy in a filtered region), so
@@ -18,7 +18,7 @@ public static class ServiceCollectionExtension
/// token swaps in the real HTTP adapter behind the same Application contract — <b>callers never change</b>. An
/// unconfigured/typo'd provider falls closed to the mock. This makes a partial rollout the normal case (real SMS
/// + real geocoder while payments stay mocked in a pre-launch environment). Real adapters read credentials from
/// <c>Seams:*</c> (user-secrets/environment) and get an <see cref="System.Net.Http.HttpClient"/> from the
/// <c>Seams:*</c> (appsettings/environment) and get an <see cref="System.Net.Http.HttpClient"/> from the
/// <c>IHttpClientFactory</c>. (The real in-app <c>INotificationDispatcher</c> needs the database, so it is
/// registered in the Persistence layer.)
/// </summary>